密码学 36:密钥如何生成、保存、轮换与撤销——KMS、信封加密与恢复
服务将一笔合成订单加密进数据库,后来轮换了保管密钥,旧订单却都打不开。另一服务宣称“已经撤销密钥”,此前被复制出去的旧明文却依然能读。这两种故障都说明密钥生命周期不只是调用一次加密函数:必须明确谁持有哪个密钥、哪里记录它的版本、轮换有没有重包裹历史密钥、灾难后拿什么恢复,以及删除权限不能追回已泄漏的材料。
从数据密钥到封装密钥
信封加密通常用随机数据密钥 DEK 保护内容,再用受管封装密钥 KEK 保护 DEK。数据密文、两层使用的算法和 nonce、KEK 标识、必要的关联数据必须作为一个可解析的记录保存:后来服务才能知道找谁解开、该在哪些订单上下文使用。记录标识是选择钥匙的索引,不是钥匙本身;一个 “kid=old” 字符串不应让应用直接相信该密文的来源或拿另一个 KEK 强行解开。
1 | |
这样轮换 KEK 时可以只重包裹 DEK,而不必重新加密大批数据。要是要换已泄露的 DEK,则重包裹旧 DEK 没用:复制了旧 DEK 与旧数据密文的人照样能解;需要生成新 DEK、重加密内容、评估哪些旧备份依旧暴露。对于 AEAD,nonce 的唯一性约束是针对同一把键:每次数据加密、每次 KEK 包裹各自使用合适随机 nonce,不能认为换了 key ID 字符串就自动换了加密密钥。AAD 将记录的订单 ID/key ID 纳入认证而不加密,撤销时仍需做权限和存量数据审计。
真 AEAD 的最小轮换实验
examples/cryptography/36_envelope_rotation.py 用成熟库的 AES-GCM,三个临时随机 256 位密钥分别当旧 KEK、新 KEK 和 DEK。用 DEK 加密合成订单,旧 KEK 包裹 DEK;解包后新 KEK 再包裹同一个 DEK,去掉旧 KEK 后通过新封装读出原订单。负例更换数据 AAD 为别的订单或把旧封装的 key ID 改成新 ID,但保留原 tag:实际 InvalidTag 说明不能替换上下文而让它“看起来仍有效”。
先按仓库内 examples/cryptography/README.md 完成环境准备;以下命令从仓库根目录执行。
1 | |
脚本不打印临时密钥,也不连接实际 KMS;进程结束即丢弃内存材料。若现实里轮换途中旧 KEK 已被销毁,而部分数据仍只存旧封装,旧订单无论 AES 多安全都无法解开;新密钥不会从密文中恢复旧密钥。恢复计划需要版本化包装记录、受控备份和定期恢复演练,同时限制人员和机器的解封权限。KMS/HSM 的真实密钥生成、审计与访问控制本实验 NOT_RUN,不能把本地 AESGCM 成功叫作企业 KMS 验收。
不同层的“撤销”尤其不能混同:从 KMS 取消应用权限,是未来解密操作的控制,不会抹掉已拿到的 DEK 或明文;TLS 证书吊销要看客户端实际吊销检查策略,网站更换证书不影响钱包已经签好的交易;JWT bearer token 撤销需要服务端策略或状态;链上权限变更要看链上规则及其生效历史。把所有操作笼统称为“换密钥完成安全修复”,会漏掉对已泄漏材料的处理。
练习及答案
画图题: 在上图标出数据存储员、KMS 操作者和解密服务各能看到哪些字节。已泄漏的是 KEK-old 而非 DEK 本身:只重包裹成 KEK-new,就足以让复制了旧包裹与数据的人不能读旧内容了吗?
答案: 数据仓库持有密文、nonce、tag、封装及 key ID;解密服务在被允许时可拿到 DEK/明文;KMS 操作者须按能力与权限模型决定能否导出 KEK(真实 HSM 往往不允许导出)。若攻击者已复制旧 KEK、旧 DEK 封装和旧数据,仅重包裹仓库当前记录不会让他手里的旧副本失效;须评估旧数据泄漏并考虑换 DEK 和重加密等措施。
实验变更题: 将 wrap(new_kek, "2026-test-new", unwrapped) 误写为仍用 old_kek 包裹但标识为新 ID,拿新 KEK 解包会怎样?去掉关联的订单 ID 检查又丢掉了什么?
答案: 新 KEK 解旧 KEK 产生的封装时 tag 验证失败;名称不能替代密钥的实际内容。若同一 DEK 下不把订单 ID 等必要上下文放入 AAD 并在接收时核对,应用可能把一条认证有效的数据密文移植给错误的订单对象;加密仍可能成功,却失去目标业务绑定。
资料与衔接
- NIST SP 800-57 Part 1 Rev.5 · NIST SP 800-38D · NIST SP 800-38F。本篇未实现规范的 AES-KW 接口。
- 07:AEAD 保护内容与上下文 · 31:HD 钱包恢复。





