Dev Study
← HTTPとTLSの仕組み
レッスン 5 / 12·約 9 分·第18期 HTTPとTLSの仕組み

TLS 1.3 は、相手の答えを推測して1往復を削る

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力今ここ
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口今ここ
  6. アプリ処理
  7. DB保存

前回は、版の札を共有すれば転送を省けることを見ました。ここから TLS です。今回は、まだ共有していない「サーバが選ぶ群」を推測で埋める仕組みを見ます。

まず結論

TLS 1.3 の鍵交換は、両端が同じ群(方式)を使わないと成り立ちません。ところがクライアントは、サーバがどの群を選ぶかを知らないまま最初の ClientHello を送ります。

そこでクライアントは群を推測し、その群の鍵交換の値を先に計算して載せます。当たれば1往復で済み、外れればサーバが HelloRetryRequest で群を指定し、往復が1つ増えます。共有していないものを推測で埋め、当たったときだけ往復を省く仕組みです。

? 読む前に予測

P-384 だけを受け付ける OpenSSL のサーバに、X25519 と P-384 を使えるクライアント(左端の X25519 の値だけを先に送る設定)がつなぐと、何が起きるでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

相手が選ぶ群を、クライアントは知らない

TLS 1.3 の仕様は、2026年7月に RFC 9846 として改訂されました。TLS 1.3 を定めていた RFC 8446 を廃止した改訂版で、版番号は同じまま互換です。既存のクイズや本線のレッスンは RFC 8446 で書かれていますが、ここでは 9846 の節番号を使います。

鍵交換には方式がいくつもあり、仕様はそれを群と呼びます。両端が同じ群を使わなければ、共通の鍵は作れません。§2 はこう定めます。

the server's share MUST be in the same group as one of the client's shares.

問題は順番です。群を選ぶのはサーバで、最初に口を開くのはクライアントです。 相手の選択を知らないまま、最初の ClientHello を送ることになります。

待つなら、先に群を尋ねて答えを聞き、それから値を送ることになり、1往復余計にかかります。TLS 1.3 は待ちません。§4.2.1 によると、ClientHello には2つの拡張が載ります。

  • supported_groups: 使える群の一覧(好む順)
  • key_share: そのうち some or all of these groups(一部または全部)について、先に計算しておいた鍵交換の値

つまりクライアントは、サーバが選びそうな群を推測して、その群の値を先に作って送っています。RFC 自身も実装上の注意(Appendix C.3)で、サーバが使える群をクライアントが do not predict it in the "key_share" extension の場合を挙げており、predict(予測)という語を使っています。

key_share の推測が当たった場合と外れた場合
ClientHellosupported_groups: 使える群の一覧 / key_share: 推測した群の値
サーバが群を選ぶ
当たり: ServerHello + key_share同じ群の値を返す。1往復で鍵がそろう
外れ: HelloRetryRequest選んだ群の名前だけを返す
外れたときだけ
ClientHello(2回目)指定された群の値1つに差し替えて送り直す
ServerHello + key_shareここで握手が続く。往復が1つ増えた
  • ・RFC 9846 §2.1 Figure 2 を簡略化。共通の群が1つも無ければ、サーバはアラートで中断する
  • ・HelloRetryRequest は1接続に1回まで(§4.2.4)
具体実物

外れを、手元で起こす

OpenSSL 3.6.3(macOS、127.0.0.1 上)で、サーバは P-384 だけを受け付け、クライアントは X25519 と P-384 を使えるが先に送る値は X25519 だけ、という食い違いを作りました。

openssl s_server -accept 18445 -cert l5-cert.pem -key l5-key.pem -groups P-384 -www -quiet &
echo | openssl s_client -connect 127.0.0.1:18445 -groups X25519:P-384 -msg
s_client -msg のメッセージ見出し(RecordHeader・InnerContent 行を除いて抜粋。2回実行して同じ並び)追跡ログ
>>> TLS 1.3, Handshake [length 011d], ClientHello
<<< TLS 1.3, Handshake [length 0058], ServerHello
>>> TLS 1.3, ChangeCipherSpec [length 0001]
>>> TLS 1.3, Handshake [length 015e], ClientHello
<<< TLS 1.3, ChangeCipherSpec [length 0001]
<<< TLS 1.3, Handshake [length 00bb], ServerHello
<<< TLS 1.3, Handshake [length 0006], EncryptedExtensions

ClientHello が2回出ています。そして、その間に届いた返事も ServerHello と表示されています。 同じ接続を -trace で見ると中身が分かります。

s_client -trace から該当行を抜粋(run 1。インデントは原文のまま)追跡ログ
ClientHello, Length=281
gmt_unix_time=0x7F013BC7
extension_type=supported_groups(10), length=6
ecdh_x25519 (29)
secp384r1 (P-384) (24)
extension_type=key_share(51), length=38
NamedGroup: ecdh_x25519 (29)
ServerHello, Length=84
gmt_unix_time=0xCF21AD74
random_bytes (len=28): E59A6111BE1D8C021E65B891C2A211167ABB8C5E079E09E2C8A8339C
extension_type=key_share(51), length=2
NamedGroup: secp384r1 (P-384) (24)
ClientHello, Length=346
gmt_unix_time=0x7F013BC7
extension_type=key_share(51), length=103
NamedGroup: secp384r1 (P-384) (24)

読み取れることは3つあります。

  1. 1回目の ClientHello は、使える群に P-384 を挙げながら、値は X25519 の分しか送っていません。推測が外れました。
  2. 「ServerHello」の random は CF21AD74 E59A6111… です。RFC 9846 §4.2.3 は、HelloRetryRequest は ServerHello と同じ構造で、random を "HelloRetryRequest" の SHA-256 という特別な値にすると定めており、その値と一致します。中の key_share は値を持たず、選んだ群 P-384 の名前だけです。
  3. 2回目の ClientHello は、key_share を P-384 の値1つに差し替えています。random は1回目と同じでした(2回の実行とも)。§4.2.2 は、送り直す ClientHello は the same ClientHello without modification, except as follows と定め、key_share の差し替えを許される変更に挙げています。

表示が ServerHello なのは、§4.2.4 の言うとおり the HelloRetryRequest has the same format as a ServerHello message だからです。RFC は as if it were a distinct message(別のメッセージであるかのように)扱うと書いており、ServerHello の一種とは言っていません。

✓ ここまでの確認

openssl s_client -msg の出力で、サーバから ServerHello が2回届いたように表示されました。1回目の正体を見分ける方法として適切なのはどれですか。

抽象原理

推測は、好みではない

推測が外れても握手は壊れず、失うのは1往復です。では、クライアントは一番好きな群を推測すればよいのでしょうか。

RFC 9846 §4.3.8 は、値を載せる群は a non-contiguous subset(飛び飛びの一部)でよく、MAY omit the most preferred groups(最も好む群を省いてよい)と定めます。新しい群は対応先が少なく先に計算しても効率が悪い、という場合を RFC は挙げています。そのうえで 9846 は、8446 に無かった文を足しました。

the omission of a share for group A and inclusion of one for group B does not mean that the client prefers B to A. Selecting a group based on KeyShareEntry may result in the use of a less preferred group than the client and server mutually support, though saving the round trip of HelloRetryRequest.

値を載せる群の選び方は好みではなく、往復を省くための賭けです。(載せた値どうしは好みの順に並べる決まりです) サーバが値のある群に乗れば往復は省けますが、両端が共に使える中で最善の群を逃すかもしれません。好みを尊重したいサーバには、まず supported_groups で選び、それから ServerHello か HelloRetryRequest かを決めるよう求めています(SHOULD)。

推測をやめることもできます。同じ節は、空の key_share で群を尋ねてよい(MAY)としつつ、at the cost of an additional round trip と書きます。推測しないなら往復で払う、という交換です。

もう1つ、9846 は Clients and Servers MUST NOT reuse a key share for multiple connections. を足しました。推測の値は接続ごとに作り直します。

具体実物

既定の OpenSSL は、2つ賭けている

群を指定せずに、同じ OpenSSL 3.6.3 どうしでつないだ場合です(2回実行し、並びと長さは同じ)。

s_client -trace から key_share の行を抜粋(上がクライアント、下がサーバ。値は一部省略)追跡ログ
extension_type=key_share(51), length=1258
NamedGroup: X25519MLKEM768 (4588)
key_exchange: (len=1216): 77804FBE…(省略)
NamedGroup: ecdh_x25519 (29)
key_exchange: (len=32): 63DB8C0595880748BA4062008C9106DFC642368CD6E915E6B818239BB920E747
extension_type=key_share(51), length=1124
NamedGroup: X25519MLKEM768 (4588)
key_exchange: (len=1120): A4E9D49F…(省略)
Negotiated TLS1.3 group: X25519MLKEM768

クライアントは X25519MLKEM768 と X25519 の2つに賭けています。1258 は拡張全体の長さで、2 + (4 + 1216) + (4 + 32) です。X25519MLKEM768 は RFC 10024(2026年8月)が定める群で、クライアントの値は ML-KEM の 1184 バイトと X25519 の 32 バイトをつないだ 1216 バイト、サーバの値は 1088 + 32 = 1120 バイトです(§4.1・§4.2)。ML-KEM の部分は DH の公開値ではありません。9846 は KEM を使う鍵交換を反映するため、"(EC)DHE" の語を "asymmetric key exchange" に一般化しました(§1.2)。

賭けには当たる前から費用がかかります。OpenSSL のマニュアルは、既定で X25519MLKEM768 を推測に入れることについて、ClientHello が大きくなり may no longer fit in a single TCP segment ため、まれにファイアウォールが握手を壊すと書いています。

✓ ここまでの確認

ClientHello の key_share に X25519 の値だけが載っていました。ここから言えることとして、RFC 9846 に即しているのはどれですか。

⇄ 2つを見比べる
具体例 A

TLS 1.3 の key_share

サーバが選ぶ群を待たずに、推測した群の値を ClientHello に載せる。当たれば1往復で鍵がそろい、外れれば HelloRetryRequest で1往復増える。当たっても外れても、値の計算と ClientHello の大きさは先に払っている。

推測の損得(本文の実測と RFC 9846)表
場合 起きること
当たり ServerHello が同じ群の値を返す
外れ HelloRetryRequest で1往復追加(1接続1回まで)
推測しない 空の key_share。必ず1往復追加
具体例 B

CPU の分岐予測(第1期 branch-prediction)

条件が確定するのを待たずに、予測した側の命令を先に実行する。当たればそのまま進み、外れれば先に進めた分を捨てて詰め直す。先走った結果は確定するまで書き込まれないので、プログラムの結果は狂わない。

片方はネットワークの往復、片方は CPU のサイクル。規模はまるで違いますが、2つが同じ賭け方をしている点と、違う点はどこでしょうか。

「外れたときに失うもの」と「当たっても払っているもの」を分けて考えてください。

この章のまとめ

  • 鍵交換は両端が同じ群でないと成り立たない。群を選ぶのはサーバだが、先に話すのはクライアント。
  • クライアントは群を推測し、その群の値を key_share に先に載せる。当たれば1往復で済む。
  • 外れるとサーバが HelloRetryRequest で群を指定し、往復が1つ増える(1接続1回まで)。形式は ServerHello と同じで、見分けは random の特別値。
  • RFC 9846 は、key_share に値が有るか無いかは好みを意味しないと明記した。推測に乗れば往復は省けるが、最善の群を逃しうる。
  • 推測には当たる前から費用がある。既定の OpenSSL 3.6.3 は2つに賭け、key_share 拡張は 1258 バイトになった。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

OpenSSL 3.6.3 の既定のクライアントで -trace を取ると、ClientHello の key_share が length=1258 と表示されました。この 1258 バイトの説明として正しいのはどれですか。

用語集(4語)
群(named group)
鍵交換の方式の名前。X25519・P-384・X25519MLKEM768 など。RFC 9846 §4.2.1 は、DH 以外の方式を含む今では「群」という語は歴史的なものだと注記している。
key_share
群ごとの鍵交換の値を運ぶ拡張。(EC)DHE なら自分側の公開値。X25519MLKEM768 ではクライアントが ML-KEM の値と X25519 の値をつないで送る(RFC 10024 §4.1)。
supported_groups
クライアントが使える群を、好む順に並べた拡張(RFC 9846 §4.3.7)。key_share の値はこの一部だけでよい。
HelloRetryRequest
推測が外れたとき、サーバが群を指定して ClientHello の送り直しを求めるメッセージ。ServerHello と同じ形式で、random が特別値。1接続に1回まで。