認証パラメタは内容が隠されていますが、以降の RPC プロトコルで自由に解釈できます。この節では、既に定義されているタイプの認証について説明します。別のサイトでは自由に新たな認証タイプを作成し、プログラム番号割り当て規則と同様の認証タイプ番号割り当て規則に従って、認証タイプ番号を割り当てることができます。認証タイプ番号はサン・ソフトで保守・管理しています。認証番号の割り当てを希望されるユーザは、RPC プログラム番号を割り当てるときと同様に、「プログラム番号の登録」に示すように、サン・マイクロシステムズ社の RPC 管理者に連絡してください。
呼び出し側は自身を証明せず、また、サーバ側も呼び出し側が誰でもかまわないという呼び出しもあります。その場合は RPC メッセージの認証証明、ベリファイア、応答ベリファイアの flavor (opaque_auth 共用体の要素識別子) は AUTH_NONE にします。AUTH_NONE タイプの認証を使用するときは、body フィールドの長さをゼロにします。
これは、AUTH_UNIXとして知られる以前に説明した認証 flavor と同じです。遠隔手続きを呼び出す側では、従来の UNIX のプロセス許可認証を使用して自分自身を証明する場合があります。そのような RPC 呼び出しメッセージでは、opaque_auth の flavor は AUTH_UNIXとなります。body には、次に示す構造体が符号化されます。
struct auth_sysparms { unsigned int stamp; string machinename<255>; uid_t uid; gid_t gid; gid_t gids<10>; };
stamp は、呼び出し側のマシンで生成できる任意の ID
machinename は、呼び出し側のマシン名
uid 呼び出し側の実効ユーザ ID
gid は、呼び出し側の実効グループ
gids は、呼び出し側がメンバであるグループの可変長配列。
認証証明に伴うベリファイアの flavor は AUTH_NONE でなければなりません。
AUTH_SYSタイプの認証を使用するときは、サーバからの応答メッセージに入っている応答ベリファイアの flavor は AUTH_NONE か AUTH_SHORTのどちらかです。
AUTH_SHORT の場合、応答ベリファイアの文字列には short_hand_verf 構造体が符号化されています。この隠された構造体を、元の AUTH_SYS 認証証明の代わりにサーバに渡すことができます。
サーバ側では、隠された short_hand_verf 構造体 (AUTH_SHORT タイプの応答ベリファイアによって返される) を呼び出し側の元の認証証明にマップするキャッシュを保存します。呼び出し側は、新たな認証証明を使用してネットワークの帯域幅とサーバの CPU サイクルを保存できます。
サーバ側では、隠された short_hand_verf 構造体をいつでもフラッシュできます。そうすると、遠隔手続き呼び出しメッセージは認証エラーにより拒絶されます。エラー原因は AUTH_REJECTEDCRED になります。この場合、呼び出し側では AUTH_SYS タイプの元の認証証明を試すこともできます。 図 B-1を参照してください。
AUTH_SYS タイプの認証には次のような問題があります。
異なるオペレーティングシステムのマシンが同じネットワークに接続している場合に、呼び出し側の ID が一意に決まるとは限りません。
ベリファイアがないため簡単に認証証明をごまかすことができます。AUTH_DES タイプの認証はこの 2 つの問題を解決するための方法です。
最初の問題を解決するには、呼び出し側をオペレーティングシステム固有の整数ではなく単純文字列で呼びます。この文字列のことを、呼び出し側の netname (ネットワーク名) といいます。サーバでは、呼び出し側の識別のためにだけ呼び出し側の名前を使用します。したがって、名前ドメイン内では呼び出し側を一意に識別できるようなネットワーク名を設定しなければなりません。
遠隔サーバを呼び出す各ユーザに一意のネットワーク名を生成するのは、それぞれのオペレーティングシステムで実現されている AUTH_DES 認証機能の責任です。オペレーティングシステムには既に、システムのローカルユーザを識別します。通常はこれを単純に拡張してネットワーク名とします。たとえば、ユーザ ID を 515 とすると、"UNIX.515@sun.com" というネットワーク名を割り当てることができます。このネットワーク名は、確実に一意の名前にするために 3 つの要素で構成されています。後ろから見ると、インターネットには sun.com という名前ドメインは 1 つしかありません。その名前ドメイン内では、ID が 515 の UNIXユーザは 1 人しかいません。ただし、同一の名前空間にある別のオペレーティングシステム (たとえば VMS)上のユーザが偶然同じユーザ ID を持つことはあり得ます。そのような 2 人のユーザを区別するために、オペレーティングシステム名を追加します。そうすると、一方のユーザは "UNIX.515@sun.com" となり、他方のユーザは "VMS.515@sun.com" となります。
最初のフィールドは実際にはオペレーティングシステム名とは別の命名方法で指定します。現在、その命名方法とオペレーティングシステム名とがほぼ1 対 1 に対応しているだけです。命名方法の標準が確立すれば、最初のフィールドにはオペレーティングシステム名ではなくその標準規約に従った名前を入れます。
AUTH_SYS 認証とは違って、AUTH_DES 認証にはベリファイアがあり、サーバはクライアントの認証証明が正しいかどうかを確認できます(また、その反対方向の認証確認もできます)。ベリファイアの主な内容は暗号化されたタイムスタンプです。サーバは暗号化されたタイムスタンプを解読し、もしそれが実際の時刻に近ければ、クライアントが正しく暗号化したものと考えられます。クライアントが正しくタイムスタンプを暗号化するには、RPC セッションの会話キーを知っていなければなりません。会話キーを知っているクライアントならば、本当のクライアントのはずです。
会話キーは、クライアントが生成して最初の RPC 呼び出しでサーバに通知する DES [5] キーです。会話キーは、最初のトランザクションで公開キー方式で暗号化されます。AUTH_DES 認証で使用する公開キー方式は、192 ビットキーを使用する Diffie-Hellman [3] 暗号化手法です。この暗号化方式については後に詳しく説明します。
この認証方法が正しく機能するためには、クライアントとサーバで時刻が一致していなければなりません。ネットワークの時刻同期が保証できないときは、会話を始める前にクライアントの方でサーバと時刻を合わせることができます。rpcbind の提供する手続き RPCBPROC_GETTIME を使用すれば、現在時刻を取り出すことができます。
サーバはクライアントのタイムスタンプが正当なものかどうか判定します。2 番目以降のすべてのトランザクションに対して、サーバは次の 2 つの項目をチェックします。
タイムスタンプが、同じクライアントからの以前のものより大きい値になっている。
タイムスタンプが失効していない。サーバの時刻が、クライアントのタイムスタンプにクライアントウィンドウと呼ばれる値を加えた時刻より後ならば、タイムスタンプは失効しています。ウィンドウの値は、最初のトランザクションのときにクライアントが暗号化してサーバに引き渡します。ウィンドウとは、認証証明の存在時間と考えることができます。
最初のトランザクションでは、サーバはタイムスタンプが失効していないことを確認します。さらに、クライアントは最初のトランザクションで、ウィンドウベリファイアと呼ばれるウィンドウの値より 1 少ない値を暗号化して送信します。そうしないと、認証証明がサーバに棄却されてしまいます。
クライアントはサーバから返されたベリファイアが正当なものかどうか調べなければなりません。サーバは、クライアントから受信したタイムスタンプから 1 秒少ない値を暗号化してクライアントに送り返します。クライアントはそれ以外の値を受け取った場合は、それを拒絶します。
最初のトランザクションの後で、サーバの AUTH_DES 認証サブシステムからクライアントへのベリファイアの中に整数値のニックネームが返されます。クライアントは以降のトランザクションで、ネットワーク名、暗号化された DES キー、ウィンドウを毎回渡す代わりに、ニックネームを使用できます。ニックネームは、サーバが各クライアントのネットワーク名、復号化された DES キー、ウィンドウを保存しているテーブルのインデックスのようなものですが、クライアントからは隠されたデータとしてしか使用できません。
クライアントとサーバのクロックは最初は同期していても、やがて同期が壊れることがあります。その場合、クライアント側の RPC サブシステムは RPC_AUTHERROR を受け取りますので、その時点で再び同期を取る必要があります。
クライアントとサーバの時刻が同期していてもクライアントが RPC_AUTHERROR を受け取ることがあります。その原因は、サーバのニックネームテーブルの大きさには制限があり、足りなくなるとエントリが失われることがあるためです。その場合、クライアントは元の認証証明を再送信しなければなりません。サーバはそれに新たなニックネームを割り当てます。もしもサーバがクラッシュすると、ニックネームテーブル全体が失われ、全クライアントが元の認証証明を再送信しなければなりません。
/* * 認証証明には 2 種類あります。1 つは完全なネットワーク名を使用する方法で、 * もう 1 つはサーバがクライアントに割り当てたニックネーム (符号なし整数) を * 使用する方法です。クライアントからサーバへの初めてのトランザクションでは * 完全なネットワーク名を使用しなければなりません。それに対して、サーバは * クライアントにニックネームを返します。クライアントはそれ以降、サーバへの * トランザクションでニックネームを使用できます。必ずニックネームを * 使用しなければならないわけではありませんが、パフォーマンスから考えても、 * ニックネームを使用する方がよいでしょう。 */ enum authdes_namekind { ADN_FULLNAME = 0, ADN_NICKNAME = 1 }; /* * 暗号化した DES データのための 64 ビットブロック */ typedef opaque des_block[8]; /* * ユーザのネットワーク名の最大長 */ const MAXNETNAMELEN = 255; /* * クライアントのネットワーク名、暗号化された会話キー、 * ウィンドウの含まれたフルネーム。 * ウィンドウは認証証明の存在時間を示します。 * ベリファイアに示されたタイムスタンプにウィンドウを加えた時刻が * 過ぎている場合は、サーバは要求を無効として許可しません。要求を * 再送信しないために、最初のトランザクション以外は、 * タイムスタンプが以前の値より大きくないと、サーバはそれを受け入れません。 * 最初のトランザクションの場合は、サーバはウィンドウベリファイアが * ウィンドウより 1 小さい値であることを確認します。 */ struct authdes_fullname { string name<MAXNETNAMELEN>; /* クライアント名 */ des_block key; /* PK で暗号化された会話キー */ unsigned int window; /* 暗号化されたウィンドウ */ }; /* 注: PK は公開キーの略 */ /* * 認証証明はフルネームかニックネーム */ unionauthdes_credswitch(authdes_namekindadc_namekind){ case ADN_FULLNAME: authdes_fullname adc_fullname; case ADN_NICKNAME: unsigned int adc_nickname; }; /* * タイムスタンプには、1970 年 1 月 1 日の午前 0 時からの秒数を符号化 */ struct timestamp { unsigned int seconds; /* 秒数 */ unsigned int useconds; /* マイクロ秒 */ }; /* * ベリファイア: クライアント側 */ struct authdes_verf_clnt { timestamp adv_timestamp; /* 暗号化されたタイムスタンプ */ unsigned int adv_winverf; /* 暗号化されたウィンドウベリファイア */ }; /* * ベリファイア: サーバ側 * サーバは、クライアントから渡されたタイムスタンプから 1 秒少ない値を * 暗号化して返します。また、クライアントが以降のトランザクションで使用できる * ニックネームを暗号化せずに渡します。 */ struct authdes_verf_svr { timestamp adv_timeverf; /* 暗号化されたベリファイア */ unsigned int adv_nickname; /* クライアントの新しいニックネーム */ };
この暗号化手法では、2 つの定数 PROOT と HEXMODULUSを使用します。DES 認証プロトコルでは、この 2 つの定数として次の値を使用します。
const PROOT = 3; const HEXMODULUS = /* 16 進 */ "d4a0ba0250b6fd2ec626e7efd637df76c716e22d0944b88b";
この暗号化手法は次の例で説明するとわかりやすいでしょう。ここに A と B という 2 人の人が互いに暗号化したメッセージを送信するとします。Aと B はそれぞれランダムに秘密キーを生成し、このキーは誰にも教えません。秘密キーをそれぞれ SK(A) と SK(B) とします。また、2 人は公開ディレクトリにそれぞれ公開キーを示します。公開キーは次のように計算されます。
PK(A) = (PROOT ** SK(A)) mod HEXMODULUS PK(B) = (PROOT ** SK(B)) mod HEXMODULUS
** という記号はべき乗を表します。
ここで A と B は、互いに秘密キーを知らせ合うことなく 2 人の間の共通キー CK(A,B) を求めることができます。
A は次のように計算します。
CK(A, B) = (PK(B) ** SK(A)) mod HEXMODULUS
B は次のように計算します。
CK(A, B) = (PK(A) ** SK(B)) mod HEXMODULUS
上の 2 つの式から次の等式が得られます。
(PK(B)**SK(A)) mod HEXMODULUS = (PK(A)**SK(B)) mod HEXMODULUS
ここで、mod HEXMODULUS という部分を両辺から取り除いてモジュロ計算を省略し、プロセスを簡単にします。
PK(B) ** SK(A) = PK(A) ** SK(B)
次に、PK(B) を先に B が計算した値で置き換えます。PK(A) も同様に置き換えます。
((PROOT ** SK(B)) ** SK(A) = (PROOT ** SK(A)) ** SK(B)
この式は次のように書き換えられます。
PROOT ** (SK(A) * SK(B)) = PROOT ** (SK(A) * SK(B))
共通キー CK(A,B) は、プロトコルで使用されるタイムスタンプの暗号化には使用しません。共通キーは会話キーの暗号化にだけ使用し、タイムスタンプの暗号化には会話キーを使用します。これは、共通キーを使用する回数をできるだけ少なくして、共通キーが破られないようにするためです。会話時間は比較的短いため、会話キーの方が破られる心配がずっと少ないからです。
会話キーは、56 ビットの DES キーを使用して暗号化します。共通キーは 192 ビットですので、共通キーから次のようにして 56 ビットを選択し、ビット数を減らします。共通キーから中央の 8 バイトを選択し、各バイトの下位ビットにパリティを加えます。こうして、8 ビットのパリティの付いた 56 ビットキーが生成されます。
AUTH_KERB の SunOS 5.x 実装に使用されたカーネルは、Kerberos のコードをオペレーティングシステムのカーネルへコンパイルしないで、kerbd という代理の RPC デーモンを使用します。このデーモンは、以下の 3 つの手続きをエクスポートします。詳細については、kerbd(1M) マニュアルページを参照してください。
主体名、インスタンス、領域が指定されると、暗号化されたチケットおよび DES セッションキーを返す KSETKCRED。
UNIX 固有の KGETUCRED。KGETUCRED は、主な名前がサーバにもわかるユーザ名にマップされると想定し、ユーザの ID、グループ ID、およびグループリストを返します。
Kerberos の内容を的確に説明するには、現在 Kerberos を実装しているサービスであるネットワークファイルシステム (NFS) を例として使用するのが良いでしょう。サーバ s の NFS サービスは、nfs.s という周知の一次名を持つとします。クライアント c の特権ユーザは、root という主体名と、インスタンス c をもっているとします。AUTH_DES の場合とは異なり、ユーザのチケット発行用のチケットの期限が切れた場合は、kinit() を再び呼び出さなければならないことに注意してください。Kerberos マウントの NFS サービスは、新しいチケット発行用のチケットを獲得するまで成功しません。
この節全体を通して、AUTH_KERB を使用した NFS マウント要求について説明します。マウント要求は、SunOS のルートで実行されるので、ユーザの識別情報は、root.c.になります。
クライアント c は、マウントするディレクトリのファイルハンドルを獲得するために、サーバ s に MOUNTPROC_MOUNT 要求を実行します。クライアントのマウントプログラムは、ファイルハンドル、mountflavor、時間同期アドレス、サーバの既知の主体名である nfs.s を、クライアントのカーネルに渡して、NFS マウントシステムコールを実行します。次に、クライアントのカーネルが時間同期ホストでサーバに接続し、クライアント/ サーバ間の時間差を取得します。
クライアントのカーネルは、次の RPC 呼び出しを行います。(1) チケットおよびセッションキーを獲得するためにのローカルの kerbd への KSETKCRED 呼び出し。(2) フルネームの資格およびベリファイアを使用した、サーバのNFSサービスへの NFSPROC_GETATTR 呼び出し。サーバは、呼び出しを受信し、ローカルの kerbdへ KGETKCRED 呼び出しを行ってクライアントのチケットを検査します。
サーバの kerbd と Kerberos ライブラリは、チケットの暗号を解除し、主体名および DESセッションキーを他のデータの中に返します。サーバは、チケットがまだ有効であることをチェックし、セッションキーを使用して資格、ベリファイアの DES-encrypted の部分の暗号を解除し、ベリファイアが有効であることを検査します。
この時に返される可能性のある Kerberos 認証エラーは、下記のとおりです。
ベリファイアが無効な場合 (資格にある暗号が解除された winと、ベリファイアでの win +1 は、一致しません)、またはタイムスタンプがウィンドウの範囲外の場合は、AUTH_BADCRED が返されます。
エラーを受信しない場合、サーバはクライアントの識別情報をキャッシュに書き込み、NFS 回答に返されるニックネーム (小さい整数) を割り当てります。その時サーバは、クライアントがサーバと同じ領域かどうかをチェックします。クライアントがサーバと同じ領域の場合、サーバは、KGETUCRED をローカルの kerbd に呼び出して、主体名を UNIX の資格に変換します。変換できない場合、ユーザは匿名であるとマークされます。サーバは、ファイルシステムのエクスポート情報に対するこれらの資格を検査します。次の 3 つのケースを考えてください。
KGETUCRED 呼び出しが失敗し、匿名の要求が受け入れられた場合、匿名のユーザに UNIX 資格が割り当てられます。
KGETUCRED 呼び出しが失敗し、匿名の要求が受け入れられない場合、NFS 呼び出しは失敗し、AUTH_TOOWEAK が返されます。
KGETUCRED 呼び出しが成功する場合は、資格が割り当てられ、その後にルートのパーミッションのチェックも含む、正常な保護検査が行われます。
次に、サーバが、ニックネームおよびサーバのベリファイアを組み込んで NFS 回答を送信します。クライアントは回答を受信し、ベリファイアの暗号の解除と妥当性検査を行い、今後の呼び出しのためにニックネームを格納します。クライアントがサーバに 2 番目の NFS 呼び出しを行うと、先にサーバに書込まれた呼び出しが繰り返されます。クライアントのカーネルが、以前に記述されたニックネーム資格およびベリファイアを使用して、サーバの NFS サービスに NFSPROC_STATVFS 呼び出しを行います。サーバは呼び出しを受信し、ニックネームの妥当性検査を行います。これが範囲外であれば、エラー AUTH_BADCRED を返します。サーバは、獲得したばかりのセッションキーを使用して、ベリファイアの DES-encrypted 部分の暗号を解除し、ベリファイアの妥当性検査を行います。
この時に返される可能性のある Kerberos 認証エラーは、次のとおりです。
タイムスタンプが無効で、やり直しが検出されるか、タイムスタンプがウィンドウの範囲外の場合は、AUTH_REJECTEDVERF を返します。
エラーが受信されない場合、サーバは、ニックネームを使用して、呼び出し側の UNIX 資格を検出します。それから、サーバはファイルシステムのエクスポート情報に対するこれらの資格を検査し、ニックネームおよびサーバのベリファイアを組み込んだ NFS 回答を送信します。クライアントは回答を受信し、ベリファイアの暗号の解除および妥当性検査を行い、これからの呼び出しのためにニックネームを格納します。最後に、クライアントのNFS マウントシステムコールが返り、要求が終了します。
例 B-3 (AUTH_KERB) は、例 B-2 で示された AUTH_DES と似ています。両者の違いに注意してください。
#define AUTH_KERB 4 /* * 資格には 2 種類あります。1 つはクライアントが (前もって暗号化された) * Kerberos チケット送信する資格で、もう 1 つはクライアントがサーバに指定され * た「ニックネーム」(符号なしの整数のみ) を使用する資格です。クライアントは、 * サーバへの初めてのトランザクションでは、フルネームを使用しなければなりませ * ん。それに対してサーバはクライアントにニックネームを返します。クライアントは * それ以降、サーバへのトランザクションでニックネームを使用できます。 * (チケットの期限が切れるまで)。 必ずニックネームを使用しなけれ * ばならないわけではありませんが、パフォーマンスから考えても、ニック * ネームを使用する方がよいでしょう。 */ enum authkerb_namekind { AKN_FULLNAME = 0, AKN_NICKNAME = 1 }; /* * フルネームには、暗号化されたサービスチケットと有効期限が含まれます。 * 実際、この有効期限は、資格の有効期限と同じです。ベリファイアの * タイムスタンプに示されている時間に有効期限を加えた時間がすでに経過すると、 * サーバは要求を満了にして、もはや承諾しません。要求がやり直されないようにす * るため、サーバは、はじめのトランザクション以外の場合に、タイムスタンプが以 * 前のタイムスタンプより大きくないかどうかをチェックする必要があります。はじ * めのトランザクションでは、サーバはウィンドウベリファイアがウィンドウより小 * さいことを検査します。 */ struct authkerb_fullname { KTEXT_ST ticket; /* Kerberos サービスチケット */ unsigned long window; /* 暗号化されたウィンドウ */ }; /* * 資格はフルネームかニックネーム */ union authkerb_credswitch(authkerb_namekind akc_namekind){ case AKN_FULLNAME: authkerb_fullname akc_fullname; case AKN_NICKNAME: unsigned long akc_nickname; }; /* * タイムスタンプには、1970 年 1 月 1 日の午前 0 時からの秒数を符号化 */ struct timestamp { unsigned long seconds; /* 秒 */ unsigned long useconds; /* マイクロ秒 */ }; /* * ベリファイア:クライアント側 */ struct authkerb_verf_clnt { timestamp akv_timestamp; /* 暗号化されたタイムスタンプ */ unsigned long akv_winverf; /* 暗号化されたウィンドウベリファイア */ }; /* ベリファイア:サーバ側Verifier: server variety * クライアントによりサーバは、タイムスタンプ(暗号化)が与えられた。 * また、サーバは、クライアントがニックネームを今後の(暗号化されて * いない)トランザクションで使用するように指定します。 */ struct authkerb_verf_svr { timestamp akv_timeverf; /* 暗号化されたベリファイア */ unsigned long akv_nickname; /* クライアントの新しいニックネーム */ };