エンベロープ暗号化と鍵の更新
本文と鍵のどちらを更新したか、旧データの復号に何が残っている必要があるかを説明します。
説明
包絡暗号では、本文を暗号化するデータ鍵DEKと、その鍵を保護する鍵KEKを分けます。KEKを更新するとき、本文全体を暗号化し直さずにDEKの保護だけを変更できる場合があります。
公開試料DEKで文書をAES-GCM暗号化し、DEKをAES Key Wrapで保護します。KEKの世代更新、旧世代の無効化、鍵の消失、保存コピーからの復元を順に試します。
本文と鍵のどちらを更新したか、旧データの復号に何が残っている必要があるかを説明します。
1本文をDEKで暗号化する
DEKは16バイトの公開試料です。本文には注文IDをAADとして結び付けます。
from sc11 import *
vault = Vault()
dek = bytes.fromhex("00112233445566778899aabbccddeeff")
nonce = bytes(12)
aad = b"document-7"
box = AESGCM(dek).encryptDEKで本文を認証付き暗号化する(nonce,b"confidential-record",aad)
print(len(box),box.hex())本文の長さに16バイトのタグを加えた暗号文です。次に、この本文を戻すためのDEKを別の鍵で保護します。
2世代v1のKEKでDEKを包む
AES Key Wrapは鍵データを保護する方式です。包んだ結果と、どの世代で包んだかを保存します。
wrapped1 = vault.wrap("v1",dek)v1のKEKで鍵を包む
print(wrapped1[0],len(wrapped1[1]),wrapped1[1].hex())
print(vault.unwrap(wrapped1)同じ世代で鍵を取り出す == dek)16バイトのDEKが24バイトに包まれ、v1の鍵で取り出せます。鍵識別子だけでは元のDEKを復元できません。
3DEKの保護をv2へ更新する
v1でDEKを取り出し、v2のKEKで包み直します。本文boxは変更しません。
old_box = box
wrapped2 = vault.wrap("v2"取り出したDEKをv2で包み直す,vault.unwrap(wrapped1))
print(wrapped2[1].hex(),wrapped2[1] == wrapped1[1])
print(box == old_box本文が変わっていないことを確認する)
print(AESGCM(vault.unwrap(wrapped2)).decrypt(nonce,box,aad))包んだ鍵データは変わり、本文は同じまま復号できます。これはKEKの更新であり、DEK自体を変更した操作ではありません。
4旧世代の利用を無効化する
vaultの方針としてv1を無効にします。既に取り出したDEKは別に存在しています。
vault.enabled.remove("v1")旧世代の利用を止める
print(vault.unwrap(wrapped1))
print(vault.unwrap(wrapped2) == dek)
print(AESGCM(dek).decrypt取得済みDEKからの復号を比較する(nonce,box,aad))v1経由はdisabledですが、v2と取得済みDEKでは復号できます。鍵管理サービスの無効化だけで、配布済みの平文鍵まで消えるわけではありません。
5包まれた鍵を改変する
v2の包まれたデータの末尾1ビットを変更します。
raw = wrapped2[1]
bad = ("v2",raw[:-1]+bytes([raw[-1]^1包まれた鍵を1ビット変更する]))
print(vault.unwrap(bad)鍵の整合性を検査する)integrity_failedです。AES Key Wrapは鍵データの整合性も確認するため、変化した鍵をそのまま返しません。
6KEKを失った場合を調べる
復旧用コピーを別に保持したうえで、vaultからv2の鍵材料を取り除きます。
backup = vault.keys["v2"]
del vault.keys["v2"]v2の鍵材料を取り除く
print(vault.unwrap(wrapped2)鍵がない状態で取り出しを試す)
print(len(wrapped2[1]),len(box))暗号文と包まれたDEKがあってもkey_missingです。鍵材料を失うと、保存済みデータの可用性にも影響します。
7復旧用コピーで鍵を戻す
別に保管したv2の鍵を戻し、保存済みデータを復号します。
vault.keys["v2"] = backup復旧用コピーから鍵材料を戻す
print(AESGCM(vault.unwrap(wrapped2)).decrypt(nonce,box,aad))
print(vault.unwrap(wrapped1))v2のデータは戻り、v1は無効のままです。バックアップの存在は可用性を支える一方、読取り権限や破棄対象にも含める必要があります。
まとめ
本文用DEKと、DEKを保護するKEKを分けます。
KEKの更新とDEKの更新は異なる操作です。
無効化・配布済み鍵・バックアップを含めて鍵の寿命を管理します。
操作履歴と解説を振り返れます。
Python 3.15 / cryptography 50.0.2 / 公開試料と固定時刻
from sc11 import *
vault = Vault()
dek = bytes.fromhex("00112233445566778899aabbccddeeff")
nonce = bytes(12)
aad = b"document-7"
box = AESGCM(dek).encrypt(nonce,b"confidential-record",aad)
print(len(box),box.hex())35 7e9649512c77dabaeb2a0abb124037e6b59d2b94dd0dfa02deece12bd37d58ce404845
wrapped1 = vault.wrap("v1",dek)
print(wrapped1[0],len(wrapped1[1]),wrapped1[1].hex())
print(vault.unwrap(wrapped1) == dek)v1 24 1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5 True
old_box = box
wrapped2 = vault.wrap("v2",vault.unwrap(wrapped1))
print(wrapped2[1].hex(),wrapped2[1] == wrapped1[1])
print(box == old_box)
print(AESGCM(vault.unwrap(wrapped2)).decrypt(nonce,box,aad))39fb6b2b485c1e58c5be48f619c4a3841a2b711a37e13d94 False True b'confidential-record'
vault.enabled.remove("v1")
print(vault.unwrap(wrapped1))
print(vault.unwrap(wrapped2) == dek)
print(AESGCM(dek).decrypt(nonce,box,aad))disabled True b'confidential-record'
raw = wrapped2[1]
bad = ("v2",raw[:-1]+bytes([raw[-1]^1]))
print(vault.unwrap(bad))integrity_failed
backup = vault.keys["v2"]
del vault.keys["v2"]
print(vault.unwrap(wrapped2))
print(len(wrapped2[1]),len(box))key_missing 24 35
vault.keys["v2"] = backup
print(AESGCM(vault.unwrap(wrapped2)).decrypt(nonce,box,aad))
print(vault.unwrap(wrapped1))b'confidential-record' disabled
演習完了