この章では、gss-server サンプルプログラムのソースコードについて段階的に説明します。次の項目について説明します。
サーバー側プログラム例 gss-server は、前章で説明した gss-client と連携して動作します。gss-server の基本目的は、gssapi-client からラップ済みメッセージを受け取り、そのメッセージに署名して戻すことです。
以降では、gss-server がどのように動作するかを段階的に説明します。gss-server は GSS-API の機能説明用のプログラム例であるため、関連する部分だけを詳しく説明します。2 つのアプリケーションの完全なソースコードは付録に含まれています。また、次の場所からダウンロードすることもできます。
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 は、応答を待つポートの番号です。port が指定されていない場合、プログラムはデフォルトでポート 4444 を使用します。
-verbose を指定すると、gss-server 実行時にメッセージが表示されます。
-inetd は、プログラムが inetd デーモンを使ってポートで応答を待つべきであることを示します。-inetd は、stdin と stdout を使ってクライアントに接続します。
-once を指定すると、1 つの接続しか作成されなくなります。
mechanism は、使用するセキュリティー機構 (Kerberos v5 など) の名前です。機構が指定されていない場合、GSS-API はデフォルトの機構を使用します。
service-name は、クライアントが要求するネットワークサービス (telnet、ftp、login など) の名前です。
次に、一般的なコマンド行の例を示します。
% gss-server -port 8080 -once -mech kerberos_v5 erebos.eng nfs "hello" |
gss-server の main() 関数は次の作業を実行します。
コマンド行引数を解析し、それらを変数に代入します
機構に対応するサービスの資格を獲得します
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 is 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() は、その後、サービスの資格を戻します。
server_acquire_creds() は GSS-API 関数 gss_acquire_cred() を使って、サーバーが提供するサービスの資格を取得します。server_acquire_creds() が gss_acquire_cred() にアクセスする前に、server_acquire_creds() は次の 2 つの作業を行う必要があります。
資格を取得できるように、機構リストの中身を検査し、そのリストに単一の機構だけが含まれるようにします。
1 つの資格を複数の機構で共有できる場合、gss_acquire_cred() 関数はこのような機構すべての資格を戻します。したがって、gss_acquire_cred() は入力として機構の「セット」を受け取ります。「GSS-API における資格の操作」を参照してください。しかしながら、この例も含めてほとんどの場合、単一の資格が複数の機構で動作することはありません。gss-server プログラムでは、コマンド行から単一の機構が指定されるか、またはデフォルトの機構が使用されます (指定されなかった場合)。したがって、最初の作業は、gss_acquire_cred() に渡される機構セットに 1 つの機構 (デフォルトまたはそれ以外) だけが入っていることを確認することです。次にコードを示します。
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 へのポインタです。3 番目の引数 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 は、適用される機構や資格の有効期間をプログラムが知る必要がないことを示します。
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 は、inetd が接続の受け渡しに使用する標準入力を指定して sign_server() を呼び出します。それ以外の場合、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 など) をサポートするかどうかを示すさまざまなフラグです。
2 つの NULL 引数は、コンテキストの有効期間と、サーバーがクライアントのプロキシとして動作できるかどうかを、プログラムが知る必要がないことを示しています。
gss_accept_sec_context() が maj_stat に GSS_S_CONTINUE_NEEDED を設定している限り、受け入れループは継続します (エラーの場合を除く)。maj_stat の値が GSS_S_CONTINUE_NEEDED でも 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() への 2 つの引数に注目してください。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 を生成し、その結果を 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() は、コンテキストをエクスポートするのにかかった時間を表示し、次に、インポートするのにかかった時間を表示します。test_import_export_context() は、実際には機能しない関数ですが、GSS-API のインポートおよびエクスポート機能をどのように使えばよいかを示しています。また、test_import_export_context() は、コンテキスト操作時のタイムスタンプの使い方も示しています。
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);