本节摘要:加密解决"窃听",消息认证码(MAC)解决"篡改与伪造"——它是对称世界里完整性与认证性的合订本,HMAC 是其中的通用构造。本节拆解 HMAC 的结构、加密与认证的次序之争,并把旧素材中"对称密钥管理"并入本节:从生成、分发、存储、轮换到销毁,密钥的一生决定了整个体制的实际安全上限。
一个常见错觉:密文被改一个比特,解密出来的就是乱码,所以"加密自带完整性"。现实不是这样。CBC 模式翻转密文的某一位,明文对应块会变成不可预测的乱码,但攻击者常常根本不需要"有意义的明文"——他改的是某一位的含义:转账报文里"授权"字段的位置、金额高位字节、协议里的标志位。多数分组模式不提供任何篡改检测,接收方若不校验就直接执行,被改的报文照单全收。
消息认证码(MAC)补上这块板:发送方用共享密钥对消息计算出一个短标签 tag,随密文一同发出;接收方用同一密钥重算并比对。没有密钥,攻击者既不能为篡改后的消息配出合法标签,也不能凭空伪造"来自合法方"的消息——篡改与伪造两条路同时封死。注意它与散列的分工:无密钥的哈希(第 5 章)只能挡"意外损坏",挡不住会主动重算哈希的攻击者;MAC 多出来的那个密钥,才是"认证"的来源。
朴素做法大多是错的。把"明文的哈希"附在密文后?攻击者篡改密文后可以重新计算无密钥哈希,形同虚设。用"密钥拼消息再哈希"?会撞上哈希函数的长度扩展攻击(MD5、SHA-1 这类 Merkle-Damgård 结构的软肋)。工业界认可的通用构造是 HMAC:
HMAC(K, m) = H((K⊕opad) ∥ H((K⊕ipad) ∥ m)) 其中 ipad=0x36×块长,opad=0x5c×块长
密钥先与常量异或、填充到哈希函数块长(SHA-256 为 64 字节),内外两层包裹消息。这个双层结构同时抵御长度扩展与密钥恢复,而且不要求底层哈希本身抗碰撞——即使 SHA-1 碰撞沦陷后,HMAC-SHA1 的认证安全性依然成立。标准库里的现成实现配合"恒定时间比较"就是工程正解:
import hmac, hashlib key = b"shared-secret-2024" # 双方共享的认证密钥 msg = b"TRANSFER 10000 TO ACCOUNT A" tag = hmac.new(key, msg, hashlib.sha256).digest() print(tag.hex()[:32]) # 32 字节标签的前 16 字节 # 验证:必须用恒定时间比较,防止逐字节试探的时序攻击 def verify(key, msg, tag): return hmac.compare_digest(hmac.new(key, msg, hashlib.sha256).digest(), tag) print(verify(key, msg, tag)) # True print(verify(key, msg.replace(b"10000", b"90000"), tag)) # False —— 篡改即暴露
密文与标签的组装次序在历史上引发过真实的流血事故。先加密后认证(Encrypt-then-MAC,EtM)被证明是结构上最优的:认证对象是密文,解密前先验签,伪造或篡改的密文根本不进解密器,填充预言机一类攻击无从触发。先认证后加密(MAC-then-Encrypt,MtE)是 SSL/TLS 1.2 之前的选择,认证的是明文,解密器要先把"可能被篡改过的"密文解完、再比对标签——解密失败与校验失败的时序差异泄露给攻击者,2013 年的 Lucky13 攻击正是靠统计填充错误耗时恢复明文,TLS 1.3 因此整体转向 AEAD。先认证后加密(AtE)是 SSH 的路线,安全性与实现复杂度的折中。
现代结论干脆利落:新协议一律用 AEAD(AES-GCM、ChaCha20-Poly1305),一次调用同时给出密文与标签,次序由构造内部解决,不再给实现者留犯错机会。3.3 节的 GCM 之争在此收束。
密钥管理是"算法之外"最容易出现全线崩溃的环节,值得按生命周期逐段过一遍。
💡 关键直觉:密钥是整个体制里唯一"必须烂在肚子里"的东西。算法公开不可怕(柯克霍夫原则),密钥管理失守才是不可逆的——算法失守有标准可换,密钥失守意味着历史密文全部重新估值。
对称密码的拼图至此完整。但"第一把钥匙怎么送到对方手里"的原始悖论依然悬置——下一章,两位加州的密码学家将用一场数学魔术把它终结。