Linux基础知识(17)- Kerberos (二) | krb5 API 的 C 程序示例

发布时间 2023-04-24 22:50:20作者: 垄山小站


在 “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 kprincpw = 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", &kprincpw);
                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, kprincpw, (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 (kprincpw) krb5_free_principal(context, kprincpw);
                
                if (my_creds_ptr) krb5_free_cred_contents(context, &my_creds);
                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 kprincpw = 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", &kprincpw);
                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, kprincpw, (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, kprincpw);
                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 (kprincpw) krb5_free_principal(context, kprincpw);
                
                if (my_creds_ptr) krb5_free_cred_contents(context, &my_creds);
                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;
            }

 

        $ 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 都退出。