HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

サーバー名の末尾にドットを付けたリクエストを送ると予期せぬ応答が返ってくる問題

前置き

以下の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
$ 

サーバープログラム

以下のような簡単なプログラムを作って起動する。テストだから、必要なパラメータはすべてべた書きだがご容赦を。

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の実装によってドットで終わるホスト名が許されなくなった。

通常はドットで終わるホスト名でアクセスすることはないだろうから問題になることは少ないのだろうが、これは罠だなぁ‥ともあれ、疑問が解消されてよかった‥

参考