サーバー名の末尾にドットを付けたリクエストを送ると予期せぬ応答が返ってくる問題
前置き
以下の2つの環境での挙動が違うことに悩んでいた。
何をしたときの挙動かというと、
$ echo '' | openssl s_client -connect localhost:8443 -servername hhelibex.local. -showcerts
のように、サーバー名の末尾にドットがついたリクエストを送ったときにSSL/TLSハンドシェイクに失敗する場合としない場合があるということ。 つまり、失敗する場合には以下のようなレスポンスが返ってくる。
CONNECTED(00000003) 139918566537120:error:140773F2:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert unexpected message:s23_clnt.c:769: --- no peer certificate available --- No client certificate CA names sent --- SSL handshake has read 7 bytes and written 324 bytes --- New, (NONE), Cipher is (NONE) Secure Renegotiation IS NOT supported Compression: NONE Expansion: NONE No ALPN negotiated SSL-Session: Protocol : TLSv1.2 Cipher : 0000 Session-ID: Session-ID-ctx: Master-Key: Key-Arg : None Krb5 Principal: None PSK identity: None PSK identity hint: None Start Time: 1520436816 Timeout : 300 (sec) Verify return code: 0 (ok) ---
成功する場合には、以下のようにCipherに適切な文字列が返ってくるし、ドットを付けない場合も同様に成功する。また、CentOS 6環境のJDKをOpenJDK 1.7.0に切り替えてみても成功するようになる。
(省略) Cipher : ECDHE-RSA-AES256-GCM-SHA384 (省略) --- DONE
そこで、Java 7とJava 8で何かが変わったのだろうと思い、その部分を特定するためにTomcat 6のソースを眺めてみたりしてものすごく遠回りしたのだが、「もしかして、根本的なところで仕様が変わっているんじゃね?」と思い始め、別の環境(CentOS 7)で簡単なサーバープログラムを動かしてみたら何か分かるかもということで試してみた。
事前準備
まず、使用するJDK環境を2つ用意する。OpenJDK 1.7.0とOpenJDK 1.8.0。インストールの手順は省略する。
で、alternativesで切り替えて挙動を見る。
事前準備としては、コンパイラ(javac)はOpenJDK 1.7.0にしておき、インタプリタは必要に応じて切り替える。最初はOpenJDK 1.7.0にしておく。
$ sudo alternatives --config javac 2 プログラムがあり 'javac' を提供します。 選択 コマンド ----------------------------------------------- *+ 1 java-1.8.0-openjdk.x86_64 (/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.151-5.b12.el7_4.x86_64/bin/javac) 2 java-1.7.0-openjdk.x86_64 (/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.171-2.6.13.0.el7_4.x86_64/bin/javac) Enter を押して現在の選択 [+] を保持するか、選択番号を入力します:2 $ sudo alternatives --config java 2 プログラムがあり 'java' を提供します。 選択 コマンド ----------------------------------------------- *+ 1 java-1.8.0-openjdk.x86_64 (/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.151-5.b12.el7_4.x86_64/jre/bin/java) 2 java-1.7.0-openjdk.x86_64 (/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.171-2.6.13.0.el7_4.x86_64/jre/bin/java) Enter を押して現在の選択 [+] を保持するか、選択番号を入力します:2 $ javac -version javac 1.7.0_171 $ java -version java version "1.7.0_171" OpenJDK Runtime Environment (rhel-2.6.13.0.el7_4-x86_64 u171-b01) OpenJDK 64-Bit Server VM (build 24.171-b01, mixed mode) $
次に、サーバー証明書を適当に作っておく。
$ openssl genrsa -aes256 2048 -out server.key Generating RSA private key, 2048 bit long modulus ....................................+++ ......................+++ e is 65537 (0x10001) Enter pass phrase: test Verifying - Enter pass phrase: test -----BEGIN RSA PRIVATE KEY----- (省略) -----END RSA PRIVATE KEY----- $ openssl rsa -in server.key -out server.key writing RSA key $ openssl req -new -sha256 -key server.key -out server.csr You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [XX]:JP State or Province Name (full name) []:Hokkaido Locality Name (eg, city) [Default City]:Sapporo Organization Name (eg, company) [Default Company Ltd]:HHeLiBeX Ltd. Organizational Unit Name (eg, section) []: Common Name (eg, your name or your server's hostname) []:hhelibex.local Email Address []: Please enter the following 'extra' attributes to be sent with your certificate request A challenge password []: An optional company name []: $ openssl x509 -in server.csr -out server.crt -req -signkey server.key -days 365 Signature ok subject=/C=JP/ST=Hokkaido/L=Sapporo/O=HHeLiBeX Ltd./CN=hhelibex.local Getting Private key $ openssl pkcs12 -export -inkey server.key -in server.crt -out server.p12 Enter Export Password: changeit Verifying - Enter Export Password: changeit $
サーバープログラム
以下のような簡単なプログラムを作って起動する。テストだから、必要なパラメータはすべてべた書きだがご容赦を。
- SSLServer.java
import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.FileInputStream; import java.io.InputStreamReader; import java.io.IOException; import java.io.OutputStreamWriter; import java.net.ServerSocket; import java.net.Socket; import java.security.KeyStore; import javax.net.ServerSocketFactory; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; public class SSLServer { public static void main(String[] args) { try { String keyStoreFile = "server.p12"; char[] keyStorePassword = "changeit".toCharArray(); KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(new FileInputStream(keyStoreFile), keyStorePassword); KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); kmf.init(keyStore, keyStorePassword); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(kmf.getKeyManagers() , null , null); ServerSocketFactory ssf = sslContext.getServerSocketFactory(); ServerSocket serverSocket = ssf.createServerSocket(8443); while (true) { System.out.println("--------" + System.getProperty("java.version") + "--------"); System.out.println("Waiting for SSL connection"); Socket socket = null; BufferedReader in = null; BufferedWriter out = null; try { socket = serverSocket.accept(); if (socket == null) { System.out.println("Client socket is null, ignored."); continue; } System.out.println("Accepted."); in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); String msg = in.readLine(); System.out.println("Message from client: " + msg); } catch (Exception e) { e.printStackTrace(); } finally { if (in != null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } if (out != null) { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } if (socket != null) { socket.close(); } } } } catch (Exception e) { e.printStackTrace(); } } }
接続テスト(サーバー:OpenJDK 1.7.0)
- 起動
$ sudo alternatives --config java 2 プログラムがあり 'java' を提供します。 選択 コマンド ----------------------------------------------- * 1 java-1.8.0-openjdk.x86_64 (/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.151-5.b12.el7_4.x86_64/jre/bin/java) + 2 java-1.7.0-openjdk.x86_64 (/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.171-2.6.13.0.el7_4.x86_64/jre/bin/java) Enter を押して現在の選択 [+] を保持するか、選択番号を入力します:2 $ java SSLServer --------1.7.0_171-------- Waiting for SSL connection
- クライアントから接続
$ echo "Hello World" | openssl s_client -connect localhost:8443 -servername hhelibex.local. -showcerts CONNECTED(00000003) depth=0 C = JP, ST = Hokkaido, L = Sapporo, O = HHeLiBeX Ltd., CN = hhelibex.local verify error:num=18:self signed certificate verify return:1 depth=0 C = JP, ST = Hokkaido, L = Sapporo, O = HHeLiBeX Ltd., CN = hhelibex.local verify return:1 --- Certificate chain 0 s:/C=JP/ST=Hokkaido/L=Sapporo/O=HHeLiBeX Ltd./CN=hhelibex.local i:/C=JP/ST=Hokkaido/L=Sapporo/O=HHeLiBeX Ltd./CN=hhelibex.local -----BEGIN CERTIFICATE----- MIIDQjCCAioCCQC3i2D7IVfOsTANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJK UDERMA8GA1UECAwISG9ra2FpZG8xEDAOBgNVBAcMB1NhcHBvcm8xFjAUBgNVBAoM DUhIZUxpQmVYIEx0ZC4xFzAVBgNVBAMMDmhoZWxpYmV4LmxvY2FsMB4XDTE4MDMw NzE2MjU1NloXDTE5MDMwNzE2MjU1NlowYzELMAkGA1UEBhMCSlAxETAPBgNVBAgM CEhva2thaWRvMRAwDgYDVQQHDAdTYXBwb3JvMRYwFAYDVQQKDA1ISGVMaUJlWCBM dGQuMRcwFQYDVQQDDA5oaGVsaWJleC5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQAD ggEPADCCAQoCggEBAM2I/AxPvGX4Pu9BqwmP0XoAAXpCYIrRNNa+bw1irLHQ82o+ J1bEGev9x6/jYhA+L7AYHMGnE6gbg1azrxyc06OdzN5X6OTC9xia6S7+LP3Ar8P6 c3BURqoU7TWOZdT7/KmPfgPC/uNty0lgA1U74PsdSMDE6VPU/MVyRoezsDLiXPrV p9eve9bXHiuRF1G+Y0lO3Ym3fGilIZa7HEEqeVLTrKWmS2odOlIT/t8VAfCKBds9 nd38hwPThB0k9F6fhkUgwDeEZZdXXtMh+UNKEHdkI/VGji6uL72sAnwTc1H2VoAc srKcdmzAWh8Kj9ynVenoAjWUTD4LV3L/JF76PLcCAwEAATANBgkqhkiG9w0BAQsF AAOCAQEAgpx32ml5YAacemf63fk9fNS7czjUZqvHBtfR4B6Vl+nmwHnIOazjSgRz WRv86ZnP9t2bh5myhFZtg47BLI6gW8Ca4a92lehz1Cl6tB5sqbBk0vm3Jd78SzIV T1Kxx9CgOEEgT54WuLyRpuaQwICQrKZgWWysCojRtiK/7tZ0amCW8CMfakLdvAaW NF3814ZPS5AiIWrxaQ4XVEw2kB8yGapHO9dLMO81Jr9xpzjG2ENMR7aAxzPcdFvE HJmIAyNYN8e6J/KcYAlAGz3Jo+ppcqn3De2GlFWwvPKiuVrDTeuM9r68u+rRFrmA wTxbkeP/rGxXRzG3G2VQmgY7rtBI6A== -----END CERTIFICATE----- --- Server certificate subject=/C=JP/ST=Hokkaido/L=Sapporo/O=HHeLiBeX Ltd./CN=hhelibex.local issuer=/C=JP/ST=Hokkaido/L=Sapporo/O=HHeLiBeX Ltd./CN=hhelibex.local --- No client certificate CA names sent Peer signing digest: SHA512 Server Temp Key: ECDH, P-256, 256 bits --- SSL handshake has read 1378 bytes and written 495 bytes --- New, TLSv1/SSLv3, Cipher is ECDHE-RSA-AES256-SHA384 Server public key is 2048 bit Secure Renegotiation IS supported Compression: NONE Expansion: NONE No ALPN negotiated SSL-Session: Protocol : TLSv1.2 Cipher : ECDHE-RSA-AES256-SHA384 Session-ID: 5AA01ABEC478A8640AF9BFA21DB39BD751CC7BD651F29EFCEA4A14F91B2D7FC8 Session-ID-ctx: Master-Key: 47C8AF2C33540069495BD8372059306B8411D19F684007DFF8E88BEE5653690CCE5C010145C58D54962A14C29418F89D Key-Arg : None Krb5 Principal: None PSK identity: None PSK identity hint: None Start Time: 1520442046 Timeout : 300 (sec) Verify return code: 18 (self signed certificate) --- DONE $
- サーバー側でのメッセージ出力
メッセージは、起動時からの続き。
Accepted. Message from client: Hello World --------1.7.0_171-------- Waiting for SSL connection ^C $
サーバー証明書が問題なく取得できた。クライアントからのメッセージ「Hello World」もちゃんと送られている。
接続テスト(サーバー:OpenJDK 1.8.0)
- 起動
$ sudo alternatives --config java 2 プログラムがあり 'java' を提供します。 選択 コマンド ----------------------------------------------- * 1 java-1.8.0-openjdk.x86_64 (/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.151-5.b12.el7_4.x86_64/jre/bin/java) + 2 java-1.7.0-openjdk.x86_64 (/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.171-2.6.13.0.el7_4.x86_64/jre/bin/java) Enter を押して現在の選択 [+] を保持するか、選択番号を入力します:1 $ java SSLServer --------1.8.0_151-------- Waiting for SSL connection
- クライアントから接続
$ echo "Hello World" | openssl s_client -connect localhost:8443 -servername hhelibex.local. -showcerts CONNECTED(00000003) 140566511749024:error:140773F2:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert unexpected message:s23_clnt.c:769: --- no peer certificate available --- No client certificate CA names sent --- SSL handshake has read 7 bytes and written 313 bytes --- New, (NONE), Cipher is (NONE) Secure Renegotiation IS NOT supported Compression: NONE Expansion: NONE No ALPN negotiated SSL-Session: Protocol : TLSv1.2 Cipher : 0000 Session-ID: Session-ID-ctx: Master-Key: Key-Arg : None Krb5 Principal: None PSK identity: None PSK identity hint: None Start Time: 1520441888 Timeout : 300 (sec) Verify return code: 0 (ok) --- $
- サーバー側でのメッセージ出力
メッセージは、起動時からの続き。
Accepted. javax.net.ssl.SSLProtocolException: Illegal server name, type=host_name(0), name=hhelibex.local., value=68:68:65:6c:69:62:65:78:2e:6c:6f:63:61:6c:2e at sun.security.ssl.ServerNameExtension.<init>(ServerNameExtension.java:143) at sun.security.ssl.HelloExtensions.<init>(HelloExtensions.java:78) at sun.security.ssl.HandshakeMessage$ClientHello.<init>(HandshakeMessage.java:245) at sun.security.ssl.ServerHandshaker.processMessage(ServerHandshaker.java:220) at sun.security.ssl.Handshaker.processLoop(Handshaker.java:1026) at sun.security.ssl.Handshaker.process_record(Handshaker.java:961) at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1072) at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1385) at sun.security.ssl.SSLSocketImpl.readDataRecord(SSLSocketImpl.java:938) at sun.security.ssl.AppInputStream.read(AppInputStream.java:105) at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284) at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326) at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178) at java.io.InputStreamReader.read(InputStreamReader.java:184) at java.io.BufferedReader.fill(BufferedReader.java:161) at java.io.BufferedReader.readLine(BufferedReader.java:324) at java.io.BufferedReader.readLine(BufferedReader.java:389) at SSLServer.main(SSLServer.java:50) Caused by: java.lang.IllegalArgumentException: Server name value of host_name cannot have the trailing dot at javax.net.ssl.SNIHostName.checkHostName(SNIHostName.java:319) at javax.net.ssl.SNIHostName.<init>(SNIHostName.java:183) at sun.security.ssl.ServerNameExtension.<init>(ServerNameExtension.java:137) ... 17 more --------1.8.0_151-------- Waiting for SSL connection ^C $
今度は例外が発生し、接続が切られている。もちろん、クライアント側では証明書の取得はできず。
検証
失敗したOpenJDK 1.8.0の場合の例外メッセージを見てみると、以下のようなメッセージが読み取れる。
- 「Illegal server name, ・・・」
- 「Server name value of host_name cannot have the trailing dot」
そこで、例外メッセージにあるjavax.net.ssl.SNIHostNameを見てみる。
すると、こんなことが書いてある。
TLS拡張(RFC 6066)のセクション3「Server Name Indication」で説明されているように、「HostName」には、クライアントが理解できるサーバーの完全修飾DNSホスト名が含まれます。ホスト名のエンコードされたサーバー名の値は、ASCIIエンコーディングを使用したドットで終わらないバイト文字列として表現されます。
また、
導入されたバージョン
1.8
「ドットで終わらないバイト文字列」!!
なるほど、RFC 6066がJava 8から実装され、ドットで終わるホスト名が許されなくなったということらしい。
これで冒頭の疑問が解ける。成功する方はJava 7なのでドットで終わるホスト名がまだ許されていたが、失敗する方はJava 8からのRFC 6066の実装によってドットで終わるホスト名が許されなくなった。
通常はドットで終わるホスト名でアクセスすることはないだろうから問題になることは少ないのだろうが、これは罠だなぁ‥ともあれ、疑問が解消されてよかった‥
参考
- JavaのSSLSocketでSSLクライアントとSSLサーバーを実装する:CodeZine(コードジン)
- ソースコードは会員登録しないとダウンロードできないが、SSLServerSocketを使ったプログラムをまともに書いたことが無かったので、とても助かった
- RFC 6066
- RFC 6066の原文