AES-GCMによる認証付き暗号化
どの値が秘密になり、どの値が認証されるかを説明します。
説明
認証付き暗号は秘匿と改変検出を組み合わせます。AES-GCMはnonce、平文、追加認証データAADから暗号文とタグを作ります。AADは暗号化しませんが、復号時に同じ値を与えないと検証に失敗します。
注文本文を暗号化してから、本文・タグ・宛先を一つずつ変更します。最後に圧縮前後の長さを比較し、暗号化しても残る長さの情報を確認します。
どの値が秘密になり、どの値が認証されるかを説明します。
1本文と認証対象のヘッダーを用意する
公開試料鍵を使います。nonceは12バイト、タグはこのAPIでは16バイトです。同じ鍵でnonceを再利用してはいけません。
from sc05 import *
import zlib
key = bytes(16)
nonce = bytes(12)
plain = b"order=7&amount=100"
aad = b"tenant=red"
sealed = AESGCM(key).encrypt(nonce,plain,aad)本文を暗号化してAADも認証する
print(len(plain),len(sealed),sealed.hex())暗号文の長さは平文と同じで、末尾に16バイトのタグが加わります。AAD自体はsealedへ格納されません。
2暗号文とタグを分けて表示する
末尾16バイトをタグとして切り出します。
print(sealed[:-16]暗号文部分を取得する.hex())
print(sealed[-16:]認証タグを取得する.hex())
print(open_gcm(認証して復号するkey,nonce,sealed,aad))同じnonceとAADなら元の本文へ戻ります。タグを確認する前の平文をアプリへ渡さない実装が必要です。
3暗号文を変更する
先頭1ビットを反転し、元のタグのまま提示します。
print(open_gcm(key,nonce,flip(sealed,0)暗号文の先頭を変更する,aad))InvalidTagとなり、変更された本文は返りません。暗号文もタグの検証対象です。
4タグを変更する
暗号文を変えず、末尾のタグだけ1ビット変更します。
print(open_gcm(key,nonce,flip(sealed,len(sealed)-1)タグの末尾を変更する,aad))タグだけの改変も拒否されます。本文が同じだから受け入れてよい、とはなりません。
5宛先テナントを変更する
暗号文を別のテナントへ持ち込んだ場合です。検証側が期待するAADをblueに変えます。
print(open_gcm(key,nonce,sealed,b"tenant=blue"検証側の宛先情報を変更する))
print(aad)ヘッダーは平文のままですが、変更したヘッダーでは検証できません。AADは秘匿する情報の置き場ではありません。
6nonceを変更する
同じ暗号文に別のnonceを付けた場合も確認します。
print(open_gcm(key,flip(nonce,0)nonceの1ビットを変更する,sealed,aad))nonceが異なると検証に失敗します。nonceは秘密でなくても、暗号文と正しく対応付けて保存する必要があります。
7圧縮による長さの違いを測る
繰返しの多い576バイトを圧縮します。以降の暗号化では新しいnonceを使います。
data = b"order=7&amount=100;" * 32
packed = zlib.compress(data)繰返しを圧縮する
raw_box = AESGCM(key).encrypt(bytes.fromhex("000000000000000000000001"),data,aad)
zip_box = AESGCM(key).encrypt(bytes.fromhex("000000000000000000000002"),packed,aad)
print圧縮前後と暗号化後の長さを比較する(len(data),len(packed),len(raw_box),len(zip_box))GCMは平文長に16バイトを加えた長さになるため、圧縮の差が暗号文長にも残ります。内容が秘密でも長さは別途考える必要があります。
8暗号化後の圧縮と比較する
同じ圧縮関数を暗号文へ適用します。
print(len(raw_box),len(zlib.compress(raw_box)暗号文を圧縮した長さを測る))
print(zlib.decompress(認証・復号後に展開するAESGCM(key).decrypt(bytes.fromhex("000000000000000000000002"),zip_box,aad)) == data)この試料では暗号文の繰返しが減り、先に圧縮したときの効果は得られません。秘密と攻撃者が選べる文を一緒に圧縮すると、長さが推測の手掛かりになる場合もあります。
まとめ
暗号文とタグとAADを一組として検証します。
AADは認証されますが暗号化されません。
暗号化しても長さの情報が残り、圧縮の順序も影響します。
操作履歴と解説を振り返れます。
Python 3.15 / cryptography 50.0.2 / 公開試料と固定時刻
from sc05 import *
import zlib
key = bytes(16)
nonce = bytes(12)
plain = b"order=7&amount=100"
aad = b"tenant=red"
sealed = AESGCM(key).encrypt(nonce,plain,aad)
print(len(plain),len(sealed),sealed.hex())18 34 6cfabeab128b94b49245adcc1fc6c349c7a5d25b52606d0b75f796cbc2ee18662d4e
print(sealed[:-16].hex())
print(sealed[-16:].hex())
print(open_gcm(key,nonce,sealed,aad))6cfabeab128b94b49245adcc1fc6c349c7a5 d25b52606d0b75f796cbc2ee18662d4e b'order=7&amount=100'
print(open_gcm(key,nonce,flip(sealed,0),aad))InvalidTag
print(open_gcm(key,nonce,flip(sealed,len(sealed)-1),aad))InvalidTag
print(open_gcm(key,nonce,sealed,b"tenant=blue"))
print(aad)InvalidTag b'tenant=red'
print(open_gcm(key,flip(nonce,0),sealed,aad))InvalidTag
data = b"order=7&amount=100;" * 32
packed = zlib.compress(data)
raw_box = AESGCM(key).encrypt(bytes.fromhex("000000000000000000000001"),data,aad)
zip_box = AESGCM(key).encrypt(bytes.fromhex("000000000000000000000002"),packed,aad)
print(len(data),len(packed),len(raw_box),len(zip_box))608 34 624 50
print(len(raw_box),len(zlib.compress(raw_box)))
print(zlib.decompress(AESGCM(key).decrypt(bytes.fromhex("000000000000000000000002"),zip_box,aad)) == data)624 635 True
演習完了