1. 概要

Secured Socket Layer(SSL)は、ネットワークを介した通信のセキュリティを提供する暗号化プロトコルです。 このチュートリアルでは、SSLハンドシェイクの失敗につながる可能性のあるさまざまなシナリオとその方法について説明します。

JSSE を使用したSSLの概要では、SSLの基本について詳しく説明していることに注意してください。

2. 用語

セキュリティの脆弱性のため、標準としてのSSLはトランスポート層セキュリティ(TLS)に取って代わられていることに注意することが重要です。 Javaを含むほとんどのプログラミング言語には、SSLとTLSの両方をサポートするライブラリがあります。

SSLの開始以来、OpenSSLやJavaなどの多くの製品や言語にはSSLへの参照があり、TLSが引き継いだ後も保持されていました。 このため、このチュートリアルの残りの部分では、SSLという用語を使用して一般的に暗号化プロトコルを指します。

3. 設定

このチュートリアルでは、 Java Socket API を使用して、ネットワーク接続をシミュレートする簡単なサーバーおよびクライアントアプリケーションを作成します。

3.1. クライアントとサーバーの作成

Javaでは、 s ocketsを使用して、ネットワークを介してサーバーとクライアント間の通信チャネルを確立できます。 ソケットは、JavaのJava Secure Socket Extension(JSSE)の一部です。

簡単なサーバーを定義することから始めましょう:

int port = 8443;
ServerSocketFactory factory = SSLServerSocketFactory.getDefault();
try (ServerSocket listener = factory.createServerSocket(port)) {
    SSLServerSocket sslListener = (SSLServerSocket) listener;
    sslListener.setNeedClientAuth(true);
    sslListener.setEnabledCipherSuites(
      new String[] { "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256" });
    sslListener.setEnabledProtocols(
      new String[] { "TLSv1.2" });
    while (true) {
        try (Socket socket = sslListener.accept()) {
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            out.println("Hello World!");
        }
    }
}

上記で定義されたサーバーは、「HelloWorld!」というメッセージを返します。 接続されたクライアントに。

次に、 SimpleServer:に接続する基本的なクライアントを定義しましょう。

String host = "localhost";
int port = 8443;
SocketFactory factory = SSLSocketFactory.getDefault();
try (Socket connection = factory.createSocket(host, port)) {
    ((SSLSocket) connection).setEnabledCipherSuites(
      new String[] { "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256" });
    ((SSLSocket) connection).setEnabledProtocols(
      new String[] { "TLSv1.2" });
    
    SSLParameters sslParams = new SSLParameters();
    sslParams.setEndpointIdentificationAlgorithm("HTTPS");
    ((SSLSocket) connection).setSSLParameters(sslParams);
    
    BufferedReader input = new BufferedReader(
      new InputStreamReader(connection.getInputStream()));
    return input.readLine();
}

クライアントは、サーバーから返されたメッセージを出力します。

3.2. Javaでの証明書の作成

SSLは、ネットワーク通信の機密性、整合性、および信頼性を提供します。 証明書は、信頼性を確立する限り重要な役割を果たします。

通常、これらの証明書は認証局によって購入および署名されますが、このチュートリアルでは、自己署名証明書を使用します。

これを実現するために、JDKに付属している keytool、を使用できます。

$ keytool -genkey -keypass password \
                  -storepass password \
                  -keystore serverkeystore.jks

上記のコマンドは、対話型シェルを開始して、共通名(CN)や識別名(DN)などの証明書の情報を収集します。 関連するすべての詳細を提供すると、サーバーの秘密鍵とその公開証明書を含むファイルserverkeystore.jksが生成されます。

serverkeystore.jks は、Java独自のJava Key Store(JKS)形式で保存されていることに注意してください。最近、keytoolは、PKCS#12の使用を検討する必要があることを通知します。また、サポートしています。

さらに、 keytool を使用して、生成されたキーストアファイルから公開証明書を抽出できます。

$ keytool -export -storepass password \
                  -file server.cer \
                  -keystore serverkeystore.jks

上記のコマンドは、公開証明書をキーストアからファイルserver.cerとしてエクスポートします。 エクスポートされた証明書をトラストストアに追加して、クライアントに使用してみましょう。

$ keytool -import -v -trustcacerts \
                     -file server.cer \
                     -keypass password \
                     -storepass password \
                     -keystore clienttruststore.jks

これで、サーバー用のキーストアとクライアント用の対応するトラストストアが生成されました。 ハンドシェイクの失敗の可能性について説明するときに、これらの生成されたファイルの使用について説明します。

また、Javaのキーストアの使用法の詳細については、前のチュートリアルを参照してください。

4. SSLハンドシェイク

SSLハンドシェイクはクライアントとサーバーがネットワーク経由の接続を保護するために必要な信頼とロジスティクスを確立するメカニズムです

これは非常に組織化された手順であり、この詳細を理解すると、失敗することが多い理由を理解するのに役立ちます。これについては、次のセクションで説明します。

SSLハンドシェイクの一般的な手順は次のとおりです。

  1. クライアントは、使用可能なSSLバージョンと暗号スイートのリストを提供します
  2. サーバーは特定のSSLバージョンと暗号スイートに同意し、その証明書で応答します
  3. クライアントは証明書から公開鍵を抽出し、暗号化された「プレマスター鍵」で応答します
  4. サーバーは、秘密鍵を使用して「プレマスター鍵」を復号化します
  5. クライアントとサーバーは、交換された「プレマスターキー」を使用して「共有シークレット」を計算します
  6. クライアントとサーバーは、「共有シークレット」を使用して暗号化と復号化が成功したことを確認するメッセージを交換します

ほとんどの手順はSSLハンドシェイクで同じですが、一方向SSLと双方向SSLの間には微妙な違いがあります。 これらの違いを簡単に確認しましょう。

4.1. 一方向SSLでのハンドシェイク

上記の手順を参照すると、手順2で証明書の交換について説明します。 一方向SSLでは、クライアントが公開証明書を介してサーバーを信頼できる必要があります。 このは、接続を要求するすべてのクライアントを信頼するようにサーバーを離れます。 サーバーがクライアントからの公開証明書を要求して検証する方法はありません。これはセキュリティリスクをもたらす可能性があります。

4.2. 双方向SSLでのハンドシェイク

一方向SSLでは、サーバーはすべてのクライアントを信頼する必要があります。 ただし、双方向SSLは、サーバーが信頼できるクライアントを確立できるようにする機能を追加します。 双方向ハンドシェイクの間、接続が正常に確立される前に、クライアントとサーバーの両方が互いの公開証明書を提示して受け入れる必要があります。

5. ハンドシェイクの失敗シナリオ

その簡単なレビューを行った後、障害シナリオをより明確に見ることができます。

一方向または双方向通信でのSSLハンドシェイクは、複数の理由で失敗する可能性があります。 これらの各理由を調べ、障害をシミュレートし、そのようなシナリオを回避する方法を理解します。

これらの各シナリオでは、前に作成したSimpleClientSimpleServerを使用します。

5.1. サーバー証明書がありません

SimpleServer を実行し、SimpleClientを介して接続してみましょう。 「HelloWorld!」というメッセージが表示されることを期待していますが、例外があります。

Exception in thread "main" javax.net.ssl.SSLHandshakeException: 
  Received fatal alert: handshake_failure

さて、これは何かがうまくいかなかったことを示しています。 上記のSSLHandshakeExceptionは、抽象的な方法で、は、サーバーに接続するときにクライアントが証明書を受信しなかったことを示しています。

この問題に対処するために、以前に生成したキーストアをシステムプロパティとしてサーバーに渡すことで使用します。

-Djavax.net.ssl.keyStore=clientkeystore.jks -Djavax.net.ssl.keyStorePassword=password

キーストアファイルパスのシステムプロパティは絶対パスであるか、キーストアファイルをJavaコマンドを呼び出してサーバーを起動するのと同じディレクトリに配置する必要があることに注意してください。 キーストアのJavaシステムプロパティは相対パスをサポートしていません。

これは、期待する出力を得るのに役立ちますか? 次のサブセクションで調べてみましょう。

5.2. 信頼できないサーバー証明書

前のサブセクションで変更を加えてSimpleServerSimpleClientを再度実行すると、出力として何が得られますか。

Exception in thread "main" javax.net.ssl.SSLHandshakeException: 
  sun.security.validator.ValidatorException: 
  PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: 
  unable to find valid certification path to requested target

まあ、それは私たちが期待したように正確には機能しませんでしたが、別の理由で失敗したようです。

この特定の障害は、サーバーが認証局(CA)によって署名されていない自己署名証明書を使用していることが原因で発生します。

実際、証明書がデフォルトのトラストストアにあるもの以外のものによって署名されている場合は常に、このエラーが表示されます。 JDKのデフォルトのトラストストアには、通常、使用中の一般的なCAに関する情報が付属しています。

ここでこの問題を解決するには、SimpleClientSimpleServerによって提示された証明書を信頼させる必要があります。 以前に生成したトラストストアをシステムプロパティとしてクライアントに渡すことにより、それらを使用してみましょう。

-Djavax.net.ssl.trustStore=clienttruststore.jks -Djavax.net.ssl.trustStorePassword=password

これは理想的なソリューションではないことに注意してください。理想的なシナリオでは、自己署名証明書ではなく、クライアントがデフォルトで信頼できる認証局(CA)によって認定された証明書を使用する必要があります。[X228X ]

次のサブセクションに進んで、期待どおりの出力が得られるかどうかを確認しましょう。

5.3. クライアント証明書がありません

前のサブセクションからの変更を適用して、SimpleServerとSimpleClientをもう一度実行してみましょう。

Exception in thread "main" java.net.SocketException: 
  Software caused connection abort: recv failed

繰り返しますが、私たちが期待したものではありません。 ここでのSocketExceptionは、サーバーがクライアントを信頼できなかったことを示しています。 これは、双方向SSLを設定しているためです。 SimpleServer には、次のものがあります。

((SSLServerSocket) listener).setNeedClientAuth(true);

上記のコードは、公開証明書によるクライアント認証にSSLServerSocketが必要であることを示しています。

以前のキーストアとトラストストアを作成したときに使用したのと同様の方法で、クライアントのキーストアとサーバーの対応するトラストストアを作成できます。

サーバーを再起動し、次のシステムプロパティを渡します。

-Djavax.net.ssl.keyStore=serverkeystore.jks \
    -Djavax.net.ssl.keyStorePassword=password \
    -Djavax.net.ssl.trustStore=servertruststore.jks \
    -Djavax.net.ssl.trustStorePassword=password

次に、次のシステムプロパティを渡して、クライアントを再起動します。

-Djavax.net.ssl.keyStore=clientkeystore.jks \
    -Djavax.net.ssl.keyStorePassword=password \
    -Djavax.net.ssl.trustStore=clienttruststore.jks \
    -Djavax.net.ssl.trustStorePassword=password

最後に、必要な出力が得られます。

Hello World!

5.4. 不正な証明書

上記のエラーとは別に、証明書の作成方法に関連するさまざまな理由により、ハンドシェイクが失敗する可能性があります。 一般的なエラーの1つは、誤ったCNに関連しています。 以前に作成したサーバーキーストアの詳細を調べてみましょう。

keytool -v -list -keystore serverkeystore.jks

上記のコマンドを実行すると、キーストアの詳細、具体的には所有者を確認できます。

...
Owner: CN=localhost, OU=technology, O=baeldung, L=city, ST=state, C=xx
...

この証明書の所有者のCNはlocalhostに設定されています。 所有者のCNは、サーバーのホストと正確に一致する必要があります。 不一致がある場合は、SSLHandshakeExceptionが発生します。

CNをローカルホスト以外のものとしてサーバー証明書を再生成してみましょう。 再生成された証明書を使用してSimpleServerおよびSimpleClientを実行すると、すぐに失敗します。

Exception in thread "main" javax.net.ssl.SSLHandshakeException: 
    java.security.cert.CertificateException: 
    No name matching localhost found

上記の例外トレースは、クライアントがlocalhostという名前の証明書を予期していたことを明確に示していますが、見つかりませんでした。

JSSEは、デフォルトではホスト名の検証を義務付けていないことに注意してください。 HTTPSを明示的に使用することにより、SimpleClientでホスト名の検証を有効にしました。

SSLParameters sslParams = new SSLParameters();
sslParams.setEndpointIdentificationAlgorithm("HTTPS");
((SSLSocket) connection).setSSLParameters(sslParams);

ホスト名の検証は失敗の一般的な原因であり、一般に、セキュリティを強化するために常に実施する必要があります。 ホスト名の検証とTLSによるセキュリティにおけるその重要性の詳細については、この記事を参照してください。

5.5. 互換性のないSSLバージョン

現在、動作中のSSLおよびTLSのさまざまなバージョンを含むさまざまな暗号化プロトコルがあります。

前述のように、SSLは一般に、暗号化の強さからTLSに取って代わられています。 暗号化プロトコルとバージョンは、ハンドシェイク中にクライアントとサーバーが合意する必要がある追加の要素です。

たとえば、サーバーがSSL3の暗号化プロトコルを使用し、クライアントがTLS1.3を使用している場合、サーバーは暗号化プロトコルに同意できず、SSLHandshakeExceptionが生成されます。

SimpleClient で、プロトコルをサーバーに設定されているプロトコルと互換性のないものに変更しましょう。

((SSLSocket) connection).setEnabledProtocols(new String[] { "TLSv1.1" });

クライアントを再度実行すると、SSLHandshakeExceptionが発生します。

Exception in thread "main" javax.net.ssl.SSLHandshakeException: 
  No appropriate protocol (protocol is disabled or cipher suites are inappropriate)

このような場合の例外トレースは抽象的であり、正確な問題を示していません。 これらのタイプの問題を解決するには、クライアントとサーバーの両方が同じまたは互換性のある暗号化プロトコルを使用していることを確認する必要があります。

5.6. 互換性のない暗号スイート

クライアントとサーバーは、メッセージの暗号化に使用する暗号スイートについても合意する必要があります。

ハンドシェイク中に、クライアントは使用可能な暗号のリストを提示し、サーバーはリストから選択された暗号で応答します。 サーバーは、適切な暗号を選択できない場合、SSLHandshakeExceptionを生成します。

SimpleClient で、暗号スイートをサーバーで使用されている暗号スイートと互換性のないものに変更しましょう。

((SSLSocket) connection).setEnabledCipherSuites(
  new String[] { "TLS_RSA_WITH_AES_128_GCM_SHA256" });

クライアントを再起動すると、SSLHandshakeExceptionが発生します。

Exception in thread "main" javax.net.ssl.SSLHandshakeException: 
  Received fatal alert: handshake_failure

繰り返しますが、例外トレースは非常に抽象的であり、正確な問題を示していません。 このようなエラーの解決策は、クライアントとサーバーの両方で使用されている有効な暗号スイートを確認し、少なくとも1つの共通スイートが使用可能であることを確認することです。

通常、クライアントとサーバーはさまざまな暗号スイートを使用するように構成されているため、このエラーが発生する可能性は低くなります。 このエラーが発生した場合は、通常、サーバーが非常に選択的な暗号を使用するように構成されていることが原因です。サーバーは、セキュリティ上の理由から、選択的な暗号のセットを適用することを選択する場合があります。

6. 結論

このチュートリアルでは、Javaソケットを使用したSSLの設定について学習しました。 次に、一方向および双方向SSLを使用したSSLハンドシェイクについて説明しました。 最後に、SSLハンドシェイクが失敗する可能性のある理由のリストを確認し、解決策について説明しました。

いつものように、例のコードはGitHubから入手できます。