ONC+ 開発ガイド

認証プロトコル

認証パラメータは内容が隠されていますが、以降の RPC プロトコルで自由に解釈できます。この節では、既に定義されているタイプの認証について説明します。別のサイトでは自由に新たな認証タイプを作成し、プログラム番号割り当て規則と同様の認証タイプ番号割り当て規則に従って、認証タイプ番号を割り当てることができます。認証タイプ番号は Sun で保守、管理しています。RPC プログラム番号などの認証番号の割り当てや登録を行うには、Sun の RPC 管理者までご連絡ください。

AUTH_NONE

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

AUTH_SYS

これは AUTH_UNIX として知られる、以前に説明した認証 flavor と同じです。リモートプロシージャを呼び出す側では、従来の UNIX のプロセス許可認証を使用して自分自身を証明する場合があります。そのような RPC 呼び出しメッセージでは、opaque_authflavorAUTH_SYS となります。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

呼び出し側の実行グループ ID

gids

呼び出し側がメンバーであるグループの可変長配列

資格に伴うベリファイアの 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 認証プロセスマップ

Graphic

AUTH_DES 認証

AUTH_SYS 認証を使用した場合には、次の状況が発生することがあります。

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] 暗号化手法です。この暗号化方式については Diffie-Hellman の暗号化手法 で詳しく説明します。

この検証方法が正しく機能するためには、クライアントとサーバーで時刻が一致していなければなりません。ネットワークの時刻同期が保証できないときは、会話を始める前にクライアントの方でサーバーと時刻を合わせることができます。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 の S 実装に使用されたカーネルは、Kerberos のコードをオペレーティングシステムのカーネルへコンパイルしないで、kerbd という代理の RPC デーモンを使用します。このデーモンは、以下の 3 つの手続きをエクスポートします。

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

NFS マウント例

この節全体を通して、AUTH_KERB を使用した NFS マウント要求について説明します。マウント要求は、ルートで実行されるので、ユーザーの識別情報は、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 の資格に変換します。以前の名前が変換できない場合は、ユーザーは匿名と指定されます。サーバーは、ファイルシステムのエクスポート情報に対するこれらの資格を検査します。次の場合について検討します。

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

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

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

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

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

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

KERB 認証プロトコル

次の AUTH_KERB の例は AUTH_DES と多くの点で似ていることが次のコーディング例からわかります。両者の違いに注意してください。


例 B–3 AUTH_DES 認証プロトコル

#define AUTH_KERB 4
/*
 * 資格は 2 種類ある。1 つはクライアントが
 *  
Kerberos チケット (以前に暗号化された) を送信する際の資格で、
 * もう 1 つはサーバーがクライアントに与えた
 * ニックネーム (単純な符号なし整数)。
 * クライアントはサーバーとの最初のトランザクションではそのフルネームを
 * 使用する必要がある。その際、サーバーはクライアントへニックネームを返す。
 * クライアントはそれ以降のサーバーとのすべてのトランザクションで
 * このニックネームを使用できる (チケットが期限切れとなるまで)。
 * ニックネームは使用しなくてもかまいませんが、パフォーマンス上の理由から
 * 使用するほうが望ましい。
 */
enum authkerb_namekind {
 	AKN_FULLNAME = 0,
 	AKN_NICKNAME = 1
 };
 
/*
 * フルネームには暗号化されたサービスチケットと
 * ウィンドウが含まれる。ウィンドウは資格の有効期限。
 * ベリファイアのタイムスタンプに記載された時刻に
 * ウィンドウの時間を加えた時刻が過ぎると、サーバーは要求を期限切れとし、
 * 許可しない。要求を再送信しないように、最初のトランザクションを除き、
 * サーバーは前回よりも大きな値のタイムスタンプを要求する。
 * 最初のトランザクションでは、サーバーはタイムスタンプを
 * チェックする代わりに、ウィンドウベリファイアがウィンドウより 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; /* クライアントの新しいニックネーム */
};