TLS 1.3 による通信データの暗号化 — AEADの4入力・鍵スケジュール・ノンス管理(読書ノート)¶
作成日: 2026-06-29 出典: - Software Design 2026年7月号 連載「暗号のひみつ」第7回「TLSによる通信データの暗号化」(荒木誠/@tex2e) - 一次資料での裏取り: RFC 8446 The Transport Layer Security (TLS) Protocol Version 1.3(2018年8月)§5.2/§5.3/§7.1/§7.3 関連: cloudflare_2026_サプライチェーン攻撃リスク、[[aead_aes-gcm_認証付き暗号_整理]](前回号=2026年6月号の AEAD 回。未作成なら作る価値あり)
0. 要点(3行)¶
- TLS 1.3 は AEAD(認証付き暗号)を必須化 した(RFC 8446, 2018)。TLS 1.2 の「CBC + MAC」が抱えたパディングオラクル攻撃のような構造的弱点を、古い方式を排除して断つ。
- 肝は 「AEAD の安全性を実装者の注意ではなくプロトコルの構造で担保する」 こと。AEAD の4入力(鍵・ノンス・平文・AAD)のうち、鍵は HKDF による鍵分離で、ノンスは IV ⊕ 送信カウンタの自動生成で、人間が誤らない形に設計されている。
- ノンス重複は AES-GCM 等の安全性を一発で破綻させる最大の地雷。TLS 1.3 は静的 IV と単調増加カウンタの XOR で同一鍵下のノンス重複を構造的にゼロにしている — ここが本記事の白眉。
1. TLS とは何を守るのか(3つの性質)¶
TLS(Transport Layer Security)は、安全でない通信路の上で安全にデータをやりとりするしくみ。「安全」は次の3性質を指す。
| 性質 | 意味 | TLS での担保手段 |
|---|---|---|
| 真正性(Authenticity) | 通信相手が本物(なりすましでない) | 公開鍵暗号 — サーバー証明書による相手認証 |
| 機密性(Confidentiality) | 通信内容が第三者に読まれない | 共通鍵暗号 — データの暗号化 |
| 完全性(Integrity) | 通信内容が途中で書き換えられていない | 共通鍵暗号 — 改ざん検知(認証タグ) |
ポイントは、公開鍵暗号と共通鍵暗号の役割分担。公開鍵暗号は「相手認証」と「鍵交換による共通鍵の安全な共有」に使い、実データの暗号化は速い共通鍵暗号(AEAD)が担う。
TLS 通信は2段階:
- ハンドシェイク — 公開鍵暗号で互いの正当性を確認し、以降で使う共通鍵を安全に共有する
- アプリケーションデータの暗号化通信 — 共有した共通鍵で HTTP リクエスト/レスポンスを AEAD 暗号化してやりとりする
2. TLS 1.3 の特徴 — なぜ AEAD 必須化なのか¶
- RFC 8446 として2018年に標準化。TLS 1.2 から設計を大幅に見直し、より高速・安全に。
- 最大の変更点: データ暗号化方式を AEAD(認証付き暗号)に一本化。
- TLS 1.2 の「CBC モード + MAC」は パディングオラクル攻撃という構造的弱点を抱えていた(記事の注1: パディング処理の MAC 計算時間差から平文を推測する Lucky Thirteen 攻撃がその一例)。
- TLS 1.3 はこうした古い方式を排除し AEAD のみに統一して、より安全なプロトコルにした。
- AEAD で実際に使う鍵は、鍵交換で得た「鍵素材」をそのまま使うのではなく、HKDF(HMAC-based Extract-and-Expand Key Derivation Function) を通して段階的に導出する。この「鍵素材 → 用途ごとのトラフィック鍵」の段階導出が 鍵スケジュール。
3. ハンドシェイクの流れ(鍵交換 → 認証 → 暗号化通信)¶
TLS 1.3 フルハンドシェイクは3フェーズで進む。
クライアント サーバー
│ ── ClientHello ───────────────────▶ │ ┐
│ (暗号スイート一覧 + ECDHE公開値 KeyShare) │ │ 鍵交換
│ ◀─ ServerHello ──────────────────── │ ┘ (採用スイート + 公開値)
│ ── ここで鍵素材が揃い、以降は AEAD 暗号化 ──
│ ◀─ {EncryptedExtensions} │ ┐
│ ◀─ {Certificate}{CertificateVerify} │ │ 認証({ } は暗号化済み)
│ ◀─ {Finished} ────────────────────── │ ┘
│ ── {Finished} ────────────────────▶ │
│ ── {Application Data} ◀───────────▶ │ 暗号化通信
- 鍵交換: 鍵そのものではなく公開値を交換する。両者は ECDHE(楕円曲線ディフィー・ヘルマン鍵交換) の公開値を交換し、「自分の秘密値 × 相手の公開値」から同じ鍵素材を得る。
- TLS 1.3 はこの時点でトラフィック鍵を導出できるため、ServerHello 以降の
EncryptedExtensions(ALPN 等サーバー固有情報)からのハンドシェイクメッセージがすべて暗号化される(上図{ })。TLS 1.2 より暗号化開始が早いのが特徴。 - 認証: サーバーは
Certificateで証明書を、CertificateVerifyで「証明書に対応する秘密鍵で作った署名」を送り、自分が証明書の正当な所有者であることを示す。 - 完了: 互いに
Finishedを送り、同じ鍵を共有できたことを確認 → アプリケーションデータを AEAD で暗号化できる状態に。
4. 鍵スケジュール — 鍵分離と不可逆性¶
4-1. HKDF の2段(Extract と Expand)¶
HKDF は HMAC ベースの鍵導出関数で、2段で動く。
- Extract(抽出): 入力された鍵素材から暗号学的な強度(エントロピー)を抽出する。
HKDF-Extractが各シークレットの導出に使われる。 - Expand(展開): 用途に応じたラベルを指定して、必要長の値を生成する。各シークレットからのトラフィックシークレット導出には、
HKDF-Expandを利用したDerive-Secret関数が使われる。
4-2. シークレットの階層(Early → Handshake → Master → Application Traffic)¶
鍵スケジュールは HKDF-Extract と Derive-Secret を交互に適用していく構造。各シークレットに Derive-Secret(., "derived", "") を適用した結果が、次段 HKDF-Extract の入力(ソルト)になる。
0 (全0バイト列)
│ PSK(事前共有鍵。無ければ全0) ──▶ HKDF-Extract
▼
Early Secret … 主に PSK セッション再開用。フルハンドシェイクでも起点として常に計算
│ Derive-Secret(., "derived", "")
│ (EC)DHE 共有鍵 ───────────────▶ HKDF-Extract
▼
Handshake Secret ──┬─ Derive-Secret(., "c hs traffic", ctx) ─▶ クライアント側ハンドシェイク鍵/IV
└─ Derive-Secret(., "s hs traffic", ctx) ─▶ サーバー側ハンドシェイク鍵/IV
│ Derive-Secret(., "derived", "")
│ 0 ──────────────────────────▶ HKDF-Extract
▼
Master Secret ──┬─ Derive-Secret(., "c ap traffic", ctx) ─▶ client_application_traffic_secret_0
└─ Derive-Secret(., "s ap traffic", ctx) ─▶ server_application_traffic_secret_0
- ここでの 「シークレット(Secret)」は鍵そのものではなく、鍵を導出するための中間成果物である点に注意(AEAD に渡す「トラフィック鍵」とは別物)。
- ラベルは用途・通信方向ごとに別(
c=client,s=server,hs=handshake,ap=application)。これが 鍵分離: 用途ごとに独立した鍵を使い分ける。
4-3. なぜ安全か — 鍵分離 × 不可逆性¶
- HKDF はハッシュ関数ベースの 擬似乱数関数(PRF) としての性質を持つため、下流の鍵から上流のシークレットを逆算・推測できない(不可逆性)。
- 帰結: あるトラフィックシークレットが漏洩しても、他用途の鍵や上位シークレットには波及しない。
- TLS 1.3 はこの 鍵分離 + 不可逆性の組み合わせで、AEAD を安全に運用する鍵管理を実現している。
5. トラフィック鍵と IV の導出(HKDF-Expand-Label)¶
ハンドシェイク完了後、両者は手元で application_traffic_secret_0 を導出して保持する(同じ手順なのでクライアント/サーバーで一致)。ここから AEAD の入力を組み立てる。
client_application_traffic_secret_0
├─ HKDF-Expand-Label(secret, "key", "", key_length) ─▶ write_key(トラフィック鍵)
└─ HKDF-Expand-Label(secret, "iv", "", iv_length) ─▶ write_iv(静的IV)
- HKDF の性質上、ラベルが
"key"と"iv"のようにわずかに違うだけで、出力は完全に独立した別値になる。1つのシークレットから鍵と IV を安全に分離できる理由。 - Application Traffic Secret はクライアント用とサーバー用に分かれる。各エンドポイントは「自分側を送信用、相手側を受信用」として使う(送受信で別鍵)。
📌 一次資料での補足(記事の簡略表記 vs RFC 8446 の正確表記) 記事は
HKDF-Expand-Label(., "key", "")と書くが、RFC 8446 §7.1 ではHkdfLabel構造体のlabelフィールドが"tls13 " + Labelと定義される。つまり内部的な実ラベルは"tls13 key"/"tls13 iv"。 同様にDerive-Secret(Secret, Label, Messages) = HKDF-Expand-Label(Secret, Label, Transcript-Hash(Messages), Hash.length)で、Context にはそれまでのハンドシェイクメッセージのトランスクリプトハッシュが入る(記事のctx)。
6. AEAD 暗号化処理 — 4つの入力¶
AEAD 暗号は 「鍵・ノンス・平文・AAD」の4入力を取り、「暗号文 + 認証タグ」を出力する。RFC 8446 §5.2 の呼び出しは AEAD-Encrypt(write_key, nonce, additional_data, plaintext)。
┌─ 鍵 = write_key
├─ ノンス = write_iv ⊕ 送信カウンタ ┌─ 暗号化レコード(暗号文)
入力 ───┤ ─AEAD─┤
├─ 平文 = アプリデータを TLSPlaintext 構造体化 └─ 認証タグ
└─ AAD = レコードヘッダ → TLSCiphertext に格納し TCP 送信
(opaque_type 0x17 ‖ legacy_record_version 0x0303 ‖ length)
受信側は同じ手順で導出した同じ鍵とノンスでタグを検証し、検証成功時のみ平文を取り出す(改ざんがあればタグ検証が失敗して破棄)。
6-1. ノンス — 重複ゼロを構造で保証(本記事の核心)¶
同じ鍵のもとでノンスが一度でも重複すると AES-GCM 等の安全性は破綻する。TLS 1.3 はこの一意性を「実装者の注意」ではなくプロトコルの構造で担保する。
送信カウンタ(64bit, レコードごとに +1。鍵が使われ始めた時点で 0)
│ 8バイト big-endian にして iv_length まで左をゼロ埋め
▼
padded_seq XOR write_iv(静的IV) = per-record nonce(レコードごとに変化)
- カウンタは送信用の鍵ごとに独立して用意され、1レコード送るたびに単調増加する。
- 同一鍵下でカウンタは増え続けるので、同じ鍵でノンスが重複することは構造的に起こらない。
- 記事の注3: 実際には 64bit カウンタを左側にゼロ埋めして IV 長に合わせてから XOR する(RFC 8446 §5.3 と一致)。
6-2. AAD — 暗号化しないヘッダまで完全性で守る¶
- AAD には TLS レコードの通信ヘッダ(パケットのタイプ/バージョン情報=長さ等)が入る。
- ヘッダ自体は暗号化されずに送信されるが、AAD として認証タグの計算対象に含まれるため、途中で書き換えられると受信側のタグ検証が失敗する。
- これにより TLS 1.3 は、暗号化されない通信ヘッダまで含めたレコード全体の完全性を AEAD だけで保証する(MAC を別に持たない AEAD の利点)。
- RFC 8446 §5.2 の AAD =
TLSCiphertext.opaque_type(0x17=application_data) ‖ legacy_record_version(0x0303) ‖ length。legacy_record_versionが0x0303(=TLS 1.2)なのは中間装置との互換性のため。
7. まとめ — AEAD の4入力を「構造」でどう安全にしているか¶
本記事の本質は「AEAD の4入力それぞれを、人間が誤らない形でプロトコルに織り込んだ」という1点に集約できる。
| AEAD の入力 | 危険な失敗 | TLS 1.3 の構造的対策 |
|---|---|---|
| 鍵 | 用途を跨いだ鍵使い回し・漏洩の波及 | HKDF による鍵分離(用途/方向別ラベル)+ 不可逆性で漏洩を局所化 |
| ノンス | 同一鍵下での重複(即破綻) | 静的IV ⊕ 単調増加カウンタ で重複を構造的にゼロ化 |
| 平文 | — | TLSPlaintext 構造体に格納して入力 |
| AAD | 平文ヘッダの改ざん | レコードヘッダを AAD に含め、暗号化しないヘッダの完全性もタグで保証 |
一言で: TLS 1.3 の安全性は「強い暗号を選んだ」からではなく、AEAD を正しく使うための前提(鍵分離・ノンス一意性・ヘッダ完全性)を、実装者の注意力に頼らずプロトコルの型として強制したことに効いている。「絶対値(暗号アルゴリズムの強さ)」より「相対関係(4入力をどう組み立てるかの規律)」が本質。
いつ効くか / どこで思い出すか¶
- HTTPS/TLS の設計・トラブルシュートで「なぜこの鍵/IV/ノンスがこう作られるのか」を辿るとき
- 自分で AEAD(AES-GCM 等)を使う実装をするとき → ノンス一意性の担保責任は呼び出し側にある、を思い出す(TLS はそれを構造化した好例)
- 鍵管理レビューで「鍵分離されているか」「鍵漏洩の波及範囲(blast radius)は局所化されているか」を問うとき
参考リンク¶
- RFC 8446 — The Transport Layer Security (TLS) Protocol Version 1.3: https://www.rfc-editor.org/rfc/rfc8446
- §5.2 Record Payload Protection(AAD・AEAD-Encrypt): https://www.rfc-editor.org/rfc/rfc8446#section-5.2
- §5.3 Per-Record Nonce(ノンス構成): https://www.rfc-editor.org/rfc/rfc8446#section-5.3
- §7.1 Key Schedule(HKDF-Expand-Label / Derive-Secret / シークレット階層): https://www.rfc-editor.org/rfc/rfc8446#section-7.1
- §7.3 Traffic Key Calculation(write_key / write_iv): https://www.rfc-editor.org/rfc/rfc8446#section-7.3
- Software Design 2026年7月号 連載「暗号のひみつ」第7回(荒木誠 / X: @tex2e)
- 著者(Mako / @tex2e)のブログ「晴耕雨読」TLS 1.3 連載: https://tex2e.github.io/blog/protocol/1-intro (第1回「TLS 1.3/暗号化通信」。プロトコル詳細を実装視点で解説。Security Camp 2023「暗号化通信ゼミ」講師)
作成: 2026-06-29 / 最終更新: 2026-06-29