LDAP/SASL/GSSAPI/Kerberos编程API--krb5应用服务(UDP)

君不见长松卧壑困风霜,时来屹立扶明堂。这篇文章主要讲述LDAP/SASL/GSSAPI/Kerberos编程API--krb5应用服务(UDP)相关的知识,希望能为你提供帮助。
上篇介绍的< krb5应用服务> 是面向连接的TCP,本篇介绍面向无连接UDP的krb5应用,参考MIT krb5源码UDP的例子,主要演示KRB message发送(客户)/接收(服务)过程。
一.准备工作
请参考< LDAP/SASL/GSSAPI/Kerberos编程API(5)--krb5应用服务> (https://blog.51cto.com/u_13752418/2778563)

Kerberos服务器(KDC)vmkdc 应用服务器vmsrv.ctp.net192.168.1.20仅接收/etc/krb5.keytab(由应用服务主体所导出) 客户机vmcln.ctp.net192.168.1.40仅发送领域CTP.NET 用户主体krblinlin@CTP.NET 应用服务主体mysv/vmsrv.ctp.net

二.应用服务器
1.源代码
//源文件名:krbsrv.c #include < krb5.h> #include < stdio.h> #include < netdb.h> int main(int argc, char *argv[])krb5_context context; krb5_auth_context auth_context = NULL; krb5_error_code retval; krb5_principal server; retval = krb5_init_context(& context); if (retval)exit(1); retval = krb5_sname_to_principal( context, "vmsrv.ctp.net.",// #1 "mysv",// 应用服务名 KRB5_NT_SRV_HST, & server); if (retval)exit(1); int sock = -1; struct sockaddr_in sockin; if ((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0)exit(1); sockin.sin_family = AF_INET; sockin.sin_addr.s_addr = INADDR_ANY; sockin.sin_port = htons(12345); //端口号 if (bind(sock, (struct sockaddr *) & sockin, sizeof(sockin)))exit(1); for(; ; )//循环服务器 int i; socklen_t len; krb5_data packet, message; unsigned char pktbuf[BUFSIZ]; // #2/* GET KRB_AP_REQ MESSAGE */ len = sizeof(struct sockaddr_in); if ((i = recvfrom(sock, (char *)pktbuf, sizeof(pktbuf),0,NULL, & len)) < 0)perror("receiving datagram"); exit(1); packet.length = i; packet.data = https://www.songbingjia.com/android/(krb5_pointer) pktbuf; // #3/* Check authentication info */ if ((retval = krb5_rd_req(context, & auth_context, & packet, server, NULL, //缺省/etc/krb5.keytab NULL, NULL)))printf("Err while reading KRB_AP_REQ request:%s\\n", krb5_get_error_message(context, retval)); exit(1); printf("recv KRB_AP_REQ message OK\\n"); //krb5_free_data_contents(context, & packet); // #4// Set foreign_addr for rd_safe() and rd_priv()if ((retval = krb5_auth_con_setaddrs(context, auth_context, NULL, //本地 NULL//远程,即客户(相对本应用服务器); 空为不验证客户地址 )))printf("Err while setting foreign addr:%s\\n", krb5_get_error_message(context, retval)); exit(1); if ((retval = krb5_auth_con_setports(context, auth_context, NULL, NULL)))printf("Err while setting foreign port:%s\\n", krb5_get_error_message(context, retval)); exit(1); krb5_auth_con_setflags(context,auth_context, 65536); // #5for (int j=0; j< 2; j++)//测试两个/* GET KRB_MK_SAFE MESSAGE */ len = sizeof(struct sockaddr_in); if ((i = recvfrom(sock, (char *)pktbuf, sizeof(pktbuf), 0,NULL,& len)) < 0)printf("error:receiving safe datagram"); exit(1); packet.length = i; packet.data = https://www.songbingjia.com/android/(krb5_pointer) pktbuf; if ((retval = krb5_rd_safe(context, auth_context, & packet,& message, NULL)))printf("Err while verifying SAFE message:%s\\n", krb5_get_error_message(context, retval)); exit(1); printf("%d > recv safe message OK,is:%s\\n",j+1,message.data); krb5_free_data_contents(context, & message); //krb5_free_data_contents(context, & packet); // #6//--v-- krb5_auth_con_free(context, auth_context); // #7 auth_context = NULL; // #8 //--^-- ; krb5_free_principal(context, server); krb5_free_context(context); exit(0);

2.解析
1)见代码注释
2)作为服务端程序,需要完善的错误处理机制,保证出错也不停机。但本实验为简单对错误直接exit(1)结束程序
3)API用法详见/usr/include/krb5/krb5.h
关键两个API函数krb5_rd_req和krb5_rd_safe
其次两个API函数krb5_auth_con_setaddrs和krb5_auth_con_setports(Set the local and remote addresses/port in an auth context)
4)流程
4.1)recvfrom收到客户KRB_AP_REQ请求包,krb5_rd_req验证KRB_AP_REQ包并生成krb5_auth_context(Authentication context)
4.2)recvfrom收到客户KRB-SAFE message包,krb5_rd_safe根据krb5_auth_context来验证并解包得到客户真正用户数据
4.3)recvfrom/krb5_rd_req只需一次就可(一个循环服务一个krb5_auth_context),recvfrom/krb5_rd_safe可多次(如本文一个循环服务测试两个KRB-SAFE)
KRB-SAFE message包中的用户数据并不加密的,即客户传输给应用服务的过程用户数据是明文的。
如果传输过程要求加密,可使用krb5_mk_priv/krb5_rd_priv对,客户由krb5_mk_priv加密并打包,应用服务由krb5_rd_priv解密并解包。
5)应用服务主体名
#1处主机全名需含最后终结句号即" vmsrv.ctp.net."
#1处设置为NULL或不含终结句号的" vmsrv.ctp.net" ,都无法拼接出正确的主体名mysv/vmsrv.ctp.net,导致在KDC数据库找不到主体
上篇关于TCP文章< krb5应用服务> 类似本文#1处设置为NULL或不含终结句号却一切正常,不知为何本文不行,不知是否和DNS相关(配置DNS记录确有需含终结句号的情形)
6 )
#4、#6处需注释掉,否则导致程序崩溃退出提示double free or corruption (out)错误信息。
原因猜测:
krb5_free_data_contents释放packet,可能实际是要释放pktbuf(见#3处),而pktbuf(见#2处)定义为字符数组是在栈空间,pktbuf离开作用域会自动释放,因而再krb5_free_data_contents就造成重复释放而崩溃。
假如pktbuf定义为字符串指针应该krb5_free_data_contents没问题,但可能不能再重复free(pktbuf),本文没再验证。
7)krb5_auth_con_setflags影响krb5_rd_safe
7.1)KRB5_AUTH_CONTEXT_DO_TIME置1
假如注释掉#5处krb5_auth_con_setflags,则其缺省值为65537(可由krb5_auth_con_getflags获取值),即首位KRB5_AUTH_CONTEXT_DO_TIME(0x00000001)为1
这时运行应用服务在krb5_rd_safe后提示时间偏差太大错误
Err while verifying SAFE message:Clock skew too great
是没架设NTP时间服务器原因吗?本实验虽没架设NTP,但krb5_rd_req正常,客户机kinit正常,KDC、应用服务器、客户机date后日期时间几乎一致,不知为何krb5_rd_safe还存在时间偏差太大,不是默认允许时间偏差5分钟吗?
查/usr/include/krb5/krb5.h中的krb5_rd_safe有提到
  • If the #KRB5_AUTH_CONTEXT_DO_TIME flag is set in @a auth_context, then the
  • timestamp in the message is verified to be within the permitted clock skew
  • of the current time, and the message is checked against an in-memory replay
  • cache to detect reflections or replays.
7.2)KRB5_AUTH_CONTEXT_DO_TIME置0
所以本实验在#5处必需去掉KRB5_AUTH_CONTEXT_DO_TIME位,即需设置值65536=65537-1,krb5_rd_safe才成功,但不知是否有安全漏洞?如失去阻止重播攻击?
如果客户机和应用服务器真的时间偏差5分钟以上,不知krb5_rd_safe成功还是失败?
7.3)
我没深入研究krb5_auth_con_setflags设置各标志位的意义,或许保留KRB5_AUTH_CONTEXT_DO_TIME位然后设置其它位flag也许krb5_rd_safe不出错。
本文也仅仅是实验应用服务器能简单地验证客户身份,至于是否存在安全隐患我未知,我也不是安全专家,网络攻击的安全问题请读者自己斟酌。
8)#8处一定要auth_context赋NULL此句,否则进入下个for循环会出现段错误
原因应该是#7处已释放auth_context资源,krb5_rd_req有判断auth_context不为NULL就不创建新的auth_context,导致auth_context空资源。
3.编译
linlin@debian:~$ gcc -o krbsrv krbsrv.c -lkrb5
三.客户机
1.源代码
//源文件名:krbcln.c #include < krb5.h> #include < stdio.h> #include < netdb.h> #include < string.h> int main(int argc, char *argv[])krb5_contextcontext; krb5_error_code retval; krb5_ccache ccdef; krb5_auth_contextauth_context=NULL; krb5_data packet,inbuf; retval = krb5_init_context(& context); if (retval)perror("while initializing krb5"); exit(1); /* Get credentials for server */ if ((retval = krb5_cc_default(context, & ccdef)))perror("while getting default ccache"); exit(1); static int sockfd; static struct sockaddr_in their_addr; if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) perror("socket"); exit(1); their_addr.sin_family = AF_INET; their_addr.sin_port = htons(12345); their_addr.sin_addr.s_addr = inet_addr("192.168.1.20"); bzero(& (their_addr.sin_zero), 8); //Create KRB_AP_REQ message if ((retval = krb5_mk_req(context,& auth_context,0, "mysv",//应用服务名 "vmsrv.ctp.net.",//主机全名需含最后终结句号 NULL,ccdef,& packet)))printf("Err:%s\\n", krb5_get_error_message(context, retval)); exit(1); //--v-- #9 //Send authentication info to serverif (sendto(sockfd,(char *)packet.data,(unsigned) packet.length,0,(struct sockaddr *)& their_addr,sizeof(struct sockaddr))< 0)printf("error:while sending KRB_AP_REQ message\\n"); exit(1); printf("send KRB_AP_REQ message OK\\n"); krb5_free_data_contents(context, & packet); //--^-- #9//--v-- #10 struct sockaddr_in my_addr; krb5_auth_con_setflags(context,auth_context, 65536); krb5_address addr; addr.addrtype= ADDRTYPE_INET; addr.length=sizeof(my_addr.sin_addr); addr.contents=(krb5_octet *)& my_addr.sin_addr; krb5_auth_con_setaddrs(context,auth_context, & addr, //本地,不能NULL,但sockaddr_in(my_addr)可不需设置 NULL//远程,即服务器(相对本客户机) ); addr.addrtype = ADDRTYPE_IPPORT; addr.length = sizeof(my_addr.sin_port); addr.contents = (krb5_octet *)& my_addr.sin_port; krb5_auth_con_setports(context, auth_context,& addr,NULL); inbuf.data="https://www.songbingjia.com/android/abc123"; //用户数据 inbuf.length=strlen(inbuf.data); retval=krb5_mk_safe(context,auth_context,& inbuf,& packet,NULL); //Format KRB-SAFE message ; 用户数据(inbuf)转换为KRB-SAFE(packet)if (sendto(sockfd,(char *)packet.data,(unsigned) packet.length,0,(struct sockaddr *)& their_addr,sizeof(struct sockaddr))< 0)printf("error:while sending mk_safe message\\n"); exit(1); printf("send safe message OK,is:%s\\n",inbuf.data); krb5_free_data_contents(context, & packet); //--^-- #10krb5_auth_con_free(context, auth_context); krb5_free_context(context); exit(0);

2.解析
1)见代码注释
2)关键两个API函数krb5_mk_req和krb5_mk_safe
3.编译
1 )Create、send KRB_AP_REQ message和send KRB-SAFE message
linlin@debian:~$ gcc -o krbcln1 krbcln.c -lkrb5
2 )仅Create KRB_AP_REQ message和send KRB-SAFE message
注释掉#9
linlin@debian:~$ gcc -o krbcln2 krbcln.c -lkrb5
3 )仅Create KRB_AP_REQ message
注释掉#9、#10
linlin@debian:~$ gcc -o krbcln3 krbcln.c -lkrb5
四.运行测试
只测试只有一台客户机的情况
1.
所有主机都关机,然后按顺序执行步骤:
KDC开机-> 客户机开机-> 客户机运行kinit-> 客户机运行krbcln3-> KDC停止Kerberos-> 客户机运行krbcln1-> 应用服务器开机-> 应用服务器运行krbsrv-> 客户机运行krbcln1-> 客户机运行krbcln2-> 客户机运行krbcln3
1)KDC开机
2)客户机开机
3)客户机运行kinit
linlin@vmcln:~$ kinit --no-forwardable krblinlin
krblinlin@CTP.NETs Password:
kinit后查看KDC日志heimdal-kdc.log,可见客户机(192.168.1.40)AS-REQ请求
2022-03-11T02:27:46 AS-REQ krblinlin@CTP.NET from IPv4:192.168.1.40 for krbtgt/CTP.NET@CTP.NET 2022-03-11T02:27:46 Client sent patypes: REQ-ENC-PA-REP 2022-03-11T02:27:46 Looking for PK-INIT(ietf) pa-data -- krblinlin@CTP.NET 2022-03-11T02:27:46 Looking for PK-INIT(win2k) pa-data -- krblinlin@CTP.NET 2022-03-11T02:27:46 Looking for ENC-TS pa-data -- krblinlin@CTP.NET 2022-03-11T02:27:46 Need to use PA-ENC-TIMESTAMP/PA-PK-AS-REQ 2022-03-11T02:27:46 sending 293 bytes to IPv4:192.168.1.40 2022-03-11T02:27:46 AS-REQ krblinlin@CTP.NET from IPv4:192.168.1.40 for krbtgt/CTP.NET@CTP.NET 2022-03-11T02:27:46 Client sent patypes: ENC-TS, REQ-ENC-PA-REP 2022-03-11T02:27:46 Looking for PK-INIT(ietf) pa-data -- krblinlin@CTP.NET 2022-03-11T02:27:46 Looking for PK-INIT(win2k) pa-data -- krblinlin@CTP.NET 2022-03-11T02:27:46 Looking for ENC-TS pa-data -- krblinlin@CTP.NET 2022-03-11T02:27:46 ENC-TS Pre-authentication succeeded -- krblinlin@CTP.NET using aes256-cts-hmac-sha1-96 2022-03-11T02:27:46 ENC-TS pre-authentication succeeded -- krblinlin@CTP.NET 2022-03-11T02:27:46 AS-REQ authtime: 2022-03-11T02:27:46 starttime: unset endtime: 2022-09-09T17:27:42 renew till: unset 2022-03-11T02:27:46 Client supported enctypes: aes256-cts-hmac-sha1-96, aes128-cts-hmac-sha1-96, aes256-cts-hmac-sha384-192, aes128-cts-hmac-sha256-128, des3-cbc-sha1, arcfour-hmac-md5, using aes256-cts-hmac-sha1-96/aes256-cts-hmac-sha1-96 2022-03-11T02:27:46 sending 681 bytes to IPv4:192.168.1.40

查看票据
linlin@vmcln:~$ klist Credentials cache: FILE:/tmp/krb5cc_1000 Principal: krblinlin@CTP.NET IssuedExpiresPrincipal Mar 11 02:27:46 2022Sep9 17:27:42 2022krbtgt/CTP.NET@CTP.NET linlin@vmcln:~$

4)客户机运行krbcln3
linlin@vmcln:~$ ./krbcln3
krbcln3后查看KDC日志heimdal-kdc.log,可见客户机TGS-REQ请求
2022-03-11T02:40:50 Got TGS FAST request 2022-03-11T02:40:50 TGS-REQ krblinlin@CTP.NET from IPv4:192.168.1.40 for mysv/vmsrv.ctp.net@CTP.NET [canonicalize] 2022-03-11T02:40:50 TGS-REQ authtime: 2022-03-11T02:27:46 starttime: 2022-03-11T02:40:50 endtime: 2022-09-09T17:27:42 renew till: unset 2022-03-11T02:40:50 sending 643 bytes to IPv4:192.168.1.40

linlin@vmcln:~$ klist Credentials cache: FILE:/tmp/krb5cc_1000 Principal: krblinlin@CTP.NET IssuedExpiresPrincipal Mar 11 02:27:46 2022Sep9 17:27:42 2022krbtgt/CTP.NET@CTP.NET Mar 11 02:40:50 2022Sep9 17:27:42 2022mysv/vmsrv.ctp.net@ Mar 11 02:40:50 2022Sep9 17:27:42 2022mysv/vmsrv.ctp.net@CTP.NET linlin@vmcln:~$

运行krbcln3后,可以看出票据多了mysv/vmsrv.ctp.net
5)KDC停止Kerberos
root@vmkdc:~# /etc/init.d/heimdal-kdc stop
6)客户机运行krbcln1
linlin@vmcln:~$ ./krbcln1
send KRB_AP_REQ message OK
send safe message OK,is:abc123
说明:此时应用服务器没开机,KDC服务器也停止Kerberos,UDP也能正常发出去
7)应用服务器开机
8)应用服务器运行krbsrv
linlin@vmsrv:~$ ./krbsrv
recv KRB_AP_REQ message OK
1 > recv safe message OK,is:abc123
2 > recv safe message OK,is:abc123
说明:此时KDC服务器已停止Kerberos,提示行1> 对应客户krbcln1,提示行2> 对应客户krbcln2
9)客户机运行krbcln1
客户机发送数据
linlin@vmcln:~$ ./krbcln1
send KRB_AP_REQ message OK
send safe message OK,is:abc123
10)客户机运行krbcln2
linlin@vmcln:~$ ./krbcln2
send safe message OK,is:abc123
说明:此时KDC服务器已停止Kerberos,客户和应用服务交互正常
11)客户机运行krbcln3
linlin@vmcln:~$ ./krbcln3 linlin@vmcln:~$

运行正常,没有出错,并且klist结果没变化,说明在服务票据已存在时不会再发KRB_TGS_REQ
12)在KDC已停止Kerberos情况下,客户重新kinit
linlin@vmcln:~$ kinit --no-forwardable krblinlin krblinlin@CTP.NETs Password: kinit: krb5_get_init_creds: unable to reach any KDC in realm CTP.NET linlin@vmcln:~$ klist Credentials cache: FILE:/tmp/krb5cc_1000 Principal: krblinlin@CTP.NET IssuedExpiresPrincipal Mar 11 02:27:46 2022Sep9 17:27:42 2022krbtgt/CTP.NET@CTP.NET Mar 11 02:40:50 2022Sep9 17:27:42 2022mysv/vmsrv.ctp.net@ Mar 11 02:40:50 2022Sep9 17:27:42 2022mysv/vmsrv.ctp.net@CTP.NETlinlin@vmsrv:~$ ./krbsrv recv KRB_AP_REQ message OK 1 > recv safe message OK,is:abc123 2 > recv safe message OK,is:abc123linlin@vmcln:~$ ./krbcln1 send KRB_AP_REQ message OK send safe message OK,is:abc123 linlin@vmcln:~$ ./krbcln2 send safe message OK,is:abc123 linlin@vmcln:~$

kinit提示失败,但不会销毁原来的票据,所以客户和应用服务交互仍正常
13)小结
关键点应用服务器已事先存放好krb5.keytab文件;
应用服务器和KDC不交互;
客户和KDC交互两次,第一次获得krbtgt,第二次获取应用服务票据;
然后只要客户机票据一直存在,后续客户只和应用服务交互(可以只客户到服务的单向),客户不再和KDC交互。
2.所有主机都开机,KDC服务器运行Kerberos,客户机已kinit
1)客户机运行命令顺序:krbcln1-> krbcln2-> krbcln1-> krbcln2
1.1)
linlin@vmsrv:~$ ./krbsrv recv KRB_AP_REQ message OK 1 > recv safe message OK,is:abc1232 > recv safe message OK,is:abc123recv KRB_AP_REQ message OK 1 > recv safe message OK,is:abc1232 > recv safe message OK,is:abc123

说明:提示行1> 对应客户krbcln1,提示行2> 对应客户krbcln2
1.2)过程即 KRB_AP_REQ-> KRB-SAFE=> KRB-SAFE
linlin@vmcln:~$ ./krbcln1 send KRB_AP_REQ message OK send safe message OK,is:abc123linlin@vmcln:~$ ./krbcln2 send safe message OK,is:abc123linlin@vmcln:~$ ./krbcln1 send KRB_AP_REQ message OK send safe message OK,is:abc123linlin@vmcln:~$ ./krbcln2 send safe message OK,is:abc123

2)客户机先运行命令krbcln2
2.1)
linlin@vmsrv:~$ ./krbsrv
Err while reading KRB_AP_REQ request:Invalid message type
linlin@vmsrv:~$
说明:先运行的krbcln2发送的是KRB-SAFE message,应用服务krb5_rd_req到的是KRB-SAFE message(不是KRB_AP_REQ message),所以验证出身份错误
2.2)直接发送KRB-SAFE,没先发送KRB_AP_REQ
linlin@vmcln:~$ ./krbcln2
send safe message OK,is:abc123
3)客户机运行命令顺序:krbcln1-> kinit-> krbcln2
3.1)
linlin@vmsrv:~$ ./krbsrv
recv KRB_AP_REQ message OK
1 > recv safe message OK,is:abc123
Err while verifying SAFE message:Message stream modified(在客户重新生成票据后,运行krbcln2导致应用服务krb5_rd_safe验证出身份错误)
linlin@vmsrv:~$
说明客户重生成票据,身份信息发生了变化,在应用服务验证不通过
3.2)
发送KRB_AP_REQ、KRB-SAFE
linlin@vmcln:~$ ./krbcln1
send KRB_AP_REQ message OK
send safe message OK,is:abc123
重新生成票据
linlin@vmcln:~$ kinit --no-forwardable krblinlin
krblinlin@CTP.NETs Password:
发送KRB-SAFE
linlin@vmcln:~$ ./krbcln2
send safe message OK,is:abc123
五.后记
1.下图是讲解Kerberos理论经常提及,理论的东西我也不懂,本文只是介绍使用API
-------- 1. KRB_AS_REQ----- ||---------------> || |||| || 2. KRB_AS_REP|| ||< ---------------|| |||| |客户机||KDC| || 3. KRB_TGS_REQ || ||---------------> || |||| || 4. KRB_TGS_REP || ||< ---------------|| ||----- ||------------------ || 5. KRB_AP_REQ|| ||---------------> || |||应用服务器| || 6. KRB_AP_REP|/etc/krb5.keytab| ||< ---------------|| --------------------------

图A
1)1~2应是对应kinit
2)1 ~ 4是客户机和KDC必需联机,完成1 ~ 4后KDC可脱机
3)5~6和KDC无关
4)应用服务器已事先布置/etc/krb5.keytab,可以不和KDC联机
5)3~5应是对应krb5_mk_req
本客户机源码没见有明显KRB_TGS_REQ的API使用,仅是使用krb5_mk_req函数。
从运行krbcln3仅krb5_mk_req就获得服务票据,可推测krb5_mk_req构造KRB_AP_REQ过程中,如果TGS服务票据不存在则krb5_mk_req先向KDC发出KRB_TGS_REQ请求,如果已事先获得服务票据则krb5_mk_req应就只执行第5步骤。
6)3~4应是对应krb5_get_credentials
见8)
7)第5应是对应krb5_mk_req_extended
见8)
8)完整krb5_mk_req实现见MIT krb5源码 ../src/lib/krb5/krb/mk_req.c
/* Formats a KRB_AP_REQ message into outbuf. ... */krb5_error_code KRB5_CALLCONV krb5_mk_req(krb5_context context, krb5_auth_context *auth_context, krb5_flags ap_req_options, const char *service, const char *hostname, krb5_data *in_data, krb5_ccache ccache, krb5_data *outbuf)krb5_error_coderetval; krb5_principalserver; krb5_creds* credsp; krb5_credscreds; retval = krb5_sname_to_principal(context, hostname, service, KRB5_NT_SRV_HST, & server); //拼接应用服务主体名 if (retval) return retval; //--v-- 获取应用服务票据,对应图A的3~4 /* obtain ticket & session key */ memset(& creds, 0, sizeof(creds)); if ((retval = krb5_copy_principal(context, server, & creds.server))) goto cleanup_princ; if ((retval = krb5_cc_get_principal(context, ccache, & creds.client))) goto cleanup_creds; if ((retval = krb5_get_credentials(context, 0, ccache, & creds, & credsp))) //get a service ticket goto cleanup_creds; //--^--//--v-- 构造KRB_AP_REQ,对应图A的第5步骤 retval = krb5_mk_req_extended(context, auth_context, ap_req_options, in_data, credsp, outbuf); //in_data是用于校验,所以krb5_mk_req_extended应没有象krb5_mk_safe携带用户数据的功能//--^--krb5_free_creds(context, credsp); cleanup_creds: krb5_free_cred_contents(context, & creds); cleanup_princ: krb5_free_principal(context, server); return retval;

可见krb5_mk_req功能就是获取应用服务票据和构造KRB_AP_REQ,也是调用了几个公共API,没有调用MIT krb5源码内部函数,可以看作对API的再次封装,因此我们的客户程序可参考照抄krb5_mk_req代码以清晰区分3~5步骤。
而krb5_mk_req_extended是基本的API,其实现(见../src/lib/krb5/krb/mk_req_ext.c)调用了MIT krb5源码内部函数,用户应用程序只能使用API而不能参考抄写其内部实现源码。
见内部头文件../src/include/k5-int.h
... /* * This prototype for k5-int.h (Krb5 internals include file) * includes the user-visible definitions from krb5.h and then * includes other definitions that are not user-visible but are * required for compiling Kerberos internal routines. ... */ ... #include "krb5.h" ...

9)如图A,本UDP实验只1~5,并加多krb5_mk_safe用户数据
10)回顾上篇< krb5应用服务> TCP循环服务器,对照图A
C/S TCP 常用krb5_sendauth/krb5_recvauth这对API,这两个也是使用了krb5_mk_req_extended/krb5_rd_req等。
但正如上篇所讲,krb5_recvauth是阻塞的,是不断接收直到指定字节数为止才认为接收完KRB_AP_REQ,如果客户发送垃圾数据不退出可导致服务端阻塞一直占用tcp连接。
对于TCP,应该也是只1~5足够,不需krb5_mk_safe,只要服务端krb5_rd_req验证过了,后续在TCP传输就是可信,如果传输用户数据要达到如SSL加密效果,可使用krb5_mk_priv加密。
所以用户程序可以遵从图A自己灵活使用最基本的API函数krb5_mk_req/krb5_rd_req编写认证过程(如仅简单的单向认证),服务端发现错误/垃圾数据及时关闭tcp连接,不需使用krb5_sendauth/krb5_recvauth
10.1)krb5_recvauth调用关系
krb5_recvauth |-- recvauth_common |-- krb5_rd_req |-- krb5_read_message |-- krb5_net_read

见../src/lib/krb5/os/read_msg.c
krb5_error_code krb5_read_message(krb5_context context, krb5_pointer fdp, krb5_data *inbuf)krb5_int32len; //32位整型,其意义为KRB message长度 intlen2, ilen; //ilen意义等同len char*buf = NULL; intfd = *( (int *) fdp); *inbuf = empty_data(); if ((len2 = krb5_net_read(context, fd, (char *)& len, 4)) != 4) //先读出头4个字节 return((len2 < 0) ? errno : ECONNABORTED); len = ntohl(len); //头4个字节网络整型转换为本地整型if ((len & VALID_UINT_BITS) != (krb5_ui_4) len)/* Overflow size_t??? */ return ENOMEM; ilen = (int)len; if (ilen) /* * We may want to include a sanity check here someday.... */ if (!(buf = malloc(ilen)))//分配空间 return(ENOMEM); if ((len2 = krb5_net_read(context, fd, buf, ilen)) != ilen)//读ilen(即len数值)个字节 free(buf); return((len2 < 0) ? errno : ECONNABORTED); *inbuf = make_data(buf, ilen); return(0);

从上可看出krb5_read_message调用了两个krb5_net_read,第一个读头4个字节并转换为本地32位整型值len(指明下一个即将要读的长度),第二个就读len个字节
见../src/lib/krb5/os/net_read.c
int krb5_net_read(krb5_context context, int fd, char *buf, int len)int cc, len2 = 0; do cc = SOCKET_READ((SOCKET)fd, buf, len); //实际为read系统调用,见../src/include/port-sockets.h中宏定义 if (cc < 0) if (SOCKET_ERRNO == SOCKET_EINTR) continue; /* XXX this interface sucks! */ errno = SOCKET_ERRNO; return(cc); /* errno is already set */else if (cc == 0)//当发送方close连接 return(len2); else buf += cc; len2 += cc; len -= cc; while (len > 0); return(len2);

从上可看出krb5_net_read就是TCP接收数据的通常做法,循环读直到读完指定len个字节或发送方close
krb5_net_read要求阻塞型I/O
10.2)上篇客户机(无krb5)发送垃圾数据测试
不同于UDP是数据报能一次性读完数据,TCP是流式要一直读到对方关闭连接或者读取指定长度字节。TCP发送方的一次发送,接收方可能需多次接收。
如客户发送" ABCDEFG" ,紧接发送" 12345" ,则服务端接收情况可能如" ABC" -> " DEF" -> " G12" -> " 345"
上篇客户发送" abcdefgh" -> " 1234567890" -> ...
跟踪krb5_recvauth(实际即krb5_net_read里循环读)如下:
read(4, "abcd", 4)= 4第一个krb5_net_read读头4个字节垃圾数据,转换为本地整型值即1633837924,超大的数值,并且在krb5_read_message里要分配如此之大的空间 read(4, "efgh\\n", 1633837924)= 5第二个krb5_net_read指定要读1633837924超大个字节 read(4, "1234567890\\n", 1633837919)= 11所以表现读循环 ... read(4,...而一旦客户停止发送(但不close),因实际读取字节数小于指定字节长度,循环到开头read等待数据到来,所以krb5_recvauth表现为读阻塞 ...

说明:系统调用read第1入参是描述符,第3入参是读取长度
可见krb5_recvauth也没限制接收数据的大小,恶意客户发送满32位int的4字节头,服务端不但阻塞而且要分配超大空间(足撑死服务器)。
10.3)改写上篇客户机(无krb5)发送垃圾数据测试代码
改为
if (connect(sock, (struct sockaddr *)& remote, sizeof(struct sockaddr)) < 0) printf( " connect: error\\n"); close(sock); sock = -1; exit(1); int num=1; num=htonl(num); //转为网络字节序 for (; ; )//循环测试if (send(sock,(char *)& num,4,0)==-1) //发送数值num(网络字节序) printf("error-send\\n"); exit(1); close(sock);

客户循环发送数值1 : " \\0\\0\\0\\1" (串1) -> " \\0\\0\\0\\1" (串2) -> " \\0\\0\\0\\1" (串3) -> " \\0\\0\\0\\1" (串4) -> ...
说明:跟踪信息是每个字节按8进制显示.
krb5_recvauth调用了至少两次krb5_read_message(见../src/lib/krb5/krb/recvauth.c)
跟踪应用服务krb5_recvauth如下:
accept(3,...) = 4--v-- 第1个krb5_read_message read(4, "\\0\\0\\0\\1", 4)= 4第1个krb5_net_read读头4字节,其串内容意义表示长度为1 read(4, "\\0", 1)= 1第2个krb5_net_read读1个字节,即客户第2串"\\0\\0\\0\\1"的头个 --^----v-- 第2个krb5_read_message read(4, "\\0\\0\\1", 4)= 3 \\第1个krb5_net_read,因上面已读走客户第2串的头个"\\0",所以读走第2串剩下3个字节"\\0\\0\\1",返回实际读取字节数3(小于4,第3入参表明要读4个字节) read(4, "\\0", 1)= 1 /因要满足读取头4个字节,所以需再补读客户第3串的头1个字节(第3入参为1)"\\0",最终得到"\\0\\0\\1\\0"(即网络字节序整型值0x00 00 01 00,其意义表示长度为256) read(4, "\\0\\0\\1", 256)= 3第2个krb5_net_read要读256(0x0100)个字节,读客户第3串剩下3个字节,所以read返回3 read(4, "\\0\\0\\0\\1", 253)= 4数值253=256-3 read(4, "\\0\\0\\0\\1", 249)= 4...逐个减(因为客户每次发送是固定4字节,所以跟踪信息都是减4) read(4, "\\0\\0\\0\\1", 245)= 4 ... read(4, "\\0\\0\\0\\1", 17)= 4 read(4, "\\0\\0\\0\\1", 13)= 4 read(4, "\\0\\0\\0\\1", 9)= 4 read(4, "\\0\\0\\0\\1", 5)= 4 read(4, "\\0", 1)= 1直到读完规定的字节数就不再读(虽然最后完整串应"\\0\\0\\0\\1",但最后仅需读头个"\\0"便满足256个字节),krb5_recvauth返回错误 --^-- sendmsg(4, msg_name=NULL, msg_namelen=0, msg_iov=[iov_base="\\1", iov_len=1], msg_iovlen=1, msg_controllen=0, msg_flags=0, MSG_NOSIGNAL) = 1 write(1, " auth failed\\n", 13 auth failed )= 13 close(4)= 0 write(1, "close\\n", 6close )= 6 exit_group(0)= ? +++ exited with 0 +++

10.4)krb5_sendauth就不分析了,肯定是和krb5_recvauth对应的
10.5)自己实现类似krb5_recvauth
简化过程,解决读阻塞,根据应用场景满足定制需求.至简可参照krb5应用服务(UDP)例子,应该仅需krb5_rd_req,甚至不需krb5_auth_con_setaddrs/krb5_auth_con_setports(为KRB-SAFE message)。
防止恶意客户、解决读阻塞方法
方法1:一次性读
简单地认为应用服务器能一次性读完数据包,只调用一次读(不循环读),就krb5_rd_req(即使真的一次未读完KRB_AP_REQ,也停止再读,让krb5_rd_req验证身份出错,关闭连接)。
又分两种情况:
a)必须知道KRB_AP_REQ长度
就如krb5_recvauth头4个字节指明KRB_AP_REQ长度。
正常客户就长度值4个字节加上KRB_AP_REQ一起打包一次发送(最好不要分开发送)。
服务端则要2次读,先读头4个字节,如果其意义长度值过小或过大就当作恶意客户,就关闭连接,不再继续读。
存在客户只发送4个字节便不再发送,服务端第2次读阻塞。
b)不必知道KRB_AP_REQ长度
省掉客户发/服务收头4个字节,只要了解到KRB5协议的KRB_AP_REQ最大能达到多大长度maxlen,以此maxlen作read固定长度。
正常客户就KRB_AP_REQ一次发送,但就要求正常客户发送完KRB_AP_REQ不能马上紧接着发送下个数据,防止应用服务器一次接收是KRB_AP_REQ+下个数据部分(因为不知KRB_AP_REQ长度,不知那部分数据多余)。
正常客户可sleep几秒才发送下个数据; 或通常采取客户/服务应答模式,服务将认证结果回答给客户,客户接收结果决定是否发送下个数据。具体是应用协议层面了。
只不过我没深入了解KRB5协议,不清楚KRB_AP_REQ最大能达到多大长度; 我对网络经验也不足,不知是否会因KRB_AP_REQ过大导致TCP服务端总是无法一次性读完。
偶尔的未能一次读完对身份认证问题不大,大不了客户重新登录再次发送身份认证请求。
同样,客户可以发起连接,但不发送数据,也会造成服务端阻塞。
无论1次读还是2次读,仍会出现读阻塞问题,所以在read前加超时处理是必须的。
方法2:循环读,超时处理
如同krb5_recvauth头4个字节指明长度,正常客户保证服务端能得到完整正确KRB_AP_REQ,读处理如同krb5_net_read循环read,只是在read前加上超时关闭连接防止恶意客户。分两个读循环,第一个头4字节,第二个KRB_AP_REQ
for(条件); //读循环//--v-- 超时处理 int rc; fd_set fds; struct timeval tv; FD_ZERO(& fds); FD_SET(acc,& fds); tv.tv_sec = tv.tv_usec = 15; //超时15秒 rc = select(acc+1, & fds, NULL, NULL, & tv); if (rc < 0) printf(" select failed\\n"); close(acc); break; if (FD_ISSET(acc,& fds)) printf(" select ok\\n"); else printf(" time out\\n"); close(acc); //超时便关闭连接 break; //退出循环//--^-- read(acc, ...); ...

【LDAP/SASL/GSSAPI/Kerberos编程API--krb5应用服务(UDP)】2.UDP登录会话
本实验是单个客户机遵循顺序:krbcln1-> krbcln2-> krbcln1-> krbcln2,对于单个客户机的测试是没问题。
如果是多个客户机,因为应用服务器是UDP,即使本实验是循环服务器,也是可同时接收来自多个不同客户机的数据。
本实验应用服务器的每个krb5_rd_req-> krb5_rd_safe-> krb5_rd_safe(即3个recvfrom)循环,可能同个循环里来自不同客户机数据,导致验证失败。
本小节讨论支持多个UDP客户、登录会话,不考虑UDP传输可靠性、报文顺序问题。
1)结合KRB message对比udp、tcp循环服务器
1.1)udp
sock = socket(PF_INET, SOCK_DGRAM, 0); //数据报bind(sock,...); for(; ; ) //循环服务器recvfrom(sock,...); // #11 recvfrom(sock,...); // #12 recvfrom(sock,...); // #13

因为udp可同时接收来自不同客户机数据,所以同个循环里#11、#12、#13可能来自不同客户机。
正常次序: #11 KRB_AP_REQ message -> #12 SAFE message(携带用户数据) -> #13 SAFE message(携带用户数据)
假如#11 krb5_rd_req(..., & auth_context,...)成功,#12、#13在该auth_context也krb5_rd_safe成功,说明来自同一客户、同一会话,可信。
因为udp不保证报文的次序,即使同个循环里同一客户也可能KRB_AP_REQ message和SAFE message次序打乱,本文不讨论报文次序问题。
我没深入研究krb5 API,猜想一个KRB_AP_REQ对应一个auth_context,应该无法多个KRB_AP_REQ使用同个auth_context。
因此来自不同客户,可能要同时维护不同客户各自auth_context,可建立一个数组,不同客户auth_context记录到数组节点。
数组: --------------------------- | 0 | 1 | 2 | 3 | 4 | ... | --------------------------- |||... ||v |v... |... | | | v 节点 ----------------- |客户地址+端口号| ----------------- |客户登录时间| ----------------- |auth_context| -----------------

图B
每次recvfrom()可得到客户的地址+端口号,并到数组查找[客户地址+端口号]匹配节点,存在就krb5_rd_safe(节点),不存在就krb5_rd_req产生auth_context新加到空节点。
因为数组是有限的,如果某节点在线时间太长(udp无连接的,应该无什么在线),将其踢出(注意要释放节点auth_context); 如果数组满并且无在线过期节点,只能抛弃recvfrom到来的客户。
如果客户机是多网卡多地址,客户机每次发送可能因路由而每次客户地址不同,则图B方案节点匹配客户地址就不合适。
所以图B方案记录客户地址仅支持单网卡单地址客户机。
一个不用数组的笨拙方案:
客户只KRB_AP_REQ,不SAFE message。因KRB_AP_REQ无法含用户数据,但用户程序也可以自己处理,客户自己将KRB_AP_REQ和用户数据打包一起发送,服务自己解包KRB_AP_REQ和用户数据。
这样客户的每次发送数据,都是KRB_AP_REQ+用户数据,不发SAFE message(携带用户数据),也能达到认证效果。但很笨重,KRB_AP_REQ(约500字节)远比SAFE message(不携带用户数据时约100字节)长度大,并且频繁生成/释放auth_context也不好。
1.2)tcp
sock = socket(PF_INET, SOCK_STREAM, 0); //流bind(sock,...); listen(sock, 5); for(; ; )acc = accept(sock,...); recv(acc, ... ,len,0); // #21 recv(acc, ... ,len,0); // #22 recv(acc, ... ,len,0); // #23close(acc);

tcp是面向acc这个连接,所以在同一循环里#21、#22、#23都来自相同客户。
当前客户在#21不管是正常的KRB_AP_REQ message还是无效的数据,只要krb5_rd_req()成功就执行下一步#22、#23,失败就close(acc)继续循环响应下个客户。
#22、#23是普通用户数据,因为连接可信,无需SAFE message。
1.3)循环服务器I/O阻塞
无数据到来,recvfrom、recv都是阻塞。
对udp来说,一个客户没到来,总有其它客户到来,所以recvfrom总有机会等到数据,除非没任何客户(那要求不阻塞也没什么意义)。
对tcp来说,如果客户机A accept了,但没发数据,则服务器一直阻塞,别的客户机B也一直得不到accept,所以服务器解决tcp阻塞往往采用多路复用、多进程、多线程。
2)从客户的角度理解会话
会话这个词太泛了,就缩小涵义到登录会话,从用户登录到用户注销的过程。又有本地登录、远程登录之分,再缩小到远程登录会话。同一用户可同时多个登录,一般是认为是不同会话。
2.1)一条连接每会话
会话涵义缩小到最小、最基本的一条tcp连接,天然地维持会话(可信、可靠,不考虑tcp会话劫持极端情况)。客户端则是小到一个进程一个端口号一条tcp永久连接。
例如ssh,又如上1.2)小节。
2.2)一个端口每会话
会话涵义以端口为标识,参照1.1)小节图B,一个进程一个udp端口号,以auth_context维持会话。
进程端口号虽然是随机,但也不排除刚启动进程[客户地址+端口号]恰好出现在图B中(也说明该端口号老进程已消亡),那只能关闭杀死进程重新启动以产生新随机端口号,而不是仅仅该进程重新发KRB_AP_REQ(因为端口号是首次发送分配,后续发送以此端口号)。
同客户机在运行的不同进程是不会出现相同端口号。
2.3)多条连接每会话
会话涵义稍微扩大,如http是基于tcp的请求/响应的短连接,连接即建即拆; 所以浏览器网站的登录往往使用存放本地cookie(类似krb5票据)维持会话。
2.4)多条命令每会话
会话涵义再次扩大,客户机执行不是单一进程单一命令,而是一组命令多个不同进程维持同一个会话。当然对tcp来说也是多连接,对于udp仍是无连接。
如本实验客户分为多个进程命令,kinit、krbcln1、krbcln2,登录kinit就是一个会话开始,只要票据未被销毁,auth_context未被释放都是kinit启动的同一个会话。
因此1.1)小节图B记录端口号对于多命令就不合适,因为客户进程的首次发送端口号通常是随机的,运行不同命令其分配到的端口号可能不同。
对于tcp,1.2)小节在#21若krb5_rd_req(..., & auth_context,...)成功,后续多个命令多个tcp连接可使用同个auth_context(需用safe messge验证)维持同一个会话。
3)现实客/服大多需双向互相发送/接收数据,krb5也支持客/服双向认证、双向地址验证
( 附:简易分布式容器平台 cocont-ver0.0.2.zip 源代码 下载地址 https://cowtransfer.com/s/72ad585f10e841提取码: y43f7y )

    推荐阅读