このチュートリアルでは、相互に通信するアプリケーション間でセキュアなメッセージ交換を実行するためのJava GSS-APIの使用方法を示す2つのサンプル・アプリケーションを紹介します。ここでは、クライアント・アプリケーションとサーバー・アプリケーションを例として使用しています。
Java GSS-APIは、「セキュリティ・メカニズム」と呼ばれるメカニズムを使用して、これらのサービスを提供します。GSS-API実装には、Kerberos V5メカニズムのサポートと、他のベンダー固有の任意の選択肢が含まれます。このチュートリアルでは、Kerberos V5メカニズムを使用しています。
クライアントとサーバー間の認証を実行し、セキュアな通信用の暗号化鍵を確立するために、GSS-APIメカニズムは、接続の両側のローカル・エンティティ用の特定のクレデンシャルにアクセスする必要があります。このチュートリアルでは、クライアント側で使用するクレデンシャルはKerberosチケットで構成され、サーバー側のクレデンシャルは長期Kerberos秘密鍵で構成されます。Kerberosチケットには、ホスト・アドレスをオプションで組み込むことができ、IPv4とIPv6の両方のホスト・アドレスがサポートされています。Java GSS-APIは、メカニズムが、これらのクレデンシャルを、スレッドのアクセス制御コンテキストに関連付けられたサブジェクトから取得することを要求します。
サブジェクトにこの種のクレデンシャルを追加するために、クライアント・アプリケーションおよびサーバー・アプリケーションは、通常、Kerberos LoginModuleを使用してJAAS認証を最初に実行します。実行方法の詳細は、「JAAS認証」チュートリアルを参照してください。認証されたサブジェクトをスレッドのアクセス制御コンテキストに関連付ける方法については、「JAAS承認」を参照してください。これらの操作を自動的に実行するユーティリティも用意されています。Loginユーティリティの使用方法については、「JAAS Loginユーティリティの使用」チュートリアルを参照してください。
このチュートリアルでは、クライアントおよびサーバーでJAAS認証を実行せず、Loginユーティリティを使用しません。その代わり、システム・プロパティjavax.security.auth.useSubjectCredsOnly
をfalse
に設定する必要があります。この設定により、JAASによって設定された既存のサブジェクトから必要なクレデンシャルを取得する際、GSSメカニズムを必要とする制限が緩和されます。「useSubjectCredsOnlyシステム・プロパティ」を参照してください。
注意:
これは簡略化された入門用のチュートリアルです。たとえば、ポリシー・ファイルは含まれておらず、サンプル・コードの実行にセキュリティ・マネージャを使用していません。実際には、Java GSS-APIを使用するコードは、セキュリティ・マネージャを使用して実行すべきであるため、必要なアクセス権が明示的に付与されていないかぎり、セキュリティ関連の操作は許可されません。
別のチュートリアルとして、JAAS LoginユーティリティおよびJava GSS-APIを使用したセキュアなメッセージ交換があり、これは、Loginユーティリティ、ポリシー・ファイル、およびより複雑なログイン構成ファイルを利用することを除き、このチュートリアルと同様です。JAAS認証の実行時に常に必要となるログイン構成ファイル(付録B: JAASログイン構成ファイルを参照)は、必要な認証モジュールを指定します。
この一連のすべてのチュートリアルでは、認証およびアプリケーションのセキュアな通信をサポートする基盤の技術として、Kerberos V5を使用しています。Kerberos要件を参照してください。
チュートリアルのコードを最初に実行してみる場合、「SampleClientおよびSampleServerプログラムの実行」を先に読んでから、その他のセクションに戻り、学習を続けてください。
このチュートリアルで使用するアプリケーションの名前は、SampleClient
およびSampleServer
です。
次に、SampleClient
およびSampleServer
アプリケーションの実行方法のサマリーを示します。
SampleServer
アプリケーションを実行します。SampleServer
SampleClient
アプリケーションを実行します(通常別のマシンを使用)。SampleClient
SampleServer
を表すKerberosプリンシパルの名前(Kerberosユーザー名およびサービス・プリンシパル名を参照)、(2) SampleServer
を実行するホスト(マシン)の名前、(3) SampleServer
がクライアント接続を待機するポート番号です。SampleServer
へのソケット接続を試みます。SampleServer
により受け入れられます。両方のアプリケーションが、ソケット入力および出力ストリームからのDataInputStreamおよびDataOutputStreamを初期化して、将来のデータ交換に使用します。SampleClient
およびSampleServer
は、それぞれGSSContextをインスタンス化し、以後のセキュアなデータ交換を可能にする共有コンテキストをプロトコルに従って確立します。SampleClient
およびSampleServer
は、メッセージをセキュアに交換できます。SampleClient
およびSampleServer
は、メッセージ交換の完了後に、クリーンアップ操作を実行します。実際のコードおよび詳細は、後述のセクションで示します。
SampleClient.java
およびSampleServer.java
プログラムの全体のコードは、main
メソッド内に配置されており、次の区分にさらに分割できます。
注意:
これらのプログラムが使用するJava GSS-APIクラス(GSSManager、GSSContext、GSSName、GSSCredential、MessagePropおよびOid)は、org.ietf.jgssパッケージ内にあります。
クライアントとサーバーのmain
メソッドが最初に行うことは、コマンド行引数の読取りです。
SampleClient
は、次の3つの引数を受け付けます。
SampleServer
を表すKerberosプリンシパルの名前(Kerberosユーザー名およびサービス・プリンシパル名を参照)。SampleServer
を実行するマシン。SampleServer
が接続を待機するポートのポート番号。次に、コマンド行引数を読み取るコードを示します。
if (args.length < 3) { System.out.println("Usage: java <options> Login SampleClient " + " <servicePrincipal> <hostName> <port>"); System.exit(-1); } String server = args[0]; String hostName = args[1]; int port = Integer.parseInt(args[2]);
SampleServer
が受け付ける引数は、次の1つだけです。
SampleServer
がクライアントの接続の待機に使用するポート番号。この番号は、SampleClient
プログラムの実行時に指定したポート番号と同じにする必要があります。次に、コマンド行引数を読み取るコードを示します。
if (args.length != 1) { System.out.println( "Usage: java <options> Login SampleServer <localPort>"); System.exit(-1); } int localPort = Integer.parseInt(args[0]);
Java GSS-APIは、トークン(不透明なバイト・データ)を作成および解釈するメソッドを提供します。トークンには2つのピア間でセキュアに交換されるメッセージが含まれていますが、実際にトークンを転送するメソッドはピアによって異なります。SampleClient
およびSampleServer
アプリケーションの場合、クライアントとサーバーの間にソケット接続を確立し、ソケット入力および出力ストリームを使用してデータを交換します。
SampleClient
には、引数としてSampleServer
を実行中のホスト・マシン名と、SampleServer
が接続を待機するポート番号が渡されました。これで、SampleClient
からSampleServer
へのソケット接続確立に必要なすべてが整いました。次のコードを使用して、接続を設定し、将来のデータ交換に備えてDataInputStreamおよびDataOutputStreamを初期化します。
Socket socket = new Socket(hostName, port); DataInputStream inStream = new DataInputStream(socket.getInputStream()); DataOutputStream outStream = new DataOutputStream(socket.getOutputStream()); System.out.println("Connected to server " + socket.getInetAddress());
SampleServer
アプリケーションには、引数として、クライアントからの接続の待機に使用するポート番号が渡されます。これにより、指定されたポートの接続を待機するServerSocket
が作成されます。
ServerSocket ss = new ServerSocket(localPort);
次に、ServerSocket
はクライアントから接続を待機および受け付け、クライアントとの将来のデータ交換に備えてDataInputStreamおよびDataOutputStreamを初期化します。
Socket socket = ss.accept(); DataInputStream inStream = new DataInputStream(socket.getInputStream()); DataOutputStream outStream = new DataOutputStream(socket.getOutputStream()); System.out.println("Got connection from client " + socket.getInetAddress());
accept
メソッドは、クライアント(ここではSampleClient
)がSampleServer
のホストおよびポート上で接続を要求するまで待機します。SampleClient
はこの接続を介して操作を実行します
Socket socket = new Socket(hostName, port);
接続の要求および確立が実行されると、accept
メソッドは新規ポートにバインドされた新規Socketオブジェクトを返します。サーバーは、この新規ソケット経由でクライアントと通信しつつ、元のポートにバインドされたServerSocket
に対する他のクライアントからの接続要求を待機できます。このため、一般に、サーバー・プログラムは複数の接続要求を処理可能なループを保持します。
SampleServer
の基本的なループ構造を、次に示します。
while (true) { Socket socket = ss.accept(); <Establish input and output streams for the connection>; <Establish a context with the client>; <Exchange messages with the client>; <Clean up>; }
クライアント接続は、元のポートでキューに入れられます。このため、SampleServer
の使用するプログラム構造では、接続を行う最初のクライアントとのやり取りが完了してからでないと、次の接続を受け付けることができません。実際のところ、サーバーは次のように、クライアントごとに1つのスレッドを使用することで、複数のクライアントに同時に対応できます。
while (true) { <accept a connection>; <create a thread to handle the client>; }
2つのアプリケーションがJava GSS-APIを利用してセキュアにメッセージを交換するためには、クレデンシャルを使用して、事前にジョイント・セキュリティ・コンテキストを確立しておく必要があります。(注意: SampleClient
の場合、クレデンシャルは、SampleClient
が自動的に実行され、Loginユーティリティによってユーザーが認証された時点で確立されます。SampleServer
の場合も同様です。)セキュリティ・コンテキストは、共有状態の情報(暗号化鍵などを含む)をカプセル化します。暗号化が要求された場合、こうした鍵を使用して、交換するメッセージを暗号化できます。
セキュリティ・コンテキスト確立の一部として、コンテキスト・イニシエータ(ここではSampleClient
)がアクセプタ(SampleServer
)に認証され、それに対し、アクセプタがイニシエータに認証されることが必要になる場合があります。これを、「相互認証」が行われると言います。
両方のアプリケーションが、GSSContextオブジェクトを作成および使用して、セキュリティ・コンテキストを構成する共有情報を確立および保守します。
コンテキスト・オブジェクトをインスタンス化する方法は、コンテキスト・イニシエータとコンテキスト・アクセプタで異なります。イニシエータは、GSSContextをインスタンス化した後で、目的のセキュリティ・コンテキストの特性を決定する様々なコンテキスト・オプション(相互認証を行うかどうかの指定など)を設定できます。目的の特性をすべて設定したら、イニシエータはinitSecContext
メソッドを呼び出します。このメソッドは、アクセプタのacceptSecContext
メソッドが必要とするトークンを生成します。
Java GSS-APIメソッドは、アプリケーション間で交換されるトークンを準備するために存在します。ただし、実際にアプリケーション間でトークンを転送するのは、アプリケーションの役割です。このため、イニシエータは、initSecContext
の呼出しによりトークンを受け取ると、それをアクセプタに送信します。アクセプタは、acceptSecContext
を呼び出してトークンを渡します。それに対し、acceptSecContext
メソッドがトークンを返すことができます。その場合、アクセプタは、そのトークンをイニシエータに送信し、イニシエータはinitSecContext
を再度呼び出してこのトークンを渡すようにすべきです。initSecContext
またはacceptSecContext
がトークンを返すたびに、メソッドを呼び出したアプリケーションはトークンをピアに送信し、ピアはトークンを適切なメソッド(acceptSecContext
またはinitSecContext
)に渡すようにしてください。この処理は、コンテキストが完全に確立される(コンテキストのisEstablished
メソッドがtrue
を返す)まで継続して行われます。
サンプル・アプリケーション用のコンテキスト確立コードについては、次で説明します。
このクライアント/サーバーのシナリオでは、SampleClientはコンテキスト・イニシエータです。次に、セキュリティ・コンテキストを確立するための基本的な手順を示します。
initSecContext
を呼び出すたびに、返されたトークンをすべてSampleServer
に送信し、SampleServer
からトークンを受け取ります(トークンが存在する場合)。GSSContextは、GSSContextのインスタンス化、およびいずれかのcreateContext
メソッドの呼出しにより作成されます。GSSManagerクラスは、他の重要なGSS APIクラスのファクトリとして機能します。これにより、GSSContext、GSSCredentialおよびGSSNameインタフェースを実装するクラスのインスタンスを作成できます。
SampleClient
は、GSSManagerのstaticメソッドgetInstance
を呼び出すことにより、デフォルトのGSSManagerサブクラスのインスタンスを取得します。
GSSManager manager = GSSManager.getInstance();
デフォルトGSSManagerサブクラスのcreate*
メソッド(createContext
など)が返すクラスの実装は、基盤となるテクノロジとしてKerberosをサポートします。
イニシエータ側でコンテキストを作成するGSSManagerファクトリ・メソッドは、次のシグニチャを保持します。
GSSContext createContext(GSSName peer, Oid mech, GSSCredential myCred, int lifetime);
次に、この引数について説明します。そのあとで、createContext
への完全な呼出しを示します。
クライアント/サーバー・パラダイムにおけるピアは、サーバーです。peer
引数には、サーバーを表すサービス・プリンシパルのGSSNameを指定する必要があります。(「Kerberosユーザー名およびサービス・プリンシパル名」を参照)。サービス・プリンシパル名を表す文字列は、SampleClient
の最初の引数として渡されます。SampleClientは引数をserver
という名前のローカルのString変数に格納します。GSSNameのインスタンス化には、GSSManager manager
が(そのcreateName
メソッドのいずれかを呼び出すことにより)使用されます。SampleClient
は、次の署名を使用して、createName
メソッドを呼び出します。
GSSName createName(String nameStr, Oid nameType);
SampleClient
は、nameStr
引数としてserver
Stringを渡します。
2番目の引数は、Oidです。Oidは、汎用オブジェクト識別子(Universal Object Identifier)を表します。Oidは、メカニズムと名前の型を識別するためにGSS-APIフレームワーク内で用いられる、グローバルに解釈できる階層的な識別子です。Oidの構造およびエンコードは、ISOIEC-8824およびISOIEC-8825標準で定義されています。createName
メソッドに渡されるOidは、厳密にはメカニズムOidではなく、名前型Oidです。
GSS-APIでは、文字列名は、しばしばメカニズムに依存しない形式からメカニズム固有の形式にマッピングされます。通常、Oidでは、メカニズムがマッピング方法を認識できるよう、文字列の名前形式が指定されます。null
のOidは、名前が、メカニズムの使用するネイティブの形式になっていることを示します。これが当てはまるのは、適切なKerberos Version 5名の形式であるserver
Stringの場合です。このため、SampleClient
は、Oidとしてnull
を渡します。次に、その呼出しを示します。
GSSName serverName = manager.createName(server, null);
GSSManager createContext
メソッドの2番目の引数はOidです。これは、コンテキスト確立時のクライアントとサーバー間の認証、およびその後のクライアントとサーバー間のセキュアな通信に使用されるメカニズムを表します。
このチュートリアルでは、セキュリティ・メカニズムとしてKerberos V5を使用しています。Kerberos V5メカニズムのOidは、RFC 1964で、「1.2.840.113554.1.2.2」と定義されているので、このOidを作成します。
Oid krb5Oid = new Oid("1.2.840.113554.1.2.2");
SampleClient
は、createContext
の2番目の引数としてkrb5Oid
を渡します。
GSSManager createContext
メソッドの3番目の引数は、呼出し側のクレデンシャルを表すGSSCredentialです。SampleClientの場合と同様、この引数にnull
を渡す場合、デフォルトのクレデンシャルが使用されます。
コンテキストのインスタンス化が完了したら、コンセプト・アクセプタを使用してコンテキストを実際に確立する前に、コンテキスト・イニシエータで、目的のセキュリティ・コンテキストの特性を決定する様々なオプションを設定できます。それらの各オプションは、インスタンス化されたコンテキスト上でrequest
メソッドを呼び出すことで設定されます。大半のrequest
メソッドは、その機能を要求するかどうかを示すboolean
引数を取ります。要求は、常に受け入れられるわけではないため、コンテキストの確立後に、いずれかのget
メソッドを呼び出して、それを確認できます。
SampleClient
は、次を要求します。
wrap
という名前のコンテキスト・メソッドの暗号化を有効にすることを意味します。暗号が実際に使用されるのは、wrap
メソッドに渡されるMessagePropオブジェクトにより機密性が要求される場合だけです。wrap
およびgetMIC
メソッドの整合性を要求します。整合性が要求されると、これらのメソッドの呼出し時に、メッセージ整合コード(MIC)として知られる暗号化タグが生成されます。getMIC
の呼出しにより返されるトークン内に、生成されたMICが含まれます。wrap
が呼び出されると、MICがメッセージ(元のメッセージまたはメッセージ暗号化の結果、機密性が適用されたかどうかにより異なる)とともに1つのトークンの一部としてパッケージ化されます。次に、メッセージに対するMICを検証して、メッセージが途中で変更されていないことを確認できます。GSSException
context
に対してこれらの要求を生成するSampleClient
コードを、次に示します。
context.requestMutualAuth(true); // Mutual authentication context.requestConf(true); // Will use encryption later context.requestInteg(true); // Will use integrity later
コンテキストが確立された後、クライアントは、アクセサ・メソッド(getMutualAuthState、getConfState、getIntegStateなど)を呼び出して、コンテキスト状態を明示的に確認し、いずれかのセキュリティ・コンテキストが望ましい状態でない場合はそのコンテキストを破棄する必要があります。
注意:
デフォルトのGSSManager実装およびKerberosメカニズムを使用する場合、これらの要求は常に許可されます。
SampleClient
は、GSSContextのインスタンス化および使用するコンテキスト・オプションの指定後に、SampleServer
とのセキュリティ・コンテキストを実際に確立できます。このために、SampleClient
はループ構造を保持します。各ループにより、次が反復実行されます
initSecContext
メソッドの呼出し。最初の呼出しの場合、メソッドにはnull
のトークンが渡されます。それ以外の場合、SampleServer
からSampleClient
に直前に送信されたトークン(SampleServer
によるacceptSecContext
の呼出しで生成されるトークン)が渡されます。initSecContext
により返されたトークン(存在する場合)のSampleServer
への送信。initSecContext
への最初の呼出しでは、常にトークンが生成されます。最後の呼出しでは、トークンが返されない場合があります。SampleClient
はSampleServer
から別のトークンを受け取って、次のループ処理を開始します。initSecContext
から返される、またはSampleServer
から受け取るトークンは、バイト配列に格納されます。トークンは、SampleClient
およびSampleServer
間での送信時に不透明なデータとして処理され、Java GSS-APIメソッドにより解釈されます。
initSecContext
引数は、トークン、トークン開始位置の配列内の開始オフセット、およびトークンの長さを含むバイト配列です。最初の呼出しでは、SampleClient
は、SampleServer
からトークンをまだ受け取っていないため、nullのトークンを渡します。
SampleServer
とトークンを交換する際、SampleClient
は、SampleServer
へのソケット接続用入力および出力ストリームを使用して設定した、DataInputStream inStream
およびDataOutputStream outStream
を使用します。トークンを記述する場合、常に、トークンのバイト数を最初に記述してから、トークン自体を記述してください。その理由は、「SampleClientとSampleServerのメッセージ交換」セクションの導入部で説明します。
次に、SampleClient
のコンテキスト確立ループ、およびクライアントおよびサーバーの実体、相互認証を実際に行うかどうかなどの情報を表示するコードを示します。
byte[] token = new byte[0];
while (!context.isEstablished()) {
// token is ignored on the first call
token = context.initSecContext(token, 0, token.length);
// Send a token to the server if one was generated by
// initSecContext
if (token != null) {
System.out.println("Will send token of size "
+ token.length + " from initSecContext.");
outStream.writeInt(token.length);
outStream.write(token);
outStream.flush();
}
// If the client is done with context establishment
// then there will be no more tokens to read in this loop
if (!context.isEstablished()) {
token = new byte[inStream.readInt()];
System.out.println("Will read input token of size "
+ token.length
+ " for processing by initSecContext");
inStream.readFully(token);
}
}
System.out.println("Context Established! ");
System.out.println("Client is " + context.getSrcName());
System.out.println("Server is " + context.getTargName());
if (context.getMutualAuthState())
System.out.println("Mutual authentication took place!");
このクライアント/サーバーのシナリオでは、SampleServer
はコンテキスト・アクセプタです。次に、セキュリティ・コンテキストを確立するための基本的な手順を示します。
SampleClient
からトークンを受け取り、acceptSecContext
を呼び出してトークンを渡し、返されたトークンをすべてSampleClient
に送信します。SampleClient GSSContextのインスタンス化で説明したように、GSSContextは、GSSManagerをインスタンス化して、createContext
メソッドのいずれかを呼び出すことで作成されます。
SampleClient
と同様、SampleServer
は、GSSManagerのstaticメソッドgetInstance
を呼び出すことにより、デフォルトのGSSManagerサブクラスのインスタンスを取得します。
GSSManager manager = GSSManager.getInstance();
アクセプタ側でコンテキストを作成するGSSManagerファクトリ・メソッドは、次のシグニチャを保持します。
GSSContext createContext(GSSCredential myCred);
SampleServer
の場合と同様、GSSCredential引数にnull
を渡す場合、デフォルトのクレデンシャルが使用されます。コンテキストは、次の方法でインスタンス化されます。
GSSContext context = manager.createContext((GSSCredential)null);
SampleServerは、GSSContextをインスタンス化したあとで、SampleClientとのセキュリティ・コンテキストを確立できます。これを実行するため、SampleServerはコンテキストが確立されるまで反復するループ構造を保持します。各ループにより、次の操作が反復実行されます。
initSecContext
呼出しの結果です。acceptSecContext
メソッドの呼び出し、および直前に受け取ったトークンのメソッドへの引渡し。acceptSecContext
がトークンを返す場合、SampleServerはこのトークンをSampleClientに送信し、コンテキストがまだ確立されていない場合、次のループ処理を開始します。acceptSecContext
から返される、またはSampleClientから受け取るトークンは、バイト配列に格納されます。
acceptSecContext
引数は、トークン、トークン開始位置の配列内の開始オフセット、およびトークンの長さを含むバイト配列です。
SampleClientとトークンを交換する際、SampleServerは、SampleClientへのソケット接続用入力および出力ストリームを使用して設定した、DataInputStream inStream
およびDataOutputStream outStream
を使用します。
次に、SampleServerのコンテキスト確立ループを示します。
byte[] token = null;
while (!context.isEstablished()) {
token = new byte[inStream.readInt()];
System.out.println("Will read input token of size "
+ token.length
+ " for processing by acceptSecContext");
inStream.readFully(token);
token = context.acceptSecContext(token, 0, token.length);
// Send a token to the peer if one was generated by
// acceptSecContext
if (token != null) {
System.out.println("Will send token of size "
+ token.length
+ " from acceptSecContext.");
outStream.writeInt(token.length);
outStream.write(token);
outStream.flush();
}
}
System.out.print("Context Established! ");
System.out.println("Client is " + context.getSrcName());
System.out.println("Server is " + context.getTargName());
if (context.getMutualAuthState())
System.out.println("Mutual authentication took place!");
SampleClient
とSampleServer
の間でセキュリティ・コンテキストが確立されると、そのコンテキストを使用してメッセージをセキュアに交換できます。
メッセージのセキュアな交換のためにメッセージを準備するメソッドには、wrap
とgetMIC
の2種類があります。実際には、2つのwrap
メソッド(および2つのgetMIC
メソッド)が存在します。2つのメソッドの相違点は、入力メッセージの位置(バイト配列または入力ストリーム)および出力先(バイト配列の戻り値または出力ストリーム)です。
メッセージ交換用のこれらのメソッド、および対応するメソッド(生成されるトークンのピアが解釈を行うための)について、次で説明します。
wrap
メソッドは、メッセージ交換用のプライマリ・メソッドです。
SampleClient
により呼び出されるwrap
メソッドのシグニチャは、次のようになります。
byte[] wrap (byte[] inBuf, int offset, interface len, MessageProp msgProp)
wrap
に、メッセージ(inBuf
内)、inBuf
内でのメッセージ開始位置のオフセット(offset
)、およびメッセージの長さ(len
)を渡します。使用するQOP (Quality-of-Protection)、および機密性(暗号化)が必要かどうかを指定する際に使用するMessagePropも渡します。QOP値により、使用する暗号化整合および暗号化(必要な場合)アルゴリズムが決定されます。基盤となるメカニズムのプロバイダにより、さまざまなQOP値に対応するアルゴリズムが指定されます。たとえば、Kerberos V5に対応する値は、RFC 1964のセクション4.2で定義されています。デフォルトのQOPを要求する場合は、通常QOP値に0を指定します。
wrap
メソッドは、メッセージを含むトークンおよびその暗号化メッセージ整合コード(MIC)を返します。MessagePropにより機密性が指定されている場合、トークンに格納されたメッセージは暗号化されます。返されるトークンは、不透明なデータとして扱われるため、その形式について知る必要はありません。返されるトークンをピア・アプリケーションに送信すると、unwrap
メソッドが呼び出されてトークンの「ラップが解除」され、元のメッセージの取得および整合性の検証が行われます。
指定されたメッセージの暗号化メッセージ整合コード(MIC)を含むトークンの取得だけを行う場合、getMIC
を呼び出します。たとえば、ピアが同じデータを保持していることを確認する場合、データ自体を相互に転送しなくても、データのMICを転送するだけで目的を達成できます。
SampleServer
により呼び出されるgetMIC
メソッドのシグニチャは、次のようになります。
byte[] getMIC (byte[] inMsg, int offset, int len, MessageProp msgProp)
getMIC
に、メッセージ(inMsg
内)、inMsg
内でのメッセージ開始位置のオフセット(offset
)、およびメッセージの長さ(len
)を渡します。使用するQOP (Quality-of-Protection)の指定に使うMessagePropも渡します。デフォルトのQOPを要求する場合は、通常QOP値に0を指定します。
getMIC
により作成されたトークンおよびMICの計算に使用するメッセージ(またはMICを計算する予定のメッセージ)を保持する場合、verifyMIC
メソッドを呼び出して、メッセージのMICを検証できます。検証が成功する(GSSExceptionがスローされない)場合、メッセージが、MIC計算時のメッセージと正確に一致することが保証されます。通常、アプリケーションからメッセージを受け取るピアは、MICを検証して、途中でメッセージが変更または破損していないかどうかを確認できるように、MICも期待します。注: メッセージに加え、MICも必要であることが前もってわかっている場合は、wrap
およびunwrap
メソッドを使用する方が便利です。ただし、メッセージとMICを別個に受け取る状況も考えられます。
前述のgetMIC
に対応するverifyMIC
のシグニチャを、次に示します。
void verifyMIC (byte[] inToken, int tokOffset, int tokLen, byte[] inMsg, int msgOffset, int msgLen, MessageProp msgProp);
これにより、inMsg
(長さがmsgLen
で、オフセットmsgOffset
から始まる)内のメッセージに対応する、inToken
(長さがtokLen
で、オフセットtokOffset
から始まる)内のMICの検証が実行されます。基盤となるメカニズムは、MessagePropを使用して呼出し側に情報(メッセージに適用された保護の強さを示すQOPなど)を返します。
SampleClientとSampleServerとの間のメッセージ交換の概要を次に示します。そのあと、コードの詳細を示します。
これらのステップは、GSS-APIクライアントおよびサーバーの検証に使用される「標準的な」ステップです。GSS-APIライブラリの異なる実装間の相互運用性をチェックする、広く普及したテスト・プログラムとなったGSS-APIクライアントおよびGSS-APIサーバーは、MITのグループが記述しました。これらのGSS-APIサンプル・アプリケーションは、MITからダウンロード可能なKerberosディストリビューションの一部です(http://web.mit.edu/kerberos)。MITから入手可能なこのクライアントおよびサーバーが準拠するプロトコルでは、コンテキストの確立後にクライアントが送信したメッセージに、MICが追加されて返されます。GSS-APIライブラリを実装する場合、そのライブラリ実装を使用するクライアントまたはサーバーを、別のGSS-APIライブラリ実装を使用する対応するピア・サーバーまたはクライアントに対して実行して、テストを行うのが一般的な方法です。両方のライブラリ実装が標準に準拠する場合、2つのピアの通信は成功します。
使用するクライアントまたはサーバーを、C言語で記述されたクライアントまたはサーバー(MIT提供のクライアントやサーバーなど)に対してテストする場合、トークンの交換方法を考慮する必要があります。C言語によるGSS-APIの実装には、ストリーム・ベースのメソッドが含まれません。ピアにストリーム・ベースのメソッドが存在しない状況でトークンを記述する場合、最初にバイト数を記述してから、トークンを記述する必要があります。同様に、トークンを読み取る際、バイト数を読み取ってからトークンを読み取る必要があります。SampleClient
およびSampleServer
は、実際にこの処理を実行します。
SampleClient
とSampleServer
のメッセージ交換のサマリーを次に示します。
SampleClient
がwrap
を呼び出して、メッセージのMICを暗号化および計算します。SampleClient
が、wrap
から返されたトークンをSampleServer
に送信します。SampleServer
が、unwrap
を呼び出して元のメッセージを取得し、整合性を検証します。SampleServer
がgetMIC
を呼び出して、復号化したメッセージに対するMICを計算します。SampleServer
が、getMIC
により返されたトークン(MICを含む)をSampleClient
に送信します。SampleClient
がverifyMIC
を呼び出して、SampleServer
により送信されたMICが元のメッセージの有効なMICかどうかを検証します。メッセージの暗号化、MICの計算、および結果のSampleServer
への送信を実行するSampleClient
コードを、次に示します。
byte[] messageBytes = "Hello There!\0".getBytes();
/*
* The first MessageProp argument is 0 to request
* the default Quality-of-Protection.
* The second argument is true to request
* privacy (encryption of the message).
*/
MessageProp prop = new MessageProp(0, true);
/*
* Encrypt the data and send it across. Integrity protection
* is always applied, irrespective of encryption.
*/
token = context.wrap(messageBytes, 0, messageBytes.length,
prop);
System.out.println("Will send wrap token of size "
+ token.length);
outStream.writeInt(token.length);
outStream.write(token);
outStream.flush();
次のSampleServer
コードは、SampleClient
により送信されたラップ済のトークンを読み取り、「ラップを解除」して元のメッセージを取得し、整合性を検証します。この場合、メッセージが暗号化されているため、ラップの解除には復号化も含まれます。
注意:
ここでは、整合性チェックは成功するものと考えることができます。ただし、一般に、整合性チェックの失敗は、メッセージが途中で変更されたことが原因です。unwrap
メソッドは、整合性チェックの失敗に遭遇すると、GSSExceptionをメジャー・エラー・コードGSSException.BAD_MICとともにスローします。
/*
* Create a MessageProp which unwrap will use to return
* information such as the Quality-of-Protection that was
* applied to the wrapped token, whether or not it was
* encrypted, etc. Since the initial MessageProp values
* are ignored, it doesn't matter what they are set to.
*/
MessageProp prop = new MessageProp(0, false);
/*
* Read the token. This uses the same token byte array
* as that used during context establishment.
*/
token = new byte[inStream.readInt()];
System.out.println("Will read token of size "
+ token.length);
inStream.readFully(token);
byte[] bytes = context.unwrap(token, 0, token.length, prop);
String str = new String(bytes);
System.out.println("Received data \""
+ str + "\" of length " + str.length());
System.out.println("Encryption applied: "
+ prop.getPrivacy());
次に、SampleServer
は、復号化されたメッセージのMICを生成して、SampleClient
に送信します。このことは必須ではなく、単に、復号化されたメッセージのMICの生成を示しています。復号化されたメッセージは、SampleClient
がラップしてSampleServer
に送信した元のメッセージと正確に同じになるはずです。SampleServer
がこれを生成してSampleClient
に送信し、その後SampleClient
が検証することで、SampleServer
が保持する復号化済のメッセージは、SampleClient
から送信された元のメッセージと正確に一致することが、SampleClient
に対して保証されます。
/*
* First reset the QOP of the MessageProp to 0
* to ensure the default Quality-of-Protection
* is applied.
*/
prop.setQOP(0);
token = context.getMIC(bytes, 0, bytes.length, prop);
System.out.println("Will send MIC token of size "
+ token.length);
outStream.writeInt(token.length);
outStream.write(token);
outStream.flush();
次のSampleClient
コードは、SampleServer
により計算された(復号化済メッセージに対する) MICを読み取って、このMICが元のメッセージのMICなのかどうかを検証します。これにより、SampleServer
が保持する復号化されたメッセージが、元のメッセージと同じであることが保証されます。
token = new byte[inStream.readInt()];
System.out.println("Will read token of size " + token.length);
inStream.readFully(token);
/*
* Recall messageBytes is the byte array containing
* the original message and prop is the MessageProp
* already instantiated by SampleClient.
*/
context.verifyMIC(token, 0, token.length,
messageBytes, 0, messageBytes.length,
prop);
System.out.println("Verified received MIC for message.");
このチュートリアルでは、ベースとなる認証およびセキュアな通信技術としてKerberos V5が使用されているため、ユーザーまたはサービスが要求される場合、常にKerberosスタイルのプリンシパル名が使用されます(プリンシパルを参照)。
たとえば、SampleClient
を実行する場合、ユーザー名の指定が求められます。Kerberosスタイルのユーザー名は、Kerberos認証用だけに割り当てられたユーザー名です。ベース・ユーザー名(mjones
など)、「@
」およびレルムの順序で構成されます(例、mjones@KRBNT-OPERATIONS.EXAMPLE.COM
)。
通常、SampleServer
などのサーバー・プログラムは、「サービス」を提供し、特定の「サービス・プリンシパル」に代わって実行されるプログラムと見なされます。SampleServer
のサービス・プリンシパル名が必要とされるのは、次の場合です。
SampleServer
を実行し、SampleClient
がSampleServerへの接続を試みると、基盤となるKerberosメカニズムによりKerberos KDCへの認証が試みられます。ログインするように求められます。適切なサービス・プリンシパルとしてログインしてください。SampleClient
を実行する場合、引数の1つはサービス・プリンシパル名です。これは必須です。このため、SampleClient
は適切なサービスを使用してセキュリティ・コンテキストの確立を開始できます。SampleClient
およびSampleServer
プログラム(このチュートリアル用でない)をセキュリティ・マネージャとともに実行した場合、クライアントおよびサーバー・ポリシー・ファイルはそれぞれ、サービス・プリンシパル名と等価な名前、および「initiate」や「accept」(セキュリティ・コンテキスト確立の開始または受け入れ)と等価なアクションを保持するServicePermissionを必要とします。このドキュメント全体を通して、および関連するログイン構成ファイルでは、service_principal@your_realm
は、環境内で使用される実際の名前に置き換えられるプレースホルダーとして使用されます。サービス・プリンシパル名として、任意の Kerberosプリンシパルを実際に使用できます。このため、このチュートリアルを実行してみる場合、クライアント・ユーザー名とサービス・プリンシパル名の両方に自分のユーザー名を使用できます。
通常、本番稼動環境では、システム管理者は、サーバーを特定のプリンシパルのみで実行し、特定の名前を割り当てて使用します。たいてい、割り当てるKerberos形式のサービス・プリンシパル名は、次のようになります。
service_name/machine_name@realm;
たとえば、KRBNT-OPERATIONS.EXAMPLE.COM
というレルム内のraven
という名前のマシンでnfsサービスを実行する場合、サービス・プリンシパル名は次のようになります
nfs/raven@KRBNT-OPERATIONS.EXAMPLE.COM
ただし、このようなマルチコンポーネント名は必須ではありません。ユーザー・プリンシパル名のような、シングルコンポーネント名も使用できます。たとえば、インストールによって、レルム内のすべてのftpサーバーで同じftpサービス・プリンシパルftp@realm
を使用する場合と、ftpサーバーごとに異なるftpプリンシパルを使用する(たとえば、マシンhost1
、host2
のftpプリンシパルがそれぞれftp/host1@realm
、ftp/host2@realm
となる)場合があります。
ユーザーまたはサービス・プリンシパル名のレルムがデフォルト・レルムの場合は(Kerberos要件を参照)、Kerberosにログインする際、ユーザー名を求めるプロンプトが表示された時点で、レルムを指定しないことも可能です。このため、たとえばユーザー名がmjones@KRBNT-OPERATIONS.EXAMPLE.COM
で、SampleClient
を実行する場合、ユーザー名が要求されたら、レルムを省略して単にmjones
と指定できます。名前はKerberosプリンシパル名のコンテキストで解釈され、必要に応じてデフォルトのレルムが付けられます。
GSSManagerのcreateName
メソッドにより、プリンシパル名がGSSNameに変換される場合にも、レルムの指定を省略できます。たとえば、SampleClient
の実行時に、引数の1つにサーバー・サービス・プリンシパル名を指定します。この場合、SampleClient
が名前をcreateName
メソッドなどに渡し、このメソッドが必要に応じてデフォルトのレルムを追加するため、名前を指定する際にレルムを省略できます。
プリンシパル名をログイン構成ファイルおよびポリシー・ファイルで使用する場合は、常にレルムを含めて名前を指定することをお薦めします。理由は、これらのファイルのパーサーの動作が実装に依存しないため、プリンシパル名の使用前にデフォルトのレルムが追加される場合と、追加されない場合があるためです。名前にレルムが指定されていない場合、以降のアクションは失敗します。
このチュートリアルでは、JAASメソッドを直接的(JAAS認証およびJAAS承認チュートリアルの場合のように)または間接的(たとえば、JAAS Loginユーティリティの使用チュートリアルやJAAS LoginユーティリティおよびJava GSS-APIを使用したセキュアなメッセージ交換チュートリアルで解説されているLoginユーティリティを介して)に呼び出すのではなく、基盤となるKerberosメカニズムがSampleClient
およびSampleServer
を実行するユーザーのクレデンシャルを取得するようにしました。
Oracleの提供するKerberosメカニズムのデフォルト実装では、Kerberos名およびパスワードの入力が求められ、Kerberos KDCに対して指定されたユーザーまたはサービスの認証が行われます。メカニズムでは、この認証処理にJAASが使用されています。
JAASはプラガブル認証フレームワークをサポートします。これは、呼出し側アプリケーションに任意の型の認証モジュールをプラグインできるということです。特定のアプリケーションで使用されるログイン・モジュールは、ログイン構成によって指定されます。Oracle提供のデフォルトのJAAS実装では、ファイル内にログイン構成情報を指定する必要があります。(注: ベンダーによってはファイルベースの実装が提供されない場合もあります。)ログイン構成ファイルとその内容、および使用するログイン構成ファイルの指定方法の詳細は、付録B: JAASログイン構成ファイルを参照してください。
このチュートリアルでは、構成ファイル内にKerberosログイン・モジュールcom.sun.security.auth.module.Krb5LoginModule
が指定されます。このログイン・モジュールは、Kerberos名とパスワードの入力を求めるプロンプトを表示し、Kerberos KDCに対する認証処理を実行しようとします。
ログイン構成ファイルに、クライアント側が使用するエントリとサーバー側が使用するエントリの2つが含まれる場合、SampleClient
とSampleServer
の両方で同じログイン構成ファイルを使用できます。
このチュートリアルで使用するログイン構成ファイルbcsLogin.conf
を、次に示します。
com.sun.security.jgss.initiate { com.sun.security.auth.module.Krb5LoginModule required; }; com.sun.security.jgss.accept { com.sun.security.auth.module.Krb5LoginModule required storeKey=true };
SunのGSS-APIメカニズムの実装では、新しいクレデンシャルが必要な場合、com.sun.security.jgss.initiate
という名前のエントリとcom.sun.security.jgss.accept
という名前のエントリが使用されます。このチュートリアルではKerberos V5メカニズムを使用するため、これらのクレデンシャルの取得にKerberosログイン・モジュールの呼出しが必要です。このため、エントリ内にKrb5LoginModuleを必須モジュールとして記載してあります。com.sun.security.jgss.initiate
エントリはクライアント側の構成を指定し、com.sun.security.jgss.accept
エントリはサーバー側の構成を指定します。
Krb5LoginModuleが成功するのは、指定されたエンティティでのKerberos KDCへのログインが成功した場合だけです。SampleClient
またはSampleServer
の実行時、ユーザーは名前とパスワードの入力を求められます。
SampleServer
のエントリstoreKey=true
は、ログイン時に指定されたパスワードから秘密鍵を計算すること、およびログインにより作成されたサブジェクトのprivateクレデンシャルに秘密鍵を格納することを意味します。この鍵は、SampleClient
とSampleServer
との間でセキュリティ・コンテキストを確立する際、相互認証に利用されます。
Krb5LoginModuleJavadoc APIドキュメントでは、Krb5LoginModuleクラスでサポートされる構成オプションについて説明します。
このチュートリアルでは、システム・プロパティjavax.security.auth.useSubjectCredsOnly
をfalse
に設定します。この設定により、JAASによって設定された既存のサブジェクトから必要なクレデンシャルを取得する際、GSSメカニズムを必要とする通常の制限が緩和されます。この制限緩和により、メカニズムが、ベンダー固有の位置からクレデンシャルを取得できるようになります。たとえば、オペレーティング・システムのキャッシュ(存在する場合)の使用を選択するベンダーや、ディスク上の保護されたファイルから読取りを選択するベンダーがあります。
この制限が緩和された場合でも、OracleのKerberosメカニズムにより、スレッドのアクセス制御コンテキストに関連付けられたサブジェクト内のクレデンシャルを検索します。ただし、クレデンシャルが見つからなかった場合、Kerberosモジュールを使用してJAAS認証を実行し、新規クレデンシャルを取得します。Kerberosモジュールは、Kerberosプリンシパル名とパスワードの入力を求めます。他のベンダーが提供するKerberosメカニズムの実装では、このプロパティをfalse
に設定した場合の動作が異なる場合があることに注意してください。各実装の動作を判断するには、それらのドキュメントを参照してください。
SampleClient
およびSampleServer
プログラムを実行するには、次の操作を行います。
SampleServer
の実行準備では、次の操作を行います。
SampleServer
を実行するマシンからアクセス可能なディレクトリにコピーします。 SampleServer.java
ソース・ファイル。bcsLogin.conf
ログイン構成ファイル。SampleServer.java
を次のようにコンパイルします。 javac SampleServer.java
SampleClient
の実行準備では、次の操作を行います。
SampleClient
を実行するマシンからアクセス可能なディレクトリにコピーします。 SampleClient.java
ソース・ファイル。bcsLogin.conf
ログイン構成ファイル。SampleClient.java
を次のようにコンパイルします。 javac SampleClient.java
SampleClient
を実行する前に、必ずSampleServer
を実行してください。SampleClient
はSampleServer
へのソケット接続を試みるため、SampleServer
が稼動していないとソケット接続が受け付けられず、失敗します。
SampleServer
を実行する場合、SampleServerを稼動する予定のマシンで実行してください。このマシン名(ホスト名)は、SampleClient
の引数として指定します。サービス・プリンシパル名は、ログイン構成ファイルやポリシー・ファイルなど、いくつかの場所に表示されます。
SampleServer
の実行用に準備したディレクトリに移動します。次を指定して、SampleServer
を実行します
-Djava.security.krb5.realm=<your_realm>
(使用するKerberosレルム)。
たとえば、レルムがKRBNT-OPERATIONS.EXAMPLE.COM
の場合、-Djava.security.krb5.realm=KRBNT-OPERATIONS.EXAMPLE.COM
のように指定します。
-Djava.security.krb5.kdc=<your_kdc>
(使用するKerberos KDC)。
たとえば、KDCがsamplekdc.example.com
の場合、-Djava.security.krb5.kdc=samplekdc.example.com
のように指定します。
-Djavax.security.auth.useSubjectCredsOnly=false
(ベースとなるメカニズムによるクレデンシャルの取得方法の指定)。「useSubjectCredsOnlyシステム・プロパティ」を参照してください。
-Djava.security.auth.login.config=bcsLogin.conf
。使用するログイン構成ファイルとしてbcsLogin.conf
を指定します。
SampleServer
に必要な引数は、クライアント接続の待機に使用するポート番号だけです。通常は使用しない大きなポート番号であれば、どの番号でも選択できます。(例、4444)。
次に、Microsoft WindowsおよびSolaris、LinuxおよびmacOSシステムで使用するすべてのコマンドを示します。
注意:
重要: このコマンドの、<port_number>
を適切なポート番号に、<your_realm>
を使用するKerberosレルムに、<your_kdc>
を使用するKerberos KDCにそれぞれ置き換えてください。
java.security.krb5.kdc
システム・プロパティは、「:」記号を複数のKDCのセパレータとして解釈します。KDCがデフォルト・ポート(88)での接続を待機していない場合は、krb5.conf
ファイルでデフォルト・レルムおよびそのKDCを指定し、このファイルの名前を使用してシステム・プロパティjava.security.krb5.kdc.conf
を設定する必要があります。
-Djava.security.krb5.conf=<your_krb5.conf_file>
次に、コマンドを示します。
java -Djava.security.krb5.realm=<your_realm> -Djava.security.krb5.kdc=<your_kdc> -Djavax.security.auth.useSubjectCredsOnly=false -Djava.security.auth.login.config=bcsLogin.conf SampleServer <port_number>
コマンド全体を1行(または、Solaris、LinuxまたはmacOSでは、最後の行を除く各行が、続いていることを示す「\」で終わる複数行で)で表現してください。ここでは、読みやすくするために複数行に分けて表示してあります。このコマンドは非常に長いため、.batファイル(Windowsの場合)または.shファイル(Solaris、LinuxまたはmacOSの場合)に記述して、そのファイルを実行して、コマンドを実行する必要がある場合があります。
コードは、指定されたポートでのソケット接続を待機します。入力を求められたら、サービス・プリンシパルのKerberos名およびパスワードを入力してください。ログイン構成ファイルで指定された基盤となるKerberos認証メカニズムにより、サービス・プリンシパルのKerberosへのログインが行われます。SampleServer
ログイン時のトラブルシューティングについては、「トラブルシューティング」を参照してください。
SampleClient
を実行するため、最初にSampleClient
の実行準備を行ったディレクトリに移動します。次を指定して、SampleClient
を実行します
-Djava.security.krb5.realm=<your_realm>
(使用するKerberosレルム)。-Djava.security.krb5.kdc=<your_kdc>
(使用するKerberos KDC)。-Djavax.security.auth.useSubjectCredsOnly=false
(ベースとなるメカニズムによるクレデンシャルの取得方法の指定)。-Djava.security.auth.login.config=bcsLogin.conf
。使用するログイン構成ファイルとしてbcsLogin.conf
を指定します。SampleClient
の引数は、(1) SampleServer
を表すサービス・プリンシパルのKerberos名(Kerberosユーザー名およびサービス・プリンシパル名を参照)、(2) SampleServer
を実行するホスト(マシン)の名前、(3) SampleServer
がクライアント接続を待機するポート番号です。
次に、WindowsおよびSolaris、LinuxおよびmacOSシステムで使用するすべてのコマンドを示します。
注意:
重要: このコマンドの、<service_principal>
、<host>
、<port_number>
、<your_realm>
および<your_kdc>
を、適切な値に置き換えてください(ポート番号は、SampleServer
の引数として渡したポート番号と同じにする必要がある)。値を引用符で囲む必要はありません。
次に、コマンドを示します。
java -Djava.security.krb5.realm=<your_realm> -Djava.security.krb5.kdc=<your_kdc> -Djavax.security.auth.useSubjectCredsOnly=false -Djava.security.auth.login.config=bcsLogin.conf SampleClient <service_principal> <host> <port_number>
コマンド全体を1行で入力してください。ここでは、読みやすくするために複数行に分けて表示してあります。SampleServer
を実行するコマンドと同様、コマンド・ウィンドウに直接入力するにはコマンドが長すぎる場合、.batファイル(Windowsの場合)または.shファイル(Solaris、LinuxおよびmacOSの場合)に記述して、そのファイルを実行してください。
入力が求められたら、Kerberosユーザー名およびパスワードを入力します。ログイン構成ファイルで指定された基盤となるKerberos認証メカニズムにより、Kerberosへのログインが行われます。SampleClient
コードは、SampleServer
へのソケット接続を要求します。SampleServer
が接続を受け付けると、SampleClient
およびSampleServer
により、このチュートリアルで解説した方法で、共有コンテキストの確立およびメッセージの交換が行われます。
ログイン時のトラブルシューティングについては、「トラブルシューティング」を参照してください。