- ブラウザURL入力今ここ
- DNS名前→IP
- ルータ経路選択
- ロードバランサ振り分け
- Nginx受け口今ここ
- アプリ処理
- 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(予測)という語を使っています。
- ・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
>>> 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 で見ると中身が分かります。
ClientHello, Length=281gmt_unix_time=0x7F013BC7extension_type=supported_groups(10), length=6ecdh_x25519 (29)secp384r1 (P-384) (24)extension_type=key_share(51), length=38NamedGroup: ecdh_x25519 (29)ServerHello, Length=84gmt_unix_time=0xCF21AD74random_bytes (len=28): E59A6111BE1D8C021E65B891C2A211167ABB8C5E079E09E2C8A8339Cextension_type=key_share(51), length=2NamedGroup: secp384r1 (P-384) (24)ClientHello, Length=346gmt_unix_time=0x7F013BC7extension_type=key_share(51), length=103NamedGroup: secp384r1 (P-384) (24)
読み取れることは3つあります。
- 1回目の ClientHello は、使える群に P-384 を挙げながら、値は X25519 の分しか送っていません。推測が外れました。
- 「ServerHello」の random は
CF21AD74E59A6111…です。RFC 9846 §4.2.3 は、HelloRetryRequest は ServerHello と同じ構造で、random を "HelloRetryRequest" の SHA-256 という特別な値にすると定めており、その値と一致します。中の key_share は値を持たず、選んだ群 P-384 の名前だけです。 - 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回実行し、並びと長さは同じ)。
extension_type=key_share(51), length=1258NamedGroup: X25519MLKEM768 (4588)key_exchange: (len=1216): 77804FBE…(省略)NamedGroup: ecdh_x25519 (29)key_exchange: (len=32): 63DB8C0595880748BA4062008C9106DFC642368CD6E915E6B818239BB920E747extension_type=key_share(51), length=1124NamedGroup: 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 に即しているのはどれですか。
TLS 1.3 の key_share
サーバが選ぶ群を待たずに、推測した群の値を ClientHello に載せる。当たれば1往復で鍵がそろい、外れれば HelloRetryRequest で1往復増える。当たっても外れても、値の計算と ClientHello の大きさは先に払っている。
場合 起きること当たり ServerHello が同じ群の値を返す外れ HelloRetryRequest で1往復追加(1接続1回まで)推測しない 空の key_share。必ず1往復追加
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 バイトになった。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
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回まで。
出典(一次ソース)
- RFC 9846 — The Transport Layer Security (TLS) Protocol Version 1.3(2026年7月、RFC 8446 を廃止)§1.2, §2, §2.1, §4.2.1〜§4.2.4, §4.3.7, §4.3.8, Appendix C.3 ↗
- RFC 8446 — TLS 1.3(初版。既存のクイズとレッスンはこちらの節番号) ↗
- RFC 10024 — Post-Quantum Traditional (PQ/T) Hybrid Key Agreement Mechanisms for TLS 1.3(2026年8月)§4.1, §4.2 ↗
- OpenSSL 3.6 マニュアル SSL_CTX_set1_groups_list(3) ↗