密码学 04:HMAC 为什么需要秘密
服务端收到一个订单和一个 SHA-256 值,重新计算后两者一致。第 00、03 篇已经指出问题:攻击者能改订单,也能随手再算一个公开摘要。于是有人在订单与服务端之间增加了双方事先知道、攻击者不知道的密钥,再问:能否仅凭一串公开的认证值发现攻击者改过订单?可以,但要记住一个代价:接收者既然拥有同一把密钥,也有能力给任意新订单生成同样有效的值。
本篇把这种机制称为消息认证码(MAC),选择标准化的 HMAC-SHA-256 做最小实验。过去的文章有时把 HMAC 口语化地称为“对称密钥签名”;为了避免与私钥签、公钥验的数字签名混淆,这里固定叫 MAC。谁握有秘密、谁能验证与谁能伪造,是这一区别的核心。
换掉可公开重算的东西
设合成订单的字节已明确为 b"order=demo-001;quantity=1"。服务端和调用方事先通过可信渠道共享一把独立的密钥,消息通过可能受攻击者操控的链路送达。发送者计算 HMAC,把订单与 tag 一起交给接收者;接收者用同一把密钥重新计算并按安全接口比较。
1 | |
没有这个共享秘密时,攻击者自己重算公开摘要便可掩盖改动。有了秘密,在 HMAC 的假设与密钥保管成立时,未持有密钥的人难以给新消息生成有效 tag;标签不负责隐藏消息内容,窃听者照样能读到这笔订单。系统想同时保密时,还要采用符合威胁模型的通道保护或认证加密;07 才谈 AEAD。
“把密钥和消息拼起来再 SHA-256 一次”并不是这里使用的 HMAC 规范,也可能引出意想不到的构造问题。本实验调用 Python 标准库 hmac.new(key, msg, hashlib.sha256),不手写密码算法。比较使用 hmac.compare_digest;这个函数是避免在标签比较处使用普通字符串比较的手段,但不能仅凭此函数就宣称整个应用恒时、安全或不存在侧信道。
接收者也可以产生认证值
这张图有一条常被略去的箭头:接收者也知道 K。想象调用方和服务方事后争论 quantity=9 是谁写的。仅给第三方展示 HMAC(K, b"...quantity=9"),无法区分是发送方生成的,还是服务方自己生成的;第三方如果不知 K 甚至不能独立验证。HMAC 很适合预先信任关系下的双边消息认证,却不能直接担当“所有节点都能公开验证、验证节点却无权生成新授权”的交易签名。
1 | |
第二段是之后公钥签名章节要满足的结构,不是本文程序实现。公钥从哪里来、如何与地址或服务身份绑定、签名到底签了哪些交易字段,分别在 12–14 与 26–28 核对。数字签名也不是万用的法律“不可否认”:秘密泄漏、代签授权、证据保存和身份制度仍会影响实际争议。
认证值不识别旧订单
攻击者如果只改 quantity=1 为 quantity=9,却保留旧 tag,服务端校验失败;用错密钥也失败。但如果从链路上记录一份原封不动的有效订单和 tag,再传一次,同样的 HMAC 计算仍会通过。要避免重复下单,还要在被认证的数据里绑定订单 ID、作用域或时限,并让接收者在可靠状态中检查是否已消费。仅添加随机数字段而服务端不保存或检查,不会自动产生防重放效果;只检查时间也挡不住窗口内再发。
TLS 通道可以让路径上的攻击者难以窃取或修改其中的记录,却不替业务服务分辨“用户主动再次下单”。如果订单服务经由 TLS 终止代理,代理掌握解密后的请求,后端也需要知道究竟信任代理到什么程度。给应用另加 HMAC 不是所有架构的必选项:先确定信任边界与端点,再决定是不是需要独立的请求认证。把 HMAC 密钥嵌入公开浏览器代码等于公布它,不能据此认证“特定浏览器”;密钥的发放方式决定能否把 tag 解释为谁的消息。
真实 HMAC 原语与失败对照
examples/cryptography/04_hmac.py 用 RFC 4231 第 4.2 节的公开测试向量核对库的输出:密钥为重复的 0b 字节,消息是 Hi There,预期 HMAC-SHA-256 为 b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7。这条测试向量的密钥是公开资料,不能当实际服务秘密。合成订单同样使用公开的演示密钥,不属于安全部署。
1 | |
2026-10-06 UTC,Python 3.12.3 上运行两条命令,退出码均为 0,4 项测试通过。真实标准库原语验证了测试向量,改单不换 tag 与换错密钥都返回 false。把服务端置于与客户端拥有同一密钥的位置,服务端为改单重新生成 tag,则返回 true:这个失败对照具体说明无法用共享密钥区分是哪一端产生了 tag。重复原单和旧 tag 的校验也返回 true,显示 HMAC 未记录消费状态。这里执行的是原语和模拟订单字节,真实 HTTP API 与密钥分发协议仍 NOT_RUN。
两道带答案的练习
推导题。 调用方 A、服务端 B、审计者 C 分别持有什么,C 能否只凭消息与 HMAC tag 判断是谁签的?如果 C 也获得 K,又改变了哪条信任边界?
可核对答案: A 与 B 都持有共享密钥 K,都可生成和校验 tag;C 原先没有 K,不能独立验证,更不能区分 A/B。C 一旦得到 K,可以验证,也可以给新消息生成有效 tag;把校验能力交给公众的同时也交出了伪造能力,与公钥验签结构不同。
变更题。 将 04_hmac.py 中 changed 改回与 order 完全一样,重跑代码。哪项失败对照会报错,真实服务能否靠保留原 tag 发现“又发了一次”?
可核对答案: 实验要求 changed_message_with_original_tag_valid 为 false 的断言将失败,因为没有修改任何输入字节;同一个有效旧 tag 仍可验证。要阻止重复效果,应在服务端绑定并原子消费可信的业务唯一值,而不是期待 HMAC 判断消息是不是以前见过。
资料与导航
- RFC 2104,HMAC:共享秘密的 MAC 构造,具体密钥与消息输入。
- RFC 4231,第 4.2 节:HMAC-SHA-256 的公开测试向量与预期输出。
- 01 窃听、篡改、冒充与重放 · 03 哈希能证明什么;05 正文存在后再添加链接。






