本章将对 gss-server 样例程序的源代码进行详细介绍,其中包含以下主题:
样例服务器端程序 gss-server 可以与上一章中介绍的 gss-client 结合使用。gss-server 的基本用途是从 gssapi-client 接收和返回经过包装的消息并对其进行签名。
以下几节将提供有关 gss-server 工作方式的逐步说明。由于 gss-server 是一个用于说明 GSSAPI 功能的样例程序,因此仅详细讨论该程序的相关部分。这两个应用程序的完整源代码可在附录中找到,也可以从以下网址下载:
http://developers.sun.com/prodtech/solaris/downloads/index.html
gss-structure 应用程序执行以下步骤:
解析命令行。
如果指定了机制,请将该机制的名称转换为内部格式。
获取调用方的凭证。
查看用户是否指定了要使用 inetd 守护进程进行连接。
与客户机建立连接。
从客户机接收数据。
对数据进行签名并返回数据。
释放名称空间并退出。
gss-server 在命令行中采用以下形式:
gss-server [-port port] [-verbose] [-inetd] [-once] [-logfile file] \ [-mech mechanism] service-name
port 是要侦听的端口号。如果未指定任何端口,则程序使用端口 4444 作为缺省端口。
-verbose 会导致在运行 gss-server 时显示消息。
-inetd 表示程序应当使用 inetd 守护进程来侦听端口。-inetd 使用 stdin 和 stdout 连接到客户机。
-once 表示仅建立单实例连接。
mechanism 是要使用的安全机制的名称,如 Kerberos v5。如果未指定任何机制,GSS-API 将使用缺省机制。
service-name 是客户机所请求的网络服务的名称,如 telnet、ftp 或登录服务。
典型的命令行可能如以下示例所示:
% gss-server -port 8080 -once -mech kerberos_v5 erebos.eng nfs "hello" |
解析命令行参数并为变量指定参数
获取与机制对应的服务的凭证
调用 sign_server() 函数,该函数会执行涉及到对消息进行签名和返回消息等工作
释放已经获取的凭证
释放机制的 OID 名称空间
在连接仍处于打开状态时关闭连接
此示例的源代码也可以通过 Sun 下载中心获取。请访问 http://www.sun.com/download/products.xml?id=41912db5
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() 之前必须执行以下两个任务:
检查机制列表并将该列表缩减成单个机制以获取凭证。
如果多个机制可以共享单个凭证,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 表示应当使用缺省机制。
将服务名称转换为 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_SERVICE 是 name_buf 中字符串的名称类型。在这种情况下,名称类型表示应当将字符串解释为 service@host 格式的服务。
执行这些任务之后,服务器程序可以调用 gss_acquire_cred():
maj_stat = gss_acquire_cred(&min_stat, server_name, 0, desiredMechs, GSS_C_ACCEPT, server_creds, NULL, NULL);
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
/* * 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; }
获取服务的凭证之后,可以使用 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
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
/* * 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 */
min_stat 是基础机制返回的错误状态。
context 是所建立的上下文。
server_creds 是要提供的服务的凭证(请参见获取凭证)。
recv_tok 是 recv_token() 从客户机接收的令牌。
GSS_C_NO_CHANNEL_BINDINGS 是一个标志,表示不使用通道绑定(请参见在 GSS-API 中使用通道绑定)。
client 是客户机的 ASCII 名称。
oid 表示机制(采用 OID 格式)。
send_tok 是要发送到客户机的令牌。
ret_flags 是用于指明上下文是否支持指定选项的各种标志,如 message-sequence-detection。
两个 NULL 参数表示程序无需知道上下文将保持有效的时间长度或者服务器是否可以充当客户机的代理。
只要 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_buf 和 xmit_buf。然后,sign_server() 会使用 gss_delete_sec_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
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; }
返回到 main() 函数中,应用程序会使用 gss_release_cred() 来删除服务凭证。如果已经为机制指定了 OID,则程序会使用 gss_release_oid() 删除该 OID 并退出。
(void) gss_release_cred(&min_stat, &server_creds);