Edited at

SSLContextのTLSv1インスタンスからSSLEngineを作成した場合にClientHelloで送信されるCipherSuitesが8u192以降で変わる

久々にJDKのバージョン違いで挙動が違ってハマったのでメモとして残しておく。

SSL/TLSについて付け焼き刃の知識で書いているので勘違いしたままの箇所や理解が足りない部分があるかもしれない。


何が起こるのか

下記のコードの出力が、8u191以前と8u192以降で違う。

public static void main(String[] args) throws Exception {

SSLContext context = SSLContext.getInstance("TLSv1");
context.init(null, null, null);

SSLEngine engine = context.createSSLEngine();
engine.setUseClientMode(true);

System.out.println(String.join("\n", engine.getEnabledProtocols()));
System.out.println(String.join("\n", engine.getEnabledCipherSuites()));
}


8u191の場合

% java -version

java version "1.8.0_191"
Java(TM) SE Runtime Environment (build 1.8.0_191-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)

% java SSLEngineTest
TLSv1
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
TLS_RSA_WITH_AES_256_CBC_SHA256
TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384
TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384
TLS_DHE_RSA_WITH_AES_256_CBC_SHA256
TLS_DHE_DSS_WITH_AES_256_CBC_SHA256
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_AES_256_CBC_SHA
TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA
TLS_ECDH_RSA_WITH_AES_256_CBC_SHA
TLS_DHE_RSA_WITH_AES_256_CBC_SHA
TLS_DHE_DSS_WITH_AES_256_CBC_SHA
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
TLS_RSA_WITH_AES_128_CBC_SHA256
TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256
TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256
TLS_DHE_RSA_WITH_AES_128_CBC_SHA256
TLS_DHE_DSS_WITH_AES_128_CBC_SHA256
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
TLS_RSA_WITH_AES_128_CBC_SHA
TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA
TLS_ECDH_RSA_WITH_AES_128_CBC_SHA
TLS_DHE_RSA_WITH_AES_128_CBC_SHA
TLS_DHE_DSS_WITH_AES_128_CBC_SHA
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384
TLS_DHE_RSA_WITH_AES_256_GCM_SHA384
TLS_DHE_DSS_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256
TLS_DHE_RSA_WITH_AES_128_GCM_SHA256
TLS_DHE_DSS_WITH_AES_128_GCM_SHA256
TLS_EMPTY_RENEGOTIATION_INFO_SCSV

8u181でも同じ結果を得たので、おそらく8u191以前なら上記の結果となるはず。


8u192の場合

% java -version

java version "1.8.0_192"
Java(TM) SE Runtime Environment (build 1.8.0_192-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.192-b12, mixed mode)

% java SSLEngineTest
TLSv1
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_AES_256_CBC_SHA
TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA
TLS_ECDH_RSA_WITH_AES_256_CBC_SHA
TLS_DHE_RSA_WITH_AES_256_CBC_SHA
TLS_DHE_DSS_WITH_AES_256_CBC_SHA
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
TLS_RSA_WITH_AES_128_CBC_SHA
TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA
TLS_ECDH_RSA_WITH_AES_128_CBC_SHA
TLS_DHE_RSA_WITH_AES_128_CBC_SHA
TLS_DHE_DSS_WITH_AES_128_CBC_SHA
TLS_EMPTY_RENEGOTIATION_INFO_SCSV

8u201, 8u202, 8u212で同様の結果を得た。


なぜ?

わからない……。

8u192のリリースノートを見ると、確かにCipherSuites関連で変更は入っているが、単にデフォルトで有効なものを設定できるようになった、という変更なのでここに影響するべき話ではないような気がする。

CipherSuitesの差分を見ると8u192以降で無効になっているのは末尾にSHA256SHA384が付いているもの。これらはHMACを表すものらしく、ついでにHMACにSHA256が追加されたのはTLS 1.2らしい。

ということは、8u191でTLSv1でSSLContextを生成しているにも関わらずTLS 1.2以降で追加されたCipherSuiteが有効化されてしまっていたのが、8u192以降でむしろ正常化された?ように見える。


これが原因で実際に直面した問題

ESB Mule 3.9.0は、内部でTLSv1SSLContextをデフォルトで使用する。

ただし、有効なTLSプロトコルのデフォルトは、TLSv1.1およびTLSv1.2に設定されている。

ESB Muleは内部でGrizzly Async Http Clientを利用している。

GrizzlyによるSSLEngineの初期化処理で、SSLEngineがTLSv1のSSLContextによって生成され、Clientモード有効化のあと、有効なTLSプロトコルがセットされる。

これによって、「プロトコルとしてTLS 1.2は利用可能だが、ClientHelloで送信するCipherSuitesの候補値にTLS 1.2以降で利用可能になったものが含まれないSSLEngine」が生成されてしまう。

このSSLEngineで「TLS 1.2以降で利用可能になったCipherSuitesのみをサポートするHTTPSサーバー」にアクセスすると、ClientHello送信後、handshake_failure(40)でAlertが返ってきてエラーとなる。

ちなみにこの問題は ESB Mule 3.9.1 で修正されている。


SSLContextはどのprotocolで使うべきか

このご時世でTLSv1など特定のTLSバージョンに限定したSSLContextインスタンスを使う必要がある場面は限られる。おそらく使うべきなのは下記の3種類のうちの1つなはず。


  • SSLContext.getDefault()

  • SSLContext.getInstance("SSL")

  • SSLContext.getInstance("TLS")

SSLContext.getInstanceの引数に指定可能な値については標準アルゴリズム名のドキュメントを参照。

もし諸事情あってTLSv1のSSLContextを使い続ける必要がある場合、setEnableCipherSuitesを呼べば任意のCipherSuitesを有効化できる。ただし、JDKのデフォルト値を使わないということはセキュリティのリスクを伴う(アップデートしても古くなったCipherSuitesが無効にならない)ので注意。


その他

JavaのSSL関連のデバッグ情報はシステムプロパティで-Djava.ssl.debug=sslとすると有効化できる。

読み方はSSL/TLS 接続のデバッグのドキュメントにある。

サーバー側のSSL設定状況を知りたい場合はSSLLabsのSSL Server Testが便利っぽい。