クライアントと ALB の TLS バージョンネゴシエーションの様子を Wireshark で確認してみた

2024.04.23

いわさです。

AWS のサービスを使って SSL/TLS 終端を行う時に、TLS 1.3 を使えるサービスが増えてきました。
そこで、先日は現時点で各サービスの TLS プロトコルのサポート状況をまとめてみました。

TLS 1.3 が有効なサービスが多いのですが、セキュリティポリシー上 TLS 1.2 も対応している場合であれば TLS 1.2 が上限のクライアントでも接続することが出来ます。
TLS 1.3 として直接互換性があるわけではないのですが、TLS プロトコルとして異なるバージョンの TLS をサポートするメカニズムがあります。
具体的には次の RFC 8446「付録D. 下位互換性」にも記述されています。

TLS 1.xおよびSSL 3.0は、互換性のあるClientHelloメッセージを使用します。 サーバーは、ClientHello形式の互換性が維持され、クライアントとサーバーの両方で少なくとも1つのプロトコルバージョンがサポートされている限り、TLSの将来のバージョンを使用しようとするクライアントも処理できます。

雑にいうとクライアントから送信される ClientHello メッセージにはクライアント側でサポートされる TLS プロトコルバージョンや暗号スイートの情報が含まれており、受信したサーバーがその中でサポートされているバージョンや暗号スイートを判断することでネゴシエートされます。

バージョンネゴシエーションの雑な図解

サーバーは TLS をサポートしていればまぁ何でも良いのですが、ここでは ALB を例にしてみます。

まず、クライアントで利用できるプロトコルや暗号スイートはクライアント環境ごとに決まっています。
アプリケーション上で明示的に実装されている場合もあれば、ブラウザや OS のバージョンに依存している場合もあります。
いずれにせよクライアントごとに自分が対応できる TLS バージョンと、利用できる暗号スイートの一覧があります。

そして、前述のとおりサーバーはサーバーで、同じように対応できる TLS バージョンと利用できる暗号スイートの一覧の概念があります。 ALB の場合は次のページに、どのセキュリティポリシーでどの TLS プロトコルバージョン、暗号スイートがサポートとなるのかが明記されています。

Client Hello

まずはじめにクライアントが自分が使えるプロトコルと暗号スイート(他にも色々送信しますが割愛)をサーバーに送信します。

Server Hello

サーバーはその内容を受信し、自分が対応できるプロトコルと暗号スイートが含まれている場合に、「これでいこうぜ」という情報を返します。

あるいは、クライアントとサーバーで共通で使えるバージョンや暗号スイートが無い場合は、ここでお断りされます。
例えば、次のようにクライアントの最大 TLS バージョンが 1.2 で、サーバーの最小 TLS バージョンが 1.3 の場合はネゴシエーションに失敗します。

cURL でのぞいてみる

上記のフローを cURL で確認してみます。
curl -v(verbose オプション) を使ってみましょう。

% curl https://hoge0415.fuga.tak1wa.com/ -v         
* Host hoge0415.fuga.tak1wa.com:443 was resolved.
* IPv6: (none)
* IPv4: 18.178.151.3, 18.177.166.8
*   Trying 18.178.151.3:443...
* Connected to hoge0415.fuga.tak1wa.com (18.178.151.3) port 443
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / x25519 / RSASSA-PSS
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=*.fuga.tak1wa.com
*  start date: Apr 14 00:00:00 2024 GMT
*  expire date: May 13 23:59:59 2025 GMT
*  subjectAltName: host "hoge0415.fuga.tak1wa.com" matched cert's "*.fuga.tak1wa.com"
*  issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M02
*  SSL certificate verify ok.
*   Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 1: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 2: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://hoge0415.fuga.tak1wa.com/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: hoge0415.fuga.tak1wa.com]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.7.1]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: hoge0415.fuga.tak1wa.com
> User-Agent: curl/8.7.1
> Accept: */*
> 
* Request completely sent off
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/2 200 
< server: awselb/2.0
< date: Mon, 22 Apr 2024 11:01:06 GMT
< content-type: application/json
< content-length: 15
< 
* Connection #0 to host hoge0415.fuga.tak1wa.com left intact
{"hoge":"fuga"}

上記ハイライト部分でそれらしきやり取りが行われていますね。
このオプションだと中身までは見れないのですが。

trace オプション

cURL だとtraceオプションを使うともう少し確認できるようになります。
ただ、人の眼にはちょっと易しくはないですね。

% curl https://hoge0415.fuga.tak1wa.com/ --trace -
== Info: Host hoge0415.fuga.tak1wa.com:443 was resolved.
== Info: IPv6: (none)
== Info: IPv4: 18.178.151.3, 18.177.166.8
== Info:   Trying 18.178.151.3:443...
== Info: Connected to hoge0415.fuga.tak1wa.com (18.178.151.3) port 443
== Info: ALPN: curl offers h2,http/1.1
=> Send SSL data, 5 bytes (0x5)
0000: 16 03 01 02 00                                  .....
== Info: TLSv1.3 (OUT), TLS handshake, Client hello (1):
=> Send SSL data, 512 bytes (0x200)
0000: 01 00 01 fc 03 03 c7 9c ac b0 52 ec 40 5a 23 1c ..........R.@Z#.

:

01f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
<= Recv SSL data, 5 bytes (0x5)
0000: 16 03 03 00 7a                                  ....z
== Info: TLSv1.3 (IN), TLS handshake, Server hello (2):
<= Recv SSL data, 122 bytes (0x7a)
0000: 02 00 00 76 03 03 a2 63 ed 28 bd 26 52 50 5b fa ...v...c.(.&RP[.

:

0070: 4b 5b 3d 42 1a 6a 86 89 b1 46                   K[=B.j...F
<= Recv SSL data, 5 bytes (0x5)
0000: 14 03 03 00 01                                  .....
<= Recv SSL data, 5 bytes (0x5)
0000: 17 03 03 00 24                                  ....$
<= Recv SSL data, 1 bytes (0x1)
0000: 16                                              .
== Info: TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):

:

Wireshark で見やすく

私にはまだ早いので、Wireshark を使って簡単に確認してみましょう。みなさん大好きな Wireshark。

TLS バージョンやら暗号スイートを指定してみる

cURL を実行して、その時のキャプチャを見てみたいのですが、オプションなしでデフォルトだと結構な量の情報が送信されてノイジーなので、今回は TLS に関するところを少し指定して試してみます。

cURL で暗号スイートを指定する場合ですが、TLS 1.3 と TLS 1.3 以前で異なるので注意しましょう。
ドキュメントはこちら。

Wireshark でキャプチャを開始し、cURL からリクエストを送信してみます。

% curl --tlsv1.3 --tls-max 1.3 --tls13-ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_AES_128_CCM_SHA256 "https://hoge0415.fuga.tak1wa.com/" -v
* Host hoge0415.fuga.tak1wa.com:443 was resolved.
* IPv6: (none)
* IPv4: 18.177.166.8, 18.178.151.3
*   Trying 18.177.166.8:443...
* Connected to hoge0415.fuga.tak1wa.com (18.177.166.8) port 443
* ALPN: curl offers h2,http/1.1
* TLS 1.3 cipher selection: TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_AES_128_CCM_SHA256
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / x25519 / RSASSA-PSS
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=*.fuga.tak1wa.com
*  start date: Apr 14 00:00:00 2024 GMT
*  expire date: May 13 23:59:59 2025 GMT
*  subjectAltName: host "hoge0415.fuga.tak1wa.com" matched cert's "*.fuga.tak1wa.com"
*  issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M02
*  SSL certificate verify ok.
*   Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 1: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
*   Certificate level 2: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://hoge0415.fuga.tak1wa.com/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: hoge0415.fuga.tak1wa.com]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.7.1]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: hoge0415.fuga.tak1wa.com
> User-Agent: curl/8.7.1
> Accept: */*
> 
* Request completely sent off
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/2 200 
< server: awselb/2.0
< date: Mon, 22 Apr 2024 11:56:36 GMT
< content-type: application/json
< content-length: 15
< 
* Connection #0 to host hoge0415.fuga.tak1wa.com left intact
{"hoge":"fuga"}

上記から18.177.166.8に送信されていることがわかりますね。
送信先 IP アドレスでフィルタリングしてみます。

Client Hello メッセージと Server Hello メッセージが確認出来ます。
上記を選択してみると、より詳細な情報を構造化した形式で確認出来ます。見やすいです。

Client Hello の Cipher Suites には cURL のtls13-ciphersオプションでコロン区切りで指定した 3 つの暗号スイートが情報として送信されています。
また、最低 TLS バージョンに 1.3 を指定しているので、supported_versionsが TLS 1.3 のみです。
複数バージョンをサポートする場合は優先度順に複数設定されます。 それぞれのフィールドの詳細な仕様が知りたい方は冒頭の RFC を読みましょう。

Server Hello では決定された暗号スイートと TLS バージョンが返答されていますね。
今回は意図的に ALB のセキュリティポリシーがサポートする暗号スイートが含まれるものを cURL から指定しています。

失敗するように指定してみる

次は、サポートされていない暗号スイートを指定してみます。
コンソール情報は割愛しますが、cURL では接続に失敗していました。

その結果を Wireshark で見てみると次のようになりました。

まずは Client Hello です。
TLS 1.3 のみをサポートする ALB に対して、クライアントから TLS 1.2 のみを指定しました。
supported_versionsが含まれていないですね。

Client Hello にsupported_versionsが含まれていない場合にサーバーがどうふるまうべきかは、RFC の「D.2. 古いクライアントとの交渉」に記載されています。

「supported_versions」拡張機能がなく、サーバーがClientHello.legacy_versionよりも大きいバージョンのみをサポートする場合、サーバーは「protocol_version」アラートでハンドシェイクを中止する必要があります。

Server Hello を確認してみると、アラートメッセージ「Protocol Version」が送信されていますね。

さいごに

本日は クライアントと ALB の TLS バージョンネゴシエーションの様子を Wireshark で確認してみました。

あまり中身を覗く機会は多く無いと思いますが、ネゴシエーション上の問題が生じた際にこのように中身を見てみるのは良いかもしれませんね。
RFC と突合して見てみると、どういう時にどういうアラートメッセージが設定されるのかとか、しっかり書いてあるのでトラブルシューティングの際に役立てることも出来そうです。