ONC+ 開発ガイド

認証プロトコル

認証パラメータは内容が隠されていますが、以降の RPC プロトコルで自由に解釈できます。この節では、既に定義されているタイプの認証について説明します。別のサイトでは自由に新たな認証タイプを作成し、プログラム番号割り当て規則と同様の認証タイプ番号割り当て規則に従って、認証タイプ番号を割り当てることができます。認証タイプ番号はサン・ソフトで保守・管理しています。認証番号の割り当てを希望されるユーザーは、RPC プログラム番号を割り当てるときと同様に、「プログラム番号の登録」に示すように、サン・マイクロシステムズ社の RPC 管理者に連絡してください。

AUTH_NONE

呼び出し側は自身を証明せず、また、サーバー側も呼び出し側が誰でもかまわないという呼び出しもあります。その場合は RPC メッセージの認証証明、ベリファイア、応答ベリファイアの flavor (opaque_auth 共用体の要素識別子) は AUTH_NONE にします。AUTH_NONE タイプの認証を使用するときは、body フィールドの長さをゼロにします。

AUTH_SYS

これは、AUTH_UNIX として知られる以前に説明した認証 flavor と同じです。遠隔手続きを呼び出す側では、従来の UNIX のプロセス許可認証を使用して自分自身を証明する場合があります。そのような RPC 呼び出しメッセージでは、opaque_authflavorAUTH_UNIXとなります。body には、次に示す構造体が符号化されます。

struct auth_sysparms {
  	unsigned int  stamp;
  	string machinename<255>;
  	uid_t uid;
  	gid_t gid;
  	gid_t gids<10>;
 };

認証証明に伴うベリファイアの flavorAUTH_NONE でなければなりません。

AUTH_SHORT タイプのベリファイア

AUTH_SYSタイプの認証を使用するときは、サーバーからの応答メッセージに入っている応答ベリファイアの flavorAUTH_NONEAUTH_SHORT のどちらかです。

AUTH_SHORT の場合、応答ベリファイアの文字列には short_hand_verf 構造体が符号化されています。この隠された構造体を、元の AUTH_SYS 認証証明の代わりにサーバーに渡すことができます。

サーバー側では、隠された short_hand_verf 構造体 (AUTH_SHORT タイプの応答ベリファイアによって返される) を呼び出し側の元の認証証明にマップするキャッシュを保存します。呼び出し側は、新たな認証証明を使用してネットワークの帯域幅とサーバーの CPU サイクルを保存できます。

サーバー側では、隠された short_hand_verf 構造体をいつでもフラッシュできます。そうすると、遠隔手続き呼び出しメッセージは認証エラーにより拒絶されます。エラー原因は AUTH_REJECTEDCRED になります。この場合、呼び出し側では AUTH_SYS タイプの元の認証証明を試すこともできます。 図 B-1 を参照してください。

図 B-1 認証過程のマップ

Graphic

AUTH_DES タイプの認証

AUTH_SYS タイプの認証には次のような問題があります。

  1. 異なるオペレーティングシステムのマシンが同じネットワークに接続している場合に、呼び出し側の ID が一意に決まるとは限りません。

  2. ベリファイアがないため簡単に認証証明をごまかすことができます。AUTH_DES タイプの認証はこの 2 つの問題を解決するための方法です。

最初の問題を解決するには、呼び出し側をオペレーティングシステム固有の整数ではなく単純文字列で呼びます。この文字列のことを、呼び出し側の netname (ネットワーク名) といいます。サーバーでは、呼び出し側の識別のためにだけ呼び出し側の名前を使用します。したがって、名前ドメイン内では呼び出し側を一意に識別できるようなネットワーク名を設定しなければなりません。

遠隔サーバーを呼び出す各ユーザーに一意のネットワーク名を生成するのは、それぞれのオペレーティングシステムで実現されている AUTH_DES 認証機能の責任です。オペレーティングシステムには既に、システムのローカルユーザーを識別します。通常はこれを単純に拡張してネットワーク名とします。たとえば、ユーザー ID を 515 とすると、「UNIX.515@sun.com」いうネットワーク名を割り当てることができます。このネットワーク名は、確実に一意の名前にするために 3 つの要素で構成されています。後ろから見ると、インターネットには sun.com という名前ドメインは 1 つしかありません。その名前ドメイン内では、ID が 515UNIX ユーザーは 1 人しかいません。ただし、同一の名前空間にある別のオペレーティングシステム (たとえば VMS) 上のユーザーが偶然同じユーザー ID を持つことはあります。そのような 2 人のユーザーを区別するために、オペレーティングシステム名を追加します。そうすると、一方のユーザーは「UNIX.515@sun.com」となり、他方のユーザーは「VMS.515@sun.com」となります。

最初のフィールドは実際にはオペレーティングシステム名とは別の命名方法で指定します。現在、その命名方法とオペレーティングシステム名とがほぼ 1 対 1 に対応しているだけです。命名方法の標準が確立すれば、最初のフィールドにはオペレーティングシステム名ではなくその標準規約に従った名前を入れます。

AUTH_DES 認証のベリファイア

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 を受け取ることがあります。その原因は、サーバーのニックネームテーブルの大きさには制限があり、足りなくなるとエントリが失われることがあるためです。その場合、クライアントは元の認証証明を再送信しなければなりません。サーバーはそれに新たなニックネームを割り当てます。もしもサーバーがクラッシュすると、ニックネームテーブル全体が失われ、全クライアントが元の認証証明を再送信しなければなりません。

DES 認証プロトコル (XDR 言語で記述)


例 B-2 AUTH_DES 認証プロトコル

/*
 * 認証証明には 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; /* クライアントの新しいニックネーム */
};
 

Diffie-Hellman の暗号化手法

この暗号化手法では、2 つの定数 PROOTHEXMODULUSを使用します。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 認証プロトコル

AUTH_KERB の SunOS 5.x 実装に使用されたカーネルは、Kerberos のコードをオペレーティングシステムのカーネルへコンパイルしないで、kerbd という代理の RPC デーモンを使用します。このデーモンは、以下の 3 つの手続きをエクスポートします。詳細については、kerbd(1M) マニュアルページを参照してください。

  1. クライアントによって提供された認証プロトコルを検査するために、サーバー側の RPC が使用する KGETKCRED

  2. 主体名、インスタンス、領域が指定されると、暗号化されたチケットおよび DES セッション鍵を返す KSETKCRED

  3. UNIX 固有の KGETUCREDKGETUCRED は、主な名前がサーバーにもわかるユーザー名にマップされると想定し、ユーザーの ID、グループ ID、およびグループリストを返す

Kerberos の内容を的確に説明するには、現在 Kerberos を実装しているサービスであるネットワークファイルシステム (NFS) を例として使用するのが良いでしょう。サーバー s の NFS サービスは、nfs.s という周知の主体名を持つとします。クライアント c の特権ユーザーは、root という一次名と、インスタンス c をもっているとします。AUTH_DES の場合とは異なり、ユーザーのチケット発行用のチケットの期限が切れた場合は、kinit() を再び呼び出さなければならないことに注意してください。Kerberos マウントの NFS サービスは、新しいチケット発行用のチケットを獲得するまで成功しません。

NFS マウント例

この節全体を通して、AUTH_KERB を使用した NFS マウント要求について説明します。マウント要求は、SunOS のルートで実行されるので、ユーザーの識別情報は、root.c.になります。

クライアント c は、マウントするディレクトリのファイルハンドルを獲得するために、サーバー sMOUNTPROC_MOUNT 要求を実行します。クライアントのマウントプログラムは、ファイルハンドル、mountflavor、時間同期アドレス、サーバーの既知の主体名である nfs.s を、クライアントのカーネルに渡して、NFS マウントシステムコールを実行します。次に、クライアントのカーネルが時間同期ホストでサーバーに接続し、クライアント/ サーバー間の時間差を取得します。

クライアントのカーネルは、次の RPC 呼び出しを行います。(1) チケットおよびセッションキーを獲得するためにのローカルの kerbd への KSETKCRED 呼び出し。(2) フルネームの資格およびベリファイアを使用した、サーバーの NFS サービスへの NFSPROC_GETATTR 呼び出し。サーバーは、呼び出しを受信し、ローカルの kerbdKGETKCRED 呼び出しを行ってクライアントのチケットを検査します。

サーバーの kerbd と Kerberos ライブラリは、チケットの暗号を解除し、主体名および DES セッション鍵を他のデータの中に返します。サーバーは、チケットがまだ有効であることをチェックし、セッション鍵を使用して資格、ベリファイアの DES の暗号化された部分を複号化し、ベリファイアが有効であることを検査します。

この時に返される可能性のある Kerberos 認証エラーは、下記のとおりです。

エラーを受信しない場合、サーバーはクライアントの識別情報をキャッシュに書き込み、NFS 回答に返されるニックネーム (小さい整数) を割り当てります。その時サーバーは、クライアントがサーバーと同じ領域かどうかをチェックします。クライアントがサーバーと同じ領域の場合、サーバーは、KGETUCRED をローカルの kerbd に呼び出して、主体名を UNIX の資格に変換します。変換できない場合、ユーザーは匿名であるとマークされます。サーバーは、ファイルシステムのエクスポート情報に対するこれらの資格を検査します。次の 3 つのケースを考えてください。

  1. KGETUCRED 呼び出しが失敗し、匿名の要求が受け入れられた場合、匿名のユーザーに UNIX 資格が割り当てられます。

  2. KGETUCRED 呼び出しが失敗し、匿名の要求が受け入れられない場合、NFS 呼び出しは失敗し、AUTH_TOOWEAK が返されます。

  3. KGETUCRED 呼び出しが成功する場合は、資格が割り当てられ、その後にルートのアクセス権のチェックも含む、正常な保護検査が行われます。

次に、サーバーが、ニックネームおよびサーバーのベリファイアを組み込んで NFS 回答を送信します。クライアントは回答を受信し、ベリファイアの複号化と妥当性検査を行い、今後の呼び出しのためにニックネームを格納します。クライアントがサーバーに 2 番目の NFS 呼び出しを行うと、先にサーバーに書込まれた呼び出しが繰り返されます。クライアントのカーネルが、以前に記述されたニックネーム資格およびベリファイアを使用して、サーバーの NFS サービスに NFSPROC_STATVFS 呼び出しを行います。サーバーは呼び出しを受信し、ニックネームの妥当性検査を行います。これが範囲外であれば、エラー AUTH_BADCRED を返します。サーバーは、獲得したばかりのセッション鍵を使用して、ベリファイアの DES の暗号化された部分を複号化し、ベリファイアの妥当性検査を行います。

この時に返される可能性のある Kerberos 認証エラーは、次のとおりです。

エラーが受信されない場合、サーバーは、ニックネームを使用して、呼び出し側の UNIX 資格を検出します。それから、サーバーはファイルシステムのエクスポート情報に対するこれらの資格を検査し、ニックネームおよびサーバーのベリファイアを組み込んだ NFS 回答を送信します。クライアントは回答を受信し、ベリファイアの複号化および妥当性検査を行い、これからの呼び出しのためにニックネームを格納します。最後に、クライアントのNFS マウントシステムコールが返り、要求が終了します。

KERB 認証プロトコル (XDR 言語で記述)

例 B-3 (AUTH_KERB) は、例 B-2 で示された AUTH_DES と似ています。両者の違いに注意してください。


例 B-3 Kerb 認証プロトコル

#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;  /* 暗号化されたウィンドウベリファイア */
};
 
/*
ベリファイア:サーバー側
 * クライアントによりサーバーは、タイムスタンプ(暗号化)が与えられた。
 * また、サーバーは、クライアントがニックネームを今後の(暗号化されて
 * いない)トランザクションで使用するように指定します。
 */
struct authkerb_verf_svr {
 	timestamp akv_timeverf;    /* 暗号化されたベリファイア */
 	unsigned long akv_nickname; /* クライアントの新しいニックネーム */
};