一覧SC-11-2 · 鍵・秘密情報の寿命

エンベロープ暗号化と鍵の更新

本文と鍵のどちらを更新したか、旧データの復号に何が残っている必要があるかを説明します。

説明

包絡暗号では、本文を暗号化するデータ鍵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 / 公開試料と固定時刻

1
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
2
wrapped1 = vault.wrap("v1",dek) print(wrapped1[0],len(wrapped1[1]),wrapped1[1].hex()) print(vault.unwrap(wrapped1) == dek)
v1 24 1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5
True
3
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'
4
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'
5
raw = wrapped2[1] bad = ("v2",raw[:-1]+bytes([raw[-1]^1])) print(vault.unwrap(bad))
integrity_failed
6
backup = vault.keys["v2"] del vault.keys["v2"] print(vault.unwrap(wrapped2)) print(len(wrapped2[1]),len(box))
key_missing
24 35
7
vault.keys["v2"] = backup print(AESGCM(vault.unwrap(wrapped2)).decrypt(nonce,box,aad)) print(vault.unwrap(wrapped1))
b'confidential-record'
disabled

演習完了