Solaris 开发者安全性指南

第 6 章 GSS-API 服务器示例

本章将对 gss-server 样例程序的源代码进行详细介绍,其中包含以下主题:

GSSAPI 服务器示例概述

样例服务器端程序 gss-server 可以与上一章中介绍的 gss-client 结合使用。gss-server 的基本用途是从 gssapi-client 接收和返回经过包装的消息并对其进行签名。

以下几节将提供有关 gss-server 工作方式的逐步说明。由于 gss-server 是一个用于说明 GSSAPI 功能的样例程序,因此仅详细讨论该程序的相关部分。这两个应用程序的完整源代码可在附录中找到,也可以从以下网址下载:

http://developers.sun.com/prodtech/solaris/downloads/index.html

GSSAPI 服务器示例结构

gss-structure 应用程序执行以下步骤:

  1. 解析命令行。

  2. 如果指定了机制,请将该机制的名称转换为内部格式。

  3. 获取调用方的凭证。

  4. 查看用户是否指定了要使用 inetd 守护进程进行连接。

  5. 与客户机建立连接。

  6. 从客户机接收数据。

  7. 对数据进行签名并返回数据。

  8. 释放名称空间并退出。

运行 GSSAPI 服务器示例

gss-server 在命令行中采用以下形式:

gss-server [-port port] [-verbose] [-inetd] [-once] [-logfile file] \

                 [-mech mechanism] service-name

典型的命令行可能如以下示例所示:


% gss-server -port 8080 -once -mech kerberos_v5 erebos.eng nfs "hello"

GSSAPI 服务器示例:main() 函数

gss-server main() 函数可执行以下任务:


注 –

此示例的源代码也可以通过 Sun 下载中心获取。请访问 http://www.sun.com/download/products.xml?id=41912db5



示例 6–1 gss-server 示例:main()

int

main(argc, argv)

     int argc;

     char **argv;

{

     char *service_name;

     gss_cred_id_t server_creds;

     OM_uint32 min_stat;

     u_short port = 4444;

     int s;

     int once = 0;

     int do_inetd = 0;



     log = stdout;

     display_file = stdout;



     /* Parse command-line arguments. */

     argc--; argv++;

     while (argc) {

	  if (strcmp(*argv, "-port") == 0) {

	       argc--; argv++;

	       if (!argc) usage();

	       port = atoi(*argv);

	  } else if (strcmp(*argv, "-verbose") == 0) {

	      verbose = 1;

	  } else if (strcmp(*argv, "-once") == 0) {

	      once = 1;

	  } else if (strcmp(*argv, "-inetd") == 0) {

	      do_inetd = 1;

	  } else if (strcmp(*argv, "-logfile") == 0) {

	      argc--; argv++;

	      if (!argc) usage();

	      log = fopen(*argv, "a");

	      display_file = log;

	      if (!log) {

		  perror(*argv);

		  exit(1);

	      }

	  } else

	       break;

	  argc--; argv++;

     }

     if (argc != 1)

	  usage();



     if ((*argv)[0] == '-')

	  usage();



     service_name = *argv;



     /* Acquire service credentials. */

     if (server_acquire_creds(service_name, &server_creds) < 0)

	 return -1;

     

     if (do_inetd) {

	 close(1);

	 close(2);



 /* Sign and return message. */

	 sign_server(0, server_creds);

	 close(0);

     } else {

	 int stmp;



	 if ((stmp = create_socket(port)) >= 0) {

	     do {

		 /* Accept a TCP connection */

		 if ((s = accept(stmp, NULL, 0)) < 0) {

		     perror("accepting connection");

		     continue;

		 }

		 /* this return value is not checked, because there's

		    not really anything to do if it fails */

		 sign_server(s, server_creds);

		 close(s);

	     } while (!once);



	     close(stmp);

	 }

     }



     /* Close down and clean up. */

     (void) gss_release_cred(&min_stat, &server_creds);



     /*NOTREACHED*/

     (void) close(s);

     return 0;

}

获取凭证

凭证由基础机制创建,而不是由客户机应用程序、服务器应用程序或 GSS-API 创建。客户机程序通常具有在登录时获取的凭证。服务器需要始终明确获取凭证。

gss-server 程序包含 server_acquire_creds() 函数,该函数用于获取要提供的服务的凭证。server_acquire_creds() 将该服务的名称和要使用的安全机制用作输入,然后返回该服务的凭证。

server_acquire_creds() 使用 GSS-API 函数 gss_acquire_cred() 来获取服务器所提供的服务的凭证。server_acquire_creds() 访问 gss_acquire_cred() 之前必须执行以下两个任务:

  1. 检查机制列表并将该列表缩减成单个机制以获取凭证。

    如果多个机制可以共享单个凭证,gss_acquire_cred() 函数将返回所有这些机制的凭证。因此,gss_acquire_cred() 会将一组机制用作输入。(请参见在 GSS-API 中使用凭证。)但是,在大多数情况(包括此情况)下,单个凭证可能并不适用于多个机制。gss-server 程序中会在命令行上指定单个机制或使用缺省机制。因此,第一个任务是确保传递给 gss_acquire_cred() 的那组机制中包含单个机制(缺省机制或其他机制),如下所示:

    if (mechOid != GSS_C_NULL_OID) {
    
         desiredMechs = &mechOidSet;
    
         mechOidSet.count = 1;
    
         mechOidSet.elements = mechOid;
    
    } else
    
         desiredMechs = GSS_C_NULL_OID_SET;

    GSS_C_NULL_OID_SET 表示应当使用缺省机制。

  2. 将服务名称转换为 GSS-API 格式。

    由于 gss_acquire_cred() 会接受 gss_name_t 结构形式的服务名称作为参数,因此必须将服务名称转换为这种格式。可使用 gss_import_name() 函数执行此转换。与所有 GSS-API 函数一样,此函数要求参数采用 GSS-API 类型,因此必须首先将服务名称复制到 GSS-API 缓冲区,如下所示:

         name_buf.value = service_name;
    
         name_buf.length = strlen(name_buf.value) + 1;
    
         maj_stat = gss_import_name(&min_stat, &name_buf,
    
                    (gss_OID) GSS_C_NT_HOSTBASED_SERVICE, &server_name);
    
         if (maj_stat != GSS_S_COMPLETE) {
    
              display_status("importing name", maj_stat, min_stat);
    
              if (mechOid != GSS_C_NO_OID)
    
                    gss_release_oid(&min_stat, &mechOid);
    
              return -1;
    
         }

    另请注意非标准函数 gss_release_oid() 的用法。

    输入是 name_buf 中字符串形式的服务名称。输出是一个指向 gss_name_t 结构 server_name 的指针。第三个参数 GSS_C_NT_HOSTBASED_SERVICEname_buf 中字符串的名称类型。在这种情况下,名称类型表示应当将字符串解释为 service@host 格式的服务。

执行这些任务之后,服务器程序可以调用 gss_acquire_cred()

maj_stat = gss_acquire_cred(&min_stat, server_name, 0,

                                 desiredMechs, GSS_C_ACCEPT,

                                 server_creds, NULL, NULL);

以下源代码说明了 server_acquire_creds() 函数。


注 –

此示例的源代码也可以通过 Sun 下载中心获取。请访问 http://www.sun.com/download/products.xml?id=41912db5



示例 6–2 server_acquire_creds() 函数的样例代码

/*

 * Function: server_acquire_creds

 *

 * Purpose: imports a service name and acquires credentials for it

 *

 * Arguments:

 *

 *      service_name    (r) the ASCII service name

        mechType        (r) the mechanism type to use

 *      server_creds    (w) the GSS-API service credentials

 *

 * Returns: 0 on success, -1 on failure

 *

 * Effects:

 *

 * The service name is imported with gss_import_name, and service

 * credentials are acquired with gss_acquire_cred.  If either operation

 * fails, an error message is displayed and -1 is returned; otherwise,

 * 0 is returned.

 */

int server_acquire_creds(service_name, mechOid, server_creds)

     char *service_name;

     gss_OID mechOid;

     gss_cred_id_t *server_creds;

{

     gss_buffer_desc name_buf;

     gss_name_t server_name;

     OM_uint32 maj_stat, min_stat;

     gss_OID_set_desc mechOidSet;

     gss_OID_set desiredMechs = GSS_C_NULL_OID_SET;



     if (mechOid != GSS_C_NULL_OID) {

                desiredMechs = &mechOidSet;

                mechOidSet.count = 1;

                mechOidSet.elements = mechOid;

     } else

                desiredMechs = GSS_C_NULL_OID_SET;



     name_buf.value = service_name;

     name_buf.length = strlen(name_buf.value) + 1;

     maj_stat = gss_import_name(&min_stat, &name_buf,

                (gss_OID) GSS_C_NT_HOSTBASED_SERVICE, &server_name);

     if (maj_stat != GSS_S_COMPLETE) {

          display_status("importing name", maj_stat, min_stat);

          if (mechOid != GSS_C_NO_OID)

                gss_release_oid(&min_stat, &mechOid);

          return -1;

     }



     maj_stat = gss_acquire_cred(&min_stat, server_name, 0,

                                 desiredMechs, GSS_C_ACCEPT,

                                 server_creds, NULL, NULL);



     if (maj_stat != GSS_S_COMPLETE) {

          display_status("acquiring credentials", maj_stat, min_stat);

          return -1;

     }



     (void) gss_release_name(&min_stat, &server_name);



     return 0;

}

检查 inetd

获取服务的凭证之后,可以使用 gss-server 来查看用户是否指定了 inetd。main 函数按如下方式检查 inetd

if (do_inetd) {

	 close(1);

	 close(2);

如果用户指定了使用 inetd,则程序将关闭标准输出和标准错误。然后,gss-server 会针对标准输入调用 sign_server()inetd 将使用该函数传递连接。否则,gss-server 会创建一个套接字,使用 TCP 函数 accept() 接受该套接字的连接,然后针对 accept() 返回的文件描述符调用 sign_server()

如果未使用 inetd,程序会创建连接和上下文,直到终止程序为止。但是,如果用户指定了 -once 选项,则循环将在首次连接之后终止。

从客户机接收数据

检查 inetd 之后,gss-server 程序会调用 sign_server(),该函数用于执行程序的主要工作。sign_server() 首先通过调用 server_establish_context() 来建立上下文。

sign_server() 可执行以下任务:

这些任务将在以下各节中进行介绍。以下源代码说明了 sign_server() 函数。


注 –

此示例的源代码也可以通过 Sun 下载中心获取。请访问 http://www.sun.com/download/products.xml?id=41912db5



示例 6–3 sign_server() 函数

int sign_server(s, server_creds)

     int s;

     gss_cred_id_t server_creds;

{

     gss_buffer_desc client_name, xmit_buf, msg_buf;

     gss_ctx_id_t context;

     OM_uint32 maj_stat, min_stat;

     int i, conf_state, ret_flags;

     char	*cp;

     

     /* Establish a context with the client */

     if (server_establish_context(s, server_creds, &context,

				  &client_name, &ret_flags) < 0)

	return(-1);

	  

     printf("Accepted connection: \"%.*s\"\n",

	    (int) client_name.length, (char *) client_name.value);

     (void) gss_release_buffer(&min_stat, &client_name);



     for (i=0; i < 3; i++)

	     if (test_import_export_context(&context))

		     return -1;



     /* Receive the sealed message token */

     if (recv_token(s, &xmit_buf) < 0)

	return(-1);

	  

     if (verbose && log) {

	fprintf(log, "Sealed message token:\n");

	print_token(&xmit_buf);

     }



     maj_stat = gss_unwrap(&min_stat, context, &xmit_buf, &msg_buf,

			   &conf_state, (gss_qop_t *) NULL);

     if (maj_stat != GSS_S_COMPLETE) {

	display_status("unsealing message", maj_stat, min_stat);

	return(-1);

     } else if (! conf_state) {

	fprintf(stderr, "Warning!  Message not encrypted.\n");

     }



     (void) gss_release_buffer(&min_stat, &xmit_buf);



     fprintf(log, "Received message: ");

     cp = msg_buf.value;

     if ((isprint(cp[0]) || isspace(cp[0])) &&

	 (isprint(cp[1]) || isspace(cp[1]))) {

	fprintf(log, "\"%.*s\"\n", msg_buf.length, msg_buf.value);

     } else {

	printf("\n");

	print_token(&msg_buf);

     }

	  

     /* Produce a signature block for the message */

     maj_stat = gss_get_mic(&min_stat, context, GSS_C_QOP_DEFAULT,

			    &msg_buf, &xmit_buf);

     if (maj_stat != GSS_S_COMPLETE) {

	display_status("signing message", maj_stat, min_stat);

	return(-1);

     }



     (void) gss_release_buffer(&min_stat, &msg_buf);



     /* Send the signature block to the client */

     if (send_token(s, &xmit_buf) < 0)

	return(-1);



     (void) gss_release_buffer(&min_stat, &xmit_buf);



     /* Delete context */

     maj_stat = gss_delete_sec_context(&min_stat, &context, NULL);

     if (maj_stat != GSS_S_COMPLETE) {

	display_status("deleting context", maj_stat, min_stat);

	return(-1);

     }



     fflush(log);



     return(0);

}

接受上下文

建立上下文通常涉及到在客户机和服务器之间执行一系列令牌交换。为了保持程序的可移植性,应当在循环中同时执行上下文接受和上下文初始化。用于接受上下文的循环与用于建立上下文的循环非常相似,但是二者的方向相反。请与建立与服务器的安全上下文进行比较。

以下源代码说明了 server_establish_context() 函数。


注 –

此示例的源代码也可以通过 Sun 下载中心获取。请访问 http://www.sun.com/download/products.xml?id=41912db5



示例 6–4 server_establish_context() 函数

/*

 * Function: server_establish_context

 *

 * Purpose: establishes a GSS-API context as a specified service with

 * an incoming client, and returns the context handle and associated

 * client name

 *

 * Arguments:

 *

 *      s               (r) an established TCP connection to the client

 *      service_creds   (r) server credentials, from gss_acquire_cred

 *      context         (w) the established GSS-API context

 *      client_name     (w) the client's ASCII name

 *

 * Returns: 0 on success, -1 on failure

 *

 * Effects:

 *

 * Any valid client request is accepted.  If a context is established,

 * its handle is returned in context and the client name is returned

 * in client_name and 0 is returned.  If unsuccessful, an error

 * message is displayed and -1 is returned.

 */

int server_establish_context(s, server_creds, context, client_name, ret_flags)

     int s;

     gss_cred_id_t server_creds;

     gss_ctx_id_t *context;

     gss_buffer_t client_name;

     OM_uint32 *ret_flags;

{

     gss_buffer_desc send_tok, recv_tok;

     gss_name_t client;

     gss_OID doid;

     OM_uint32 maj_stat, min_stat, acc_sec_min_stat;

     gss_buffer_desc    oid_name;



     *context = GSS_C_NO_CONTEXT;

     

     do {

          if (recv_token(s, &recv_tok) < 0)

               return -1;



          if (verbose && log) {

              fprintf(log, "Received token (size=%d): \n", recv_tok.length);

              print_token(&recv_tok);

          }



          maj_stat =

               gss_accept_sec_context(&acc_sec_min_stat,

                                      context,

                                      server_creds,

                                      &recv_tok,

                                      GSS_C_NO_CHANNEL_BINDINGS,

                                      &client,

                                      &doid,

                                      &send_tok,

                                      ret_flags,

                                      NULL,     /* ignore time_rec */

                                      NULL);    /* ignore del_cred_handle */



          (void) gss_release_buffer(&min_stat, &recv_tok);



          if (send_tok.length != 0) {

              if (verbose && log) {

                  fprintf(log,

                          "Sending accept_sec_context token (size=%d):\n",

                          send_tok.length);

                  print_token(&send_tok);

              }

               if (send_token(s, &send_tok) < 0) {

                    fprintf(log, "failure sending token\n");

                    return -1;

               }



               (void) gss_release_buffer(&min_stat, &send_tok);

          }

          if (maj_stat!=GSS_S_COMPLETE && maj_stat!=GSS_S_CONTINUE_NEEDED) {

               display_status("accepting context", maj_stat,

                              acc_sec_min_stat);

               if (*context == GSS_C_NO_CONTEXT)

                       gss_delete_sec_context(&min_stat, context,

                                              GSS_C_NO_BUFFER);

               return -1;

          }



          if (verbose && log) {

              if (maj_stat == GSS_S_CONTINUE_NEEDED)

                  fprintf(log, "continue needed...\n");

              else

                  fprintf(log, "\n");

              fflush(log);

          }

     } while (maj_stat == GSS_S_CONTINUE_NEEDED);



     /* display the flags */

     display_ctx_flags(*ret_flags);



     if (verbose && log) {

         maj_stat = gss_oid_to_str(&min_stat, doid, &oid_name);

         if (maj_stat != GSS_S_COMPLETE) {

             display_status("converting oid->string", maj_stat, min_stat);

             return -1;

         }

         fprintf(log, "Accepted connection using mechanism OID %.*s.\n",

                 (int) oid_name.length, (char *) oid_name.value);

         (void) gss_release_buffer(&min_stat, &oid_name);

     }



     maj_stat = gss_display_name(&min_stat, client, client_name, &doid);

     if (maj_stat != GSS_S_COMPLETE) {

          display_status("displaying name", maj_stat, min_stat);

          return -1;

     }

     maj_stat = gss_release_name(&min_stat, &client);

     if (maj_stat != GSS_S_COMPLETE) {

          display_status("releasing name", maj_stat, min_stat);

          return -1;

     }

     return 0;

}

sign_server() 函数使用以下源代码,通过调用 server_establish_context() 来接受上下文。

/* Establish a context with the client */

     if (server_establish_context(s, server_creds, &context,

				  &client_name, &ret_flags) < 0)

	return(-1);

server_establish_context() 函数首先查找客户机在初始化上下文的过程中发送的令牌。由于 GSS-API 本身不会收发令牌,因此程序必须各自具有用于执行这些任务的例程。服务器使用 recv_token() 来接收令牌:

     do {

          if (recv_token(s, &recv_tok) < 0)

               return -1;

接下来,server_establish_context() 会调用 GSS-API 函数 gss_accept_sec_context()

     maj_stat = gss_accept_sec_context(&min_stat,

                                      context,

                                      server_creds,

                                      &recv_tok,

                                      GSS_C_NO_CHANNEL_BINDINGS,

                                      &client,

                                      &doid,

                                      &send_tok,

                                      ret_flags,

                                      NULL,     /* ignore time_rec */

                                      NULL);    /* ignore del_cred_handle */

只要 gss_accept_sec_context()maj_stat 设置为 GSS_S_CONTINUE_NEEDED,接受循环就将继续并且不会遇到任何错误。如果 maj_stat 与该值或者 GSS_S_COMPLETE 不等,则表明存在问题,循环将退出。

无论是否存在要发送回客户机的令牌,gss_accept_sec_context() 都会返回一个表示 send_tok 长度的正值。下一步是查看是否存在要发送的令牌,如果存在,则发送该令牌:

     if (send_tok.length != 0) {

          . . .

          if (send_token(s, &send_tok) < 0) {

               fprintf(log, "failure sending token\n");

               return -1;

          }



          (void) gss_release_buffer(&min_stat, &send_tok);

          }

展开消息

接受上下文之后,sign_server() 将接收客户机已发送的消息。由于 GSS-API 不提供用于接收令牌的函数,因此程序将使用 recv_token() 函数:

if (recv_token(s, &xmit_buf) < 0)

     return(-1);

由于消息可能已经过加密,因此程序将使用 GSS-API 函数 gss_unwrap() 来展开消息:

maj_stat = gss_unwrap(&min_stat, context, &xmit_buf, &msg_buf,

                           &conf_state, (gss_qop_t *) NULL);

     if (maj_stat != GSS_S_COMPLETE) {

        display_status("unwrapping message", maj_stat, min_stat);

        return(-1);

     } else if (! conf_state) {

        fprintf(stderr, "Warning!  Message not encrypted.\n");

     }



     (void) gss_release_buffer(&min_stat, &xmit_buf);

gss_unwrap() 会提取 recv_token() 已经放在 xmit_buf 中的消息,转换该消息并将结果放在 msg_buf 中。请注意 gss_unwrap() 的两个参数。conf_state 标志用于指明是否已经向该消息应用了保密性(即加密)。最后一个 NULL 表示程序无需知道用于保护该消息的 QOP。

消息的签名和返回

此时,sign_server() 函数需要对消息进行签名。对消息进行签名需要将消息的消息完整性代码(即 MIC)返回到客户机。返回消息可证明消息已成功发送和展开。为了获取 MIC,sign_server() 将使用 gss_get_mic() 函数:

maj_stat = gss_get_mic(&min_stat, context, GSS_C_QOP_DEFAULT,

                            &msg_buf, &xmit_buf);

gss_get_mic()msg_buf 中查找该消息,生成 MIC,并将该 MIC 存储到 xmit_buf 中。然后,服务器会使用 send_token() 将该 MIC 发回到客户机。客户机会使用 gss_verify_mic() 来检验该 MIC。请参见读取和检验 GSS-API 客户机中的签名块

最后,sign_server() 会执行一些清除操作。sign_server() 会使用 gss_release_buffer() 来释放 GSS-API 缓冲区 msg_bufxmit_buf。然后,sign_server() 会使用 gss_delete_sec_context() 来销毁上下文。

使用 test_import_export_context() 函数

通过 GSS-API 可导出和导入上下文。使用这些活动可以在多进程程序中的不同进程之间共享上下文。sign_server() 包含一个概念证明函数 test_import_export_context(),用于说明如何导出和导入上下文。test_import_export_context() 不在进程之间传递上下文,而是会显示导出所需的时间,然后导入上下文。虽然 test_import_export_context() 是一个虚构的函数,但可用于指示如何使用 GSS-API 导入和导出函数,并且还可以指示如何使用时间标记来处理上下文。

test_import_export_context() 的源代码如以下示例所示。


注 –

此示例的源代码也可以通过 Sun 下载中心获取。请访问 http://www.sun.com/download/products.xml?id=41912db5



示例 6–5 test_import_export_context()

int test_import_export_context(context)

        gss_ctx_id_t *context;

{

        OM_uint32       min_stat, maj_stat;

        gss_buffer_desc context_token, copied_token;

        struct timeval tm1, tm2;



        /*

         * Attempt to save and then restore the context.

         */

        gettimeofday(&tm1, (struct timezone *)0);

        maj_stat = gss_export_sec_context(&min_stat, context, &context_token);

        if (maj_stat != GSS_S_COMPLETE) {

                display_status("exporting context", maj_stat, min_stat);

                return 1;

        }

        gettimeofday(&tm2, (struct timezone *)0);

        if (verbose && log)

                fprintf(log, "Exported context: %d bytes, %7.4f seconds\n",

                        context_token.length, timeval_subtract(&tm2, &tm1));

        copied_token.length = context_token.length;

        copied_token.value = malloc(context_token.length);

        if (copied_token.value == 0) {

            fprintf(log, "Couldn't allocate memory to copy context token.\n");

            return 1;

        }

        memcpy(copied_token.value, context_token.value, copied_token.length);

        maj_stat = gss_import_sec_context(&min_stat, &copied_token, context);

        if (maj_stat != GSS_S_COMPLETE) {

                display_status("importing context", maj_stat, min_stat);

                return 1;

        }

        free(copied_token.value);

        gettimeofday(&tm1, (struct timezone *)0);

        if (verbose && log)

                fprintf(log, "Importing context: %7.4f seconds\n",

                        timeval_subtract(&tm1, &tm2));

        (void) gss_release_buffer(&min_stat, &context_token);

        return 0;

}

在 GSSAPI 服务器示例中清除

返回到 main() 函数中,应用程序会使用 gss_release_cred() 来删除服务凭证。如果已经为机制指定了 OID,则程序会使用 gss_release_oid() 删除该 OID 并退出。

     (void) gss_release_cred(&min_stat, &server_creds);