実際に GSS-API を使用するプロセスを考える前に、4 つの基本的な概念について検証します。プリンシパル、GSS-API データ型、状態コード、およびトークンです。
ネットワークセキュリティの用語では、「プリンシパル」とは、ユーザー、プログラム、またはマシンを指します。プリンシパルはクライアントまたはサーバーのどちらにでもなり得ます。たとえば、他のマシンにログインしているユーザー (joe@machine)、ネットワークサービス (nfs@machine)、アプリケーションを実行しているマシン (swim2birds@eng.company.com) などがプリンシパルです。
GSS-API では、プリンシパルは特別なデータ型で示されます。名前を参照してください。
次の節では、より重要な表示できる GSS-API データ型について説明します。詳細は、GSS-API データ型と値を参照してください。
割り当てられたすべてのデータ領域を解放するのは呼び出し元アプリケーションの責任です。
int のサイズはプラットフォームによって異なるため、GSS-API は次の整数型を提供します。
OM_uint32
これは、32 ビットの符号なし整数です。
GSS-API はすべてのデータを内部形式で処理するため、文字列は GSS-API 関数に渡す前に GSS-API 形式に変換しておく必要があります。GSS-API は文字列を gss_buffer_desc 構造体で処理します。gss_buffer_t は gss_buffer desc 構造体へのポインタです。
typedef struct gss_buffer_desc_struct { size_t length; void *value; } gss_buffer_desc *gss_buffer_t; |
したがって、文字列は GSS-API 関数に渡す前に gss_buffer_desc 構造体に変換しておく必要があります。ここでは、メッセージを受け取ってなんらかの方法で処理する (たとえば、転送する前にメッセージに保護を適用するなど)、次のような汎用的な GSS-API 関数を考えます。
char *message_string; gss_buffer_desc input_msg_buffer; input_msg_buffer.value = message_string; input_msg_buffer.length = strlen(input_msg_buffer.value) + 1; gss_generic_function(arg1, &input_msg_buffer, arg2...); gss_release_buffer(input_msg_buffer); |
終了時には gss_release_buffer() で input_msg_buffer を解放する必要があることに注意してください。
gss_buffer_desc オブジェクトは文字列だけに使用されるわけではありません。たとえば、トークンも gss_buffer_desc オブジェクトとして処理されます。GSS-API トークンを参照してください。
「名前」はプリンシパルを指します。つまり、joe@company や nfs@machinename などのように、人、マシン、またはアプリケーションを指します。GSS-API では、名前は gss_namae_t オブジェクトとして格納され、アプリケーションでは意識する必要はありません。名前は gss_import_name() 関数によって gss_buffer_t オブジェクトから gss_name_t 形式に変換されます。インポートされたすべての名前には関連する名前型が割り当てられます。名前型とは、その名前の形式の種類を示すものです。名前型についての詳細は、OIDを参照してください。また、有効な名前型のリストについては、名前型を参照してください。
次に、gss_import_name() の例を示します。
OM_uint32 gss_import_name ( OM_uint32 *minor_status, const gss_buffer_t input_name_buffer, const gss_OID input_name_type, gss_name_t *output_name) |
実際の機構から戻される状態コード。状態コードを参照してください。
インポートされた名前が格納される gss_buffer_desc 構造体。アプリケーションはこの構造体を明示的に割り当てる必要があります。例 1–2、および 文字列および類似のデータを参照してください。使用し終わったとき、この引数は gss_release_buffer() で解放する必要があります。
input_name_buffer の形式を示す gss_OID。名前型を参照してください。また、有効な名前型のリストについては、名前型を参照してください。
名前を受け取る gss_name_t 構造体。
次に、例 1-1 で使用した汎用例を少しだけ変更します。ここでは、どのように gss_import_name() を使用するかを示します。まず、通常の文字列を gss_buffer_desc 構造体に変換して、次に、gss_import_name() で gss_name_t 構造体に変換します。
char *name_string; gss_buffer_desc input_name_buffer; gss_name_t output_name_buffer; input_name_buffer.value = name_string; input_name_buffer.length = strlen(input_name_buffer.value) + 1; gss_import_name(&minor_status, input_name_buffer, GSS_C_NT_HOSTBASED_SERVICE, &output_name); gss_release_buffer(input_name_buffer); |
インポートされた名前を gss_buffer_t オブジェクトに戻して、gss_display_name() で人が読める形式で表示することも可能です。しかし、実際の機構が名前を保存する方法の違いにより、gss_display_name() は結果の文字列がオリジナルと同じであることを保証できません。GSS-API には他にも名前を処理する関数があります。GSS-API 関数を参照してください。
gss_name_t 構造体は、単一の名前について複数のバージョンを持つことができます。つまり、GSS-API がサポートする機構ごとに 1 つのバージョンが生成されます。たとえば、「joe@company」の gss_name_t 構造体は Kerberos v5 用の名前と他の機構用の名前を持つなどです。GSS-API は gss_canonicalize_name() という関数を提供します。この関数は、内部名 (つまり、gss_name_t 構造体) と機構を入力として受け取り、その機構に特定な名前のバージョンを 1 つだけ持つ別の内部名 (これも gss_name_t 構造体) を出力します。
このような機構に固有な名前のことを「機構名 (Mechanism Name: MN)」と呼びます。機構名は、機構自身の名前を指すのではなく、その機構が生成したプリンシパルの名前を指すため、すこし紛らわしいかもしれません。図 1–3 に、このプロセスを図示します。
なぜこのような関数が便利なのでしょうか。ここでは、サーバーがクライアントから名前を受け取り、その名前をアクセス制御リストから検索する例を考えます。アクセス制御リスト (Access Control List: ACL) とは、特定のアクセス権を備えたプリンシパルのリストのことです。このためには、次のような方法が考えられます。
gss_import_name() で、クライアント名を GSS-API 内部形式でインポートします (まだインポートされていない場合)。
サーバーの中には、内部形式で名前を受け取るものもあります。この場合、この手順は必要ありません。特に、サーバーがクライアント独自の名前を検索する場合です。コンテキストの起動中、クライアント独自の名前は内部形式で渡されます。
gss_import_name() で、各 ACL 名を インポートします。
gss_compare_name() で、インポートした ACL 名とインポートしたクライアント名をそれぞれ比較します。
この手順は、クライアント名と比較する名前の数が少ない場合には有効です。しかし、非常に処理が遅いため、大きなリストを比較するときには不都合です。ACL 名ごとに gss_import_name() と gss_compare_name() を実行すると、膨大な CPU サイクルが必要です。したがって、次のような方法を使用します。
gss_import_name() で、クライアント名をインポートします (まだインポートされていない場合)。
前述の方法と同様に、サーバーが内部形式で名前を受け取る場合、この手順は必要ありません。
gss_canonicalize_name() で、クライアント名の MN を生成します。
gss_export_name() で、「エクスポート名」を生成します。エクスポート名とは、クライアント名の連続する文字列バージョンのことです。
memcmp() で、エクスポートされたクライアント名を ACL 内のすべての名前と比較します。memcmp() は高速で、オーバーヘッドの少ない関数です。
gss_export_name() は機構名 (MN) を期待するため、あらかじめ、クライアント名に対して gss_canonicalize_name() を実行する必要があります。
詳細は、gss_canonicalize_name(3GSS)、gss_export_name(3GSS)、および gss_import_name(3GSS) のマニュアルページを参照してください。
オブジェクト識別子 (Object Identifier: OID) は、セキュリティ機構、QOP の値 (保護品質の値)、および名前型などのデータを格納するときに使用します。OID は GSS-API の ggss_OID_desc 構造体に格納されます。GSS-API は次のような gss_OID_desc 構造体へのポインタ (gss_OID) を提供します。
typedef struct gss_OID_desc_struct { OM_uint32 length; void *elements; } gss_OID_desc, *gss_OID; |
さらに、gss_OID_set_desc 構造体は1つまたは複数の OID を含むことができます。
typedef struct gss_OID_set_desc_struct { size_t count; gss_OID elements; } gss_OID_set_desc, *gss_OID_set; |
アプリケーションは free()で OID を解放してはいけません。
GSS-API を使用すると、アプリケーションはどの実際のセキュリティ機構を使用するかを選択できます。しかし、アプリケーションは可能な限り、GSS-API が選択したデフォルトの機構を使用するべきです。同様に、GSS-API を使用すると、アプリケーションはどの QOP (保護品質) でデータを保護するかを指定できます。QOP とは、データの暗号化や暗号識別タグの生成に使用されるアルゴリズムのことです。しかし、可能な限り、デフォルトの QOP を使用するべきです。デフォルトの機構を表すには、機構または QOP を期待する関数に値 GSS_C_NULL_OID
を引数として渡します。
セキュリティ機構または QOP を明示的に指定するとアプリケーションの移植性を制限してしまうため、GSS-API を使用する目的が多かれ少なかれ損なわれます。他の実装の GSS-API がその機構または QOP をサポートしなかったり、サポートする方法や範囲が異なる場合があるためです。ただし、付録 C 「OID の指定」 では、使用できる機構と QOP を示して、その選択方法を示しています。
QOP とセキュリティ機構の他に、OID でも名前型を指定できます。名前型とは、関連する名前の形式を示すものです。たとえば、gss_import_name() 関数はプリンシパルの名前を文字列から gss_name_t 型に変換しますが、この関数は変換すべき文字列の形式を引数の 1 つとして受け取ります。たとえば、名前型が GSS_C_NT_HOSTBASED_SERVICE である場合、gss_import_name() 関数は入力された名前が "service@host" 形式 (たとえば、"nfs@swim2birds") であることが分かります。また、たとえば名前型が GSS_C_NT_EXPORT_NAME である場合、gss_import_name() 関数は入力された名前が GSS-API エクスポート名であることが分かります。アプリケーションは gss_inquire_names_for_mech() 関数を使用すると、指定した機構で使用できる名前型を知ることができます。GSS-API が使用する名前型のリストについては、名前型を参照してください。
すべての GSS-API 関数は 2 種類のコードを戻して、関数が成功したか失敗したかについての情報を提供します。どちらの種類の状態コードも OM_uint32 値として戻されます。次に、この 2 種類の戻りコードについて説明します。
メジャー状態コード。メジャー状態コードは、
a) 汎用 GSS-API ルーチンエラー (ルーチンに無効な機構を指定したなど)
b) 特定の GSS-API 言語バインディングに固有な呼び出しエラー (つまり、関数の引数が読めない、書き込めない、または形式が間違っているなど)
c) a) と b) の両方
のいずれかを示します。さらに、メジャー状態コードはルーチンの状態について補助的な情報も提供できます。たとえば、操作が終了していない、トークンが送信された順番が間違っているなどです。なにもエラーが発生しなかった場合、ルーチンは値が GSS_S_COMPLETE のメジャー状態コードを戻します。
メジャー状態コードは次のように戻されます。
OM_uint32 major_status ; /* GSS-API が返す状態 */ major_status = gss_generic_function(arg1, arg2 ...); |
メジャー状態戻りコードは他の OM_uint32 と同じように処理できます。次に例を示します。
OM_uint32 maj_stat; maj_sta = gss_generic_function(arg1, arg2 ...); if (maj_stat == GSS_CREDENTIALS_EXPIRED) <何らかの処理を行う...> |
マイナー状態コード。マイナー状態コードは実際の機構が戻すものです。したがって、このマニュアルでは特に説明しません。
すべての GSS-API 関数は最初の引数として OM_uint32 のマイナーコード状態を受け取ります。関数が呼び出された関数に戻るとき、マイナー状態コードは次のように格納されます。
OM_uint32 *minor_status ; /* mech が返す状態 */ major_status = gss_generic_function(&minor_status, arg1, arg2 ...); |
GSS-API が致命的なメジャーコードエラーを戻す場合でも、minor_status パラメータは常に GSS-API ルーチンによって設定されますが、他のほとんどの出力パラメータは設定されません。しかし、GSS-API ルーチンによって割り当てられた記憶領域へのポインタが戻されると期待される出力パラメータは NULL に設定されます。つまり、記憶領域は実際には割り当てられていないと示されます。このようなポインタに関連する長さフィールド (gss_buffer_desc 構造体を参照) は 0 に設定されます。このような場合は、アプリケーションはこれらのバッファを解放する必要はありません。
GSS-API における流通の基本単位は「トークン」です。GSS-API を使用するアプリケーションはトークンを使用してお互いに通信し、データを交換したり、セキュリティを取り決めたりします。トークンは gss_buffer_t データ型として宣言され、アプリケーションでは意識する必要はありません。
トークンには 2 種類あります。コンテキストレベルトークンとメッセージ毎トークンです。コンテキストレベルトークンは、主に、コンテキストが確立される (起動され、受け入れられる) ときに使用されます。しかし、その後でコンテキストを管理するためにも使用されます。
メッセージ毎トークンは、コンテキストが確立された後で使用されます。そして、データへの保護サービスを提供します。たとえば、アプリケーションがメッセージを別のアプリケーションに送信したい場合、そのアプリケーションは GSS-API を使用して暗号識別子を生成し、そのメッセージと一緒に送信します。すると、その識別子はトークンに格納されます。
メッセージ毎トークンは「メッセージ」という観点からは次のように考えることができます。メッセージとは、アプリケーションがピアに送信するデータの一部です。たとえば、ls コマンドは ftp サーバーにメッセージを送信します。メッセージ毎トークンとは、このようなメッセージに対して GSS-API が生成するもの (暗号化タグやメッセージの暗号化形式など) です。意味論上、最後の例は若干正確ではありません。トークンとは GSS-API が生成した情報にしか過ぎないため、暗号化メッセージはやはりメッセージであり、トークンとは言えません。しかし、正式にではありませんが、「メッセージ」と「メッセージ毎トークン」は同じ意味で使用されることがあります。
次の作業は、GSS-API ではなく、アプリケーションの責任です。
トークンを送受信すること。開発者はこのようなアクションを実行するために、通常、汎用的な読み取り関数と書き込み関数を作成する必要があります。このような関数の例については、send_token()と recv_token()を参照してください。
トークンはアプリケーションでは意識する必要がないため、アプリケーションにとっては各トークンに違いはありません。アプリケーションはトークンを適切な GSS-API 関数に渡す前に、トークンの内容が分からなくても、トークンを区別できる方法があります。次に、アプリケーションがトークンを区別するための方法を示します。
状態によって (つまり、プログラムの制御フローを通じて)。たとえば、アプリケーションがコンテキストを受け入れるために待機している場合、アプリケーションは、ピアがコンテキストが完全に確立されるまで待機することが判明している、つまり、メッセージ (データ) トークンは送信されないと仮定できるため、受け取るトークンがコンテキスト確立に関連するコンテキストレベルトークンであると仮定できます。コンテキストが確立された後、アプリケーションは受け取るトークンがメッセージトークンであると仮定できます。これは、トークンを処理する最も一般的な方法です。後述のサンプルプログラムでもこの方法を使用しています。
トークンを送受信するとき、アプリケーションはトークンの種類を区別できます。たとえば、アプリケーションが独自の関数でピアにトークンを送信する場合、送信するトークンの種類を示すフラグを含めることができます。
gss_buffer_t token; /* トークンを宣言 */ OM_uint32 token_flag /* トークンの型を記述するフラグ */ <get token from a GSS-API function> token_flag = MIC_TOKEN; /* トークンの種類を指定 */ send_a_token(&token, token_flag); |
3 番目の方法は明示的なタグ付けを行うことです。たとえば、アプリケーションは独自の「メタトークン」を使用できます。メタトークンとはユーザー定義の構造体であり、GSS-API 関数から受け取ったトークンとともに、GSS-API が提供するトークンをどのように使用するかを示すユーザー定義フィールドを格納できます。
GSS-API では、マルチプロセスアプリケーションにおいて、あるプロセスから別のプロセスにセキュリティコンテキストを送信できます。一般的に、マルチプロセスアプリケーションはクライアントのコンテキストを受け入れ、プロセス間で共有します。マルチプロセスアプリケーションについては、コンテキストのエクスポートとインポートを参照してください。
gss_export_context() 関数が作成するプロセス間トークンには、2 番目のプロセスがコンテキストを再構築できるような情報が含まれています。このプロセス間トークンをあるプロセスから別のプロセスに渡すのはアプリケーションの責任であり、また、トークンを別のアプリケーションに渡すのもアプリケーションの責任です。
このプロセス間トークンには鍵となる、つまり他の重要な情報が格納されることもあります。ところが、必ずしもすべての GSS-API 実装がプロセス間トークンを暗号化で保護するとは保証できません。したがって、アプリケーションはプロセス間トークンを送受信する前に保護する必要があります。たとえば、暗号化を使用できる場合は、gss_wrap() でプロセス間トークンを暗号化するなどです。
異なる GSS-API 実装間では、プロセス間トークンを転送できるとは限りません。