このチュートリアルでは、相互に通信するアプリケーション間でセキュアなメッセージ交換を実行するための Java GSS-API の使用方法を示す 2 つのサンプルアプリケーションを紹介します。ここでは、クライアントアプリケーションとサーバーアプリケーションを例として使用しています。
Java GSS-API は、「セキュリティーメカニズム」と呼ばれるメカニズムを使用して、これらのサービスを提供します。Java 2 Standard Edition プラットフォームで利用可能な GSS-API 実装には、Kerberos V5 メカニズムのサポートと、ベンダー固有の任意の選択肢が含まれます。このチュートリアルでは、Kerberos V5 メカニズムを使用します。
クライアントとサーバー間の認証を実行し、セキュアな通信用の暗号化鍵を確立するために、GSS-API メカニズムは、接続の両側のローカルエンティティー用の特定のクレデンシャルにアクセスする必要があります。このチュートリアルでは、クライアント側で使用するクレデンシャルは Kerberos チケットで構成され、サーバー側のクレデンシャルは長期 Kerberos 秘密鍵で構成されます。Kerberos チケットには、ホストアドレスをオプションで組み込むことができ、IPv4 と IPv6 の両方のホストアドレスがサポートされています。Java GSS-API は、メカニズムが、これらのクレデンシャルを、スレッドのアクセス制御コンテキストに関連付けられたサブジェクトから取得することを要求します。
通常、この種のクレデンシャルを持つサブジェクトを生成するため、クライアントアプリケーションおよびサーバーアプリケーションは、Kerberos モジュールを使用して 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 認証の実行時には常に必要なファイルで、使用する認証モジュールを指定する)。
この一連のすべてのチュートリアルでは、認証およびアプリケーションのセキュアな通信をサポートする基盤の技術として、Kerberos V5 を使用しています。「Kerberos 要件」を参照してください。
チュートリアルのコードを最初に実行してみる場合、「SampleClient および SampleServer プログラムの実行」を先に読んでから、その他のセクションに戻り、学習を続けてください。
このチュートリアルで使用するアプリケーションの名前は、SampleClient および SampleServer です。
次に、SampleClient および SampleServer アプリケーションの実行方法のサマリーを示します。
実際のコードおよび詳細は、後述のセクションで示します。
SampleClient および SampleServer プログラムの全体のコードは、main
メソッド内に配置されており、次の区分にさらに分割できます。
注:これらのプログラムが使用する Java GSS-API クラス (GSSManager、GSSContext、GSSName、GSSCredential、MessageProp、および Oid) は、org.ietf.jgss
パッケージ内にあります。
クライアントとサーバーの main
メソッドが最初に行うことは、コマンド行引数の読み取りです。
SampleClient は、次の 3 つの引数を受け付けます。
次に、コマンド行引数を読み取るコードを示します。
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 つだけです。
次に、コマンド行引数を読み取るコードを示します。
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 は、GSSManager のインスタンス化、およびいずれかの 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
への完全な呼び出しを示します。
GSSName peer
引数クライアント/サーバーというパラダイムにおけるピアは、サーバーです。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
Sring の場合です。このため、SampleClient は、Oid として null
を渡します。次に、その呼び出しを示します。
GSSName serverName = manager.createName(server, null);
Oid mech
引数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
を渡します。
GSSCredential myCred
引数GSSManager createContext
メソッドの 3 番目の引数は、呼び出し側のクレデンシャルを表す GSSCredential です。SampleClient の場合と同様、この引数に null
を渡す場合、デフォルトのクレデンシャルが使用されます。
int lifetime
引数GSSManager createContext
メソッドの最後の引数は int
で、作成するコンテキストのライフタイムを秒で指定します。SampleClient は、GSSContext.DEFAULT_LIFETIME
を渡して、デフォルトのライフタイムを要求します。
ここまでで、必要な引数をすべて説明しました。ここでは、GSSContext を作成する SampleClient の呼び出しを示します。
GSSContext context = manager.createContext(serverName, krb5Oid, null, GSSContext.DEFAULT_LIFETIME);
コンテキストのインスタンス化が完了したら、コンセプトアクセプタを使用してコンテキストを実際に確立する前に、コンテキストイニシエータで、使用するセキュリティーコンテキストの特性を決定するさまざまなオプションを設定できます。各オプションは、インスタンス化されたコンテキスト上で 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
注:デフォルトの GSSManager 実装および Kerberos メカニズムを使用する場合、これらの要求は常に許可されます。
SampleClient は、GSSContext のインスタンス化および使用するコンテキストオプションの指定後に、SampleServer とのセキュリティーコンテキストを実際に確立できます。このために、SampleClient はループ構造を保持します。各ループにより、次が反復実行されます。
initSecContext
メソッドの呼び出し。最初の呼び出しの場合、メソッドには null
のトークンが渡されます。それ以外の場合、SampleServer から SampleClient に直前に送信されたトークン (SampleServer による acceptSecContext
の呼び出しで生成されるトークン) が渡されます。initSecContext
により返されたトークン (存在する場合) の SampleServer への送信。initSecContext
への最初の呼び出しでは、常にトークンが生成されます。最後の呼び出しでは、トークンが返されない場合があります。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 はコンテキストアクセプタです。次に、セキュリティーコンテキストを確立するための基本的な手順を示します。次のとおりです。
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 が行うメッセージ交換のサマリーを、次に示します。
wrap
を呼び出して、メッセージの MIC を暗号化および計算します。wrap
から返されたトークンを SampleServer に送信します。unwrap
を呼び出して元のメッセージを取得し、整合性を検証します。getMIC
を呼び出して、複号化したメッセージに対する MIC を計算します。getMIC
により返されたトークン (MIC を含む) を 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 から送信された元のメッセージと正確に一致することが保証されます。
/* * 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.");
socket.close(); context.dispose();
このチュートリアルでは、ベースとなる認証およびセキュアな通信技術として Kerberos V5 が使用されているため、ユーザーまたはサービスが要求される場合、常に Kerberos スタイルのプリンシパル名が使用されます。
たとえば、SampleClient を実行する場合、ユーザー名の指定が求められます。Kerberos スタイルのユーザー名は、Kerberos 認証用だけに割り当てられたユーザー名です。このユーザー名は、ベースユーザー名 (例、「mjones」)、「@」、およびレルムの順序で構成されます (例、「mjones@KRBNT-OPERATIONS.EXAMPLE.COM」)。
通常、SampleServer などのサーバープログラムは、「サービス」を提供し、特定の「サービスプリンシパル」に代わって実行されるプログラムと見なされます。SampleServer のサービスプリンシパル名が必要とされるのは、次の場合です。
このドキュメントおよび関連するログイン構成ファイルを通じて、
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 を実行するユーザーのクレデンシャルを取得するようにしました。
Sun Microsystems の提供する Kerberos メカニズムのデフォルト実装では、Kerberos 名およびパスワードの入力が促されます。そのあと、Kerberos KDC に対して指定されたユーザーまたはサービスの認証が行われます。メカニズムでは、この認証処理に JAAS が使用されています。
JAAS はプラグイン可能な認証フレームワークをサポートします。これは、呼び出し側アプリケーションに任意の型の認証モジュールをプラグインできるということです。特定のアプリケーションで使用されるログインモジュールは、ログイン構成によって指定されます。Sun Microsystems 提供のデフォルトの JAAS 実装では、単一のファイル内にログイン構成情報を指定する必要があります。(注:ベンダーによってはファイルベースの実装が提供されない場合もあります。) ログイン構成ファイルとその内容、および使用するログイン構成ファイルの指定方法については、「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 との間でセキュリティーコンテキストを確立する際、相互認証に利用されます。
Krb5LoginModule に引き渡し可能なすべてのオプションの詳細は、Krb5LoginModule ドキュメントを参照してください。
このチュートリアルでは、システムプロパティー javax.security.auth.useSubjectCredsOnly
を false
に設定します。この設定により、JAAS によって設定された既存のサブジェクトから必要なクレデンシャルを取得する際、GSS メカニズムの要件が緩和されます。この制限緩和により、メカニズムが、ベンダー固有の位置からクレデンシャルを取得できるようになります。たとえば、オペレーティングシステムのキャッシュ (存在する場合) の使用を選択するベンダーや、ディスク上の保護されたファイルから読み取りを選択するベンダーがあります。
この制限が緩和された場合でも、Sun Microsystem の Kerberos メカニズムにより、スレッドのアクセス制御コンテキストに関連付けられたサブジェクト内のクレデンシャルを検索します。ただし、クレデンシャルが見つからなかった場合、Kerberos モジュールを使用して JAAS 認証を実行し、新規クレデンシャルを取得します。Kerberos モジュールは、Kerberos プリンシパル名とパスワードを求めるプロンプトを表示します。ほかのベンダーが提供する Kerberos メカニズムの実装では、このプロパティーを false
に設定した場合の動作が異なる場合があることに注意してください。各実装の動作については、提供されるドキュメントを参照してください。
SampleClient および SampleServer プログラムを実行するには、次の操作を行います。
SampleServer の実行準備では、次の操作を行います。
SampleServer.java
をコンパイルします。
javac SampleServer.java
SampleClient の実行準備では、次の操作を行います。
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 および Unix システムの両方で使用可能なすべてのコマンドを示します。
重要: このコマンドの、<port_number>
を適切なポート番号に、<your_realm>
を使用する Kerberos レルムに、<your_kdc>
を使用する Kerberos KDC にそれぞれ置き換えてください。
次に、コマンドを示します。
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 行で入力してください。ただし、UNIX システムの場合、行の最後に行継続を示す文字「\」を付けることで (最後の行を除く)、複数行に分けて入力できます。ここでは、可読性のみを考慮して、複数行に分けて表示しています。このコマンドは非常に長いため、.bat ファイル (Windows) または .sh ファイル (UNIX) に記述して、そのファイルを実行することが必要な場合があります。
SampleServer
コードは、指定されたポート上でソケット接続を待機します。入力を求められたら、サービスプリンシパルの Kerberos 名およびパスワードを入力してください。ログイン構成ファイルで指定された基盤となる Kerberos 認証メカニズムにより、サービスプリンシパルの Kerberos へのログインが行われます。
ログイン時のトラブルシューティングについては、「トラブルシューティング」を参照してください。
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 名、(2) SampleServer を実行するホスト (マシン) の名前、(3) SampleServer がクライアント接続を待機するポート番号です。
次に、Windows と Unix の両方のシステムで使用可能な全コマンドを示します。
重要: このコマンドの、<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 ファイル (UNIX) に記述して、そのファイルを実行してください。
入力が求められたら、Kerberos ユーザー名およびパスワードを入力します。ログイン構成ファイルで指定された基盤となる Kerberos 認証メカニズムにより、Kerberos へのログインが行われます。SampleClient コードは、SampleServer へのソケット接続を要求します。SampleServer が接続を受け付けると、SampleClient および SampleServer により、このチュートリアルで解説した方法で、共有コンテキストの確立およびメッセージの交換が行われます。
ログイン時のトラブルシューティングについては、「トラブルシューティング」を参照してください。