SSLハンドシェイクの失敗

SSLハンドシェイクの失敗

1. 概要

Secured Socket Layer(SSL)は、ネットワーク上の通信にセキュリティを提供する暗号化プロトコルです。 In this tutorial, we’ll discuss various scenarios that can result in an SSL handshake failure and how to it.

Introduction to SSL using JSSEは、SSLの基本をより詳細にカバーしていることに注意してください。

2. 用語

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

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

3. セットアップ

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

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

Javaでは、sockets to establish a communication channel between a server and client over the networkを使用できます。 ソケットは、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 isは、Java独自のJavaキーストア(JKS)形式で保存されていることに注意してください。 These days, keytool will remind us that we ought to consider using PKCS#12, which it also supports.

さらに、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のキーストアの使用法の詳細については、previous tutorialを参照してください。

4. SSLハンドシェイク

SSLハンドシェイクはa mechanism by which a client and server establish the trust and logistics required to secure their connection over the networkです。

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

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

  1. クライアントは、使用可能なSSLバージョンと暗号スイートのリストを提供します

  2. サーバーは特定のSSLバージョンと暗号スイートに同意し、証明書で応答します

  3. クライアントは証明書から公開鍵を抽出し、暗号化された「プリマスター鍵」で応答します

  4. サーバーは、秘密鍵を使用して「プリマスターキー」を復号化します

  5. クライアントとサーバーは、交換された「プリマスターキー」を使用して「共有秘密」を計算します

  6. クライアントとサーバーは、「共有シークレット」を使用した暗号化と復号化の成功を確認するメッセージを交換します

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

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

上記の手順を参照する場合、手順2では証明書の交換について説明します。 一方向SSLでは、クライアントが公開証明書を通じてサーバーを信頼できる必要があります。 接続を要求するこのleaves the server to trust all clients。 サーバーがクライアントから公開証明書を要求して検証する方法はありません。これはセキュリティリスクを引き起こす可能性があります。

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

一方向SSLでは、サーバーはすべてのクライアントを信頼する必要があります。 ただし、双方向SSLにより、サーバーが信頼できるクライアントを確立できるようになります。 双方向ハンドシェイク中、接続が成功する前のboth the client and server must present and accept each other’s public certificates

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

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

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

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

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

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

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

現在、これは何かがうまくいかなかったことを示しています。 上記のSSLHandshakeExceptionは、抽象的な方法で、is stating that the client when connecting to the server did not receive any certificate.

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

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

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

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

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)によって署名されていないself-signed証明書を使用していることが原因で発生します。

Really, any time the certificate is signed by something other than what is in the default truststore, we’ll see this error. JDKのデフォルトのトラストストアには、通常、使用中の一般的なCAに関する情報が付属しています。

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

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

これは理想的なソリューションではないことに注意してください。 In an ideal scenario, we should not use a self-signed certificate but a certificate which has been certified by a Certificate Authority (CA) which clients can trust by default.

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

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=example, L=city, ST=state, C=xx
...

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

CNを使用してサーバー証明書をlocalhost以外のものとして再生成してみましょう。 再生成された証明書を使用してSimpleServerSimpleClientを実行すると、すぐに失敗します。

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

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

JSSE does not mandate hostname verification by default. HTTPSを明示的に使用することにより、SimpleClientでホスト名の検証を有効にしていることに注意してください。

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

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

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)

このような場合の例外トレースは抽象的であり、正確な問題を教えてくれません。 To resolve these types of problems it is necessary to verify that both the client and server are using either the same or compatible cryptographic protocols.

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つあることを確認することです。

通常、クライアントとサーバーはさまざまな暗号スイートを使用するように構成されているため、このエラーは発生しません。 If we encounter this error it is typically because the server has been configured to use a very selective cipher.サーバーは、セキュリティ上の理由から、選択的な暗号のセットを適用することを選択する場合があります。

6. 結論

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

いつものように、例のコードはover on GitHubで利用できます。