チュートリアルの紹介および目次 次のチュートリアル フィードバック

JavaTM GSS-API を使用した、JAAS プログラミングなしの安全なメッセージ交換



このチュートリアルでは、相互に通信するアプリケーション間で安全なメッセージ交換を実行するための Java GSS-API の使用方法を示す 2 つのサンプルアプリケーションを紹介します。ここでは、クライアントアプリケーションとサーバアプリケーションを例として使用しています。

Java GSS-API は、「セキュリティ機構」と呼ばれる機構を使用して、これらのサービスを提供します。 Java 2 Standard Edition プラットフォームで利用可能な GSS-API 実装には、Kerberos V5 機構のサポートと、ベンダー固有の任意の選択肢が含まれます。このチュートリアルでは、Kerberos V5 機構を使用します。

クライアントとサーバ間の認証を実行し、安全な通信用の暗号化鍵を確立するために、GSS-API 機構は、接続の両側のローカルエンティティ用の特定のクレデンシャルにアクセスする必要があります。このチュートリアルでは、クライアント側で使用するクレデンシャルは Kerberos チケットで構成され、サーバ側のクレデンシャルは長期 Kerberos 秘密鍵で構成されます。Java GSS-API は、機構が、これらのクレデンシャルを、スレッドのアクセス制御コンテキストに関連付けられたサブジェクトから取得することを要求します。

通常、この種のクレデンシャルを持つサブジェクトを生成するため、クライアントアプリケーションおよびサーバアプリケーションは、Kerberos モジュールを使用して JAAS 認証を実行します。実行方法の詳細は、「JAAS 認証」チュートリアルを参照してください。認証されたサブジェクトをスレッドのアクセス制御コンテキストに関連付ける方法については、「JAAS 承認」を参照してください。 これらの操作を自動的に実行する ユーティリティも用意されています。 Login ユーティリティの使用方法については、「JAAS Login ユーティリティの使用」チュートリアルを参照してください。

このチュートリアルでは、クライアントおよびサーバで JAAS 認証を実行しないため、Login ユーティリティは使用しません。その代わり、システムプロパティ javax.security.auth.useSubjectCredsOnlyfalse に設定する必要があります。この設定により、JAAS によって設定された既存のサブジェクトから必要なクレデンシャルを取得する際、GSS 機構の要件が緩和されます。 「useSubjectCredsOnly システムプロパティ」を参照してください。

注:これは入門用のチュートリアルであるため、内容は簡略化されています。たとえば、ポリシーファイルは含まれておらず、サンプルコードの実行にセキュリティマネージャを使用していません。実際には、Java GSS-API を使用するコードは、セキュリティマネージャを使用して実行する必要があります。このため、必要なアクセス権が明示的に付与されていない限り、セキュリティ関連の操作は許可されません。

別のチュートリアル、「JAAS Login ユーティリティおよび Java GSS-API を使用した安全なメッセージ交換」の内容は、このユーティリティと類似していますが、Login ユーティリティ、ポリシーファイル、およびより複雑なログイン構成ファイルを利用する点が異なります (ログイン構成ファイルは、JAAS 認証の実行時には常に必要なファイルで、使用する認証モジュールを指定する)。

この一連のチュートリアルでは、認証およびアプリケーションの安全な通信をサポートする基盤の技術として、Kerberos V5 を使用しています。詳細は、「Kerberos 要件」を参照してください。

チュートリアルのコードを最初に実行してみる場合、「SampleClient および SampleServer プログラムの実行」を先に読んでから、最初に戻り、学習を続けてください。


クライアントおよびサーバアプリケーションの概要

このチュートリアルで使用するアプリケーションの名前は、SampleClient および SampleServer です。

以下に、SampleClient および SampleServer アプリケーションの実行方法の概要を示します。

  1. SampleServer アプリケーション、SampleServer を実行します。

    1. 引数に注目し、クライアントからの接続を待機するポート番号を確認します。

    2. 指定されたポート上でクライアント接続を待機する ServerSocket を作成します。

    3. 接続を待機します。

  2. SampleClient アプリケーション、SampleClient を実行します (通常別のマシンを使用)。

    1. 引数に注目し、(1) SampleServer を表す Kerberos プリンシパルの名前(「Kerberos ユーザ名およびサービスプリンシパル名」を参照)、(2) SampleServer を実行中のホスト (マシン) の名前、 (3) SampleServer がクライアント接続を待機するポート番号を確認します。

    2. 引数として渡されたホストおよびポートを使用して、SampleServer へのソケット接続を試みます。

  3. ソケット接続が SampleServer により受け入れられます。両方のアプリケーションが、ソケット入力および出力ストリームからの DataInputStream および DataOutputStream を初期化して、将来のデータ交換に使用します。

  4. SampleClient および SampleServer は、それぞれ GSSContext をインスタンス化し、以後の安全なデータ交換を可能にする共有コンテキストをプロトコルに従って確立します。

  5. これで、SampleClient および SampleServer は、メッセージを安全に交換できます。

  6. SampleClient および SampleServer は、メッセージ交換の完了後に、クリーンアップ操作を実行します。

実際のコードおよび詳細は、後述のセクションで示します。

SampleClient および SampleServer コード

SampleClient および SampleServer プログラムの全体のコードは、main メソッド内に配置されており、次の区分にさらに分割できます。

  1. コマンド行引数の取得

  2. SampleClient と SampleServer 間の転送用ソケット接続の確立

  3. セキュリティコンテキストの確立

  4. メッセージの安全な交換

  5. クリーンアップ

注:これらのプログラムが使用する Java GSS-API クラス (GSSManager、GSSContext、GSSName、GSSCredential、MessageProp、および Oid) は、org.ietf.jgss パッケージ内にあります。

コマンド行引数の取得

クライアントとサーバの main メソッドが最初に行うことは、コマンド行引数の読み取りです。

SampleClient が読み取る引数

SampleClient は、次の 3 つの引数を受け付けます。

  1. サービスプリンシパル名 -- SampleServer を表す Kerberos プリンシパルの名前(「Kerberos ユーザ名およびサービスプリンシパル名」を参照)。

  2. ホスト名 -- SampleServer を実行するマシン

  3. ポート番号 -- 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 が読み取る引数

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 コード

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 コード

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 によるコンテキスト確立

このクライアント/サーバのシナリオでは、SampleClient はコンテキストイニシエータです。以下に、セキュリティコンテキストを確立するための基本的な手順を示します。

  1. GSSContext をインスタンス化します。

  2. 使用するオプション機能をコンテキスト上に設定します。

  3. コンテキストが確立されるまでループを実行します。initSecContext を呼び出すたびに、返されたトークンをすべて SampleServer に送信し、SampleServer からトークンを受け取ります (トークンが存在する場合)。

SampleClient GSSContext のインスタンス化

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 String の場合です。このため、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 を渡して、デフォルトのライフタイムを要求します。

createContext の完全な呼び出し

ここまでで、必要な引数をすべて説明しました。ここでは、GSSContext を作成する SampleClient の呼び出しを示します。

GSSContext context = 
    manager.createContext(serverName,
                          krb5Oid,
                          null,
                          GSSContext.DEFAULT_LIFETIME);

SampleClient のオプション設定

コンテキストのインスタンス化が完了したら、コンセプトアクセプタを使用してコンテキストを確立する前に、コンテキストイニシエータで、使用するセキュリティコンテキストの特性を決定するさまざまなオプションを設定できます。各オプションは、インスタンス化されたコンテキスト上で request メソッドを呼び出すことで設定できます。大半の request メソッドは、その機能を要求するかどうかを示す boolean 引数を取ります。要求は、常に受け入れられるわけではありません。このため、コンテキストの確立後に、いずれかの get メソッドを呼び出して、要求が受け入れられたかどうかを確認できます。

SampleClient は、次のオプションを要求します。

  1. 相互認証。コンテキストイニシエータは、常にアクセプタにより認証されます。イニシエータが相互認証を要求すると、アクセプタもイニシエータに認証されます。

  2. 機密性。機密性を要求するとは、wrap という名前のコンテキストメソッドの暗号化を「有効にする」ことを意味します。暗号が実際に使用されるのは、wrap メソッドに渡される MessageProp オブジェクトにより機密性が要求される場合だけです。

  3. 保全性。これは、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 のコンテキスト確立ループ

SampleClient は、GSSContext のインスタンス化および使用するコンテキストオプションの指定後に、SampleServer とのセキュリティコンテキストを実際に確立できます。このために、SampleClient はループ構造を保持します。ループにより、次の操作が反復実行されます。

  1. コンテキストの initSecContext メソッドの呼び出し。最初の呼び出しの場合、メソッドには null のトークンが渡されます。それ以外の場合、SampleServer から SampleClient に直前に送信されたトークン (SampleServer による acceptSecContext の呼び出しで生成されるトークン) が渡されます。

  2. initSecContext により返されたトークン (存在する場合) の SampleServer への送信。initSecContext への最初の呼び出しでは、常にトークンが生成されます。最後の呼び出しでは、トークンが返されない場合があります。

  3. コンテキストが確立されたかどうかの確認。確立されていない場合、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 によるコンテキスト確立

このクライアント/サーバのシナリオでは、SampleServer はコンテキストアクセプタです。以下に、セキュリティコンテキストを確立するための基本的な手順を示します。

  1. GSSContext をインスタンス化します。

  2. コンテキストが確立されるまで ループを実行します。各ループでは、SampleClient からトークンを受け取り、acceptSecContext を呼び出してトークンを渡し、返されたトークンをすべて SampleClient に送信します。

SampleServer GSSContext のインスタンス化

「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 のコンテキスト確立ループ

SampleServer は、GSSContext をインスタンス化した後で、SampleClient とのセキュリティコンテキストを確立できます。これを実行するため、SampleServer はコンテキストが確立されるまで反復するループ構造を保持します。ループにより、次の操作が反復実行されます。

  1. SampleClient からのトークンの受け取り。このトークンは、SampleClient initSecContext 呼び出しの結果です。

  2. コンテキストの acceptSecContext メソッドの呼び出し、および直前に受け取ったトークンのメソッドへの引き渡し。

  3. 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 間のセキュリティコンテキストが確立されると、メッセージを安全に交換できます。

メッセージ交換用の GSSContext メソッド

メッセージの安全な交換を可能にするメソッドには、wrapgetMIC の 2 種類があります。実際には、2 つの wrap メソッド (および 2 つの getMIC メソッド) が存在します。2 つのメソッドの相違点は、入力メッセージの位置 (バイト配列または入力ストリーム) および出力先 (バイト配列の戻り値または出力ストリーム) です。

メッセージ交換用のこれらのメソッド、および対応するメソッド (生成されるトークンのピアが解釈を行うための) について、以下で説明します。

wrap

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 メソッドが呼び出されてトークンの「ラップが解除」され、元のメッセージの取得および保全性の検証が行われます。

getMIC

指定されたメッセージの暗号化メッセージ保全コード (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 のメッセージ交換

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 が行うメッセージ交換の概要を、次に示します。

  1. SampleClient が wrap を呼び出して、メッセージの MIC を暗号化および計算します。

  2. SampleClient が、wrap から返されたトークンを SampleServer に送信します。

  3. SampleServer が、unwrap を呼び出して元のメッセージを取得し、保全性を検証します。

  4. SampleServer が getMIC を呼び出して、複号化したメッセージに対する MIC を計算します。

  5. SampleServer が、getMIC により返されたトークン (MIC を含む) を SampleClient に送信します。

  6. SampleClient が verifyMIC を呼び出して、SampleServer により送信された MIC が元のメッセージの有効な MIC かどうかを検証します。

メッセージの暗号化および送信を行う SampleClient コード

メッセージの暗号化、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();

トークンのラップ解除、MIC の計算および送信を行う SampleServer コード

次の 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();

MIC を検証する SampleClient コード

次の SampleClient コードは、SampleServer により計算された (複号化済みメッセージに対する) 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.");

クリーンアップ

SampleClient および SampleServer は、メッセージ交換の完了後に、クリーンアップ操作を実行する必要があります。どちらにも、以下の操作を実行するコードが含まれます。
socket.close();
context.dispose();

Kerberos ユーザ名およびサービスプリンシパル名

このチュートリアルでは、基盤となる認証および安全な通信技術として Kerberos V5 が使用されているため、ユーザまたはサービスが要求される場合、常に Kerberos スタイルの プリンシパル名が使用されます。

たとえば、SampleClient を実行する場合、「ユーザ名」の指定が求められます。Kerberos スタイルのユーザ名は、Kerberos 認証用だけに割り当てられたユーザ名です。このユーザ名は、ベースユーザ名 (例、"mjones")、「@」、および領域の順序で構成されます (例、mjones@KRBNT-OPERATIONS.ABC.COM)。

通常、SampleServer などのサーバプログラムは、「サービス」を提供し、特定の「サービスプリンシパル」に代わって実行されるプログラムと見なされます。SampleServer のサービスプリンシパル名が必要とされるのは、次の場合です。

このドキュメントおよび関連するログイン構成ファイルを通じて、

service_principal@your_realm
という形式で記述された部分は、自分の環境で使用する実際の名前で置き換えてください。サービスプリンシパル名として、「任意の」Kerberos プリンシパルを実際に使用できます。このため、このチュートリアルを実行してみる場合、クライアントユーザ名とサービスプリンシパル名の両方に自分のユーザ名を使用できます。

通常、本番稼動環境では、システム管理者は、サーバを特定のプリンシパルのみで実行し、特定の名前を割り当てて使用します。大抵、割り当てる Kerberos 形式のサービスプリンシパル名は、次のようになります。

service_name/machine_name@realm;

たとえば、「KRBNT-OPERATIONS.ABC.COM」という領域内の「raven」という名前のマシンで nfs サービスを実行する場合、サービスプリンシパル名は次のようになります。

nfs/raven@KRBNT-OPERATIONS.ABC.COM

ただし、このようなマルチコンポーネント名は必須ではありません。ユーザプリンシパル名のような、シングルコンポーネント名も使用できます。たとえば、インストールによって、領域内のすべての ftp サーバで同じ ftp サービスプリンシパル ftp@realm を使用する場合と、ftp サーバごとに異なる ftp プリンシパルを使用する (たとえば、マシン host1host2 の ftp プリンシパルがそれぞれ ftp/host1@realmftp/host2@realm となる) 場合があります。

プリンシパル名に領域を指定する必要がある場合

ユーザまたはサービスプリンシパル名の領域がデフォルト領域の場合は (「Kerberos 要件」を参照)、Kerberos にログインする際、ユーザ名を求めるプロンプトが表示された時点で、必ずしも領域を指定する必要はありません。このため、たとえばユーザ名が「mjones@KRBNT-OPERATIONS.ABC.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 ドキュメントを参照してください。

useSubjectCredsOnly システムプロパティ

このチュートリアルでは、システムプロパティ javax.security.auth.useSubjectCredsOnlyfalse に設定します。この設定により、JAAS によって設定された既存のサブジェクトから必要なクレデンシャルを取得する際、GSS 機構の要件が緩和されます。この制限緩和により、機構が、ベンダー固有の位置からクレデンシャルを取得できるようになります。たとえば、オペレーティングシステムのキャッシュ (存在する場合) の使用を選択するベンダーや、ディスク上の保護されたファイルから読み取りを選択するベンダーがあります。

この制限が緩和された場合でも、Sun Microsystem の Kerberos スレッドのアクセス制御コンテキストに関連付けられたサブジェクト内のクレデンシャルを検索します。ただし、クレデンシャルが見つからなかった場合、Kerberos モジュールを使用して JAAS 認証を実行し、新規クレデンシャルを取得します。 Kerberos モジュールは、Kerberos プリンシパル名とパスワードを求めるプロンプトを表示します。 他のベンダーが提供する Kerberos 機構の実装では、このプロパティを false に設定した場合の動作が異なる場合があることに注意してください。各実装の動作については、提供されるドキュメントを参照してください。

SampleClient および SampleServer プログラムの実行

SampleClient および SampleServer プログラムを実行するには、次の操作を行います。

SampleServer の実行準備

SampleServer の実行準備では、以下の操作を行います。

  1. 次のファイルを、SampleServer を実行するマシンからアクセス可能なディレクトリにコピーします。

  2. SampleServer.java をコンパイルします。
    javac SampleServer.java
    

SampleClient の実行準備

SampleClient の実行準備では、以下の操作を行います。

  1. 次のファイルを、SampleClient を実行するマシンからアクセス可能なディレクトリにコピーします。

  2. SampleClient.java をコンパイルします。
    javac SampleClient.java
    

SampleServer の実行

SampleClient を実行する前に、必ず SampleServer を実行してください。SampleClient は SampleServer へのソケット接続を試みるため、SampleServer が稼動していないとソケット接続が受け付けられず、失敗します。

SampleServer を実行する場合、SampleServer を稼動する予定のマシンで実行してください。このマシン名 (ホスト名) は、SampleClient の引数として指定します。サービスプリンシパル名は、ログイン構成ファイルやポリシーファイルなど、いくつかの場所に表示されます。

SampleServer の実行用に準備したディレクトリに移動します。以下を指定して、SampleServer を実行します。

SampleServer に必要な引数は、クライアント接続の待機に使用するポート番号だけです。通常は使用しない大きなポート番号であれば、どの番号でも選択できます(例、4444)。

以下に、Win32 と 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 ファイル (Win32) または .sh ファイル (UNIX) に記述して、そのファイルを実行することが必要な場合があります。

SampleServer コードは、指定されたポート上でソケット接続を待機します。入力を求められたら、サービスプリンシパルの Kerberos 名およびパスワードを入力してください。ログイン構成ファイルで指定された基盤となる Kerberos 認証機構により、サービスプリンシパルの Kerberos へのログインが行われます。

ログイン時のトラブルシューティングについては、「トラブルシューティング」を参照してください。

SampleClient の実行

SampleClient を実行するため、最初に SampleClient の実行準備を行なったディレクトリに移動します。以下を指定して、SampleClient を実行します。

SampleClient の引数は、 (1) SampleServer を表すサービスプリンシパルの Kerberos 名、(2) SampleServer を実行するホスト (マシン) の名前、(3) SampleServer がクライアント接続を待機するポート番号です。

以下に、Win32 と 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 ファイル (Win32) または .sh ファイル (UNIX) に記述して、そのファイルを実行してください。

入力が求められたら、Kerberos ユーザ名およびパスワードを入力します。ログイン構成ファイルで指定された基盤となる Kerberos 認証機構により、Kerberos へのログインが行われます。SampleClient コードは、SampleServer へのソケット接続を要求します。SampleServer が接続を受け付けると、SampleClient および SampleServer により、このチュートリアルで解説した方法で、共有コンテキストの確立およびメッセージの交換が行われます。

ログイン時のトラブルシューティングについては、「トラブルシューティング」を参照してください。



チュートリアルの紹介および目次 次のチュートリアル フィードバック