GSS-API のプログラミング

サーバー側の GSS-API: gss-server

通常クライアントは、セキュリティハンドシェークを実行するためにサーバーを必要とします。クライアントはセキュリティコンテキストを起動し、データを送信します。一方、サーバーはコンテキストを受け入れ、クライアントの ID を検証する必要があります。この中で、サーバーは自分自身をクライアントに対して認証したり (要求された場合)、データの「署名」をクライアントに提供したりする場合もあります。もちろん、これに加えて、サーバーはデータを処理する必要があります。

次に、gss-server のコマンド行形式を示します。


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

gss-server は次の作業を行います。

  1. コマンド行を解析します。

  2. コマンド行に指定された機構名を、内部形式に変換します (ある場合)。

  3. 呼び出し側の資格を獲得します。

  4. inetd デーモンを使用して接続するようにユーザーが指定しているかどうかをチェックします。

  5. 接続を確立します。

  6. データを取得します。

  7. データに署名し、署名を戻します。

  8. 名前空間を解放し、終了します。

以降では、gss-server がどのように動作するかを段階的に説明します。機能性を示すために設計されたサンプルプログラムであるため、上記の手順にあまり関係のない部分は省略しています。

概要: main() (サーバー)

gss-clientmain() 関数から始めます。main() は次の作業を実行します。

  1. コマンド行引数を解析し、各引数を変数に割り当てます。

    • port が指定されている場合、port は待機するポート番号です。port が指定されていない場合、プログラムはデフォルトでポート 4444 を使用します。

    • -verbose が指定されている場合、プログラムはデバッグに類似したモードで動作します。

    • -inetd オプションは、プログラムが inetd デーモンを使用してポートで待機することを指示します。inetdstdinstdout を使用してクライアントとの接続を処理します。

    • -once が指定されている場合、プログラムは 1 つのインスタンス接続だけを作成します。

    • (省略可能) mechanism は使用されるセキュリティ機構名 (Kerberos v5 など) です。機構が指定されていない場合、GSS-API はデフォルトの機構を使用します。

    • クライアントから要求されたネットワークサービス名 (telnetftplogin などのサービス) は service_name で指定されます。

    次に、コマンド行の例を示します。


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

  2. 機構を GSS-API オブジェクト識別子 (OID) に変換します (指定されている場合)。これは、GSS-API 関数が名前を内部形式で処理するためです。

  3. 使用される機構 (Kerberos v5 など) のために、サービス (ftp など) の資格を獲得します。

  4. sign_server() 関数を呼び出します。この関数は、接続の確立、メッセージの受け取り、その署名など、ほとんどの作業を実際に行います。

    inetd を使用するようにユーザーが指定している場合、プログラムは標準出力と標準エラーを閉じ、標準入力で sign_server() を呼び出します。inetd はこの sign_server() を使用して接続を渡します。そうでない場合、プログラムはソケットを作成し、そのソケットの接続を TCP 関数 accept() で受け入れ、accept() から戻されたファイル記述子で sign_server() を呼び出します。

    inetd を使用しない場合、プログラムは終了されるまで接続とコンテキストを作成します。しかし、ユーザーが -once オプションを指定している場合、ループは最初の接続の後で終了します。

  5. 獲得した資格を解放します。

  6. 機構 OID の名前空間を解放します。

  7. 接続を閉じます (まだ開いている場合)。

機構の OID の作成

gss-client プログラムの例と同様に、サンプルのサーバープログラムでもユーザーは機構を指定できます。しかし、すべてのアプリケーションで、GSS-API 実装が提供するデフォルトの機構を使用することを強く推奨します。デフォルトの機構を指定するには、機構の表す gss_OID を値 GSS_C_NULL_OID に設定します。コードについては、createMechOid()を参照してください。また、デフォルト以外の機構を使用する方法については、付録 C 「OID の指定」 を参照してください。

資格の獲得

クライアントアプリケーションと同様に、サーバーアプリケーションと GSS-API はどちらも資格を作成しません。資格は実際の機構が作成します。クライアントプログラムとは異なり、サーバーは必要な資格を明示的に獲得する必要があります。クライアントアプリケーションの中にも、明示的に資格を獲得するものはあります。この場合も、ここで説明する方法を使用します。しかし、一般的には、クライアントはログイン時にすでに資格を獲得しており、GSS-API はこのような資格を自動的に獲得します。

gss-server プログラムは独自の関数 server_acquire_creds() を持っています。この関数は提供されるサービスの資格を取得します。この関数は、入力としてサービス名と使用されるセキュリティ機構を受け取り、そのサービスの資格を戻します。

server_acquire_creds() は GSS-API 関数 gss_acquire_cred() を使用して、サーバーが提供するサービスの資格を取得します。しかし、これを行う前には、次の 2 つのことを処理する必要があります。

1 つの資格を複数の機構で共有できる場合、gss_acquire_cred() はこのような機構すべての資格を戻します。したがって、gss_acquire_cred() は入力として 1 つの機構ではなく、機構の集合を受け取ります (資格を参照)。しかし、ほとんどの場合 (このプログラムも含む)、1 つの資格は複数の機構で機能しません。さらに、サーバーアプリケーションでは、1 つの機構をコマンド行に指定するか、デフォルトの機構を使用します。したがって、最初に行うことは、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_acquire_cred() はサービス名を gss_name_t 構造体の形式で受け取ります。したがって、2 番目に行うことは、サービス名を 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() を使用していることに注意してください。概要: main() (クライアント)を参照してください。

入力のサービス名は name_buf に文字列として格納されます。出力は server_name という gss_name_t 構造体へのポインタです。3 番目の引数 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);

次に、各引数について説明します。

コンテキストの受け入れと、データの取得と署名

サービスの資格を獲得した後、サーバープログラムは、ユーザーが inetd を使用するように指定しているかどうかを調べて (概要: main() (サーバー)を参照)、次に、sign_server() を呼び出します。sign_server() はプログラムの主な作業を行います。sign_server() が最初に行うことは、server_establish_context() を呼び出してコンテキストを確立することです。


注 –

ここでは、inetd については説明しません。基本的に、inetd が指定された場合、プログラムは標準入力で sign_server() を呼び出します。そうでない場合、プログラムはソケットを作成し、接続を受け入れ、次に、その接続で sign_server() を呼び出します。


sign_server() は次の作業を行います。

  1. コンテキストを受け入れます。

  2. データをラップ解除します。

  3. データに署名します。

  4. データを戻します。

コンテキストの受け入れ

コンテキストの確立では、クライアントとサーバー間で一連のトークンが交換されます。したがって、プログラムの移植性を保持するために、コンテキストの受け入れとコンテキストの起動はループで行われる必要があります。実際、コンテキストを受け入れるためのループとコンテキストを確立するためのループは (まったく逆ですが) 非常によく似ています。コンテキストの確立と比較してください。

  1. サーバーが最初に行うことは、クライアントがコンテキスト起動プロセスの一部として送信したトークンを探すことです。GSS-API 自身はトークンを送受信しないことを思い出してください。したがって、この作業を行うには、プログラムは独自のルーチンを持つ必要があります。サーバーがトークンの受信用に使用するルーチンを、この例では recv_token() としています。詳細は、recv_token()を参照してください。


         do {
              if (recv_token(s, &recv_tok) < 0)
                   return -1;
  2. 次に、プログラムは 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,    /* time_rec を無視する */
                                       NULL);   /* del_cred_handle を無視する */
    次に、各引数について説明します。
    • min_stat は実際の機構から戻されるエラー状態です。

    • context は確立されているコンテキストです。

    • server_creds は提供されているサーバーの資格です (資格の獲得を参照)。

    • recv_tokrecv_token() でクライアントから受信したトークンです。

    • GSS_C_NO_CHANNEL_BINDINGS はチャネルバインディングを使用しないことを示すフラグです (チャネルバインディングを参照)。

    • client はクライアント名 (ASCII 文字) です。

    • oid は機構です (OID 形式)。

    • send_tok はクライアントに送信するトークンです。

    • ret_flags は、コンテキストがサポートするオプション (メッセージ順序検出など) を示すさまざまなフラグです。

    • NULL は、コンテキストが有効である期間の長さにも、サーバーがクライアントのプロキシとして動作できるかどうかにも、プログラムが関与していないことを示します。

    gss_accept_sec_context()maj_statGSS_S_CONTINUE_NEEDED を設定している限り、受け入れループは継続します (エラーの場合を除く)。maj_stat の値が GSS_S_CONTINUE_NEEDED でも GSS_S_COMPLETE でもない場合、問題が発生し、ループが終了したことを示します。

  3. クライアントに返送するトークンが存在する場合、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);
              }

メッセージのラップ解除

コンテキストを受け入れた後、サーバーはクライアントから送信されたメッセージを受信します。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 にプログラムが関与していないことを示します。

メッセージへの署名とメッセージの返送

最後にサーバーがすることは、メッセージに署名することです。つまり、メッセージの MIC (メッセージ整合性コード。メッセージに関連付けられた一意なタグ) をクライアントに戻すことによって、メッセージの送信とラップ解除が正常に完了したことをクライアントに証明します。このためには、プログラムは gss_get_mic() 関数を使用します。


maj_stat = gss_get_mic(&min_stat, context, GSS_C_QOP_DEFAULT,
                            &msg_buf, &xmit_buf);
この関数は msg_buf 内のメッセージを調べて、そのメッセージから MIC を生成し、その結果を xmit_buf に格納します。次に、サーバーは send_token() で MIC をクライアントに返送します。すると、クライアントは gss_verify_mic() で MIC を検証します。メッセージの検証を参照してください。

最後に、sign_server() はいくつかのクリーンアップを実行します。つまり、gss_release_buffer() で GSS-API バッファの msg_bufxmit_buf を解放し、次に、gss_delete_sec_context() でコンテキストを無効にします。

コンテキストのインポートとエクスポート

コンテキストのエクスポートとインポートで説明したとおり、GSS-API を使用すると、コンテキストをエクスポートおよびインポートできます。通常、これを行う理由は、マルチプロセスプログラムにおける異なるプロセス間でコンテキストを共有することです。

sign_server() には検証用の関数 test_import_export_context() があります。この関数は、コンテキストのエクスポートとインポートがどのように機能するかを示します。この関数は実際にはコンテキストをプロセス間で渡しません。コンテキストをエクスポートするのにかかった時間を表示し、次に、インポートするのにかかった時間を表示するだけです。実際には機能しませんが、この関数は GSS-API のインポート関数とエクスポート関数をどのように使用するかを示すとともに、コンテキストの操作に関連するタイムスタンプをどのように使用するかの参考にもなります。test_import_export_context() の詳細は、test_import_export_context()を参照してください。

クリーンアップ

main() 関数に戻ると、アプリケーションは gss_delete_cred() でサービスの資格を削除します。機構の OID が指定されていた場合は、gss_delete_oid() で削除します。そして、終了します。