君不见长松卧壑困风霜,时来屹立扶明堂。这篇文章主要讲述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.
所以本实验在#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 )
推荐阅读
- You can remove cached packages by executing ‘yum clean packages‘. Error: GPG check FAILED
- E: Unable to correct problems, you have held broken packages.
- android studio打包weexplus项目常见错误
- 如何使用SAP云平台的Notification服务给Android应用推送通知消息
- 使用SAP云平台Android SDK创建Mobile应用
- Android中Handler与Message的简易使用
- 手机控车OBD-移动管家手机控车方案基于Android手机智能控制汽车系统的研究与实现;
- 21-pandas_apply和transform
- Windows10 家庭版(1903/1909)中用RDPWrapper-v1.6.2和autoupdate补丁开启远程桌面功能