上篇<krb5应用服务(UDP)>使用strace跟踪API,并讲到krb5_mk_req是封装了几个底层API,方便用户的使用,但无助于理解krb5运作机制 本文编写的客/服例子,仍是面向无连接UDP,客户使用底层API,目的在于跟踪票据和消息收发

票据分内存票据和文件票据,本文使用文件系统上的文件票据以方便跟踪观察 客户和应用服务器的交互比较简单,不表 客户和KDC服务器交互较为复杂,本文仅仅简单观察KDC的日志变化 本文没使用高级的跟踪手段(如进程跟踪的strace、ltrace,网络抓包的tcpdump、nmap协议分析等等)

总之,两个观察点: 文件票据(客户机上) KDC日志(KDC服务器上)

一.准备工作 1.

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

linlin@vmcln:~$ id -u
1000
linlin@vmcln:~$

当前登录用户的uid是1000

linlin@vmcln:~$ klist
klist: No ticket file: /tmp/krb5cc_1000
linlin@vmcln:~$

当前登录用户的文件票据是/tmp/krb5cc_1000

不同登录用户的默认票据名称因uid不同而不同

为测试方便,以uid为1000(linlin)的用户登录,客户程序写死/tmp/krb5cc_1000

二.应用服务器 1.源代码

//源文件名:krbsrv.c
#include <krb5.h>
#include <stdio.h>
#include <netdb.h>

main()
{
  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.",  // 主机全名需含最后终结句号
                                    "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;
    unsigned char pktbuf[BUFSIZ];

//--v--
// #1
//--^--
    
    len = sizeof(struct sockaddr_in);

    //--v-- #2
    if ((i = recvfrom(sock, (char *)pktbuf, sizeof(pktbuf),0,NULL, &len)) < 0)
    {
      exit(1);
    }
    //--^--

    packet.length = i;
    packet.data = (krb5_pointer) pktbuf;

    if ((retval = krb5_rd_req(context, &auth_context, &packet,server,NULL,NULL,NULL)))  // Check authentication info
    { 
      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");

//--v--
// #3
//--^--

    krb5_auth_con_free(context, auth_context);     // 释放auth_context资源
    auth_context = NULL;                           // 必须赋NULL,否则下个循环krb5_rd_req有判断auth_context不为NULL就不创建新的auth_context,因释放而此时空资源导致段错误
  };

  krb5_free_principal(context, server);
  krb5_free_context(context);
  exit(0);
}

2.#3处省略 GET KRB_MK_SAFE MESSAGE,不是必须

3.编译

linlin@debian:~$ gcc -o krbsrv krbsrv.c -lkrb5

4.运行应用服务程序

linlin@vmsrv:~$ ./krbsrv
recv KRB_AP_REQ message OK      <= 客户发出的正常KRB_AP_REQ被应用服务器验证成功

5.在#2处是GET KRB_AP_REQ MESSAGE,在其之前的#1处加sleep()代码,测试客户发出KRB_AP_REQ立即到达服务器时,应用服务延迟一段时间才recvfrom发生的情况 1)延迟略大5分钟 sleep(320);

linlin@vmsrv:~$ ./krbsrv
Err while reading KRB_AP_REQ request:  Clock skew too great     <= 时间偏差太大,验证失败
linlin@vmsrv:~$

2)延迟略小5分钟 sleep(280);

linlin@vmsrv:~$ ./krbsrv
recv KRB_AP_REQ message OK      <= 验证成功

3)说明时间偏差默认应该是5分钟

三.客户机 1.源代码

//源文件名:krbcln.c
#include <krb5.h>
#include <stdio.h>
#include <netdb.h>
#include <string.h>

main()
{
  krb5_context context;
  krb5_error_code retval;
  krb5_ccache ccdef;
  krb5_principal kprincpw = NULL;

  system("rm /tmp/krb5cc_1000");  // 为实验先删已存在票据

  retval = krb5_init_context(&context);
  if (retval)
  {
    exit(1);
  }

  //--v-- #4
  retval = krb5_parse_name(context, "krblinlin", &kprincpw);  // 可以不带@CTP.NET("krblinlin@CTP.NET"),但/etc/krb5.conf必须配置default_realm = CTP.NET
  if (retval)
  {
    exit(1);
  }
  printf("-------------------\nOK: krb5_init_context、krb5_parse_name\n");
  system("klist");   // 观察票据变化
  printf("\nPress ENTER key to Continue\n");
  getchar();         // 等待回车,先到KDC查看日志,后回车

  const char *password="linlin";  // 口令
  krb5_creds my_creds;

  retval = krb5_get_init_creds_password(context, &my_creds,kprincpw, (char *)password,NULL,NULL,0,NULL,NULL);
  if (retval)
  {
    exit(1);
  }
  printf("-------------------\nOK: krb5_get_init_creds_password\n");
  system("klist");
  printf("\nPress ENTER key to Continue\n");
  getchar();
  //--^-- 输入用户名、密码

  //--v-- #5
  retval = krb5_cc_resolve(context, "FILE:/tmp/krb5cc_1000", &ccdef);  // #6
  if (retval)
  {
    exit(1);
  }
  printf("-------------------\nOK: krb5_cc_resolve\n");    
  system("klist");
  printf("\nPress ENTER key to Continue\n");
  getchar();

  retval = krb5_cc_initialize(context, ccdef, kprincpw);   // initialize credentials cache
  if (retval)
  {
    exit(1);
  }  
  printf("-------------------\nOK: krb5_cc_initialize\n");
  system("klist");
  printf("\nPress ENTER key to Continue\n");
  getchar();
   
  retval = krb5_cc_store_cred(context, ccdef, &my_creds);  // store credentials in credentials cache
  if (retval)
  {
    exit(1);
  }
  printf("-------------------\nOK: krb5_cc_store_cred\n");
  system("klist");
  printf("\nPress ENTER key to Continue\n");
  getchar();
  //--^-- 产生krbtgt票据

  //retval = krb5_cc_default(context, &ccdef);  // #7

  //--v-- #8
  krb5_auth_context auth_context=NULL;
  krb5_data packet;
  krb5_creds * out_creds_ptr = NULL;
  krb5_principal server;

  retval = krb5_sname_to_principal(context, "vmsrv.ctp.net.", "mysv",KRB5_NT_SRV_HST, &server);  // 拼接应用服务主体名
  if (retval)
  {
    exit(1);
  }

  //memset(&my_creds, 0, sizeof(my_creds)); // #9
  retval = krb5_copy_principal(context, server, &my_creds.server);  // 必须
  if (retval)
  {
    exit(1);
  }
  printf("-------------------\nOK: krb5_sname_to_principal、krb5_copy_principal\n");
  system("klist");
  printf("\nPress ENTER key to Continue\n");
  getchar();
      
  //krb5_cc_get_principal(context, ccdef, &my_creds.client); // #10

  retval = krb5_get_credentials(context, 0, ccdef, &my_creds, &out_creds_ptr);  // 必须
  if (retval)
  { printf("Err: krb5_get_credentials -> %s\n", krb5_get_error_message(context, retval));
    exit(1);
  }
  printf("-------------------\nOK: krb5_get_credentials\n");
  system("klist");
  printf("\nPress ENTER key to Continue\n");
  getchar();
  //--^-- 产生应用服务票据

  if  ((retval = krb5_mk_req_extended(context,&auth_context,0,NULL,out_creds_ptr,&packet)  ))
  {
    exit(1);
  }
  printf("-------------------\nOK: krb5_mk_req_extended\n");

  //--v--
  struct sockaddr_in their_addr;
  their_addr.sin_family = AF_INET;
  their_addr.sin_port = htons(12345);
  struct hostent *he;
  if ((he=gethostbyname("vmsrv.ctp.net")) == NULL) //<netdb.h>
  {
    exit(1);
  }
  their_addr.sin_addr = *((struct in_addr *)he->h_addr);
  bzero(&(their_addr.sin_zero), 8);

  int sockfd;
  if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) 
  {
    exit(1);
  }

  if (sendto(sockfd,(char *)packet.data,(unsigned) packet.length,0,(struct sockaddr *)&their_addr,sizeof(struct sockaddr))<0)
  {
    exit(1);
  }
  printf("OK: sendto KRB_AP_REQ\n");
  //--^--

  krb5_free_data_contents(context, &packet);

//--v-- 
// #13
//--^--

  krb5_auth_con_free(context, auth_context);
  krb5_free_context(context);
  exit(0);
}

2.解析 1)代码段#4及#5相当于kinit

2)代码段#5已产生了ccdef,所以#7处krb5_cc_default可注释掉 krb5_cc_resolve和krb5_cc_default都是产生ccdef句柄,而krb5_cc_default缺省是文件票据(/tmp/krb5cc_1000)或由环境变量决定 如果此时去掉#7处注释,会重新产生ccdef,则有可能和原先ccdef指向的票据位置不同造成麻烦(假如#5使用内存票据是用户主体1,#7缺省文件票据用户主体2,用户会想当然以为是前者)

3)代码段#8参考了krb5_mk_req的实现代码,然后注释掉#9处memset及#10处krb5_cc_get_principal 3.1)krb5_mk_req 包含了memset及krb5_cc_get_principal

memset是为了初始化为某个值,因为C语言不保证变量初始为0 不初始化或许没问题,但一旦出问题很难排查原因 因此最好显式初始化

3.2)在#4代码段里krb5_get_init_creds_password已产生填充了my_creds.client,所以可注释掉#9及#10 如果想memset初始化 要么直接去掉#9处注释(则将原来已存在的my_creds清0了),同时必须去掉#10处注释(以便krb5_cc_get_principal填充my_creds.client,否则krb5_get_credentials段错误)

4)#13处省略krb5_mk_safe、发送用户数据

3.编译

linlin@debian:~$ gcc -o krbcln krbcln.c -lkrb5

4.运行跟踪 1)文件票据

linlin@vmcln:~$ ./krbcln
-------------------
OK: krb5_init_context、krb5_parse_name
klist: No ticket file: /tmp/krb5cc_1000

Press ENTER key to Continue
-------------------
OK: krb5_get_init_creds_password    <= 产生KDC日志AS-REQ
klist: No ticket file: /tmp/krb5cc_1000

Press ENTER key to Continue
-------------------
OK: krb5_cc_resolve
klist: No ticket file: /tmp/krb5cc_1000

Press ENTER key to Continue
-------------------
OK: krb5_cc_initialize     <= 生成空白票据文件
Credentials cache: FILE:/tmp/krb5cc_1000
        Principal: krblinlin@CTP.NET
  Issued    Expires    Principal

Press ENTER key to Continue
-------------------
OK: krb5_cc_store_cred     <= 产生了krbtgt
Credentials cache: FILE:/tmp/krb5cc_1000
        Principal: krblinlin@CTP.NET
  Issued                Expires               Principal
Dec 21 04:00:49 2023  Dec 22 04:00:49 2023  krbtgt/CTP.NET@CTP.NET

Press ENTER key to Continue
-------------------
OK: krb5_sname_to_principal、krb5_copy_principal     <= 票据没变化
Credentials cache: FILE:/tmp/krb5cc_1000
        Principal: krblinlin@CTP.NET
  Issued                Expires               Principal
Dec 21 04:00:49 2023  Dec 22 04:00:49 2023  krbtgt/CTP.NET@CTP.NET

Press ENTER key to Continue
-------------------
OK: krb5_get_credentials     <= 产生KDC日志TGS-REQ,产生了service ticket
Credentials cache: FILE:/tmp/krb5cc_1000
        Principal: krblinlin@CTP.NET
  Issued                Expires               Principal
Dec 21 04:00:49 2023  Dec 22 04:00:49 2023  krbtgt/CTP.NET@CTP.NET
Dec 21 04:11:53 2023  Dec 22 04:00:49 2023  mysv/vmsrv.ctp.net@
Dec 21 04:11:53 2023  Dec 22 04:00:49 2023  mysv/vmsrv.ctp.net@CTP.NET

Press ENTER key to Continue
-------------------
OK: krb5_mk_req_extended
OK: sendto KRB_AP_REQ
linlin@vmcln:~$

到vmsrv查看krbsrv结果为"recv KRB_AP_REQ message OK",应用服务验证成功

客户如果向多个应用服务krb5_get_credentials,应该就逐个新增应用服务到Credentials cache

2)KDC日志 2.1)krb5_init_context、krb5_parse_name无日志

2.2)krb5_get_init_creds_password产生日志

2023-12-21T04:00:49 AS-REQ krblinlin@CTP.NET from IPv4:192.168.1.40 for krbtgt/CTP.NET@CTP.NET      <= 地址192.168.1.40是客户机
2023-12-21T04:00:49 Client sent patypes: 150, REQ-ENC-PA-REP
2023-12-21T04:00:49 Looking for PK-INIT(ietf) pa-data -- krblinlin@CTP.NET
2023-12-21T04:00:49 Looking for PK-INIT(win2k) pa-data -- krblinlin@CTP.NET
2023-12-21T04:00:49 Looking for ENC-TS pa-data -- krblinlin@CTP.NET
2023-12-21T04:00:49 Need to use PA-ENC-TIMESTAMP/PA-PK-AS-REQ
2023-12-21T04:00:49 sending 293 bytes to IPv4:192.168.1.40
2023-12-21T04:00:49 AS-REQ krblinlin@CTP.NET from IPv4:192.168.1.40 for krbtgt/CTP.NET@CTP.NET
2023-12-21T04:00:49 Client sent patypes: ENC-TS, 150, REQ-ENC-PA-REP
2023-12-21T04:00:49 Looking for PK-INIT(ietf) pa-data -- krblinlin@CTP.NET
2023-12-21T04:00:49 Looking for PK-INIT(win2k) pa-data -- krblinlin@CTP.NET
2023-12-21T04:00:49 Looking for ENC-TS pa-data -- krblinlin@CTP.NET
2023-12-21T04:00:49 ENC-TS Pre-authentication succeeded -- krblinlin@CTP.NET using aes256-cts-hmac-sha1-96
2023-12-21T04:00:49 ENC-TS pre-authentication succeeded -- krblinlin@CTP.NET
2023-12-21T04:00:49 AS-REQ authtime: 2023-12-21T04:00:49 starttime: unset endtime: 2023-12-22T04:00:49 renew till: unset
2023-12-21T04:00:49 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, 25, 26, using aes256-cts-hmac-sha1-96/aes256-cts-hmac-sha1-96
2023-12-21T04:00:49 Requested flags: renewable-ok
2023-12-21T04:00:49 sending 681 bytes to IPv4:192.168.1.40

2.3)krb5_cc_resolve、krb5_cc_initialize、krb5_cc_store_cred、krb5_sname_to_principal、krb5_copy_principal无日志

2.4)krb5_get_credentials产生日志

2023-12-21T04:11:53 Got TGS FAST request
2023-12-21T04:11:53 TGS-REQ krblinlin@CTP.NET from IPv4:192.168.1.40 for mysv/vmsrv.ctp.net@CTP.NET [canonicalize]
2023-12-21T04:11:53 TGS-REQ authtime: 2023-12-21T04:00:49 starttime: 2023-12-21T04:11:53 endtime: 2023-12-22T04:00:49 renew till: unset
2023-12-21T04:11:53 sending 643 bytes to IPv4:192.168.1.40

2.5)krb5_mk_req_extended无日志

5.API调用关系 参考MIT krb5源码 ../src/lib/krb5/krb/mk_req.c ../src/lib/krb5/krb/get_creds.c

krb5_mk_req
    |-- krb5_sname_to_principal
 |  |-- krb5_copy_principal
 |  |-- krb5_cc_get_principal
 v  |-- krb5_get_credentials
    |      |-- try_get_creds (非API)
 执 |      |       |-- krb5_tkt_creds_init
 行 |      |       |-- krb5_tkt_creds_get (同步)
 顺 |      |       |         |-- krb5_tkt_creds_step (异步)
 序 |      |       |         |-- krb5_sendto_kdc (非API)
    |      |       |-- krb5_tkt_creds_get_creds
    |-- krb5_mk_req_extended
    |

6.客户程序主要数据结构

  krb5_context context;            // a krb5 library context
  krb5_ccache ccdef;               // Credential cache handle
  krb5_auth_context auth_context;  // Pre-existing or newly created auth context
  krb5_creds my_creds;             // credentials
  krb5_creds * out_creds_ptr;      // Credentials for the service with valid ticket and key
  krb5_data packet;                // AP-REQ message

auth_context用于用户数据krb5_mk_safe,本文没用到

7.改写客户程序 1)客户输入用户密码,但省略保存票据 客户krbcln.c代码段#5改为仅一行:

//--v-- #5
  retval = krb5_cc_resolve(context, "FILE:/tmp/krb5cc_1000", &ccdef);   // 没此行会导致在krb5_get_credentials因ccdef为空发生段错误
//--^--
linlin@vmcln:~$ ./krbcln-nosave
rm: 无法删除'/tmp/krb5cc_1000': 没有那个文件或目录
Err: krb5_get_credentials -> No credentials cache found (filename: /tmp/krb5cc_1000)
linlin@vmcln:~$

客户失败

2)客户不处理kinit 客户krbcln.c去掉#4及#5代码段,代而命令kinit

//源文件名:krbcln.c
#include <krb5.h>
#include <stdio.h>
#include <netdb.h>
#include <string.h>
main()
{
  krb5_context context;
  krb5_error_code retval;
  krb5_ccache ccdef;
  krb5_principal kprincpw = NULL;
  krb5_data packet;
  krb5_auth_context auth_context=NULL;   
 
  krb5_init_context(&context);
  krb5_cc_default(context, &ccdef);

  //--v-- 下面是krb5_mk_req(context,&auth_context,0,"mysv","vmsrv.ctp.net.",NULL,ccdef,&packet)的功能分解
  krb5_creds my_creds;
  krb5_creds * out_creds_ptr = NULL;
  krb5_principal server;

  krb5_sname_to_principal(context, "vmsrv.ctp.net.", "mysv",KRB5_NT_SRV_HST, &server);

  krb5_copy_principal(context, server, &my_creds.server);  /* --\                           原用'// --\'注释导致段错误查原因好久,原来'\'是续行,导致下句给注释掉      */
  krb5_cc_get_principal(context, ccdef, &my_creds.client); /* --/ 必须在krb5_get_credentials之前填充my_creds.server、my_creds.client,否则krb5_get_credentials段错误  */

  retval = krb5_get_credentials(context, 0, ccdef, &my_creds, &out_creds_ptr);  // #11
  if (retval)
  {
    exit(1);
  }
  printf("-------------------\nOK: krb5_get_credentials\n");
  system("klist");
  printf("\nPress ENTER key to Continue\n");
  getchar();

  if  ((retval = krb5_mk_req_extended(context,&auth_context,0,NULL,out_creds_ptr,&packet)  ))
  {
    exit(1);
  }
  printf("-------------------\nOK: krb5_mk_req_extended\n");
  //--^--

  //--v--
  struct sockaddr_in their_addr;
  their_addr.sin_family = AF_INET;
  their_addr.sin_port = htons(12345);
  struct hostent *he;
  he=gethostbyname("vmsrv.ctp.net");
  their_addr.sin_addr = *((struct in_addr *)he->h_addr);
  bzero(&(their_addr.sin_zero), 8);

  int sockfd;
  sockfd = socket(AF_INET, SOCK_DGRAM, 0);

  if (sendto(sockfd,(char *)packet.data,(unsigned) packet.length,0,(struct sockaddr *)&their_addr,sizeof(struct sockaddr))<0)
  {
    exit(1);
  }
  printf("OK: sendto KRB_AP_REQ\n");
  //--^--

  krb5_free_data_contents(context, &packet);
  krb5_auth_con_free(context, auth_context);
  krb5_free_context(context);
  exit(0);
}

2.1)已存在krbtgt、应用服务票据

linlin@vmcln:~$ klist
Credentials cache: FILE:/tmp/krb5cc_1000
        Principal: krblinlin@CTP.NET
  Issued                Expires               Principal
Dec 21 07:48:14 2023  Dec 22 07:48:14 2023  krbtgt/CTP.NET@CTP.NET
Dec 21 07:48:15 2023  Dec 22 07:48:14 2023  mysv/vmsrv.ctp.net@
Dec 21 07:48:15 2023  Dec 22 07:48:14 2023  mysv/vmsrv.ctp.net@CTP.NET
linlin@vmcln:~$

linlin@vmcln:~$ ./krbcln-noinit
-------------------
OK: krb5_get_credentials    <= 无KDC日志,票据没变化
Credentials cache: FILE:/tmp/krb5cc_1000
        Principal: krblinlin@CTP.NET
  Issued                Expires               Principal
Dec 21 07:48:14 2023  Dec 22 07:48:14 2023  krbtgt/CTP.NET@CTP.NET
Dec 21 07:48:15 2023  Dec 22 07:48:14 2023  mysv/vmsrv.ctp.net@
Dec 21 07:48:15 2023  Dec 22 07:48:14 2023  mysv/vmsrv.ctp.net@CTP.NET

Press ENTER key to Continue
-------------------
OK: krb5_mk_req_extended
OK: sendto KRB_AP_REQ
linlin@vmcln:~$

到vmsrv查看krbsrv结果为"recv KRB_AP_REQ message OK",应用服务验证成功

2.2)kinit

linlin@vmcln:~$ kinit --no-forwardable krblinlin
krblinlin@CTP.NET's Password:
linlin@vmcln:~$ klist
Credentials cache: FILE:/tmp/krb5cc_1000
        Principal: krblinlin@CTP.NET
  Issued                Expires               Principal
Dec 24 07:12:54 2023  Jun 23 22:12:50 2024  krbtgt/CTP.NET@CTP.NET
linlin@vmcln:~$

可见kinit仅产生krbtgt

linlin@vmcln:~$ ./krbcln-noinit
-------------------
OK: krb5_get_credentials    <= 产生KDC日志TGS-REQ,产生了service ticket
Credentials cache: FILE:/tmp/krb5cc_1000
        Principal: krblinlin@CTP.NET
  Issued                Expires               Principal
Dec 24 07:12:54 2023  Jun 23 22:12:50 2024  krbtgt/CTP.NET@CTP.NET
Dec 24 07:15:34 2023  Jun 23 22:12:50 2024  mysv/vmsrv.ctp.net@
Dec 24 07:15:34 2023  Jun 23 22:12:50 2024  mysv/vmsrv.ctp.net@CTP.NET

Press ENTER key to Continue
-------------------
OK: krb5_mk_req_extended
OK: sendto KRB_AP_REQ
linlin@vmcln:~$

应用服务验证成功

3)下面代码代替#11处的krb5_get_credentials(context, 0, ccdef, &my_creds, &out_creds_ptr)

//--v-- 
  krb5_tkt_creds_context ctx;

  retval=krb5_tkt_creds_init(context, ccdef,&my_creds, 0,&ctx);
  if (retval)
  {
    exit(1);
  }                    
  printf("-------------------\nOK: krb5_tkt_creds_init\n");
  system("klist");
  printf("\nPress ENTER key to Continue\n");
  getchar();

  retval=krb5_tkt_creds_get(context, ctx);  // #12
  if (retval)
  {
    printf("Err: Failed to krb5_tkt_creds_get -> %s\n", krb5_get_error_message(context, retval));
    exit(1);
  }
  printf("-------------------\nOK: krb5_tkt_creds_get\n");
  system("klist");
  printf("\nPress ENTER key to Continue\n");
  getchar();

  krb5_creds new_creds;

  retval=krb5_tkt_creds_get_creds( context,  ctx,&new_creds);
  if (retval)
  {
    printf("Err: Failed to krb5_tkt_creds_get_creds -> %s\n", krb5_get_error_message(context, retval));
    exit(1);
  }
  printf("-------------------\nOK: krb5_tkt_creds_get_creds\n");
  system("klist");
  printf("\nPress ENTER key to Continue\n");
  getchar();                     
                         
  out_creds_ptr = &new_creds;    
//--^--                    

3.1)已存在krbtgt、应用服务票据

linlin@vmcln:~$ klist
Credentials cache: FILE:/tmp/krb5cc_1000
        Principal: krblinlin@CTP.NET
  Issued                Expires               Principal
Dec 25 04:12:54 2023  Jun 24 19:12:49 2024  krbtgt/CTP.NET@CTP.NET
Dec 25 07:06:03 2023  Jun 24 19:12:49 2024  mysv/vmsrv.ctp.net@
Dec 25 07:06:03 2023  Jun 24 19:12:49 2024  mysv/vmsrv.ctp.net@CTP.NET
linlin@vmcln:~$ ./krbcln-tkt
-------------------
OK: krb5_tkt_creds_init
Credentials cache: FILE:/tmp/krb5cc_1000
...(略,同上klist)

Press ENTER key to Continue
-------------------
OK: krb5_tkt_creds_get
Credentials cache: FILE:/tmp/krb5cc_1000
...(同上)

Press ENTER key to Continue
-------------------
OK: krb5_tkt_creds_get_creds
Credentials cache: FILE:/tmp/krb5cc_1000
...(同上)

Press ENTER key to Continue
-------------------
OK: krb5_mk_req_extended
OK: sendto KRB_AP_REQ
linlin@vmcln:~$

无KDC日志,票据没变化,应用服务验证成功

3.2)kinit

linlin@vmcln:~$ kinit --no-forwardable krblinlin
linlin@vmcln:~$ klist
Credentials cache: FILE:/tmp/krb5cc_1000
        Principal: krblinlin@CTP.NET
  Issued                Expires               Principal
Dec 25 07:12:54 2023  Jun 24 22:12:50 2024  krbtgt/CTP.NET@CTP.NET
linlin@vmcln:~$ ./krbcln-tkt
-------------------
OK: krb5_tkt_creds_init         <= 无KDC日志
Credentials cache: FILE:/tmp/krb5cc_1000
...(略,同上klist)

Press ENTER key to Continue
-------------------
OK: krb5_tkt_creds_get          <= 产生KDC日志TGS-REQ,产生了service ticket
Credentials cache: FILE:/tmp/krb5cc_1000
        Principal: krblinlin@CTP.NET
  Issued                Expires               Principal
Dec 25 07:12:54 2023  Jun 24 22:12:50 2024  krbtgt/CTP.NET@CTP.NET
Dec 25 07:14:44 2023  Jun 24 22:12:50 2024  mysv/vmsrv.ctp.net@
Dec 25 07:14:44 2023  Jun 24 22:12:50 2024  mysv/vmsrv.ctp.net@CTP.NET

Press ENTER key to Continue
-------------------
OK: krb5_tkt_creds_get_creds    <= 无KDC日志
Credentials cache: FILE:/tmp/krb5cc_1000
...(同上krb5_tkt_creds_get)

Press ENTER key to Continue
-------------------
OK: krb5_mk_req_extended
OK: sendto KRB_AP_REQ
linlin@vmcln:~$

应用服务验证成功

KDC日志

2023-12-25T07:14:44 Got TGS FAST request
2023-12-25T07:14:44 TGS-REQ krblinlin@CTP.NET from IPv4:192.168.1.40 for mysv/vmsrv.ctp.net@CTP.NET [canonicalize]
2023-12-25T07:14:44 TGS-REQ authtime: 2023-12-25T07:12:54 starttime: 2023-12-25T07:14:44 endtime: 2024-06-24T22:12:50 renew till: unset
2023-12-25T07:14:44 sending 643 bytes to IPv4:192.168.1.40

3.3)小结 krb5_tkt_creds_get发出TGS-REQ并产生了service ticket

4)注释掉#12处的krb5_tkt_creds_get

linlin@vmcln:~$ klist
Credentials cache: FILE:/tmp/krb5cc_1000
        Principal: krblinlin@CTP.NET
  Issued                Expires               Principal
Dec 25 02:06:21 2023  Dec 26 02:06:21 2023  krbtgt/CTP.NET@CTP.NET
Dec 25 02:06:22 2023  Dec 26 02:06:21 2023  mysv/vmsrv.ctp.net@
Dec 25 02:06:22 2023  Dec 26 02:06:21 2023  mysv/vmsrv.ctp.net@CTP.NET
linlin@vmcln:~$ ./krbcln-noget
Err: Failed to krb5_tkt_creds_get_creds -> Request did not supply a ticket
linlin@vmcln:~$

即使已存在krbtgt、应用服务票据,客户也失败

查看../src/lib/krb5/krb/get_creds.c krb5_tkt_creds_init设置ctx->state为STATE_BEGIN krb5_tkt_creds_get_creds对ctx->state不是STATE_COMPLETE返回KRB5_NO_TKT_SUPPLIED(即"Request did not supply a ticket")

krb5_get_credentials
      |-- krb5_tkt_creds_init([out] ctx)
      |-- krb5_tkt_creds_get([in] ctx)
      |         |-- krb5_tkt_creds_step([in] ctx)
      |-- krb5_tkt_creds_get_creds([in] ctx)

krb5_tkt_creds_get和krb5_tkt_creds_get_creds的ctx都是输入型参数啊,难道krb5_tkt_creds_get会重新写ctx->state ? 所以[in]型的不一定只读,也可能可写?

5)下面代码代替#12处的krb5_tkt_creds_get(context, ctx)

//--v--
  krb5_data request,reply,realm;
  unsigned int flags=0;
  retval=krb5_tkt_creds_step(context,ctx,&reply,&request,&realm,&flags);
  if (retval)
  {
    printf("Err: krb5_tkt_creds_step -> %s\n", krb5_get_error_message(context, retval));
    exit(1);
  }
//--^--

5.1)已存在krbtgt、应用服务票据

linlin@vmcln:~$ klist
Credentials cache: FILE:/tmp/krb5cc_1000
        Principal: krblinlin@CTP.NET
  Issued                Expires               Principal
Dec 25 07:12:54 2023  Jun 24 22:12:50 2024  krbtgt/CTP.NET@CTP.NET
Dec 25 07:14:44 2023  Jun 24 22:12:50 2024  mysv/vmsrv.ctp.net@
Dec 25 07:14:44 2023  Jun 24 22:12:50 2024  mysv/vmsrv.ctp.net@CTP.NET
linlin@vmcln:~$
linlin@vmcln:~$ ./krbcln-step
Credentials cache: FILE:/tmp/krb5cc_1000
...(略,同上klist)
-------------------
OK: krb5_tkt_creds_get_creds
Credentials cache: FILE:/tmp/krb5cc_1000
...(同上)
-------------------
OK: krb5_mk_req_extended
OK: sendto KRB_AP_REQ
linlin@vmcln:~$

无KDC日志,应用服务验证成功


krb5_tkt_creds_step([in] ctx)
      |-- check_cache(非API)设置ctx->state为STATE_COMPLETE

数据结构ctx看是输入型参数,但API里边有操作改变了ctx

5.2)kinit

linlin@vmcln:~$ kinit --no-forwardable krblinlin
linlin@vmcln:~$ klist
Credentials cache: FILE:/tmp/krb5cc_1000
        Principal: krblinlin@CTP.NET
  Issued                Expires               Principal
Dec 25 07:58:34 2023  Jun 24 22:58:26 2024  krbtgt/CTP.NET@CTP.NET
linlin@vmcln:~$ ./krbcln-step
Credentials cache: FILE:/tmp/krb5cc_1000
...(略,同上klist)
Err: krb5_tkt_creds_get_creds -> Request did not supply a ticket
linlin@vmcln:~$

无KDC日志,客户失败,krb5_tkt_creds_get_creds没能产生out_creds_ptr

5.3)小结 krb5_tkt_creds_step仅构造TGS-REQ,不发送请求 krb5_tkt_creds_get_creds完成krb5_mk_req_extended仅需的out_creds_ptr

      API                ctx->state
---------------------------------------
krb5_tkt_creds_init    STATE_BEGIN
krb5_tkt_creds_step    STATE_COMPLETE

四.总结 1.

         客户机                                   KDC
--------------------------------                ---------
|                              | 1. KRB_AS_REQ  |       |
|                             /|--------------->|       |
|krb5_get_init_creds_password  |                |AS     |
|                             \| 2. KRB_AS_REP  |       |
|                              |<---------------|       |
|                              |                |       |
|krb5_cc_initialize            |                |       |
|(空白ticket)                  |                |       |
|                              |                |       |
|krb5_cc_store_cred            |                |       |
|(krbtgt)                      |                |       |
|                              |                | ----- |
|                              |                |       |
|                              | 3. KRB_TGS_REQ |       |
|                             /|--------------->|       |
|krb5_get_credentials          |                |       |
|      |-- krb5_tkt_creds_get  |                |TGS    |
|(Service Ticket)              |                |       |
|                             \| 4. KRB_TGS_REP |       |
|                              |<---------------|       |
|                              |                ---------
|                              |
|krb5_mk_req_extended          |
|                              |
|                              |                    应用服务器
|                              |                ------------------
|                              | 5. KRB_AP_REQ  |                |
|                              |--------------->|                |
|                              |                |                |
|                              |                |/etc/krb5.keytab|
|                              | 6. KRB_AP_REP  |                |
|                              |<---------------|                |
--------------------------------                ------------------

图A

krb5_get_init_creds_password含AS_REQ消息包及发送功能,并填充krb5_creds结构(my_creds) krb5_get_credentials、krb5_tkt_creds_get含TGS_REQ消息包及发送功能 krb5_tkt_creds_step仅构造TGS-REQ(This function constructs the next KDC request for a TGS exchange),不发送 krb5_mk_req_extended仅是构造AP_REQ消息包,不含发送功能 krb5_mk_req构造AP_REQ消息包(但不发送),中间可能会构造并发送TGS_REQ AP_REQ消息包的发送由用户自己使用套接字API发送函数

krb5_mk_safe需要由krb5_mk_req/krb5_mk_req_extended产生的auth_context,伴随而生的AP_REQ在某些场合不需发送

2.下面列出API的参数,仅列本文关心的数据结构部分

krb5_cc_resolve([out] &ccdef)/krb5_cc_default([out] &ccdef)

krb5_get_init_creds_password([out] &my_creds)
krb5_cc_initialize([in] ccdef)
krb5_cc_store_cred([in] ccdef,[in] &my_creds)

krb5_mk_req([inout] &auth_context,[in] ccdef,[out] &packet)
    |-- krb5_sname_to_principal
 |  |-- krb5_copy_principal([out] &my_creds.server)
 |  |-- krb5_cc_get_principal([in] ccdef,[out] &my_creds.client)
 v  |-- krb5_get_credentials([in] ccdef,[in] &my_creds,[out] &out_creds_ptr)
    |      |-- try_get_creds (非API)
 执 |      |       |-- krb5_tkt_creds_init([in] ccdef,[in] &my_creds,[out] &ctx)
 行 |      |       |-- krb5_tkt_creds_get([in] ctx)
 顺 |      |       |         |-- krb5_tkt_creds_step([in] ctx,[in] &reply,[out] &request)
 序 |      |       |         |-- krb5_sendto_kdc (非API)
    |      |       |-- krb5_tkt_creds_get_creds([in] ctx,[out] &new_creds)
    |      |       |-- out_creds_ptr = &new_creds
    |-- krb5_mk_req_extended([inout] &auth_context,[in] out_creds_ptr,[out] &packet)

1)ccdef以文件票据为例,其handle(句柄)实际指向文件名,不是文件描述符 刚开始,本人以为handle是文件描述符,经strace,每个传[in] ccdef 的API,都是openat("/tmp/krb5cc_1000")/close()成对,即每次都是重新打开票据文件,然后关闭 问题疑惑:假如两个API之间发生非本身客户程序的票据变化(如中间运行了kinit命令为别的用户主体),会什么情况?

strace到krb5_cc_initialize

...
unlink("/tmp/krb5cc_1000")              = -1 ENOENT (没有那个文件或目录)               <= 先删票据
openat(AT_FDCWD, "/tmp/krb5cc_1000", O_RDWR|O_CREAT|O_EXCL|O_CLOEXEC, 0600) = 3        <= 后建票据空白文件,描述符是3
...
write(3, "\5\4\0\f\0\1\0\10\0\0\0\0\0\0\0\0\0\0\0\1\0\0\0\1\0\0\0\7CTP."..., 48) = 48  <= 写文件
...
close(3)                                = 0                                            <= 关闭文件
...

其余API不含unlink

2)数据结构传递

krb5_cc_resolve/krb5_cc_default([out] &ccdef)
                                         |
                                         v                      
krb5_mk_req(                      [in] ccdef)
 |-- krb5_copy_principal(                    [out] &my_creds.server)
 |                                                         \
 |                                                          -------------
 |-- krb5_cc_get_principal(       [in] ccdef,[out] &my_creds.client)     |
 |                                                      |  --------------
 |                                                      |/
 |                                                      v
 |-- krb5_get_credentials(        [in] ccdef, [in] &my_creds,[out] &out_creds_ptr)
 |        |                                                                \
 |        |                                                                 ----------------------
 |        |-- krb5_tkt_creds_init([in] ccdef, [in] &my_creds,[out] &ctx)                          |
 |        |                                                          |                            | 
 |        |                                                          v                            |
 |        |-- krb5_tkt_creds_get(                              [in] ctx)                          |
 |        |         |-- krb5_tkt_creds_step(                   [in] ctx)                          |
 |        |-- krb5_tkt_creds_get_creds(                        [in] ctx,[out] &new_creds)         |
 |        |--                                                 out_creds_ptr = &new_creds          |
 |                                                                                                |
 |                                                                             -------------------
 |                                                                            / 
 |                                                                           v
 |-- krb5_mk_req_extended(                                      [in] out_creds_ptr)

my_creds和out_creds_ptr不知有何不同?但用户使用API时,krb5_mk_req两者都不需,krb5_mk_req_extended仅需out_creds_ptr

如果你不完全了解krb5的运作机制,请使用已完全封装好的krb5_mk_req,最为保险 如果稍懂krb5,可以使用krb5_mk_req_extended系列API 若你透彻掌握了krb5机理,完全可以使用底层的krb5_tkt_xxx系列API

五.参考 https://stuff.mit.edu/afs/sipb/user/mkgray/bar/mit/kerberos/krb5-latest/doc/appdev/refs/index.html

六.后记 1.代码段#5里的"FILE:/tmp/krb5cc_1000"改为"FILE:/tmp/krb5cc_linlin"(非缺省) 1)#7处保留注释

linlin@vmcln:~$ ./krbcln
...
OK: krb5_mk_req_extended
OK: sendto KRB_AP_REQ
linlin@vmcln:~$ klist
klist: No ticket file: /tmp/krb5cc_1000
linlin@vmcln:~$ klist -c "FILE:/tmp/krb5cc_linlin"
Credentials cache: FILE:/tmp/krb5cc_linlin
        Principal: krblinlin@CTP.NET
  Issued                Expires               Principal
Jan 15 03:17:14 2024  Jan 16 03:17:14 2024  krbtgt/CTP.NET@CTP.NET
Jan 15 03:17:30 2024  Jan 16 03:17:14 2024  mysv/vmsrv.ctp.net@
Jan 15 03:17:30 2024  Jan 16 03:17:14 2024  mysv/vmsrv.ctp.net@CTP.NET

linlin@vmcln:~$ klist -c "/tmp/krb5cc_linlin"
Credentials cache: FILE:/tmp/krb5cc_linlin
        Principal: krblinlin@CTP.NET
  Issued                Expires               Principal
...(略,同上)
linlin@vmcln:~$

应用服务验证成功

2)#7处去掉注释

linlin@vmcln:~$ ./krbcln
...
OK: krb5_sname_to_principal、krb5_copy_principal
klist: No ticket file: /tmp/krb5cc_1000
Press ENTER key to Continue
Err: krb5_get_credentials -> No credentials cache found (filename: /tmp/krb5cc_1000)
linlin@vmcln:~$

客户失败

linlin@vmcln:~$ klist -c "/tmp/krb5cc_linlin"
Credentials cache: FILE:/tmp/krb5cc_linlin
        Principal: krblinlin@CTP.NET
  Issued                Expires               Principal
Jan 15 03:27:47 2024  Jan 16 03:27:47 2024  krbtgt/CTP.NET@CTP.NET
linlin@vmcln:~$

可见krb5_cc_resolve的句柄ccdef被krb5_cc_default覆盖 除非取ccdef1、ccdef2不同变量名

3)直接krb5_cc_default代替krb5_cc_resolve

  //--v-- #5
  //retval = krb5_cc_resolve(context, "FILE:/tmp/krb5cc_1000", &ccdef);
  retval = krb5_cc_default(context, &ccdef);
...
  //--^-- 产生krbtgt票据

  //retval = krb5_cc_default(context, &ccdef);  // #7
linlin@vmcln:~$ ./krbcln
...
OK: krb5_get_credentials
Credentials cache: FILE:/tmp/krb5cc_1000
        Principal: krblinlin@CTP.NET
  Issued                Expires               Principal
Jan 15 05:50:53 2024  Jan 16 05:50:53 2024  krbtgt/CTP.NET@CTP.NET
Jan 15 05:51:05 2024  Jan 16 05:50:53 2024  mysv/vmsrv.ctp.net@
Jan 15 05:51:05 2024  Jan 16 05:50:53 2024  mysv/vmsrv.ctp.net@CTP.NET
Press ENTER key to Continue
-------------------
OK: krb5_mk_req_extended
OK: sendto KRB_AP_REQ
linlin@vmcln:~$ klist
Credentials cache: FILE:/tmp/krb5cc_1000
        Principal: krblinlin@CTP.NET
  Issued                Expires               Principal
...(同上)
linlin@vmcln:~$

应用服务验证成功

4)小结 krb5_cc_resolve和krb5_cc_default都是产生ccdef句柄,后续API是认ccdef这个指向票据的句柄 krb5_cc_resolve和krb5_cc_default一般不要同时使用 krb5_cc_resolve不需遵从krb5cc_1000命名规则 krb5_cc_default按缺省或环境变量

2.两个API之间发生kinit票据变化

linlin@vmcln:~$ ./krbcln
...
OK: krb5_sname_to_principal、krb5_copy_principal
Credentials cache: FILE:/tmp/krb5cc_1000
        Principal: krblinlin@CTP.NET
  Issued                Expires               Principal
Jan 15 06:26:05 2024  Jan 16 06:26:05 2024  krbtgt/CTP.NET@CTP.NET
Press ENTER key to Continue
====在等待回车期间kinit krblu
linlin@vmcln:~$ kinit --no-forwardable krblu
linlin@vmcln:~$ klist
Credentials cache: FILE:/tmp/krb5cc_1000
        Principal: krblu@CTP.NET
  Issued                Expires               Principal
Jan 15 06:27:05 2024  Jul 15 21:27:01 2024  krbtgt/CTP.NET@CTP.NET
linlin@vmcln:~$
====回车后
Err: krb5_get_credentials -> Matching credential not found (filename: /tmp/krb5cc_1000)
linlin@vmcln:~$

客户失败

如果期间仍是kinit krblinlin则应用服务能成功验证

3.krb5_tkt_creds_get实现 见../src/lib/krb5/krb/get_creds.c

    for (;;) {  //用死循环异步实现同步? 符合条件break
...
        code = krb5_tkt_creds_step(context, ctx, &reply, &request, &realm,
                                   &flags);
...
        code = krb5_sendto_kdc(context, &request, &realm,
                               &reply, &use_master, tcp_only);
...
    }

krb5_tkt_creds_step的帮助文档有说明reply是输入型和request是输出型 krb5_sendto_kdc非API,没见对reply、request说明,只能参见其在../src/lib/krb5/os/sendto_kdc.c中的实现代码,猜测reply是输出型,request是输入型


                             ----------------------------
                            |                            |
                            v                            |
krb5_tkt_creds_step([in] &reply,[out] &request)          |
                                         |               |
                                         v               |
krb5_sendto_kdc(               [in ?] &request,[out ?] &reply)

reply 应是TGS_REP request应是TGS_REQ

krb5_tkt_creds_step仅构造TGS_REQ,不负责发送 krb5_sendto_kdc 负责发送TGS_REQ,并且负责接收TGS_REP

因为两者前后相互传参,所以要用for循环

从上值得网络应用层协议客户API设计参考: REQ构造要单独一个API,和发送分离 发送REQ和接收REP要封装在同一个API(但不构造REQ)