GSS-API のプログラミング

クライアント側の GSS-API: gss-client

サンプルのクライアント側プログラム gss-client は、サーバーとのセキュリティコンテキストを作成し、セキュリティパラメータを確立し、文字列 (メッセージ) をサーバーに送信します。接続にはシンプルな TCP ベースのソケット接続を使用します。

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



gss-client [-port port] [-d] [-mech mech] host service [-f] msg

gss-client は主に次の作業を行います。

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

  2. 機構の OID (オブジェクト ID) を作成します (指定されている場合)。

  3. サーバーとの接続を設定します。

  4. コンテキストを確立します。

  5. メッセージをラップします。

  6. メッセージを送信します。

  7. サーバーが正しくメッセージに署名していることを検証します。

以降では、gss-client がどのように動作するかを段階的に説明します。機能性を示すために設計されたサンプルプログラムであるため、上記の手順にあまり関係のない部分は省略しています。コンテキストのインポートやエクスポート、ラップサイズの取得などの機能については、このマニュアルの他の場所を参照してください。

概要: main() (クライアント)

すべての C プログラムと同様に、プログラムの外部シェルはエントリポイント関数 main() に含まれます。main() は次の 4 つの機能を実行します。

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

    • port が指定されている場合、porthost で指定されたリモートマシンへの接続を確立するポート番号です。

    • -d フラグが設定されている場合、セキュリティ資格はサーバーに委託されます。特に、deleg_flag 変数は GSS-API 値 GSS_C_DELEG_FLAG に設定されます。-d フラグが設定されていない場合、deleg_flag は 0 に設定されます。

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

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

    • 最後に、msg は保護されたデータとしてサーバーに送信される文字列です。-f オプションが指定されている場合、msg は文字列を読み取るべきファイル名です。

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


    % gss-client -port 8080 -d -mech kerberos_v5 erebos.eng nfs "ls"
    

    次のコマンド行は機構もポートも指定せず、委託も使用しません。


    % gss-client erebos.eng nfs "ls"
    

  2. parse_oid() を呼び出して、(コマンド行に指定されている場合) セキュリティ機構名から、GSS-API OID (オブジェクト識別子) を作成します。


    if (mechanism)
             parse_oid(mechanism, &g_mechOid);

    mechanism は変換される文字列で、g_mechOid は機構の gss_OID オブジェクトへのポインタです。デフォルト以外の機構の指定については、付録 C 「OID の指定」 を参照してください。

  3. call_server() を呼び出します。この関数は、コンテキストの作成とデータの送信を実際に行います。


    if (call_server(hostname, port, g_mechOid, service_name,
                       deleg_flag, msg, use_file) < 0)
              exit(1);
  4. OID の記憶領域を解放します (まだ解放されていない場合)。


    if (g_mechOID != GSS_C_NULL_OID)
         (void) gss_release_oid(&min_stat, &g_mechoid);

    gss_release_oid() は、GSS-API の Sun の実装ではサポートされますが、すべての GSS-API 実装でサポートされるわけではなく、標準ではないと考えられます。アプリケーションは、gss_str_to_oid() で機構を割り当てるのではなく、可能な限り GSS-API が提供するデフォルトの機構を使用するべきです。したがって、gss_release_oid() コマンドは通常使用するべきではありません。

デフォルト以外の機構の指定

一般的に、GSS-API を使用するアプリケーションは特定の機構を指定せず、GSS-API 実装が提供するデフォルトの機構を使用すべきです。デフォルトの機構を指定するには、機構の表す gss_OID を値 GSS_C_NULL_OID に設定します。

デフォルト以外の機構を設定することは推奨されないため、このプログラムでは取り上げません。クライアントアプリケーションがユーザー指定の (デフォルト以外の) 機構名を解析する方法については、parse_oid()のコードを参照してください。また、デフォルト以外の OID を使用する方法については、付録 C 「OID の指定」 を参照してください。

サーバーの呼び出し

機構を gss_OID 形式で格納した後、実際の作業を行うことができます。つまり、main() は、コマンド行引数とほとんど同じ引数で、call_server() 関数を呼び出します。


call_server(hostname, port, g_mechOid, service_name,
     deleg_flag, msg, use_file);

use_file は、送信されるメッセージがファイルに格納されているかどうかを示すフラグです。

サーバーへの接続

変数を宣言した後、call_server() はまずサーバーとの接続を設定します。


if ((s = connect_to_server(host, port)) < 0)
     return -1;
s はファイル記述子で (型は int)、最初は socket() の呼び出しから戻されます。

connect_to_server() は、ソケットを使用して接続を作成するシンプルな関数です。この関数は GSS-API を使用しないため、ここでは省略します。connect_to_server() についての詳細は、connect_to_server()を参照してください。

コンテキストの確立

接続が確立された後、call_server()client_establish_context() 関数を使用して、セキュリティコンテキストを確立します。


int client_establish_context(s, service_name, deleg_flag, oid, 
     &context, &ret_flags)

コンテキストを起動するために、アプリケーションは gss_init_sec_context() 関数を使用します。ほとんどの GSS-API 関数と同様に、この関数でも名前は GSS-API 内部形式である必要があります。したがって、アプリケーションはまず、サービス名を文字列から内部形式に変換する必要があります。このためには、gss_import_name() を使用します。


maj_stat = gss_import_name(&min_stat, &send_tok, 
     (gss_OID) GSS_C_NT_HOSTBASED_SERVICE, &target_name);

この関数は引数としてサービス名を受け取り (参照できない (不透明な) GSS-API バッファ send_tok に格納される)、GSS-API 内部名 target_name に変換します。send_tok は新しい gss_buffer_desc を宣言せず、領域を節約するために使用されます。3 番目の引数は gss_OID 型で、send_tok に格納されている名前の形式を示します。この場合は GSS_C_NT_HOSTBASED_SERVICE で、サービスの形式が service@host であることを意味します。この引数に有効なその他の値については、名前型を参照してください。

サービスを GSS-API 内部形式に変換した後、コンテキストを確立する作業に進むことができます。移植性を最大限にするには、コンテキストの確立を常にループとして実行する必要があります。コンテキストの起動 (クライアント)を参照してください。

まず、アプリケーションはコンテキストを NULL に初期化します。


*gss_context = GSS_C_NO_CONTEXT;
次に、サーバーから受け取るトークンも NULL に初期化します。


token_ptr = GSS_C_NO_BUFFER;

次に、アプリケーションはループに入ります。ループは 2 つのことを検査しながら進みます。gss_init_sec_context() から戻される状態と、サーバーに送信されるトークンのサイズ (この値も gss_init_sec_context() で生成される) です。トークンのサイズが 0 の場合、サーバーがクライアントからこれ以上のトークンを期待していないことを意味します。次に、このループの疑似コードを示します。


do{
      gss_init_sec_context()
      if (コンテキストが全く確立されなかった場合)
                 エラーを出力して終了する
      if (状態が「完了」または「処理中」のどちらでもない場合
                 サービスの名前空間を解放し、エラーを出力して終了する
      if (サーバーに送信するトークンがある場合。つまり、サイズが 0 以外の場合)
                 トークンを送信する
                 if (トークンの送信が失敗した場合)
                 トークンとサービスの名前空間を解放し、エラーを出力して終了する
                 先ほど送信したトークンの名前空間を解放する
      if (コンテキストの確立が完了していない場合
                 サーバーからトークンを受け取る
} while (コンテキストが完全になるまで)

次に、gss_init_sec_context() への呼び出しを示します。


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,
                           &send_tok,
                           ret_flags,
                           NULL);

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

コンテキストを起動する前には、クライアントは資格を獲得する必要がないことに注意してください。クライアント側では、資格の管理は GSS-API によって透過的に処理されます。つまり、(通常はログイン時に) プリンシパルのために機構が作成した資格をどのように取得するかを、GSS-API は知っているということです。このため、アプリケーションは gss_init_sec_context() にデフォルトの資格を渡しています。しかし、サーバー側では、サーバーアプリケーションはコンテキストを受け入れる前に、サービスの資格を明示的に獲得する必要があります。資格の獲得を参照してください。

コンテキスト (完成されている必要はない) があることと、gss_init_sec_context() が有効な状態を戻したことを検査した後、アプリケーションは gss_init_sec_context() がサーバーに送信すべきトークンをコンテキストに渡しているかどうかを調べます。渡していない場合、これは、トークンが (これ以上) 必要でないことをサーバーが示しているためです。渡している場合、トークンをサーバーに送信します。トークンの送信が失敗した場合、トークンの名前空間とサービスを解放し、(エラーで) 終了します。トークンの存在をチェックするには、トークンの長さ (サイズ) を調べることに注意してください。


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()を参照してください。GSS-API 自身はトークンを送受信しないことに注意してください。GSS-API が作成したトークンを送受信するのは、呼び出し側アプリケーションの責任です。

サーバーが (これ以上) 送信するトークンを持っていない場合、gss_init_sec_context()GSS_S_COMPLETE を戻します。つまり、gss_init_sec_context() がこの値を戻さなかった場合、アプリケーションはまだ受け取るトークンがサーバーに残っていると判断できます。受け取りが失敗した場合、アプリケーションはサービスの名前空間を解放し、(エラーで) 終了します。


if (maj_stat == GSS_S_CONTINUE_NEEDED) {
               if (recv_token(s, &recv_tok) < 0) {
                    (void) gss_release_name(&min_stat, &target_name);
                    return -1;

最後に、プログラムはトークンのポインタをリセットします。そして、コンテキストが完全に確立されるまで、ループを繰り返します。したがって、do ループの終了は次のようになります。


} while (maj_stat == GSS_S_CONTINUE_NEEDED);

データの送信

セキュリティコンテキストが確立された後、gss-client はデータをラップし、データを送信し、そして、サーバーが戻した「署名」を検証する必要があります。他にも行うことはありますが (コンテキストについての情報の表示など)、gss-client はプログラム例であるため、ここでは省略して、データの送信と検証を行います。まず、送信すべきメッセージ (ls など) をバッファに格納します。


     if (use_file) {
         read_file(msg, &in_buf);
     } else {
         /* メッセージをラップする */
         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, conf_req_flag, 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");
     }

このように、in_buf に格納されたメッセージは、context で参照されるサーバーに送信されます。このとき、機密性サービスとデフォルトの保護品質 (QOP) も要求されます。保護品質とは、データを変換するときに適用されるアルゴリズムのことです。移植性のためには、可能な限りデフォルトの保護品質を使用するようにします。gss_wrap() はメッセージをラップし、その結果を out_buf に格納し、機密性が実際にラップで適用されたかどうかをフラグ (state) に設定します。

クライアントは独自の send_token() 関数で、ラップされたメッセージをサーバーに送信します。send_token() 関数については、コンテキストの確立を参照してください。


send_token(s, &outbuf)

メッセージの検証

次に、送信したメッセージの有効性を検証します。送信したメッセージの MIC がサーバーから戻されることが判明しているため、プログラムは独自の recv_token() 関数で MIC (Message Integrity Code) を受け取ります。そして、gss_verify_mic() でサーバーの「署名」(MIC) を検証します。


   maj_stat = gss_verify_mic(&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;
   }

gss_verify_mic() はサーバーのトークン (out_buf に格納されている) とともに受け取った MIC を、オリジナルのラップ解除されたメッセージ (in_buf に格納されている) から作成した MIC と比較します。2 つの MIC が一致した場合、メッセージの有効性は検証されたことになります。次に、クライアントは受け取ったトークンのバッファ (out_buf) を解放します。

終了時、call_server() はコンテキストを削除し、main() に戻ります。