本章将对典型的 GSS-API 客户机应用程序进行详细介绍,其中包含以下主题:
样例客户端程序 gss-client可创建与服务器之间的安全上下文,建立安全参数,并将消息字符串发送到服务器。 此程序使用基于 TCP 的简单套接字连接机制来建立连接。
以下几节将提供有关 gss-client 工作方式的逐步说明。 由于 gss-client 是一个旨在说明 GSSAPI 功能的样例程序,因此仅详细讨论该程序的相关部分。 这两个应用程序的完整源代码可在附录中找到,也可以从以下网址下载:
http://developers.sun.com/prodtech/solaris/downloads/index.html
gss-client 应用程序执行以下步骤:
解析命令行。
如果指定了机制,则为机制创建一个对象 ID (object ID, OID); 否则,使用缺省机制,此为最常见的情况。
创建与服务器的连接。
建立安全上下文。
包装和发送消息。
检验服务器是否已对消息进行正确的“签名”。
删除安全上下文。
gss-client 示例在命令行中采用以下形式:
gss-client [-port port] [-d] [-mech mech] host service-name [-f] msg |
port-用于与 host 所指定的远程计算机建立连接的端口号。
-d 标志-用于将安全凭证授予服务器。 具体来说,deleg-flag 变量会设置为 GSS-API 值 GSS_C_DELEG_FLAG。 否则,deleg-flag 设置为零。
mech-要使用的安全机制的名称,如 Kerberos v5。 如果未指定任何机制,GSS-API 将使用缺省机制。
host-服务器的名称。
service-name-客户机所请求的网络服务的名称。 telnet、ftp 和 login 服务便是一些典型的示例。
msg-作为受保护的数据发送到服务器的字符串。 如果指定了 -f 选项,则 msg 是指从中读取字符串的文件名。
客户机应用程序的典型命令行可能如以下示例所示:
% gss-client -port 8080 -d -mech kerberos_v5 erebos.eng nfs "ls" |
以下示例不指定机制、端口或授予:
% gss-client erebos.eng nfs "ls" |
与所有 C 程序一样,该程序的外部 shell 也包含在入口点函数 main() 中。main() 可执行以下四种功能:
解析命令行参数并为变量指定参数。
如果要使用缺省机制以外的机制,则调用 parse_oid() 来创建 GSS-API OID(即对象标识符)。 对象标识符来自安全机制的名称,前提是已经提供了机制名称。
调用 call_server(),这会实际创建上下文并发送数据。
发送数据之后,根据需要释放 OID 的存储空间。
此示例的源代码也可以通过 Sun 下载中心获取。请访问 http://www.sun.com/download/products.xml?id=41912db5
int main(argc, argv) int argc; char **argv; { char *msg; char service_name[128]; char hostname[128]; char *mechanism = 0; u_short port = 4444; int use_file = 0; OM_uint32 deleg_flag = 0, min_stat; 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, "-mech") == 0) { argc--; argv++; if (!argc) usage(); mechanism = *argv; } else if (strcmp(*argv, "-d") == 0) { deleg_flag = GSS_C_DELEG_FLAG; } else if (strcmp(*argv, "-f") == 0) { use_file = 1; } else break; argc--; argv++; } if (argc != 3) usage(); if (argc > 1) { strcpy(hostname, argv[0]); } else if (gethostname(hostname, sizeof(hostname)) == -1) { perror("gethostname"); exit(1); } if (argc > 2) { strcpy(service_name, argv[1]); strcat(service_name, "@"); strcat(service_name, hostname); } msg = argv[2]; /* Create GSSAPI object ID. */ if (mechanism) parse_oid(mechanism, &g_mechOid); /* Call server to create context and send data. */ if (call_server(hostname, port, g_mechOid, service_name, deleg_flag, msg, use_file) < 0) exit(1); /* Release storage space for OID, if still allocated */ if (g_mechOid != GSS_C_NULL_OID) (void) gss_release_oid(&min_stat, &gmechOid); return 0; }
call_server() 函数使用以下代码与服务器建立连接:
if ((s = connect_to_server(host, port)) < 0) return -1;
s 是一个最初通过调用 socket() 返回的 int 类型的文件描述符。
connect_to_server() 是 GSS-API 外部的一个简单函数,它使用套接字来创建连接。 connect_to_server() 的源代码如以下示例所示。
此示例的源代码也可以通过 Sun 下载中心获取。请访问 http://www.sun.com/download/products.xml?id=41912db5
int connect_to_server(host, port) char *host; u_short port; { struct sockaddr_in saddr; struct hostent *hp; int s; if ((hp = gethostbyname(host)) == NULL) { fprintf(stderr, "Unknown host: %s\n", host); return -1; } saddr.sin_family = hp->h_addrtype; memcpy((char *)&saddr.sin_addr, hp->h_addr, sizeof(saddr.sin_addr)); saddr.sin_port = htons(port); if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("creating socket"); return -1; } if (connect(s, (struct sockaddr *)&saddr, sizeof(saddr)) < 0) { perror("connecting to server"); (void) close(s); return -1; } return s; }
建立连接之后,call_server() 会使用 client_establish_context() 函数来创建安全上下文,如下所示:
if (client_establish_context(s, service-name, deleg-flag, oid, &context, &ret-flags) < 0) { (void) close(s); return -1; }
s 是一个文件描述符,表示通过 connect_to_server() 建立的连接。
service-name 是所请求的网络服务。
deleg-flag 用于指定服务器是否可以充当客户机的代理。
oid 表示机制。
context 是要创建的上下文。
ret-flags 是一个 int,用于指定 GSS-API 函数 gss_init_sec_context() 所返回的任何标志。
client_establish_context() 执行以下任务:
将服务名称转换为 GSSAPI 内部格式
在客户机和服务器之间循环执行令牌交换,直到建立完整的安全上下文为止
client_establish_context() 执行的首个任务是使用 gss_import_name() 将服务名称字符串转换为 GSS-API 内部格式。
/* * Import the name into target_name. Use send_tok to save * local variable space. */ send_tok.value = service_name; send_tok.length = strlen(service_name) + 1; maj_stat = gss_import_name(&min_stat, &send_tok, (gss_OID) GSS_C_NT_HOSTBASED_SERVICE, &target_name); if (maj_stat != GSS_S_COMPLETE) { display_status("parsing name", maj_stat, min_stat); return -1; }
gss_import_name() 提取存储在不透明 GSS_API 缓冲区 send_tok 中的服务名称,然后将服务名称字符串转换为 GSS_API 内部名称 target_name。 send_tok 用于节省空间,而不是用于声明新的 gss_buffer_desc。 第三个参数类型为 gss_OID,用于指明 send_tok 名称的格式。 此示例使用 GSS_C_NT_HOSTBASED_SERVICE,这表示服务采用 service@host 格式。 有关此参数的其他可能值,请参见名称类型。
将服务转换为 GSS-API 内部格式之后即可建立上下文。 为了尽可能提高可移植性,应当始终循环建立上下文。
进入循环之前,client_establish_context() 会初始化上下文和 token_ptr 参数。 使用 token_ptr 时需要进行选择。 token_ptr 可以指向 send_tok(发送到服务器的令牌)或 recv_tok(服务器发回的令牌)。
在循环内部,需要检查两项:
gss_init_sec_context() 返回的状态
返回状态可捕捉任何可能要求循环异常中止的错误。 当且仅当服务器还要发送另一个令牌时,gss_init_sec_context() 才会返回 GSS_S_CONTINUE_NEEDED。
gss_init_sec_context() 所生成的要发送到服务器的令牌的大小
如果令牌大小为零,则表示不再有可以发送到服务器的信息并且可以退出循环。 令牌大小可根据 token_ptr 来确定。
以下伪代码对该循环进行了说明:
do gss_init_sec_context() if no context was created exit with error; if the status is neither "complete" nor "in process" release the service namespace and exit with error; if there is a token to send to the server, that is, the size is nonzero send the token; if sending the token fails, release the token and service namespaces. Exit with error; release the namespace for the token that was just sent; if the context is not completely set up receive a token from the server; while the context is not complete
该循环从调用 gss_init_sec_context() 开始,该函数使用以下参数:
要由基础机制设置的状态码。
凭证句柄。此示例使用 GSS_C_NO_CREDENTIAL 充当缺省主体。
gss-context,表示要创建的上下文句柄。
服务的 target-name,作为 GSS_API 内部名称。
oid,机制的 ID。
请求标志。在本示例中,客户机请求执行以下操作:服务器对自身进行验证,打开消息重复功能,服务器根据请求充当代理。
对于上下文无时间限制。
没有请求使用通道绑定。
token_ptr,指向从服务器收到的令牌。
服务器实际使用的机制。机制在此处设置为 NULL,这是因为应用程序不会使用该值。
&send_tok,即 gss_init_sec_context() 所创建的要发送到服务器的令牌。
返回标志。设置为 NULL,这是因为在此示例中忽略了这些标志。
客户机在启动上下文之前无需获取凭证。 在客户端,凭证管理通过 GSS-API 以透明方式处理。即,GSS-API 知道如何获取此机制为该主体创建的凭证。因此,应用程序可以向 gss_init_sec_context() 传递缺省凭证。但是在服务器端,服务器应用程序在接受上下文之前必须明确获取服务的凭证。请参见获取凭证。
检查了上下文或其一部分是否存在,以及 gss_init_sec_context() 是否返回有效状态之后,connect_to_server() 会检查 gss_init_sec_context() 是否提供了要发送到服务器的令牌。如果不存在任何令牌,则表明服务器已经指示不需要其他任何令牌。如果提供了令牌,则必须将该令牌发送到服务器。如果发送该令牌失败,则无法确定该令牌和服务的名称空间,并且 connect_to_server() 将退出。以下算法通过查看令牌的长度来检查令牌是否存在:
if (send_tok_length != 0) { if (send_token(s, &send_tok) < 0) { (void) gss_release_buffer(&min_stat, &send_tok); (void) gss_release_name(&min_stat, &target_name); return -1; } }
send_token() 不是 GSS-API 函数,需要由用户进行编写。send_token() 函数可将令牌写入到文件描述符中。send_token() 在成功时返回 0,在失败时返回 –1。GSS-API 本身不收发令牌。调用应用程序负责收发 GSS-API 已创建的任何令牌。
下面提供了用于建立上下文循环的源代码。
此示例的源代码也可以通过 Sun 下载中心获取。请访问 http://www.sun.com/download/products.xml?id=41912db5
/* * Perform the context establishment loop. * * On each pass through the loop, token_ptr points to the token * to send to the server (or GSS_C_NO_BUFFER on the first pass). * Every generated token is stored in send_tok which is then * transmitted to the server; every received token is stored in * recv_tok, which token_ptr is then set to, to be processed by * the next call to gss_init_sec_context. * * GSS-API guarantees that send_tok's length will be non-zero * if and only if the server is expecting another token from us, * and that gss_init_sec_context returns GSS_S_CONTINUE_NEEDED if * and only if the server has another token to send us. */ token_ptr = GSS_C_NO_BUFFER; *gss_context = GSS_C_NO_CONTEXT; 1234567890123456789012345678901234567890123456789012345678901234567890123456 do { maj_stat = gss_init_sec_context(&min_stat, GSS_C_NO_CREDENTIAL, gss_context, target_name, oid, GSS_C_MUTUAL_FLAG | GSS_C_REPLAY_FLAG | deleg_flag, 0, NULL, /* no channel bindings */ token_ptr, NULL, /* ignore mech type */ &send_tok, ret_flags, NULL); /* ignore time_rec */ if (gss_context == NULL){ printf("Cannot create context\n"); return GSS_S_NO_CONTEXT; } if (token_ptr != GSS_C_NO_BUFFER) (void) gss_release_buffer(&min_stat, &recv_tok); if (maj_stat!=GSS_S_COMPLETE && maj_stat!=GSS_S_CONTINUE_NEEDED) { display_status("initializing context", maj_stat, min_stat); (void) gss_release_name(&min_stat, &target_name); return -1; } if (send_tok.length != 0){ fprintf(stdout, "Sending init_sec_context token (size=%ld)...", send_tok.length); if (send_token(s, &send_tok) < 0) { (void) gss_release_buffer(&min_stat, &send_tok); (void) gss_release_name(&min_stat, &target_name); return -1; } } (void) gss_release_buffer(&min_stat, &send_tok); if (maj_stat == GSS_S_CONTINUE_NEEDED) { fprintf(stdout, "continue needed..."); if (recv_token(s, &recv_tok) < 0) { (void) gss_release_name(&min_stat, &target_name); return -1; } token_ptr = &recv_tok; } printf("\n"); } while (maj_stat == GSS_S_CONTINUE_NEEDED);
有关 send_token() 和 recv_token() 工作方式的更多信息,请参见各种 GSS-API 样例函数。
作为样例程序,gss-client 所执行的一些功能用于说明。以下源代码不是执行基本任务所必需的,之所以提供它是为了说明以下其他操作:
保存和恢复上下文
显示上下文标志
获取上下文状态
这些操作的源代码如以下示例所示。
此示例的源代码也可以通过 Sun 下载中心获取。请访问 http://www.sun.com/download/products.xml?id=41912db5
/* Save and then restore the context */ 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; } maj_stat = gss_import_sec_context(&min_stat, &context_token, &context); if (maj_stat != GSS_S_COMPLETE) { display_status("importing context", maj_stat, min_stat); return -1; } (void) gss_release_buffer(&min_stat, &context_token); /* display the flags */ display_ctx_flags(ret_flags); /* Get context information */ maj_stat = gss_inquire_context(&min_stat, context, &src_name, &targ_name, &lifetime, &mechanism, &context_flags, &is_local, &is_open); if (maj_stat != GSS_S_COMPLETE) { display_status("inquiring context", maj_stat, min_stat); return -1; } if (maj_stat == GSS_S_CONTEXT_EXPIRED) { printf(" context expired\n"); display_status("Context is expired", maj_stat, min_stat); return -1; }
gss-client 应用程序必须首先包装(即加密)数据,然后才能将其发送。应用程序通过执行以下步骤来包装消息:
确定包装大小限制,此过程可确保协议提供经过包装的消息。
获取源和目标的名称。将名称从对象标识符转换为字符串。
获取机制名称的列表。将名称从对象标识符转换为字符串。
将消息插入到缓冲区中并对其进行包装。
将消息发送到服务器。
以下源代码可用于包装消息。
此示例的源代码也可以通过 Sun 下载中心获取。请访问 http://www.sun.com/download/products.xml?id=41912db5
/* Test gss_wrap_size_limit */ maj_stat = gss_wrap_size_limit(&min_stat, context, conf_req_flag, GSS_C_QOP_DEFAULT, req_output_size, &max_input_size); if (maj_stat != GSS_S_COMPLETE) { display_status("wrap_size_limit call", maj_stat, min_stat); } else fprintf (stderr, "gss_wrap_size_limit returned " "max input size = %d \n" "for req_output_size = %d with Integrity only\n", max_input_size , req_output_size , conf_req_flag); conf_req_flag = 1; maj_stat = gss_wrap_size_limit(&min_stat, context, conf_req_flag, GSS_C_QOP_DEFAULT, req_output_size, &max_input_size); if (maj_stat != GSS_S_COMPLETE) { display_status("wrap_size_limit call", maj_stat, min_stat); } else fprintf (stderr, "gss_wrap_size_limit returned " " max input size = %d \n" "for req_output_size = %d with " "Integrity & Privacy \n", max_input_size , req_output_size ); maj_stat = gss_display_name(&min_stat, src_name, &sname, &name_type); if (maj_stat != GSS_S_COMPLETE) { display_status("displaying source name", maj_stat, min_stat); return -1; } maj_stat = gss_display_name(&min_stat, targ_name, &tname, (gss_OID *) NULL); if (maj_stat != GSS_S_COMPLETE) { display_status("displaying target name", maj_stat, min_stat); return -1; } fprintf(stderr, "\"%.*s\" to \"%.*s\", lifetime %u, flags %x, %s, %s\n", (int) sname.length, (char *) sname.value, (int) tname.length, (char *) tname.value, lifetime, context_flags, (is_local) ? "locally initiated" : "remotely initiated", (is_open) ? "open" : "closed"); (void) gss_release_name(&min_stat, &src_name); (void) gss_release_name(&min_stat, &targ_name); (void) gss_release_buffer(&min_stat, &sname); (void) gss_release_buffer(&min_stat, &tname); maj_stat = gss_oid_to_str(&min_stat, name_type, &oid_name); if (maj_stat != GSS_S_COMPLETE) { display_status("converting oid->string", maj_stat, min_stat); return -1; } fprintf(stderr, "Name type of source name is %.*s.\n", (int) oid_name.length, (char *) oid_name.value); (void) gss_release_buffer(&min_stat, &oid_name); /* Now get the names supported by the mechanism */ maj_stat = gss_inquire_names_for_mech(&min_stat, mechanism, &mech_names); if (maj_stat != GSS_S_COMPLETE) { display_status("inquiring mech names", maj_stat, min_stat); return -1; } maj_stat = gss_oid_to_str(&min_stat, mechanism, &oid_name); if (maj_stat != GSS_S_COMPLETE) { display_status("converting oid->string", maj_stat, min_stat); return -1; } mechStr = (char *)__gss_oid_to_mech(mechanism); fprintf(stderr, "Mechanism %.*s (%s) supports %d names\n", (int) oid_name.length, (char *) oid_name.value, (mechStr == NULL ? "NULL" : mechStr), mech_names->count); (void) gss_release_buffer(&min_stat, &oid_name); for (i=0; i < mech_names->count; i++) { maj_stat = gss_oid_to_str(&min_stat, &mech_names->elements[i], &oid_name); if (maj_stat != GSS_S_COMPLETE) { display_status("converting oid->string", maj_stat, min_stat); return -1; } fprintf(stderr, " %d: %.*s\n", i, (int) oid_name.length, ( char *) oid_name.value); (void) gss_release_buffer(&min_stat, &oid_name); } (void) gss_release_oid_set(&min_stat, &mech_names); if (use_file) { read_file(msg, &in_buf); } else { /* Wrap the message */ in_buf.value = msg; in_buf.length = strlen(msg) + 1; } if (ret_flag & GSS_C_CONF_FLAG) { state = 1; else state = 0; } maj_stat = gss_wrap(&min_stat, context, 1, GSS_C_QOP_DEFAULT, &in_buf, &state, &out_buf); if (maj_stat != GSS_S_COMPLETE) { display_status("wrapping message", maj_stat, min_stat); (void) close(s); (void) gss_delete_sec_context(&min_stat, &context, GSS_C_NO_BUFFER); return -1; } else if (! state) { fprintf(stderr, "Warning! Message not encrypted.\n"); } /* Send to server */ if (send_token(s, &out_buf) < 0) { (void) close(s); (void) gss_delete_sec_context(&min_stat, &context, GSS_C_NO_BUFFER); return -1; } (void) gss_release_buffer(&min_stat, &out_buf);
现在,gss-client 程序可以测试已发送消息的有效性。服务器会针对已发送的消息返回 MIC。可以通过 recv_token() 检索此消息。
然后,使用 gss_verify_mic() 函数检验消息的签名(即 MIC)。gss_verify_mic() 用于将收到的 MIC 与未包装的原始消息进行比较。收到的 MIC 来自服务器的令牌,该令牌存储在 out_buf 中。来自未包装版本的消息的 MIC 存放在 in_buf 中。如果这两个 MIC 匹配,系统便会检验此消息。客户机随后会为所收到的令牌释放缓冲区 out_buf。
以下源代码说明了读取和检验签名块的过程。
此示例的源代码也可以通过 Sun 下载中心获取。请访问 http://www.sun.com/download/products.xml?id=41912db5
/* Read signature block into out_buf */ if (recv_token(s, &out_buf) < 0) { (void) close(s); (void) gss_delete_sec_context(&min_stat, &context, GSS_C_NO_BUFFER); return -1; } /* Verify signature block */ maj_stat = gss_(&min_stat, context, &in_buf, &out_buf, &qop_state); if (maj_stat != GSS_S_COMPLETE) { display_status("verifying signature", maj_stat, min_stat); (void) close(s); (void) gss_delete_sec_context(&min_stat, &context, GSS_C_NO_BUFFER); return -1; } (void) gss_release_buffer(&min_stat, &out_buf); if (use_file) free(in_buf.value); printf("Signature verified.\n");
call_server() 函数通过删除上下文并返回到 main() 函数来结束操作。
此示例的源代码也可以通过 Sun 下载中心获取。请访问 http://www.sun.com/download/products.xml?id=41912db5
/* Delete context */ maj_stat = gss_delete_sec_context(&min_stat, &context, &out_buf); if (maj_stat != GSS_S_COMPLETE) { display_status("deleting context", maj_stat, min_stat); (void) close(s); (void) gss_delete_sec_context(&min_stat, &context, GSS_C_NO_BUFFER); return -1; } (void) gss_release_buffer(&min_stat, &out_buf); (void) close(s); return 0;