Linux基础知识(17)- Kerberos (二) | krb5 API 的 C 程序示例
在 “Linux基础知识(16)- Kerberos (一) | Kerberos 安装配置” 里我们演示了 Kerberos 安装配置和 Kadmin 等命令行工具的用法,本文将演示 krb5 API 的使用方法。
Krb5 API: http://web.mit.edu/kerberos/krb5-current/doc/appldev/refs/api/index.html
1. 系统环境
操作系统:Ubuntu 20.04
GCC:9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.1)
本文 Kerberos 的客户端和服务端都安装在同一台主机上,主机名为 hadoop-master-vm,示例 C 程序也运行在 hadoop-master-vm 上。
2. 创建 Kerberos 主体
$ cd ~/krb5
$ sudo kadmin.local
Authenticating as principal root/admin@hadoop.com with password. # 创建 2 个无特权主体(unprivileged principal) kadmin.local: addprinc testcli Enter password for principal "testcli@hadoop.com": test1234 Re-enter password for principal "testcli@hadoop.com": test1234 Principal "testcli@hadoop.com" created. kadmin.local: addprinc testsrv/hadoop-master-vm Enter password for principal "testsrv/hadoop-master-vm@hadoop.com": Re-enter password for principal "testsrv/hadoop-master-vm@hadoop.com": Principal "testsrv/hadoop-master-vm@hadoop.com" created. # 使用 xst 命令会在当前目录下生成 keytab 文件,-norandkey 确保初始密码有效 kadmin.local: xst -k krb5_testcli.keytab -norandkey testcli@hadoop.com Entry for principal testcli@hadoop.com with kvno 1, encryption type aes256-cts-hmac-sha1-96 added to keytab WRFILE:krb5_testcli.keytab. Entry for principal testcli@hadoop.com with kvno 1, encryption type aes128-cts-hmac-sha1-96 added to keytab WRFILE:krb5_testcli.keytab. kadmin.local: xst -k krb5_testsrv.keytab -norandkey testsrv/hadoop-master-vm@hadoop.com Entry for principal testsrv/hadoop-master-vm@hadoop.com with kvno 1, encryption type aes256-cts-hmac-sha1-96 added to keytab WRFILE:krb5_testsrv.keytab. Entry for principal testsrv/hadoop-master-vm@hadoop.com with kvno 1, encryption type aes128-cts-hmac-sha1-96 added to keytab WRFILE:krb5_testsrv.keytab.
3. 单机认证
1) 不生成票据
本示例使用 "testcli@hadoop.com" 主体和它的密码,完成类似于一般验证系统的 Login 功能。
$ cd ~/krb5
$ vim krb5_login.c
#include <stdio.h> #include <krb5.h> int main(void) { krb5_context context = NULL; krb5_error_code krberr; krb5_principal client = NULL; krb5_creds *my_creds_ptr = NULL; krb5_creds my_creds; const char *errmsg; printf("krb5_init_context() ... \n"); krberr = krb5_init_context(&context); if (krberr) { errmsg = krb5_get_error_message(NULL, krberr); printf("krb5_init_context(): error -> %s\n", errmsg); goto cleanup; } // Get principal printf("krb5_parse_name() ...\n"); krberr = krb5_parse_name(context, "testcli@hadoop.com", &client); if (krberr) { errmsg = krb5_get_error_message(context, krberr); printf("krb5_parse_name(): error -> %s\n", errmsg); goto cleanup; } // Get initial credential const char *password="test1234"; printf("krb5_get_init_creds_password() ...\n"); krberr = krb5_get_init_creds_password(context, &my_creds, client, (char *)password, NULL, NULL, 0, NULL, NULL); if (krberr) { errmsg = krb5_get_error_message(context, krberr); printf("krb5_get_init_creds_password(): error -> %s\n", errmsg); goto cleanup; } my_creds_ptr = &my_creds; printf("krb5 get initial credential successfully\n"); cleanup: if (client) krb5_free_principal(context, client); if (my_creds_ptr) krb5_free_cred_contents(context, my_creds_ptr); if (context) krb5_free_context(context); return 0; }
$ gcc -o krb5_login krb5_login.c -lkrb5
$ ./krb5_login
krb5_init_context() ... krb5_parse_name() ... krb5_get_init_creds_password() ... krb5 get initial credential successfully
2) 存储票据
本示例使用 "testcli@hadoop.com" 主体和它的密码,完成类似于一般验证系统的 Login 功能,并生成 Ticket 保存到本地缓存目录。
$ cd ~/krb5
$ vim krb5_store_login.c
#include <stdio.h> #include <krb5.h> int main(void) { krb5_context context = NULL; krb5_error_code krberr; krb5_principal client = NULL; krb5_creds *my_creds_ptr = NULL; krb5_ccache ccache = NULL; krb5_creds my_creds; const char *errmsg; printf("krb5_init_context() ... \n"); krberr = krb5_init_context(&context); if (krberr) { errmsg = krb5_get_error_message(NULL, krberr); printf("krb5_init_context(): error -> %s\n", errmsg); goto cleanup; } // Get principal printf("krb5_parse_name() ...\n"); krberr = krb5_parse_name(context, "testcli@hadoop.com", &client); if (krberr) { errmsg = krb5_get_error_message(context, krberr); printf("krb5_parse_name(): error -> %s\n", errmsg); goto cleanup; } // Get initial credential const char *password="test1234"; printf("krb5_get_init_creds_password() ...\n"); krberr = krb5_get_init_creds_password(context, &my_creds, client, (char *)password, NULL, NULL, 0, NULL, NULL); if (krberr) { errmsg = krb5_get_error_message(context, krberr); printf("krb5_get_init_creds_password(): error -> %s\n", errmsg); goto cleanup; } my_creds_ptr = &my_creds; printf("krb5 get initial credential successfully\n"); // Create ticket in memory //krberr = krb5_cc_resolve(context, "MEMORY:mem_ld_krb5_cc", &ccache); // Create ticket in /tmp folder printf("krb5_cc_resolve() ...\n"); krberr = krb5_cc_resolve(context, "FILE:/tmp/krb5cc_1000", &ccache); if (krberr) { errmsg = krb5_get_error_message(context, krberr); printf("krb5_cc_resolve(): error -> %s\n", errmsg); goto cleanup; } printf("krb5_cc_initialize() ...\n"); krberr = krb5_cc_initialize(context, ccache, client); if (krberr) { errmsg = krb5_get_error_message(context, krberr); printf("krb5_cc_initialize(): error -> %s\n", errmsg); goto cleanup; } printf("krb5_cc_store_cred() ...\n"); krberr = krb5_cc_store_cred(context, ccache, &my_creds); if (krberr) { errmsg = krb5_get_error_message(context, krberr); printf("krb5_cc_store_cred(): error -> %s\n", errmsg); goto cleanup; } printf("krb5 store credential successfully\n"); cleanup: if (client) krb5_free_principal(context, client); if (my_creds_ptr) krb5_free_cred_contents(context, my_creds_ptr); if (context) krb5_free_context(context); return 0; }
$ gcc -o krb5_store_login krb5_store_login.c -lkrb5
# 查看票据列表
$ klist -f
klist: No credentials cache found (filename: /tmp/krb5cc_1000)
$ ./krb5_store_login
krb5_init_context() ... krb5_parse_name() ... krb5_get_init_creds_password() ... krb5 get creds success krb5_cc_resolve() ... krb5_cc_initialize() ... krb5_cc_store_cred() ... krb5 store creds success
# 查看票据列表
$ klist -f
Ticket cache: FILE:/tmp/krb5cc_1000 Default principal: testcli@hadoop.com Valid starting Expires Service principal 04/24/23 08:58:12 04/24/23 18:58:12 krbtgt/hadoop.com@hadoop.com renew until 04/25/23 08:58:06, Flags: FPRIA
4. Client/Server 认证
本示例使用 "testcli@hadoop.com"、testsrv/hadoop-maste-vm/@hadoop.com" 主体和他们的 Keytab,完成类似于一般验证系统的 Login 和聊天功能,Client 第一登录时,生成 Ticket 保存到本地缓存目录,Client 再次登录时,直接缓存的 Ticket。
1) Server 程序
$ cd ~/krb5
$ vim krb5_testsrv.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_ticket * ticket; const char str_keytab[100] = "/home/xxx/krb5/krb5_testsrv.keytab"; krb5_keytab keytab = NULL; struct sockaddr_in peername; int namelen = sizeof(peername); krb5_error_code krberr; krb5_principal server; const char * errmsg; printf("krb5_init_context() ... \n"); krberr = krb5_init_context(&context); if (krberr) { errmsg = krb5_get_error_message(NULL, krberr); printf("krb5_init_context(): error -> %s\n", errmsg); goto cleanup; } printf("krb5_kt_resolve() ... \n"); krberr = krb5_kt_resolve(context, str_keytab, &keytab); // Get a handle for a key table if (krberr) { errmsg = krb5_get_error_message(NULL, krberr); printf("krb5_kt_resolve(): error -> %s\n", errmsg); goto cleanup; } printf("krb5_sname_to_principal() ... \n"); krberr = krb5_sname_to_principal(context, NULL, "testsrv", KRB5_NT_SRV_HST, &server); if (krberr) { errmsg = krb5_get_error_message(NULL, krberr); printf("krb5_sname_to_principal(): error -> %s\n", errmsg); goto cleanup; } int sock = -1; if ((sock = socket(PF_INET, SOCK_STREAM, 0)) < 0) { printf("socket(): error -> sock == %d\n", sock); goto cleanup; } int acc = 0; int on = 1; struct sockaddr_in sockin; (void) setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on)); sockin.sin_family = AF_INET; sockin.sin_addr.s_addr = 0; sockin.sin_port = htons(9999); if (bind(sock, (struct sockaddr *) &sockin, sizeof(sockin))) { printf("bind(): error -> sock == %d\n", sock); goto cleanup; } printf("listen() ... \n"); if (listen(sock, 5) == -1) { printf("listen(): error -> sock == %d\n", sock); goto cleanup; } for(;;) { printf("accept() ... \n"); if ((acc = accept(sock, (struct sockaddr *)&peername, &namelen)) == -1){ printf("accept(): error -> sock == %d\n", sock); goto cleanup; } char str[100]; int done, n; krberr = krb5_recvauth(context, &auth_context, (krb5_pointer)&acc, NULL, // 应用服务版本,如 "Test_Server_1.0",这里设为 NULL 不验证版本 server, 0, // no flags keytab, // 默认 NULL 是会读取 /etc/krb5.keytab &ticket); if (krberr) { printf("krb5_recvauth(): error -> auth failed\n"); } else { krb5_auth_con_free(context, auth_context); auth_context = NULL; printf("krb5_recvauth(): auth ok\n"); done = 0; do { n = recv(acc, str, 100, 0); if (n <= 0) { // 循环读取数据,直到 n = 0 if (n < 0) printf("recv(): error -> n == %d\n", n); done = 1; // 退出 while 循环 printf("recv(): done: %i\n", done); } if (strncmp(str, "shutdown", 8) == 0) { printf("%s\n", str); goto cleanup; } printf("recv(): from client -> %s\n", str); sleep(2); // 用于测试观察,单位是秒 if (!done) { if (send(acc, str, n, 0) < 0) { printf("send(): error\n"); done = 1; printf("send(): done: %i\n", done); } printf("send(): to client -> %s\n", str); } } while (!done); } close(acc); printf("close(acc)\n"); }; // accept() 循环 cleanup: if (context) { if (ticket) krb5_free_ticket(context, ticket); if (server) krb5_free_principal(context, server); if (auth_context) krb5_auth_con_free(context, auth_context); krb5_free_context(context); } if (acc) close(acc); return 0; }
注:这是 keytab 路径里的 “xxx” 是当前 Linux 用户名,下同。
$ gcc -o krb5_testsrv krb5_testsrv.c -lkrb5
2) Client 程序
$ cd ~/krb5
$ vim krb5_testcli.c
#include <krb5.h> #include <stdio.h> #include <string.h> #include <netdb.h> #include <signal.h> const char str_keytab[100] = "/home/xxx/krb5/krb5_testcli.keytab"; const char str_ccache_file[50]= "FILE:/tmp/krb5cc_1000"; const char str_serverip[20] = "192.168.1.5"; const char str_principal[30] = "testcli"; const char *errmsg; int main(int argc, char *argv[]) { krb5_context context; krb5_principal client, server; krb5_error *err_ret; krb5_ap_rep_enc_part *rep_ret; krb5_auth_context auth_context = 0; krb5_error_code krberr; krb5_ccache ccache = NULL; printf("krb5_init_context() ... \n"); krberr = krb5_init_context(&context); if (krberr) { errmsg = krb5_get_error_message(NULL, krberr); printf("krb5_init_context(): error -> %s\n", errmsg); goto cleanup; } // Get principal printf("krb5_parse_name() ...\n"); krberr = krb5_parse_name(context, str_principal, &client); if (krberr) { errmsg = krb5_get_error_message(context, krberr); printf("krb5_parse_name(): error -> %s\n", errmsg); goto cleanup; } printf("krb5_cc_cache_match() ...\n"); krberr = krb5_cc_cache_match(context, client, &ccache); if (krberr == KRB5_CC_NOTFOUND) { int ret = getCredByKeytab(context, client, &ccache); if (ret < 0) { printf("getCredByKeytab(): error -> %d\n", ret); goto cleanup; } } else { printf("krb5_cc_default() ... \n"); krberr = krb5_cc_default(context, &ccache); if (krberr) { errmsg = krb5_get_error_message(NULL, krberr); printf("krb5_cc_default(): error -> %s\n", errmsg); goto cleanup; } printf("krb5_cc_get_principal() ... \n"); krberr = krb5_cc_get_principal(context, ccache, &client); if (krberr) { errmsg = krb5_get_error_message(NULL, krberr); printf("krb5_cc_get_principal(): error -> %s\n", errmsg); goto cleanup; } } printf("krb5_sname_to_principal('server') ... \n"); krberr = krb5_sname_to_principal(context, NULL, // KDC hostname or DNS "testsrv", // KRB5_NT_SRV_HST, &server); if (krberr) { errmsg = krb5_get_error_message(NULL, krberr); printf("krb5_sname_to_principal('server'): error -> %s\n", errmsg); goto cleanup; } (void) signal(SIGPIPE, SIG_IGN); int sock = -1; if ((sock = socket(PF_INET, SOCK_STREAM, 0)) < 0) { printf("socket(): error -> sock == %d\n", sock); goto cleanup; } struct sockaddr_in remote; remote.sin_addr.s_addr=inet_addr(str_serverip); remote.sin_family = AF_INET; remote.sin_port = htons(9999); bzero( &(remote.sin_zero), 8); printf("Connect() to %s ... \n", str_serverip); if (connect(sock, (struct sockaddr *)&remote, sizeof(struct sockaddr)) < 0) { printf("connect(): error\n"); goto cleanup; } printf("connect(): success\n"); printf("krb5_sendauth() ... \n"); krberr = krb5_sendauth(context, &auth_context, (krb5_pointer) &sock, "", // 对应服务端,如 "Test_Server_1.0"; 服务端可以 NULL没问题,经测试客户端用 NULL 运行段错误,所以这里用 "" client, server, AP_OPTS_MUTUAL_REQUIRED, NULL, 0, ccache, &err_ret, &rep_ret, NULL); if (krberr) { if (krberr == KRB5_SENDAUTH_REJECTED) { printf("krb5_sendauth(): rejected, \"%*s\"\n", err_ret->text.length, err_ret->text.data); } else { errmsg = krb5_get_error_message(NULL, krberr); printf("krb5_sendauth(): error -> %s\n", errmsg); } goto cleanup; } if (rep_ret) { // Finish auth krb5_cc_close(context, ccache); krb5_free_principal(context, server); krb5_free_principal(context, client); if (auth_context) krb5_auth_con_free(context, auth_context); krb5_free_ap_rep_enc_part(context, rep_ret); krb5_free_context(context); ccache = NULL; server = NULL; client = NULL; context = NULL; printf("krb5_sendauth(): success\n"); char str[100]; int t; while(printf("Input >"), fgets(str, 100, stdin), !feof(stdin)) { if (strncmp(str, "exit", 4) == 0 || strncmp(str, "quit", 4) == 0 ) { printf("%s\n", str); break; } if (send(sock, str, strlen(str), 0) == -1) { printf("send(): error\n"); goto cleanup; } if (strncmp(str, "shutdown", 8) == 0) { printf("%s\n", str); break; } printf("send(): to server -> %s\n", str); if ((t = recv(sock, str, 100, 0)) > 0) { str[t]='\0'; printf("recv(): from server -> %s\n", str); } else { if (t < 0) { printf("recv(): error\n"); } else { printf("recv(): server close\n"); } break; } } close(sock); sock = -1; printf("close(sock)\n"); } cleanup: if (context) { if (ccache) krb5_cc_close(context, ccache); if (client) krb5_free_principal(context, client); if (server) krb5_free_principal(context, server); krb5_free_context(context); } if (sock) close(sock); return 0; } int getCredByKeytab(krb5_context context, krb5_principal client, krb5_ccache *pccache) { krb5_error_code krberr; krb5_init_creds_context ctx; krb5_keytab keytab; krb5_creds client_creds; krb5_get_init_creds_opt *init_opts; int ret = 0; printf("krb5_kt_resolve() ... \n"); krberr = krb5_kt_resolve(context, str_keytab, &keytab); // Get a handle for a key table if (krberr) { ret = -1; goto cleanup2; } printf("krb5_get_init_creds_opt_alloc() ... \n"); krberr = krb5_get_init_creds_opt_alloc(context, &init_opts); if (krberr) { ret = -2; goto cleanup2; } printf("krb5_get_init_creds_keytab() ... \n"); krberr = krb5_get_init_creds_keytab(context, &client_creds, client, keytab, 0, NULL, init_opts); if (krberr) { ret = -3; goto cleanup2; } // Create ticket in /tmp folder printf("krb5_cc_resolve() ... \n"); krberr = krb5_cc_resolve(context, str_ccache_file, pccache); if (krberr) { ret = -4; goto cleanup2; } printf("krb5_cc_initialize() ... \n"); krberr = krb5_cc_initialize(context, *pccache, client); if (krberr) { ret = -5; goto cleanup2; } printf("krb5_cc_store_cred() ... \n"); krberr = krb5_cc_store_cred(context, *pccache, &client_creds); if (krberr) { ret = -6; goto cleanup2; } cleanup2: if (context) { if (keytab) krb5_kt_close(context, keytab); if (init_opts) krb5_get_init_creds_opt_free(context, init_opts); } return ret; }
$ gcc -o krb5_testcli krb5_testcli.c -lkrb5
3) 运行
# 运行 Server 程序
$ cd ~/krb5
$ ./krb5_testsrv
krb5_init_context() ... krb5_kt_resolve() ... krb5_sname_to_principal() ... listen() ... accept() ...
# 在另一个命令行控制台,运行 Client 程序
$ ./krb5_testcli
krb5_init_context() ... krb5_parse_name() ... krb5_cc_cache_match() ... krb5_cc_default() ... krb5_cc_get_principal() ... krb5_sname_to_principal('server') ... Connect() to 192.168.1.5 ... connect(): success krb5_sendauth() ... krb5_sendauth(): success Input > test
注: 输入文本 “test”,按回车键,Server 收到 “test” 后会发回 Client。输入 “exit” 或 “quit”,可以退出 Client 程序,Server 程序继续处于 accept 状态。输入 “shutdown”,Server 和 Client 都退出。