📑 本页目录(点开跳转)
11 · MAC 与认证加密
⏱ 28 分钟 | ⭐⭐ "加密了"到"安全了"的最后一步
🎯 一句话
第 1 章说过:加密不提供完整性。 这一章补上那一半 —— 而且组合的顺序会决定成败。
✍️ 一、MAC:带密钥的"防伪标签"
MAC = Message Authentication Code
发送方:tag = MAC(K, message),把 (message, tag) 一起发
接收方:重算 MAC(K, message),和收到的 tag 比对
⭐ 没有 K 的人无法伪造出合法的 tag
安全定义:UF-CMA(选择消息攻击下的不可伪造性)
⭐ 攻击者赢,当且仅当 m* 是【没问过的】消息,且 t* 合法。
💡 人话:就算让你看无数条"消息+标签",你也造不出一条新消息的合法标签。
⚠️ 注意 MAC 和数字签名的区别: MAC 是对称的 —— 验证者也持有 K,所以他也能伪造 ⟹ 不提供不可否认性。 需要不可否认就得用数字签名(第 17 章)。
🔧 二、两种主流 MAC
① HMAC —— 用哈希造 MAC
$$\text{HMAC}_K(M) = H\big((K\oplus \text{opad}) \,\|\, H((K \oplus \text{ipad}) \,\|\, M)\big)$$
ipad = 0x36 重复,opad = 0x5C 重复
⭐ 为什么是【双层嵌套】而不是简单的 H(K‖M):
→ 内层 H 的输出是【固定长度】的
→ 外层再哈希一次,攻击者拿到的输出不能被"接着往下算"
→ 长度扩展攻击失效 ✅(第 10 章)
🔑 HMAC 的一个漂亮性质: 即使底层哈希的抗碰撞性被打破,HMAC 仍可能是安全的。 实际上 HMAC-MD5 至今没有实用的伪造攻击,尽管 MD5 的碰撞几秒就能造。 (但仍然不要用 —— 没有理由冒这个险。)
② Poly1305 / GMAC —— 基于多项式求值
把消息看成多项式的系数,在一个有限域上求值:
tag = (m₁·rⁿ + m₂·rⁿ⁻¹ + ... + mₙ·r) + s mod p
⭐ 极快(比哈希快很多),适合高吞吐
⚠️ 但它是【一次性 MAC】:
同一个 (r, s) 用两次 → 攻击者能解出 r → 完全崩溃 💀
→ 所以必须每条消息用不同的 nonce 派生新的 (r, s)
💥 这就是 AES-GCM 的 nonce 重用为什么是灾难: 不只是泄露明文异或(流密码那部分), 还会让攻击者恢复出 GMAC 的认证密钥 → 之后能伪造任意消息 ⭐ 这比 CTR 模式单独的 nonce 重用严重得多。
⚖️ 三、组合顺序:三选一,只有一个是对的
❌ Encrypt-and-MAC(分别做)
C = Enc(K₁, M)
T = MAC(K₂, M) ← 对【明文】做 MAC
发送 (C, T)
💥 问题:MAC 不保证隐藏明文信息!
MAC 是确定性的 → 相同明文产生相同 tag
→ 攻击者能判断两条密文是否对应同一明文 💀
→ 违反 IND-CPA
用在哪:SSH(历史遗留)
❌ MAC-then-Encrypt(先认证再加密)
T = MAC(K₂, M)
C = Enc(K₁, M ‖ T) ← 把 tag 也加密了
发送 C
💥 问题:接收方【必须先解密才能验证】
→ 攻击者能让你解密任意垃圾数据
→ padding oracle 攻击的温床 ⭐(第 7 章)
用在哪:TLS 1.0/1.1 —— 这正是 BEAST、Lucky13 等一系列攻击的根源。
✅ Encrypt-then-MAC(先加密再认证)⭐
C = Enc(K₁, M)
T = MAC(K₂, C) ← 对【密文】做 MAC
发送 (C, T)
接收方:
① 先验证 T ← 用恒定时间比较
② 验证失败 → 【直接拒绝,根本不解密】⭐
③ 验证通过 → 才解密
✅ 攻击者的解密预言机变成一个只会说"无效"的东西
✅ 可证明:IND-CPA 的加密 + UF-CMA 的 MAC ⟹ IND-CCA ⭐
🔑 记住这个顺序,以及两条配套规则: ① 加密密钥和 MAC 密钥必须不同(从主密钥用 HKDF 派生出两个) ② MAC 必须覆盖所有影响解密的东西 —— 包括 IV/nonce、算法标识、AAD
💥 漏掉 IV 的后果:攻击者篡改 IV,CBC 的第一个明文块就被可控地改变, 而 MAC 检查照样通过。
📦 四、AEAD:直接用打包好的
别自己拼装 —— 用现成的认证加密:
| 算法 | 内部结构 | 什么时候用 |
|---|---|---|
| AES-GCM | CTR + GMAC | 有 AES-NI 硬件指令时最快 ⭐ |
| ChaCha20-Poly1305 | ChaCha20 + Poly1305 | 无硬件加速时更快,移动端首选 |
| AES-GCM-SIV | 抗 nonce 误用 | 无法保证 nonce 唯一时 ⭐ |
| AES-CCM | CTR + CBC-MAC | 嵌入式(代码体积小) |
| XChaCha20-Poly1305 | 192-bit nonce | 想随机生成 nonce 又不想操心碰撞 ⭐ |
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
import os
key = ChaCha20Poly1305.generate_key()
aead = ChaCha20Poly1305(key)
nonce = os.urandom(12) # ⚠️ 每条消息必须不同
ct = aead.encrypt(nonce, b"transfer 100", b"from=alice;to=bob")
# ↑ AAD:明文可见但被认证
# 篡改密文、AAD 或 nonce 中的任何一个都会抛 InvalidTag ⭐
pt = aead.decrypt(nonce, ct, b"from=alice;to=bob")
AAD(附加认证数据)该放什么:
✅ 需要明文可见、但不能被篡改的字段:
消息序号、路由信息、协议版本、时间戳、发送方 ID
💡 一个实用技巧:把【上下文】放进 AAD,能防重放和串用
比如把 "session=abc;seq=42" 放 AAD
→ 攻击者把消息重放到另一个会话就会验证失败 ⭐
⚠️ 五、AEAD 也解决不了的:重放攻击
攻击者【原样重发】一条合法消息 —— 密文没被改,MAC 也对
⭐ AEAD 无法阻止这个,因为它只保证"这条消息是真的",
不保证"这条消息是新的"
✅ 解法(必须在协议层做):
├─ 消息序号(放 AAD 里,接收方检查单调递增)⭐
├─ 时间戳 + 有效期窗口
└─ 挑战-响应(服务器发随机数,客户端必须带回来)
💡 这是一个常被忽略的边界: 密码学原语保证"真实性",协议设计保证"新鲜性"。 两件事。
🔗 和站内其他章的关系
| 相关的地方 | 和这一章的关系 |
|---|---|
| 第 1 章 加密不提供完整性 | 本章就是补那一半 ⭐ |
| 第 7 章 padding oracle | MAC-then-Encrypt 的后果 |
| 第 8 章 IND-CCA | Encrypt-then-MAC 达到它 ⭐ |
| 第 10 章 长度扩展 | HMAC 的双层结构就是为它设计的 |
| 第 5 章 nonce 铁律 | GCM 下后果更严重(认证密钥泄露) |
| 第 17 章 数字签名 | MAC 的非对称版,提供不可否认性 |
| Claude 资料库 · 触达生产系统 | 本章末尾那个 AEAD 也挡不住的重放攻击,工程解法就在那里:幂等键 + 时间窗——它属于协议层,不属于密码层 ⭐ |
| 模型上线之后 · 版本回溯与可复现 | ⚠️ 「数据快照带哈希」是无密钥的完整性:只防手滑,不防对手——有对手时哈希会被连数据一起改掉,必须换成 MAC 或签名 |
✅ 检查点
- MAC 的安全定义 UF-CMA 是什么?用人话说一遍。
- MAC 和数字签名的关键区别是什么?
- HMAC 为什么要用双层嵌套结构?
- Poly1305 为什么是"一次性 MAC"?违反会怎样?
- 为什么 AES-GCM 的 nonce 重用比 CTR 的更严重?
- 三种组合顺序各是什么?前两种分别错在哪?
- Encrypt-then-MAC 的两条配套规则是什么?漏掉 IV 会怎样?
- AAD 该放什么?举一个防重放/串用的技巧。
- AEAD 能防重放攻击吗?该怎么解决?
👀 答案
- 攻击者能任意查询"给我这条消息的 tag",仍然无法为一条没问过的消息造出合法 tag。人话:就算让你看无数条"消息+标签",你也造不出一条新消息的合法标签。
- MAC 是对称的——验证者也持有密钥,所以他也能伪造 ⟹ 不提供不可否认性。数字签名是非对称的,只有私钥持有者能签。
- 因为内层 H 的输出是固定长度的,外层再哈希一次让攻击者拿到的输出不能被接着往下算 → 长度扩展攻击失效。
- 因为它是多项式求值,同一个 (r,s) 用两次,攻击者能解出 r,之后能伪造任意消息。所以必须每条消息用不同 nonce 派生新的 (r,s)。
- 因为 CTR 的 nonce 重用只泄露明文异或;GCM 还会让攻击者恢复出 GMAC 的认证密钥,之后能伪造任意消息——从"泄露"升级成"完全失控"。
- ①Encrypt-and-MAC:对明文做 MAC,MAC 是确定性的 → 相同明文相同 tag → 能判断两条密文是否对应同一明文,违反 IND-CPA ②MAC-then-Encrypt:接收方必须先解密才能验证 → padding oracle 的温床(TLS 1.0/1.1 的 BEAST、Lucky13 根源)③✅Encrypt-then-MAC。
- ①加密密钥和 MAC 密钥必须不同(用 HKDF 从主密钥派生两个)②MAC 必须覆盖所有影响解密的东西,包括 IV/nonce、算法标识、AAD。漏掉 IV:攻击者篡改 IV 就能可控地改变 CBC 第一个明文块,而 MAC 照样通过。
- 放需要明文可见但不能被篡改的字段:消息序号、路由信息、协议版本、时间戳、发送方 ID。技巧:把上下文(如 "session=abc;seq=42")放进 AAD,攻击者重放到另一个会话就会验证失败。
- 不能。AEAD 只保证"这条消息是真的",不保证"是新的"。解法必须在协议层:消息序号(放 AAD,检查单调递增)、时间戳+窗口、挑战-响应。密码学原语保证真实性,协议设计保证新鲜性。
🛑 可以停在这里
⚡ 走神救援
MAC = 带密钥的防伪标签,安全定义 UF-CMA:看无数条"消息+标签"也造不出新消息的合法标签。⚠️MAC 是对称的,验证者也能伪造 ⟹ 不提供不可否认性(那要用数字签名)。HMAC = H((K⊕opad)‖H((K⊕ipad)‖M)),⭐双层嵌套是为了让内层输出固定长度、外层再哈希一次,使长度扩展失效;漂亮性质:HMAC-MD5 至今没有实用伪造攻击(但仍别用)。Poly1305/GMAC 基于多项式求值,极快但是一次性 MAC——⚠️同一个 (r,s) 用两次攻击者能解出 r,所以💥AES-GCM 的 nonce 重用比 CTR 严重得多:不只泄露明文异或,还会泄露 GMAC 认证密钥 → 之后能伪造任意消息。⭐三种组合顺序只有一个对:❌Encrypt-and-MAC(对明文做MAC,确定性 → 相同明文相同tag → 违反IND-CPA,SSH用的)❌MAC-then-Encrypt(必须先解密才能验证 → padding oracle 温床,TLS1.0/1.1 的 BEAST/Lucky13 根源)✅⭐Encrypt-then-MAC(对密文做MAC,验证不过就根本不解密;可证明 IND-CPA加密 + UF-CMA的MAC ⟹ IND-CCA)。两条配套规则:加密密钥和MAC密钥必须不同(HKDF派生)、MAC必须覆盖 IV/nonce/算法标识/AAD(漏掉IV则攻击者改IV就能改CBC第一块而MAC照过)。✅实际直接用 AEAD:AES-GCM、ChaCha20-Poly1305、AES-GCM-SIV(抗nonce误用)、XChaCha20(192bit nonce 可随机生成)。AAD 放需要明文可见但不可篡改的字段,⭐技巧:把 session/seq 放 AAD 能防重放和串用。⚠️AEAD 防不了重放——它只保证"真"不保证"新",⭐新鲜性必须在协议层做(序号单调递增、时间戳窗口、挑战-响应)。
下一节 👉 12-Merkle树.md