エラーメッセージ
javax.net.ssl.SSLHandshakeException: (handshake_failure) Received fatal alert: handshake_failure
別の表記
- javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure
- Received fatal alert: handshake_failure
JDK 21 以降では、アラート名が括弧付きで示され、「(handshake_failure) Received fatal alert: handshake_failure」となります。JDK 8 と 11 が出力するのは「Received fatal alert: handshake_failure」だけです。
Java (JSSE) — TLS alert 40 received from the server
このエラーの意味
サーバーがアラート 40(handshake_failure)で TLS ハンドシェイクを中止しました。JVM の ClientHello に受け入れ可能なものが何もなかった(共通の暗号スイート、グループ、署名アルゴリズムのいずれかがない)か、JVM が提示しなかったクライアント証明書をサーバーが要求しているかのどちらかです。Java が報告するのはアラートを受信したことだけです。理由はサーバーのログで確認できるほか、javax.net.debug を使えば可視化できます。
発生する理由
サーバーが、JDK で無効化された暗号スイートしか提示していない
よくある現在の JDK アップデート(ecdsa.com のテストでは 21.0.12 と 25.0.4)は、TLS_RSA_*(RSA 鍵交換)、3DES、RC4、匿名の各暗号スイートを jdk.tls.disabledAlgorithms に列挙しています。これらのスイートに限定されたレガシーなサーバーは、最新のどの JVM でも失敗します。
クライアント証明書のない相互 TLS
よくあるサーバーが証明書を要求しましたが、JVM は証明書を持っていませんでした(javax.net.ssl.keyStore が設定されていない、またはサーバーが受け入れる CA が発行した証明書がない)。
SNI がない、または認識されない
ときどきIP アドレスで接続したり、jsse.enableSNIExtension=false を指定したりすると、CDN やマルチテナントのサーバーはハンドシェイクを中止します。
最新のサーバーに対して非常に古い JVM を使っている
ときどきJava 7 や初期の Java 8 アップデートには、堅牢化されたサーバーが要求する ECDHE/GCM の暗号スイートや、TLS 1.2 をデフォルトで有効にする設定がありません。
修正方法
- 1.
ハンドシェイクを確認する
javax.net.debug=ssl:handshake を指定すると、ClientHello(提示した暗号スイート)とアラートが出力されます。-XshowSettings:security:tls では、JDK で有効なプロトコルと暗号スイートが一覧表示されます。
bash # Show what the JVM offers and how the handshake ends java -Djavax.net.debug=ssl:handshake -jar app.jar 2>&1 | grep -E 'ClientHello|cipher_suites|Alert|fatal' # Settings in effect (protocols, suites) for this JDK java -XshowSettings:security:tls -version - 2.
サーバー管理者:現行の暗号スイートを有効にする
ECDHE と AES-GCM または ChaCha20 を使う TLS 1.3 と TLS 1.2 は、サポートが継続しているすべての JDK で動作します。
text # Server side: offer ECDHE + AEAD suites that current JDKs support ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; - 3.
相互 TLS:クライアントのキーストアを設定する
クライアント証明書は、javax.net.ssl.keyStore(または KeyManagerFactory から構築した SSLContext)で指定します。
bash # Mutual TLS: give the JVM the client certificate and key java -Djavax.net.ssl.keyStore=client.p12 -Djavax.net.ssl.keyStorePassword=changeit \ -Djavax.net.ssl.keyStoreType=PKCS12 -jar app.jar - 4.
最終手段:1 つのレガシーなクライアントに限って disabledAlgorithms を調整する
-Djava.security.properties で渡す上書き用ファイルを使うと、1 つの JVM に限って jdk.tls.disabledAlgorithms から TLS_RSA_* を除外できます。デフォルト値全体をコピーし、除外する必要があるものだけを削除してください。これらの暗号スイートには前方秘匿性がないため、例外は範囲を狭く、一時的なものにとどめてください。
bash # Last resort for one legacy server: take this JDK's own value and drop only TLS_RSA_* v=$(sed -n '/^jdk.tls.disabledAlgorithms=/,/[^\\]$/p' "$JAVA_HOME/conf/security/java.security" | sed 's/\\$//' | tr -d '\n') printf '%s\n' "$v" | sed -E 's/, TLS_RSA_\*//' > legacy-rsa.security java -Djava.security.properties=legacy-rsa.security -jar legacy-client.jar
関連するエラー
No appropriate protocol (protocol is disabled or cipher suites are inappropriat…Java (JSSE)javax.net.ssl.SSLHandshakeException: (protocol_version) Received fatal alert: p…Java (JSSE)SSL routines:ssl3_read_bytes:ssl/tls alert handshake failureOpenSSL (client side)SSL routines:tls_post_process_client_hello:no shared cipherOpenSSL (server side: nginx, Apache, Postfix, openssl s_server)