一覧SC-09-2 · TLSの保護範囲

TLSレコードの暗号化と認証

暗号文に含まれる範囲と、平文で見えるが認証される範囲をバイト位置で説明します。

説明

TLS 1.3のアプリデータは、traffic secretから導出した鍵で認証付き暗号化されます。レコードごとのnonceには送信方向ごとに管理する番号が使われ、外側のヘッダーも追加認証データとして保護されます。

固定の注文データを含む40バイトのTLSレコードを復号します。本文、タグ、ヘッダー、レコード番号を変え、どの変更で検証が失敗するかを確認します。

暗号文に含まれる範囲と、平文で見えるが認証される範囲をバイト位置で説明します。

15バイトのヘッダーを分解する

最初のクライアントアプリデータを使います。外側はapplication_dataの23、互換用バージョン0303です。

from sc09 import * record = bytes.fromhex(T["application"]固定のアプリデータレコードを取得する) print(record.hex()) print(len(record),record[:5]外側のヘッダーを読む.hex(),int.from_bytes(record[3:5],"big"))

全体40バイトのうちヘッダー5、暗号化領域35バイトです。後者は本文17・内側の型1・タグ16・この試料ではパディング0から成ります。

2traffic secretから鍵とIVを導出する

TLS 1.3のHKDF-Expand-Labelでkeyとivを別々に導出します。AES-256の鍵は32バイト、IVは12バイトです。

key,iv = keys(secret("CLIENT_TRAFFIC_SECRET_0"))クライアント送信用の鍵とIVを導出する print(key.hex(),len(key)) print(iv.hex(),len(iv))

方向ごとのtraffic secretを使っています。サーバー送信側の鍵でこのレコードを復号するわけではありません。

3番号0のnonceを作る

レコード番号を12バイトへ拡張し、IVとXORします。最初の番号0ではnonceはIVと同じになります。

print(nonce(iv,0)最初のnonceを計算する.hex()) print(nonce(iv,1)次のレコードのnonceと比較する.hex())

番号1では末尾のビットが変わります。同じ鍵の間で同じnonceを再使用しないため、方向別に番号を進めます。

4認証して内側の型を取り出す

ヘッダーをAADとして渡し、暗号文とタグを認証します。内側の型は暗号化されています。

plain = decrypt(record,key,iv,0)正しい番号とヘッダーで復号する print(plain.hex()) print(inner(plain)内側の型と本文を分ける)

本文order=7&amount=100の後に0x17があり、内側もapplication_dataです。平文を返す前にタグ検証が完了しています。

5暗号文とタグを変更する

ヘッダーの直後と、末尾のタグをそれぞれ1ビット反転します。

changed = record[:5]+bytes([record[5]^1暗号文の先頭を反転する])+record[6:] tag_changed = record[:-1]+bytes([record[-1]^1タグの末尾を反転する]) print(decrypt(changed,key,iv,0)) print(decrypt(tag_changed,key,iv,0))

どちらもInvalidTagです。暗号文の破損とタグの破損を、改変された平文の受入れへ進めません。

6平文で見えるヘッダーを変更する

長さフィールドの1ビットだけを変え、同じ暗号化領域を検証します。

header_changed = record[:4]+bytes([record[4]^1ヘッダーの長さフィールドを変更する])+record[5:] print(decrypt(header_changed,key,iv,0))

ヘッダーもAADに含むためInvalidTagです。通常の受信処理では長さによる分割も行いますが、ここでは認証範囲だけを切り出して検証しています。

7同じレコードを次の番号で検証する

受信側が次に期待する番号1で、番号0のレコードを検証します。

print(decrypt(record,key,iv,1)次のレコード番号で再提示を検査する) print(decrypt(record,key,iv,0)元の条件と比較する == plain)

番号1では失敗し、元の番号0なら成功します。番号はこのレコードへ平文フィールドとして送られず、双方が接続状態として管理します。

まとめ

レコードの内側には本文と内側の型が含まれます。

外側のヘッダーもAADとして認証します。

方向別のレコード番号がnonceの計算に使われます。

操作履歴と解説を振り返れます。

Python 3.15 / cryptography 50.0.2 / 公開試料と固定時刻

1
from sc09 import * record = bytes.fromhex(T["application"]) print(record.hex()) print(len(record),record[:5].hex(),int.from_bytes(record[3:5],"big"))
1703030023063a772c8e4775a95a4ac387fae9bead17f5d0813f0b149dab1e102c7c68c7e8d55ab0
40 1703030023 35
2
key,iv = keys(secret("CLIENT_TRAFFIC_SECRET_0")) print(key.hex(),len(key)) print(iv.hex(),len(iv))
9a58ca2f0db2d9cc87869444c9964ea421589f12d8dddc4b64592071eccc44ee 32
dd85ee0ea2148a46bb1f5caa 12
3
print(nonce(iv,0).hex()) print(nonce(iv,1).hex())
dd85ee0ea2148a46bb1f5caa
dd85ee0ea2148a46bb1f5cab
4
plain = decrypt(record,key,iv,0) print(plain.hex()) print(inner(plain))
6f726465723d3726616d6f756e743d31303017
(23, b'order=7&amount=100')
5
changed = record[:5]+bytes([record[5]^1])+record[6:] tag_changed = record[:-1]+bytes([record[-1]^1]) print(decrypt(changed,key,iv,0)) print(decrypt(tag_changed,key,iv,0))
InvalidTag
InvalidTag
6
header_changed = record[:4]+bytes([record[4]^1])+record[5:] print(decrypt(header_changed,key,iv,0))
InvalidTag
7
print(decrypt(record,key,iv,1)) print(decrypt(record,key,iv,0) == plain)
InvalidTag
True

演習完了