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 / 公開試料と固定時刻
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
key,iv = keys(secret("CLIENT_TRAFFIC_SECRET_0"))
print(key.hex(),len(key))
print(iv.hex(),len(iv))9a58ca2f0db2d9cc87869444c9964ea421589f12d8dddc4b64592071eccc44ee 32 dd85ee0ea2148a46bb1f5caa 12
print(nonce(iv,0).hex())
print(nonce(iv,1).hex())dd85ee0ea2148a46bb1f5caa dd85ee0ea2148a46bb1f5cab
plain = decrypt(record,key,iv,0)
print(plain.hex())
print(inner(plain))6f726465723d3726616d6f756e743d31303017 (23, b'order=7&amount=100')
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
header_changed = record[:4]+bytes([record[4]^1])+record[5:]
print(decrypt(header_changed,key,iv,0))InvalidTag
print(decrypt(record,key,iv,1))
print(decrypt(record,key,iv,0) == plain)InvalidTag True
演習完了