Java - HTTPS通信が失敗するようになる: javax.net.ssl.SSLProtocolException: handshake alert: unrecognized_name
結論から書くと、サーバ側の設定ミスが原因だったが、どうやら Java 7 でデフォルト有効になった、SNI(wikipedia:Server Name Indication)により、エラーとして現れて来たようだ。
まず、症状の紹介から、、、
Javaアプレットを ローカルのHTTPSサイトで動作確認後、本番用HTTPSサーバで公開したところ、次のエラーが発生。発生タイミングは、JNLPファイル(https://〜/*.jnlp)を取得する時。
環境は、
クライアント側:Java:1.7.0_09。
サーバ側:Webサーバ:Apache HTTP Server 2.2.22 (mod_ssl/2.2.22, OpenSSL 1.0.0-fips)
javax.net.ssl.SSLProtocolException: handshake alert: unrecognized_name at sun.security.ssl.ClientHandshaker.handshakeAlert(Unknown Source) at sun.security.ssl.SSLSocketImpl.recvAlert(Unknown Source) at sun.security.ssl.SSLSocketImpl.readRecord(Unknown Source) at sun.security.ssl.SSLSocketImpl.performInitialHandshake(Unknown Source) at sun.security.ssl.SSLSocketImpl.startHandshake(Unknown Source) at sun.security.ssl.SSLSocketImpl.startHandshake(Unknown Source) at sun.net.www.protocol.https.HttpsClient.afterConnect(Unknown Source) at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(Unknown Source) at sun.net.www.protocol.http.HttpURLConnection.getInputStream(Unknown Source) at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(Unknown Source) at com.sun.deploy.net.HttpUtils.followRedirects(Unknown Source) at com.sun.deploy.net.BasicHttpRequest.doRequest(Unknown Source) at com.sun.deploy.net.BasicHttpRequest.doGetRequestEX(Unknown Source) at com.sun.deploy.net.DownloadEngine.actionDownload(Unknown Source) at com.sun.deploy.net.DownloadEngine._downloadCacheEntry(Unknown Source) at com.sun.deploy.cache.ResourceProviderImpl.getResourceCacheEntry(Unknown Source) at com.sun.deploy.cache.ResourceProviderImpl.getResourceCacheEntry(Unknown Source) at com.sun.deploy.cache.ResourceProviderImpl.getResource(Unknown Source) at com.sun.deploy.model.ResourceProvider.getResource(Unknown Source) at com.sun.javaws.jnl.LaunchDescFactory._buildDescriptor(Unknown Source) at com.sun.javaws.jnl.LaunchDescFactory.buildDescriptor(Unknown Source) at com.sun.javaws.jnl.LaunchDescFactory.buildDescriptor(Unknown Source) at sun.plugin2.applet.JNLP2Manager.initialize(Unknown Source) at sun.plugin2.main.client.PluginMain.initManager(Unknown Source) at sun.plugin2.main.client.PluginMain.access$200(Unknown Source) at sun.plugin2.main.client.PluginMain$2.run(Unknown Source) at java.lang.Thread.run(Unknown Source)
通信の様子を調べる
同じエラーに対応された方の書き込みを参考に、小さな再現プログラムを書いて、通信の様子を調べてみる。
再現プログラム:
public class HTTPSTest { public static void main(String [] args) throws Exception { java.net.URLConnection c = new java.net.URL("https://○○○/").openConnection(); c.setDoOutput(true); c.getOutputStream(); }
↑○○○の部分には、通信に失敗した時のホスト名を具体的に書く。
実は、後で Java 6 でも実行するので、コンパイラは Java 6 の javac を利用する。
このプログラムを、Java 7 で実行すると、確かに再現する。
> {Java 7のインストールディレクトリ}\bin\java HTTPSTest Exception in thread "main" javax.net.ssl.SSLProtocolException: handshake alert: unrecognized_name at sun.security.ssl.ClientHandshaker.handshakeAlert(Unknown Source) at sun.security.ssl.SSLSocketImpl.recvAlert(Unknown Source) at sun.security.ssl.SSLSocketImpl.readRecord(Unknown Source) at sun.security.ssl.SSLSocketImpl.performInitialHandshake(Unknown Source) at sun.security.ssl.SSLSocketImpl.startHandshake(Unknown Source) at sun.security.ssl.SSLSocketImpl.startHandshake(Unknown Source) at sun.net.www.protocol.https.HttpsClient.afterConnect(Unknown Source) at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(Unknown Source) at sun.net.www.protocol.http.HttpURLConnection.getOutputStream(Unknown Source) at sun.net.www.protocol.https.HttpsURLConnectionImpl.getOutputStream(Unknown Source) at HTTPSTest.main(HTTPSTest.java:5)
この時の通信の様子を Wireshark でパケットキャプチャしてみると、次のような通信が行われていた。
通信の様子: unrecognized_name エラー発生時 C:クライアント(Java 7 デフォルト), S:サーバ 1: C → S: TLSv1 Handshake: Client Hello: Extension server_name 2: C ← S: TLSv1 Alert: Warning: Unrecognized Name (112) TLSv1 Handshake: Server Hello TLSv1 Handshake: Certificate TLSv1 Handshake: Server Hello Done 3: C → S: TLSv1 Alert: Fatal: Unexpected Message (10)
Java のエラーに表示された「unrecognized_name」は、サーバから送信された「Unrecognized Name」を指しているようだ。wikipedia:en:Transport Layer Security(英語版) によれば、
Wikipediaより抜粋:
112 Unrecognized name TLS only; client's Server Name Indicator specified a hostname not supported by the server
クライアントが提示したSNI(wikipedia:Server Name Indication)のサーバ名が、サーバでサポートされていないホスト名を指し示しているとのこと。
方法(1) サーバ側で対応する
そこで、同じ書き込みを参考に、サーバ側のHTTPS設定を見直してみると、同様の設定ミスが発覚。次の手順でサーバを修正する。
手順:
1. Apache HTTP Server の設定ファイル httpd-ssl.conf を下記のように修正
2. サーバサービス再起動。
(設定ファイルの場所:2.2系の場合:Apacheインストールディレクトリ/conf/extra/httpd-ssl.conf)
修正前:
・ServerName がデフォルト設定のまま。
<VirtualHost _default_:443> ServerName www.example.com:443
↓
修正後:
・ServerName にサーバホスト名を正しくセット。クライアントから https://○○○/〜 でアクセスする場合の○○○の部分を設定。
・ServerAlias に、サーバホスト名の、利用する可能性がある別名を設定(別名の利用予定がなければ、ServerAlias の行は不要)。
<VirtualHost _default_:443> ServerName ○○○ ServerAlias △△△
修正後、さきほどの再現プログラムを実行すると、少なくとも、javax.net.ssl.SSLProtocolException: handshake alert: unrecognized_name のエラーは発生しなくなる。(但し、今回のサーバでは、カスタムサーバ証明書を利用していたため、別のエラー(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)が発生する。こちらのエラーの対策は、ここでは割愛。対応が必要な場合は、たとえば、Javaのkeytoolを使ってEclipseからのアクセスでオレオレ証明書の壁を突破する | shinodogg.comとかばあばの覚え書き: 自己証明書でも問答無用にするとかを参照。)
この時の通信の様子は次の通り。
サーバから「Unrecognized name」の Alert が送られなくなっていることがわかる。
通信の様子:unrecognized_name エラー解消時(1) C:クライアント(Java 7 デフォルト), S:サーバ(設定修正後) 1: C → S: TLSv1 Handshake: Client Hello: Extension server_name 2: C ← S: TLSv1 Handshake: Server Hello TLSv1 Handshake: Certificate TLSv1 Handshake: Server Hello Done 3: C → S: TLSv1 Alert: Fatal: Certificate Unknown (46)
サーバ側で対応できる場合は、この方法が一番本質的で良いようだ。
特に、不特定多数のクライアントからアクセスされるサーバの場合は尚更都合良いだろう。
方法(2) クライアント側で対応する
では、サーバ側で対応できない場合、どのようにクライアントで対応するか?
まず、調査を続けるために、サーバを元の設定ミスの状態に戻しておく。
さて、もう一度、wikipedia:en:Transport Layer Security(英語版) を参照すると、
Wikipediaより抜粋:
112 Unrecognized name TLS only; client's Server Name Indicator specified a hostname not supported by the server
「Unrecognized name」エラーは、「TLSのみ」であり、SNIに関するエラーのようなので、次のような方針が考えられる:
(2-A) TLS を利用しない
(2-B) SNI(←TLSの拡張機能) を利用しない
セキュリティの観点からは、(2-A)に比べれば、(2-B)の方が望ましいと思われる。
ここで、Java™ SE 7リリースにおけるセキュリティの拡張機能 を参照すると、次の点が関係しそうだ。
抜粋:
・SSLv2Helloをデフォルトで無効化
Java SE 7では、デフォルトで有効なプロトコルのリストからSSLv2Helloが削除されています。・JSSEクライアントのServer Name Indication(SNI)
Java SE 7リリースでは、JSSEクライアントでのServer Name Indication(SNI)拡張がサポートされます。 SNIについては、RFC 4366で説明されています。 この機能によって、TLSクライアントが仮想サーバーに接続できます。
(2-A-1) Java 6 に変更する。
ここから、少なくとも、Java 6 であれば、SNIを利用しない状態と考えられるので、試してみる。
さきほどの再現プログラムを、Java 6 で実行してみると、確かに unrecognized_name のエラーは発生しない。ちなみに、Java 6 のバージョンは、たまたまインストールしてあった、1.6.0_31を利用。
> {Java 6のインストールディレクトリ}\bin\java HTTPSTest
この時の通信の様子は次の通り。ハンドシェイク・プロトコルに、SSLv2 形式を利用しており、そもそも SNIを利用できないので、サーバも Unrecognized name を返していない。
通信の様子:unrecognized_name エラー解消時(2-A) C:クライアント(Java 6 デフォルト), S:サーバ 1: C → S: SSLv2 Handshake: Client Hello: Version TLSv1 2: C ← S: TLSv1 Handshake: Server Hello TLSv1 Handshake: Certificate TLSv1 Handshake: Server Hello Done 3: C → S: TLSv1 Alert: Fatal: Certificate Unknown (46)
再現プログラムではなく、アプレットの場合、Java 6/Java 7の両方をインストールしている環境で、 Java 6を利用させたい場合は、次の手順で変更できる:
1. Javaコントロールパネルの[Java]タブの [表示...]をクリック
2. プラットフォーム 1.7 の行の「有効」のチェックをはずす
設定変更後は、Webブラウザの全ての画面を一旦閉じて、再度開き直してからテストすることを忘れずに。
(2-A-2) Java 7 のまま、SSLv2Hello を有効にする
さて、今後のためには、Java 7 のまま対応したい。
さらに調べたところ(JSSE Reference Guide for Java SEなど)、Java 7 では次のいずれもできるようだ。
・Java 7 では、SSL/TLS のハンドシェイク・プロトコルで、SSLv2Hello を有効にできる
・Java 7 では、SNI を無効にできる
まず、SSLv2Hello を有効にする方法から。さきほどの再現プログラムの場合、システム・プロパティ https.protocols を調整して実行する。
> {Java 7のインストールディレクトリ}\bin\java -Dhttps.protocols="TLSv1,SSLv3,SSLv2Hello" HTTPSTest
この時の通信の様子は次の通り。Java 6 の時と同様、ハンドシェイク・プロトコルに、SSLv2 形式を利用するようになっている。
通信の様子:unrecognized_name エラー解消時(2-A-2) C:クライアント(Java 7, SSLv2Hello有効化), S:サーバ 1: C → S: SSLv2 Handshake: Client Hello: Version TLSv1 2: C ← S: TLSv1 Handshake: Server Hello TLSv1 Handshake: Certificate TLSv1 Handshake: Server Hello Done 3: C → S: TLSv1 Alert: Fatal: Certificate Unknown (46)
再現プログラムではなく、アプレットの場合、次の手順で SSLv2Hello を有効にできる:
1. Javaコントロールパネルの[詳細]タブの「セキュリティ」>「一般」>「SSL2.0互換のClientHello形式を使用する」にチェックをつける(デフォルトではチェックがついていない)
(2-B) Java 7 のまま、SNI(wikipedia:Server Name Indication) を無効にする
次は SNI を無効にする方法で対応してみる。
さきほどの再現プログラムの場合、システム・プロパティ jsse.enableSNIExtension を false に設定して実行する:
> {Java 7のインストールディレクトリ}\bin\java -Djsse.enableSNIExtension=false HTTPSTest
この時の通信の様子は次の通り。ハンドシェイク・プロトコルは TLSv1 であるが、SNI拡張(Extension server_name)を利用していない。これに伴い、サーバから Unrecognized name も送られなくなっている。
通信の様子:unrecognized_name エラー解消時(2-B) C:クライアント(Java 7, SNI無効化), S:サーバ 1: C → S: TLSv1 Handshake: Client Hello 2: C ← S: TLSv1 Handshake: Server Hello TLSv1 Handshake: Certificate TLSv1 Handshake: Server Hello Done 3: C → S: TLSv1 Alert: Fatal: Certificate Unknown (46)
再現プログラムではなく、アプレットの場合、次の手順で SNI を無効にできる:
1. Javaコントロールパネルの[Java]タブの [表示...]をクリック
2. プラットフォーム 1.7 の行の ランタイム・パラメータに、-Djsse.enableSNIExtension=false を追加(システム・プロパティ jsse.enableSNIExtension の値を false に設定)
まとめ
Java 6 を Java 7 に変更すると、今まで成功していたHTTPS 通信が javax.net.ssl.SSLProtocolException: handshake alert: unrecognized_name により失敗するようになることがある。
・エラーとして検知するようになった原因は、TLS拡張機能の SNI(wikipedia:Server Name Indication)が、Java 7 のデフォルトで有効になったため。
・対策としては、次の通り:
(1)サーバ側で対応する場合(本質的な対応)
HTTPSサーバの設定を見直して、クライアントがアクセスするホスト名と、HTTPSサーバが認識するホスト名を合わせる
(2)クライアント側で対応する場合(次善策)
・(2-A) SSLv2Hello を有効にする
(2-A-1) Java 6 で実行する
(2-A-2) Java 7 のまま、SSLv2Hello を有効にする
・(2-B) Java 7 のまま、SNI を無効にする
結局、今回は、本質的にサーバ側が原因であったので、サーバ側を再度修正して、クライアント側は元に戻しておいた。おしまい。
参考リンク:
・JmeterでHTTPS通信したらjavax.net.ssl.SSLProtocolException: handshake alert: unrecognized_nameが発生する - ジュラルミンの日々
・SSL handshake alert: unrecognized_name error since upgrade to Java 1.7.0 - Stack Overflow
・StrangeWill / Redmine Inline Attach Screenshot / issues / #17 - Java 1.7 "unrecognized_name" due SNI support — Bitbucket
・Java™ SE 7リリースにおけるセキュリティの拡張機能
・JSSE Reference Guide for Java SE
・Bug ID: JDK-7127374 JSSE creates SSLProtocolException on (common) warning: unrecognized_name for SNI
・Transport Layer Security - Wikipedia, the free encyclopedia