1概要

Secure Socket Layer(SSL)は、ネットワークを介した通信にセキュリティを提供する暗号化プロトコルです。

このチュートリアルでは、SSLハンドシェイクに失敗する可能性があるさまざまなシナリオとその対処方法について説明します。


JSSEを使用したSSLの紹介

では、SSLの基本について詳しく説明しています。

2.用語

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

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


3セットアップ

このチュートリアルの目的のために、https://www.baeldung.com/a-guide-to-java-sockets[Java Socket API]を使用してネットワーク接続をシミュレートする簡単なサーバーおよびクライアントアプリケーションを作成します。


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

Javaでは、ソケットを使用して、ネットワーク上でサーバーとクライアント間の通信チャネルを確立できます。ソケットは、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!");
        }
    }
}

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

次に、__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 Key Store(JKS)形式で格納されています。これは、Java独自のものです。 ** 最近では、


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のキーストアの使い方に関する詳細はhttps://www.baeldung.com/java-keystore[以前のチュートリアル]にあります。


4 SSLハンドシェイク

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

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

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

  1. クライアントは可能なSSLバージョンと暗号スイートのリストを

つかいます
。サーバーは特定のSSLバージョンと暗号スイートに同意し、応答します。

その証明書を返します
。クライアントが証明書から公開鍵を抽出し、次のように応答します。

暗号化された「プレマスターキー」
。サーバーはプライベートキーを使用して「プレマスターキー」を復号化します

  1. クライアントとサーバーは交換したものを使用して「共有シークレット」を計算します.

「プレマスターキー」
。クライアントとサーバーは、成功を確認するメッセージを交換します。

「共有秘密」を使用した暗号化と復号化

ほとんどの手順はどのSSLハンドシェイクでも同じですが、一方向SSLと双方向SSLにはわずかな違いがあります。これらの違いをすぐに見てみましょう。


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

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


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

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


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

そのクイックレビューを終えたので、失敗のシナリオをより明確に見ることができます。

一方向または双方向通信でのSSLハンドシェイクは、さまざまな理由で失敗する可能性があります。私たちはこれらの理由のそれぞれを通して調べて、失敗をシミュレートしてそしてどうすればそのようなシナリオを避けることができるか理解するでしょう。

これらの各シナリオでは、先ほど作成した

SimpleClient



SimpleServer

を使用します。


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


SimpleServer

を実行し、

SimpleClient

を介して接続してみましょう。 「Hello World!」というメッセージが表示されるはずですが、例外が表示されます。

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. 信頼できないサーバー証明書

前のサブセクションの変更を使用して

SimpleServer



SimpleClient

を再度実行すると、出力として何が得られますか。

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)によって署名されていない__自己署名証明書を使用しているという事実によって引き起こされます。

  • 実際には、証明書がデフォルトのトラストストアにあるもの以外のもので署名されているときはいつでも、このエラーが表示されます。

ここでこの問題を解決するには、

SimpleClient



SimpleServer

によって提示された証明書を信頼させる必要があります。以前に生成したトラストストアをシステムプロパティとしてクライアントに渡して使用しましょう。

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

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

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


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

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

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

繰り返しますが、私たちが期待していたことではありません。ここの

SocketException

は、サーバーがクライアントを信頼できなかったことを示しています。これは双方向SSLを設定したためです。

__SimpleServer

__weには、次のものがあります。

((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

が発生します。

localhost以外のものとしてCNを使用してサーバー証明書を再生成してみましょう。

SimpleServer



SimpleClient

を実行するために再生成された証明書を使用すると、すぐに失敗します。

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

上記の例外トレースは、クライアントがlocalhostという名前を付けた証明書を期待していなかったことを明確に示しています。

  • JSSEはデフォルトではホスト名の検証を義務付けていないことに注意してください。

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

ホスト名の検証は失敗の一般的な原因であり、一般的にはセキュリティを強化するために常に実施する必要があります。ホスト名の確認とTLSによるセキュリティの重要性についての詳細は、https://tersesystems.com/blog/2014/03/23/fixing-hostname -verification/[この記事]を参照してください。


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ハンドシェイクが失敗する可能性のある理由のリストを調べ、解決策について説明しました。

いつものように、例のコードはhttps://github.com/eugenp/tutorials/tree/master/core-java[GitHubで利用可能]で入手可能です。