TLS レコード層の内部(断片化・暗号化・シーケンス番号)
ハンドシェイク完了後、実データをどう守って運ぶのか。断片化・AEAD 保護・暗黙シーケンス番号という3つの仕組みを原理から押さえれば、TLS 1.3 のパケットを正しく読み解き実装事故も避けられる。
- 1.レコード層は上位データを最大 2^14 バイトの断片に切り、各断片を AEAD で暗号化・認証してネットワークに流す。受信側はタグ検証後に順序通り再結合する。
- 2.nonce とリプレイ防止に使うシーケンス番号は通信路に乗らない「暗黙」値。送受信で各々 0 から数え、固定 IV と XOR して per-record nonce を作る。だから盗聴者は番号を改ざんできない。
- 3.TLS 1.3 は内側の実コンテンツ型を暗号化対象に含め、外側を常に application_data に偽装し、任意長ゼロパディングで長さも隠す。これでトラフィック解析の手掛かりを削る。
レコード層とは何か:ハンドシェイクの下で実データを運ぶ土台
TLS は2層構造です。上にハンドシェイクプロトコル(鍵交換・認証)があり、その下に**レコードプロトコル(レコード層)**があります。ハンドシェイクのメッセージも、アプリケーションの HTTP データも、アラートも、すべて最終的にこのレコード層を通って TCP に乗ります。レコード層の仕事は単純明快です。
- 上位から渡されたバイト列を**一定サイズの断片(fragment)**に切る。
- 各断片を鍵で保護(暗号化+認証)してレコードにする。
- 受信側で検証・復号し、順序通りに再結合して上位へ返す。
つまりハンドシェイクが「どの鍵を使うか」を決め、レコード層が「その鍵で1レコードずつ運ぶ」担当です。TLS 1.3 では保護に AEAD(認証付き暗号) のみを使い、旧来の CBC + HMAC のような構成は廃止されました。
1つの TLS レコードが1つの TCP セグメントに対応するとは限りません。レコードは TCP のバイトストリーム上に連続して並ぶだけで、TCP の分割・結合とは独立です。受信側は「5バイトのレコードヘッダ → 続く本体」というフレーミングを TCP ストリームから自前で切り出します。だから1回の read() でレコードが途中までしか届かないこともあり、実装は長さ分が揃うまでバッファリングする必要があります。
断片化と再構成:なぜ 2^14 で区切るのか
平文(プレーンテキスト断片)の最大サイズは **2^14 バイト(16384バイト)**と決められています。上位データがこれを超えれば複数レコードに分割し、小さければ1レコードに収めます。この上限には実務的な理由があります。
- 受信側のメモリ:1レコードを復号・認証するには、その全体を受け取り切る必要があります(AEAD のタグ検証は断片全体に対して行うため、途中までで検証はできない)。上限がなければ巨大なバッファを強いられます。
- タグ検証の遅延:レコードが大きいほど、最初のバイトを上位に渡せるまでの待ち時間が伸びます。改ざん検出はレコード単位なので、粒度を細かくすれば早く弾けます。
レコードのフレーミング(TLS 1.3):
opaque_type = 23 (application_data に偽装。常にこの値)
legacy_version= 0x0303 (互換のため固定表示)
length = 暗号化後ペイロードの長さ(タグ込み、最大 2^14 + 256)
encrypted_record = AEAD-Encrypt(...) の出力
再構成は逆順です。受信側はヘッダの length 分を集めてから AEAD 復号・タグ検証し、成功したレコードの中身を到着順に連結して上位へ流します。ここで重要なのは、TLS が TCP の上で順序を前提にできる点です。TCP がバイト列の順序を保証するので、レコードも送信順に届きます。レコード自身は自分の連番を運ばないため、もし順序が狂えばタグ検証が失敗します(後述のシーケンス番号がずれるため)。
レコードの長さや個数は暗号化されません。HTTP/2 のような上位プロトコルが小さなフレームを多数送ると、レコード長の列がトラフィックパターンとして観測でき、訪問ページの推測(トラフィック解析)に使われ得ます。TLS 1.3 のパディング(後述)はこの緩和策の一つですが、断片化の粒度そのものも情報になることは意識しておくべきです。
AEAD によるレコード保護:1レコード = 1回の封印
各断片は AEAD で1回封印されます。AEAD の入力は「鍵」「nonce」「平文」「AAD(追加認証データ=暗号化しないが認証はするヘッダ情報)」で、出力は「暗号文+認証タグ」です。TLS 1.3 のレコード保護はおおよそ次の形です。
封印(送信):
inner_plaintext = content || content_type || zeros(padding)
additional_data = レコードヘッダ (type=23, version, length)
encrypted_record = AEAD-Encrypt(key, nonce, inner_plaintext, additional_data)
開封(受信):
inner_plaintext = AEAD-Decrypt(key, nonce, encrypted_record, additional_data)
// タグ検証に失敗したら接続を即時 abort(bad_record_mac アラート)
ポイントは、暗号化される平文(inner_plaintext)の末尾に実際のコンテンツ型とパディングが含まれることです。外側ヘッダの型は常に application_data(23)に固定され、本当の型(handshake か alert か application_data か)は暗号化された内側にしまわれます。これが内容型隠蔽です。AAD には外側のヘッダがそのまま入るので、長さやバージョンを改ざんすればタグ検証で弾けます。
鍵と固定 IV は、ハンドシェイクで確立した秘密から HKDF で導出した traffic secret から派生します。送信方向と受信方向で別の鍵・IV を持ち、片方向の鍵が漏れてももう片方には及びません。
タグ検証に失敗したレコードは、改ざんかビット誤りかを問わず復号結果を1バイトも上位に渡してはいけません。TLS 1.3 では原則、復号エラーで接続を bad_record_mac アラートとともに即座に終了します(RFC 8446 §5.2。decrypt_error はハンドシェイク段の署名・Finished・PSK binder 検証失敗用で、レコード層の復号失敗には使いません)。CBC + HMAC 時代に、復号後のパディング妥当性をエラーの差で読み取る padding oracle 攻撃 が成立したのは「復号してから検証する」構造ゆえでした。AEAD は暗号文に対してタグを検証してから復号結果を出すため、この種のオラクルを構造的に塞ぎます。
暗黙シーケンス番号:通信路に乗らない連番
各レコードには論理的なシーケンス番号(64ビット)が割り当てられます。決定的に重要なのは、この番号がレコードに書き込まれず、通信路を流れない点です。だから「暗黙(implicit)」シーケンス番号と呼ばれます。送信側と受信側がそれぞれ独立に、0 から始めてレコードを1つ処理するたびに +1 するカウンタを持つだけです。
この暗黙の番号は2つの役割を果たします。
1. per-record nonce の生成。 AEAD は「同じ鍵で同じ nonce を二度使わない」ことが安全性の絶対条件です(再利用は AEAD の項 のとおり認証鍵の露出すら招く)。TLS 1.3 はレコードごとに異なる nonce を、シーケンス番号と固定 IV から計算で作ります。
per-record nonce の作り方(TLS 1.3):
seq = 64ビットのシーケンス番号(レコード毎に +1)
padded_seq = seq を左ゼロ詰めして IV と同じ長さ(通常12バイト)に
nonce = padded_seq XOR write_iv // write_iv は方向ごとの固定 IV
番号は単調増加なので nonce も毎回必ず異なり、再利用が原理的に起きません。固定 IV と XOR するのは、複数接続で同じシーケンス値でも nonce がぶつからないようにするためです。
2. リプレイ・並べ替えの検出。 シーケンス番号は AEAD の nonce に組み込まれているため、攻撃者がレコードの順序を入れ替えたり、過去のレコードを再注入したりすると、受信側の期待する番号とずれて nonce が一致せず、タグ検証が必ず失敗します。番号自体は通信路に無いので、攻撃者がそれを書き換えて辻褄を合わせることもできません。
64ビットのシーケンス番号は実質枯渇しませんが、AEAD には「同一鍵で安全に処理できるレコード数」の上限があり(暗号方式ごとの解析限界)、TLS 1.3 はこれに達する前に KeyUpdate で traffic key を更新します。鍵が変わるとシーケンス番号は再び 0 にリセットされます。逆に言えば、鍵更新なしにレコードを延々と送り続ける実装は、AEAD の安全限界を超えるリスクがあります。長寿命・高スループットな接続ほど鍵更新の実装が重要です。
TLS 1.3 のパディングと内容型隠蔽
TLS 1.3 がレコード層で強化した最大の点が、コンテンツ型の隠蔽と長さの撹乱です。
旧来(TLS 1.2 以前)はレコードヘッダの型フィールドが平文で、handshake か application_data か alert かが盗聴者に丸見えでした。TLS 1.3 はこれを次のように改めました。
| 観点 | TLS 1.2 以前 | TLS 1.3 |
|---|---|---|
| 外側の型フィールド | 実際の型を平文で表示 | 常に application_data (23) に固定 |
| 本当のコンテンツ型 | ヘッダで露出 | 暗号化された内側末尾に格納し隠蔽 |
| パディング | CBC のブロック整合用のみ | 任意長ゼロパディングで長さを撹乱可能 |
| 主な保護 | 機密性・完全性 | 左記+トラフィック解析の緩和 |
内側平文の構造は 実コンテンツ || 本当の型(1バイト) || ゼロ(0バイト以上) です。受信側は復号後、末尾から先頭に向かってゼロを読み飛ばし、最初に現れた非ゼロバイトを本当のコンテンツ型と解釈します。パディングはすべてゼロなので、型の境界が一意に決まります。
パディングの長さは送信側が自由に決められます。例えば短いリクエストを一定サイズに揃えれば、レコード長から本文サイズを推測する攻撃を弱められます。ただし注意すべきトレードオフがあります。
ゼロパディングは長さを「増やす方向」にしか撹乱できず、しかも余分なバイトは帯域を消費します。長さを完全に隠すには全レコードを同一長に揃える必要があり、コストが高い。さらにレコードの個数・到着タイミングはパディングでは隠せず、依然としてトラフィック解析の手掛かりになります。試験・実務では「TLS 1.3 のパディングは長さ撹乱の緩和策であり、トラフィック解析を完全には防がない」と正確に押さえるのが肝心です。
まとめ
レコード層は、ハンドシェイクで確立した鍵を使って実データを1レコードずつ安全に運ぶ TLS の土台です。上位データを最大 2^14 バイトの断片に切り、各断片を AEAD で暗号化・認証してネットワークに流し、受信側はタグ検証後に順序通り再結合します。nonce とリプレイ防止に使うシーケンス番号は通信路を流れない暗黙の64ビットカウンタで、固定 IV と XOR して per-record nonce を作るため、再利用も並べ替えも構造的に防げます。さらに TLS 1.3 は本当のコンテンツ型を暗号化対象に含め、外側を常に application_data に偽装し、ゼロパディングで長さを撹乱して、トラフィック解析の手掛かりを削りました。
鍵そのものの導出は HKDF による鍵導出、鍵更新の背景にある前方秘匿性は 前方秘匿性とラチェット、復号順序の重要性を裏付ける事故例は padding oracle 攻撃 を合わせて参照してください。
セキュリティ Article
TLS レコード層の内部(断片化・暗号化・シーケンス番号)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
TLS
比較で見る軸
難易度: advanced / カテゴリ: セキュリティ / タグ数: 5
導入後に効く点
nonce とリプレイ防止に使うシーケンス番号は通信路に乗らない「暗黙」値。送受信で各々 0 から数え、固定 IV と XOR して per-record nonce を作る。だから盗聴者は番号を改ざんできない。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- セキュリティ
- タグ数
- 5
判断チェックリスト
- 自社の用途が「TLS / AEAD」に近いか確認する。
- 強みである「レコード層は上位データを最大 2^14 バイトの断片に切り、各断片を AEAD で暗号化・認証してネットワークに流す。受信側はタグ検証後に順序通り再結合する。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。