ttshikoのブログ

プログラマをしています。普段、様々な情報(特にICT関連の技術情報)にお世話になっているので、その恩返しをできるようになりたいと考えて始めました。

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 を無効にする

結局、今回は、本質的にサーバ側が原因であったので、サーバ側を再度修正して、クライアント側は元に戻しておいた。おしまい。

参考書籍(2015/09/05 追記):

  • このエントリをご覧くださっている方が多いようでしたので、SSL/TLSに関連いたしまして、古い本ですが、次に紹介します。この本は、自分が初めて、SSL/TLS を詳しく勉強した時の本です。当時(2005年頃)は、「"強い"暗号の輸出規制」に関する仕様に興味を持ったことがきっかけで購入しましたが、その枠組みを超えて、大変勉強になりました。

マスタリングTCP/IP SSL/TLS編

マスタリングTCP/IP SSL/TLS編