TLS 1.3 ハンドシェイクの内部と鍵導出
TLS1.3が1往復で安全な鍵を作る仕組みを、ECDHEとHKDFの鍵スケジュールまで踏み込んで理解できます。0-RTTの落とし穴も押さえます。
- 1.TLS1.3は最初のClientHelloで鍵共有値を送り、1-RTTでハンドシェイクを終える。暗号スイートはAEADと鍵交換が分離された。
- 2.ECDHEで共有秘密を作り、HKDF-Extract/Expandの鍵スケジュールで段階的にハンドシェイク鍵・アプリ鍵を導出する。
- 3.0-RTTはPSKと再開チケットで往復ゼロを実現するが、リプレイ耐性がなく非冪等処理に使ってはならない。
1.3 が「速くて安全」になった理由
TLS 1.2 までのハンドシェイクは、暗号スイートを合意してから鍵交換用の値を交換するため、実データの前に 2往復(2-RTT) が必要でした。TLS 1.3(RFC 8446)はこの構造を作り替え、クライアントが最初の ClientHello で 鍵共有の材料を投機的に同梱 します。サーバーはそれを使って即座に鍵を導出できるため、ハンドシェイクは 1往復(1-RTT) で完了します。
この投機が成立するのは、TLS 1.3 が鍵交換方式を ECDHE(楕円曲線 Diffie-Hellman、一時鍵)に事実上限定 し、選択肢を絞ったからです。RSA 鍵交換や静的 DH は仕様から削除され、前方秘匿性(Forward Secrecy)が 常に保証 されます。基礎は /network/tls/ を、土台のトランスポートは /network/tcp-udp/ を参照してください。
TLS 1.2 の暗号スイートは「鍵交換+認証+暗号+ハッシュ」を1つの名前に詰め込んでいました。TLS 1.3 では AEAD暗号とハッシュだけ(例 TLS_AES_128_GCM_SHA256)になり、鍵交換と署名は別パラメータとして supported_groups や signature_algorithms 拡張で個別にネゴシエートされます。
1-RTT ハンドシェイクの順序
-> はクライアント送信、<- はサーバー送信を表します。{} 内はハンドシェイク鍵で暗号化されたメッセージです。
-> ClientHello
+ key_share (client の ECDHE 公開値 g^x)
+ supported_groups (X25519, secp256r1 ...)
+ signature_algorithms
<- ServerHello
+ key_share (server の ECDHE 公開値 g^y)
{EncryptedExtensions}
{Certificate} サーバー証明書チェーン
{CertificateVerify} ハンドシェイク全文への署名
{Finished} HMAC によるトランスクリプト検証
-> {Finished}
-> [Application Data] アプリ鍵で暗号化
ポイントは、ServerHello 以降のメッセージが すでにハンドシェイク鍵で暗号化 されることです。証明書すら平文では流れません。CertificateVerify はそれまでの全メッセージ(トランスクリプト)に対する署名で、Finished はトランスクリプトのハッシュに対する HMAC です。これにより、途中でメッセージが改ざんされていない(ダウングレード攻撃を含む)ことが相互に検証されます。
クライアントが送った key_share の曲線をサーバーが受け付けない場合、サーバーは HelloRetryRequest を返し、希望する曲線を指定します。クライアントは正しい曲線で ClientHello を再送するため、この場合だけ往復が1つ増えます。投機が外れたときのフォールバックです。
ECDHE:共有秘密の作り方
ECDHE では、両者が一時的な秘密値(クライアント x、サーバー y)と、それに対応する公開値を持ちます。曲線上の生成元を G として、公開値はそれぞれ x*G と y*G です。互いの公開値に自分の秘密を掛けると、同じ点に到達します。
client: (y*G) を受け取り、x を掛ける -> x*y*G
server: (x*G) を受け取り、y を掛ける -> x*y*G
共有秘密 = x*y*G の x 座標(= ECDH shared secret)
x*y*G は両者の手元でだけ計算でき、ネットワークには x*G と y*G しか流れません。盗聴者が両公開値を見ても、離散対数問題のため x や y を復元できず、共有秘密に到達できません。秘密値 x,y は接続ごとに使い捨てるため、後でサーバーの長期秘密鍵が漏れても過去のセッションは復号できません(これが前方秘匿性の根拠)。
HKDF による鍵スケジュール
ECDHE が生む共有秘密は、それ自体では鍵に使いません。TLS 1.3 は HKDF(RFC 5869)の2段構成で、用途ごとの鍵を 段階的に 導出します。
HKDF-Extract(salt, ikm):入力鍵材料ikmから固定長の擬似乱数鍵を「抽出」するHKDF-Expand-Label(secret, label, transcript, len):抽出した鍵を、ラベルとトランスクリプトを混ぜて用途別に「展開」する
鍵スケジュールは3つの Secret を順に連鎖させます。前段の出力が次段の salt になる構造です。
Early Secret = HKDF-Extract(salt=0, ikm=PSK or 0)
Handshake Secret = HKDF-Extract(salt=deriv(Early), ikm=ECDHE 共有秘密)
Master Secret = HKDF-Extract(salt=deriv(Handshake),ikm=0)
各 Secret から Expand-Label で実際のトラフィック鍵を導出:
client_handshake_traffic_secret (EncryptedExtensions 以降の暗号化)
server_handshake_traffic_secret
client_application_traffic_secret (Finished 以降の実データ)
server_application_traffic_secret
トラフィック鍵には常に トランスクリプトのハッシュ が混ぜ込まれます。そのため、ハンドシェイクの内容が1ビットでも違えば導出される鍵が変わり、攻撃者がメッセージを差し替えると鍵が一致せず Finished の検証に失敗します。鍵導出と完全性検証が同じトランスクリプトに束ねられているのが TLS 1.3 設計の核心です。
資格試験では「共有秘密をそのままトラフィック鍵に使う」という誤りが頻出します。正しくは、Extract で乱数性を整え、Expand-Label で用途・長さ・文脈ごとに分離します。1本の共有秘密から複数の独立した鍵を安全に切り出すのが HKDF の役割です。
0-RTT:往復ゼロの再接続
一度ハンドシェイクしたサーバーとは、再開のための PSK(事前共有鍵) を NewSessionTicket で受け取れます。次回クライアントは ClientHello に PSK 由来の情報を載せ、その PSK から導いた early_traffic_secret で 最初のアプリデータを ClientHello と同時に送れます。これが 0-RTT で、往復はゼロです。
ただし 0-RTT データは ECDHE の新鮮な交換を経ておらず、サーバー側に「これは初めて見る送信か」を判定する状態がありません。攻撃者が 0-RTT パケットを記録して 再送(リプレイ) すると、サーバーは同じ要求を二度処理しうります。
0-RTT の early data には リプレイ耐性がありません。送金・在庫減算・注文確定のような「2回実行されると困る」操作を 0-RTT に載せてはいけません。安全なのは GET のような冪等で副作用のない読み取りに限ります。アプリ側でも 0-RTT 経由の要求を識別し、危険な処理を拒否する設計が必要です。
TLS 1.2 との差異
| 項目 | TLS 1.2 | TLS 1.3 |
|---|---|---|
| 往復回数 | 2-RTT | 1-RTT(再接続は 0-RTT も可) |
| 鍵交換 | RSA / DHE / ECDHE から選択 | ECDHE(および PSK)に限定 |
| 前方秘匿性 | ECDHE 選択時のみ | 常に保証(必須) |
| 暗号スイート | 鍵交換+認証+暗号+ハッシュを一体化 | AEAD+ハッシュのみ(鍵交換は別拡張) |
| 証明書の暗号化 | 平文で送信 | ハンドシェイク鍵で暗号化 |
| 鍵導出 | PRF(独自構成) | HKDF(Extract/Expand の標準) |
| 旧弱い暗号 | RC4・CBC・静的RSA等が残存 | AEAD のみ、脆弱方式を排除 |
実務で確認するコマンド
# ネゴシエート結果(バージョン・暗号スイート・鍵交換曲線)を確認
openssl s_client -connect example.com:443 -servername example.com </dev/null \
| grep -E "Protocol|Cipher|Server Temp Key"
# TLS 1.3 を明示して接続できるか試す
openssl s_client -connect example.com:443 -tls1_3 </dev/null
# セッションチケット(0-RTT/再開の材料)が払い出されるか観察
openssl s_client -connect example.com:443 -tls1_3 -sess_out sess.pem </dev/null
Server Temp Key 行に X25519 や P-256 が出れば ECDHE が使われている証拠です。鍵共有から HKDF の鍵スケジュールまで一本の流れとして捉えると、TLS 1.3 が「速さ」と「前方秘匿性」を両立できる理由が見えてきます。HTTP/3 が採用する QUIC はこの 1-RTT/0-RTT 設計を取り込んでいるため、/network/http-versions/ と併せて読むと理解が深まります。
ネットワーク Article
TLS 1.3 ハンドシェイクの内部と鍵導出を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
TLS
比較で見る軸
難易度: advanced / カテゴリ: ネットワーク / タグ数: 5
導入後に効く点
ECDHEで共有秘密を作り、HKDF-Extract/Expandの鍵スケジュールで段階的にハンドシェイク鍵・アプリ鍵を導出する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- ネットワーク
- タグ数
- 5
判断チェックリスト
- 自社の用途が「TLS / 暗号化」に近いか確認する。
- 強みである「TLS1.3は最初のClientHelloで鍵共有値を送り、1-RTTでハンドシェイクを終える。暗号スイートはAEADと鍵交換が分離された。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。