跳到内容

第 8 卷 / 第 1 章 / 第 1 课

1.1 对称密码工程:AEAD、Nonce 与密钥生命周期 ​

先明确攻击者能接触哪些数据、控制哪些组件,才能判断密码学提供的保证。算法选型之前,需要知道要保护什么、密钥在哪里,以及密钥泄露会影响多少数据。

先分清安全目标 ​

目标要阻止什么常见原语
机密性未授权者读到明文加密
完整性数据被悄悄修改MAC、AEAD、数字签名
身份认证消息或端点冒充MAC、签名、认证协议
不可否认性证据为特定签名的来源提供可供第三方核验的证据数字签名、身份绑定与审计记录

签名证据仍依赖私钥保管、身份绑定和事件时间;验签通过并不自动证明实际操作者是谁。

加密本身未必提供完整性。若攻击者能修改密文,而应用在认证前就处理解密结果,可能出现 padding oracle、格式探测或业务字段篡改。现代应用应优先使用经过标准化的 AEAD,而不是自行组合“加密 + 哈希”。

AEAD 同时保护密文与上下文 ​

Authenticated Encryption with Associated Data 接收:

text
ciphertext, tag = Seal(key, nonce, plaintext, aad)
plaintext       = Open(key, nonce, ciphertext, aad, tag)

plaintext 被加密;aad 不加密,但参与认证。例如消息版本、租户 ID、记录主键和协议类型可以放入 AAD,使密文不能在不同的预期上下文中继续通过校验。解密方必须从可信的请求或资源定位确定预期 AAD;若把攻击者一起搬来的租户字段也原样接受,这份绑定就没有检查到目标租户。AEAD 本身也不拒绝原封不动的重放,仍需协议维护序号、期限或去重状态。

常用选择包括 AES-GCM 与 ChaCha20-Poly1305。不要根据“哪个名字听起来更强”自行选择;优先使用平台高层 API、协议规定的套件和受维护库。算法安全还取决于 nonce、密钥、错误处理和实现侧信道。

Nonce 不需要保密,但必须满足算法约束 ​

Nonce 的核心要求通常是:同一密钥下不能重复。以 GCM 为例,nonce 重用可能泄露明文关系并破坏认证,后果远比“随机性稍差”严重。

text
key K + nonce N1 → message A
key K + nonce N1 → message B   ← 危险:同一 key/nonce pair 重用

生成策略可以是足够大的随机 nonce,也可以是每密钥单调计数器;选择取决于协议。计数器需要跨崩溃持久化并防止多节点重复,随机方案需要计算碰撞预算。不能在不知道算法要求时用时间戳、短随机数或业务 ID 代替。

解密 API 必须先验证 tag,再向调用方释放明文。认证失败只返回统一错误,不能把“格式错、padding 错、tag 错”暴露成可探测分支。

运行一次篡改与上下文测试 ​

下面需要 Python 的 cryptography 包,使用其 AESGCM 接口。返回值已包含 16 字节 tag,毋须再单独拼一个哈希。示例每次新建密钥,只加密一条消息,验证密文或 AAD 改动会失败;它没有实现生产密钥存储或跨进程 nonce 分配。

python
import os
from cryptography.exceptions import InvalidTag
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

key = AESGCM.generate_key(bit_length=256)
aead = AESGCM(key)
nonce = os.urandom(12)
aad = b'{"tenant":"acme","record":42,"version":1}'
plaintext = b"invoice total: 125"
ciphertext = aead.encrypt(nonce, plaintext, aad)
assert aead.decrypt(nonce, ciphertext, aad) == plaintext
assert len(ciphertext) == len(plaintext) + 16

altered = ciphertext[:-1] + bytes([ciphertext[-1] ^ 1])
for candidate, context in (
    (altered, aad),
    (ciphertext, b'{"tenant":"other","record":42,"version":1}'),
):
    try:
        aead.decrypt(nonce, candidate, context)
    except InvalidTag:
        pass
    else:
        raise AssertionError("unauthenticated data was accepted")

把密钥访问与数据访问分开 ​

把加密密钥写在源码、镜像、同一数据库字段或普通环境配置里,相当于把钥匙贴在保险箱上。生产系统常用 envelope encryption:

text
KMS/HSM 中的 KEK
       │ 包装
       ▼
每批或每对象的随机 DEK ──AEAD──> 业务数据

Data Encryption Key(DEK)负责高频数据加密;Key Encryption Key(KEK)留在 KMS/HSM 中,只包装 DEK。这样可以限制主密钥暴露,并在重新包装 DEK 时避免重加密全部大对象。KMS 审计记录的是向它发起的密钥操作;应用缓存 DEK 后的每次本地解密不会自动成为一条 KMS 事件,仍需应用审计。

更换 KEK 不代表现有 DEK 已自动重新包装,需按 KMS 的接口完成迁移。若旧 KEK 或 DEK 已泄露,重新包装也不能收回攻击者已获得的密钥和旧密文;事件响应可能需要新 DEK 与数据重加密。

密文记录必须自描述但不泄密,通常保存:算法套件、格式版本、key ID、nonce、ciphertext 和 tag。不要只存一个裸字节串,几年后没人知道用哪个密钥和参数解开。

轮换不等于立刻删除旧密钥 ​

密钥生命周期至少包含生成、分发、激活、使用、轮换、撤销、归档和销毁。轮换时常采用:

  1. 新写入使用新 key version;
  2. 读取按记录中的 key ID 选择旧密钥;
  3. 后台逐步重加密或只在读取时迁移;
  4. 确认所有备份和延迟副本的恢复需求后,再停用旧密钥;
  5. 最后按保留政策销毁。

若删除旧密钥早于备份到期,备份仍存在却无法恢复。若旧密钥永不退役,轮换又没有降低长期风险。

随机数必须来自 CSPRNG ​

密钥、salt、session ID、恢复码,以及选用随机策略的 nonce,应使用操作系统或密码库提供的密码学安全随机源。按协议分配的唯一计数器 nonce 不要求随机,不能把“需要唯一”混成“必须不可预测”。普通伪随机数、时间戳、进程 ID 和自制“混淆算法”都不适合生成秘密。

不要自己实现 AES、ChaCha、GCM 或随机数生成器。即使数学公式正确,常数时间、错误返回、内存清理、nonce 管理和硬件加速路径也可能引入漏洞。

设计检查 ​

  • 威胁模型要求的是机密性、完整性、认证,还是三者都有?
  • 是否使用高层 AEAD API,而非 ECB/CBC 或自制组合?
  • 同一密钥下 nonce 的唯一性如何证明并监控?
  • AAD 是否绑定租户、版本和对象身份,防止密文搬运?
  • 密钥是否与密文隔离,并受最小权限与审计保护?
  • 轮换、备份恢复和旧密钥销毁是否一起演练?
  • 认证失败是否在处理任何明文前终止?

下一课处理经常被混用的哈希、MAC、KDF 与密码存储。

参考资料 ​

Built with VitePress | Software Systems Atlas