ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Hash、MAC、HMAC 到底有什么区别?从支付验签踩坑到工程选型

Hash、MAC、HMAC 到底有什么区别?从支付验签踩坑到工程选型 1. 从一个真实踩坑案例说起为什么搞混这三个概念会出事刚入行那会儿接手过一个第三方支付回调的验签模块接口文档上写着“使用 HMAC-SHA256 对请求体签名”我当时脑子里第一反应是“HMAC 不就是带密钥的 Hash 嘛那我直接sha256(body key)不就行了”。结果联调阶段对方一直返回签名校验失败排查了整整一个下午最后翻 RFC 2104 才发现问题出在 HMAC 的构造根本不是简单拼接——它用了内外两层填充ipad 和 opad拼接顺序和密钥处理方式都有严格规定。那次之后我才真正把 Hash、MAC、HMAC 这三个东西的区别刻进脑子里。这三个概念在密码学里属于最基础的一层但恰恰因为基础很多人学的时候一带而过用的时候凭直觉最后在验签、防篡改、口令存储这些场景里反复翻车。这篇文章就是把我这些年在这三个概念上踩过的坑、做过的实验、总结出来的判断逻辑完整梳理一遍。不管你是刚接触密码学的学生还是正在做接口安全设计的后端开发或者是在 CTF 里被密码学题卡住的选手看完应该都能对这三个东西建立起清晰的边界感。核心关键词Hash、MAC、HMAC、密码学、对称加密会贯穿全文我会尽量用生活化的类比加上可直接复现的代码示例来讲避免纯理论堆砌。2. 三个概念的本质拆解它们各自解决什么问题2.1 Hash单向的“数字指纹”Hash哈希/杂凑函数的核心能力是把任意长度的输入压缩成固定长度的输出并且这个过程不可逆。你给它“hello”它给你一串 256 位的乱码你给它一部 10GB 的电影它还是给你一串 256 位的乱码。输出的长度只跟算法有关跟输入大小无关。它有三个必须同时满足的性质单向性从输出反推输入在计算上不可行。注意是“不可行”而不是“不可能”理论上穷举总能找到但代价大到没有实际意义。抗碰撞性找到两个不同输入产生相同输出在计算上不可行。这里分弱抗碰撞给定一个输入找不到另一个碰撞和强抗碰撞找不到任意一对碰撞。雪崩效应输入改一个比特输出大约一半的比特会翻转。这个性质保证了输出看起来完全随机。常见的 Hash 算法有 MD5128 位已不安全、SHA-1160 位已不安全、SHA-256、SHA-3、SM3国密256 位等。MD5 和 SHA-1 之所以被淘汰就是因为抗碰撞性被攻破了——2004 年王小云团队给出了 MD5 的高效碰撞方法2017 年 Google 的 SHAttered 项目实际构造出了 SHA-1 的碰撞样本。Hash 最典型的应用场景是完整性校验。比如你从网上下载一个系统镜像官网会附一个 SHA-256 值你下载完自己算一遍对比一致就说明文件没被篡改或损坏。系统自带的文件 hash 校验命令Windows 的certutil -hashfile、Linux 的sha256sum、macOS 的shasum -a 256就是干这个的。但这里有个关键问题裸 Hash 不能防主动篡改。因为攻击者改了文件之后可以顺手把官网上的 hash 值也改了如果他控制得了发布渠道或者更常见的场景是——攻击者拦截了你的下载请求把文件和对应的 hash 一起替换掉你对比下来还是“一致”的。这就是为什么需要 MAC。2.2 MAC带密钥的“防伪印章”MACMessage Authentication Code消息认证码在 Hash 的基础上加了一个密钥。它的输出叫 tag标签验证方必须持有相同的密钥才能重新计算出 tag 并比对。MAC 解决的是 Hash 解决不了的问题认证。具体来说它同时保证了完整性消息被改了tag 就对不上。真实性只有持有密钥的一方才能生成正确的 tag所以能确认消息来源。用一个类比Hash 像是给文件拍了一张指纹照片谁都能拍谁都能对比MAC 像是给文件盖了一个只有你和对方知道的暗号印章别人仿不出来。MAC 的实现方式有很多种不局限于 Hash。理论上任何带密钥的函数都能构造 MAC比如基于分组密码的 CBC-MAC、CMAC基于 Hash 的 HMAC甚至基于通用哈希函数的 UMAC、Poly1305。所以MAC 是一个功能概念HMAC 是它的一种具体实现。这个层级关系一定要理清楚很多人把 MAC 和 HMAC 当成两个并列的东西这是错的。2.3 HMAC用 Hash 构造 MAC 的标准方法HMACHash-based Message Authentication Code是 RFC 2104 定义的标准构造它把任意一个 Hash 函数“升级”成带密钥的 MAC。公式是这样的HMAC(K, M) H( (K ⊕ opad) || H( (K ⊕ ipad) || M ) )其中H是底层 Hash 函数如 SHA-256K是处理后的密钥如果密钥比分组长度长先 Hash 一次如果短补零到分组长度ipad是 0x36 重复分组长度次opad是 0x5c 重复分组长度次||表示拼接⊕表示异或为什么要搞这么复杂直接用H(K || M)不行吗不行这里有个著名的长度扩展攻击Length Extension Attack。对于 MD5、SHA-1、SHA-256 这类 Merkle–Damgård 结构的 Hash如果攻击者知道H(K || M)的值和K的长度他可以在不知道K的情况下计算出H(K || M || padding || M)。这意味着H(K || M)这种构造是不安全的 MAC。HMAC 的内外双层结构正是为了阻断长度扩展攻击。外层 Hash 的输入包含了内层 Hash 的完整输出攻击者没法在不知道密钥的情况下继续扩展。我当年那个支付接口的坑就是因为用了sha256(body key)这种拼接方式虽然当时没被攻击但从密码学角度它是有缺陷的。正确做法就是用 HMAC。3. 三者关系与核心差异对照把上面三个概念放在一起可以用一张表把关键差异钉死维度HashMACHMAC是否带密钥否是是能否防篡改只能防意外损坏能防主动篡改能防主动篡改能否认证来源否能能输出长度固定跟算法有关固定跟底层算法有关固定跟底层 Hash 有关典型算法MD5、SHA-256、SM3CMAC、Poly1305、HMACHMAC-SHA256、HMAC-SM3主要用途完整性校验、索引、口令存储加盐消息认证、接口验签消息认证、接口验签、会话令牌速度最快较快略慢于裸 Hash约两倍 Hash 开销层级关系基础原语功能类别MAC 的一种实现再强调一遍层级关系Hash 是基础原语MAC 是一类功能HMAC 是 MAC 的一种具体构造。你可以说“HMAC 是一种 MAC”但不能说“MAC 就是 HMAC”。从对称加密的角度看MAC 和 HMAC 都属于对称密码体系的范畴——验证方和生成方共享同一个密钥。这跟非对称签名如 SM2、RSA 签名形成对比签名用私钥签、公钥验不需要共享密钥但速度慢得多。实际工程里接口验签用 HMAC 的远比用非对称签名的多就是因为对称密钥运算快、实现简单。4. 实操从零实现并验证三者的行为差异光看公式容易飘直接上手跑一遍最实在。下面用 Python 演示环境只需要标准库不需要额外装包。4.1 裸 Hash 的完整性校验演示import hashlib def file_hash(path, algosha256): h hashlib.new(algo) with open(path, rb) as f: for chunk in iter(lambda: f.read(8192), b): h.update(chunk) return h.hexdigest() # 计算一个文件的 SHA-256 print(file_hash(test.bin))这里有个实操细节大文件一定要分块读取。我见过有人直接f.read()把几个 GB 的文件全读进内存机器直接卡死。分块大小 8192 字节是个经验值太小了系统调用频繁太大了内存占用高8KB 到 64KB 都合理。验证的时候把算出来的值和官方公布的值逐字符对比。注意有些平台公布的是大写有些是小写对比前统一转小写别因为这个低级问题误判。4.2 裸 Hash 被篡改的场景复现msg btransfer100toalice h1 hashlib.sha256(msg).hexdigest() # 攻击者篡改消息 tampered btransfer9999toalice h2 hashlib.sha256(tampered).hexdigest() print(h1 h2) # Falsehash 变了看起来 Hash 能发现篡改但问题在于如果攻击者能同时改消息和 hash 值比如他控制了传输通道接收方拿到的就是一对“自洽”的篡改数据。这就是裸 Hash 不能做认证的根本原因。4.3 HMAC 的正确实现与验证import hmac import hashlib key bmy_secret_key_2024 msg btransfer100toalice # 生成 HMAC tag hmac.new(key, msg, hashlib.sha256).hexdigest() print(tag:, tag) # 验证方重新计算并比对 def verify(key, msg, received_tag): expected hmac.new(key, msg, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, received_tag) print(verify(key, msg, tag)) # True print(verify(key, btransfer9999toalice, tag)) # False注意这里用了hmac.compare_digest而不是。这是一个非常重要的实操细节比较 MAC/tag 时必须用恒定时间比较函数。普通的在发现第一个不匹配的字符时就返回攻击者可以通过测量响应时间逐字节猜出正确的 tag这叫时序攻击Timing Attack。compare_digest保证无论哪里不匹配比较耗时都一样。这个坑我在做 API 网关的时候踩过当时用比较签名被安全团队扫出来提了高危。后来全改成恒定时间比较才过审。4.4 亲手复现长度扩展攻击为了让你直观理解为什么H(K || M)不安全我用一个简化版演示长度扩展的原理。完整的攻击需要能控制 Hash 的内部状态这里用hashpumpy或者手写 MD5 状态恢复来演示。核心思路是# 假设服务端用 H(key || message) 做 MAC # 攻击者知道 message 和对应的 tag也知道 key 的长度 # 攻击者可以构造 message || padding || evil # 并计算出对应的新 tag而无需知道 key # 伪代码示意 original_tag md5(key message) # 攻击者已知 # 攻击者把 md5 的内部状态设置为 original_tag 对应的状态 # 然后继续 update(evil)得到新 tag # 新 tag 对 (message || padding || evil) 是有效的实际攻击用hashpumpy库几行就能跑出来。我建议每个做接口安全的人都亲手跑一次跑完你就再也不会用H(K || M)了。HMAC 的双层结构正是为了防御这个攻击而设计的。5. 工程实践中的选型与避坑指南5.1 什么场景该用哪个选型其实就三条判断线第一条线需不需要密钥不需要密钥、只做完整性校验的用裸 Hash。比如文件下载校验、Git 的对象寻址、布隆过滤器的位计算。第二条线需不需要认证来源需要确认消息是谁发的、有没有被中间人改过就必须用 MAC。接口验签、会话令牌、消息队列的消息认证都属于这类。第三条线用什么构造 MAC如果底层已经有成熟的 Hash 实现几乎所有语言都有直接用 HMAC别自己发明。如果底层是分组密码比如你已经在用 AES可以用 CMAC。如果追求极致性能比如网络协议里的每包认证可以用 Poly1305。具体到常见场景用户口令存储不要用裸 Hash也不要用 HMAC。用专门的密码哈希函数如 bcrypt、scrypt、Argon2。它们内置了盐和可调的计算成本专门抵抗暴力破解。我见过太多项目用md5(password)甚至sha256(password)存口令这是重灾区。接口签名HMAC-SHA256 是事实标准。微信支付、支付宝、AWS 的 API 签名都是这个路子。JWT 签名HS256 就是 HMAC-SHA256RS256 是 RSA 签名。对称场景用 HS256需要公钥分发的场景用 RS256。文件完整性SHA-256 或 SM3。MD5 和 SHA-1 只用于非安全场景比如缓存键、去重。国密合规场景Hash 用 SM3MAC 用 HMAC-SM3。SM3 的输出是 256 位结构和 SHA-256 类似但设计不同。5.2 密钥管理的几个硬规矩HMAC 的安全性完全依赖密钥。密钥泄露了HMAC 就退化成一个公开函数谁都能伪造 tag。几条硬规矩密钥长度至少等于 Hash 输出长度。HMAC-SHA256 的密钥至少 32 字节。短密钥会降低安全强度。密钥要随机生成用密码学安全的随机源Python 的secrets、Java 的SecureRandom不要用random或时间戳。不同用途用不同密钥。签名密钥和加密密钥必须分开不同服务的签名密钥也要分开。一个密钥泄露不应该影响其他服务。密钥要能轮换。设计时就要考虑密钥版本tag 里带上版本号或者验证时支持多密钥。5.3 常见错误清单错误做法问题正确做法H(K || M)做 MAC长度扩展攻击用 HMACH(M || K)做 MAC密钥可能被碰撞攻击恢复用 HMAC用比较 tag时序攻击用恒定时间比较MD5/SHA-1 做安全用途抗碰撞性已破用 SHA-256/SM3裸 Hash 存口令彩虹表、暴力破解用 bcrypt/Argon2密钥硬编码在代码里泄露风险用密钥管理服务或环境变量密钥复用一处泄露全盘皆输按用途隔离密钥6. 常见问题排查实录6.1 签名验证一直失败怎么定位这是最高频的问题。排查顺序建议这样确认编码一致。签名前把消息转成字节时双方用的字符编码必须一样。UTF-8 是默认但有些老系统用 GBK中文参数就会出问题。确认拼接顺序和分隔符。参数排序是按字典序还是按传入顺序分隔符是还是|空值参不参与签名这些细节文档里经常写得含糊联调时一定要跟对方确认。确认密钥没有多余空格。从配置文件读密钥时行尾的换行符、首尾空格经常被忽略导致密钥不一致。确认 Hash 算法一致。HMAC-SHA256 和 HMAC-SHA1 的输出完全不同别搞混。打印中间值对比。把待签名字符串和计算出的 tag 都打日志跟对方逐字符对比。这一步最土但最有效。6.2 为什么我的 HMAC 结果和在线工具对不上大概率是输入格式问题。在线工具通常要求你输入十六进制或者 Base64而你输入的是原始字符串。另外注意密钥的编码有些工具把密钥当十六进制解析有些当 ASCII 字符串。确认清楚再对比。6.3 长度扩展攻击在实际中真的会发生吗会。历史上 Flickr 的 API 签名、某些博客平台的 cookie 认证都被这个攻击打过。虽然现在主流框架都用了 HMAC但自研系统里H(K || M)的写法依然常见。如果你在代码审计时看到这种模式直接标高危。6.4 SM3 和 SHA-256 能互相替代吗功能上可以但合规场景不能。国密合规要求用 SM3普通场景用 SHA-256 就行。HMAC-SM3 的构造和 HMAC-SHA256 一样只是底层 Hash 换成 SM3。注意 SM3 的分组长度是 512 位和 SHA-256 一样所以 ipad/opad 的处理方式相同。6.5 性能敏感场景怎么优化HMAC 的开销大约是裸 Hash 的两倍因为要跑两次 Hash。如果 QPS 很高可以考虑用硬件加速现代 CPU 有 SHA 扩展指令AES-NI 对 CMAC 也有加速。对短消息Poly1305 比 HMAC 快很多适合网络协议。批量验证时并行计算。但绝大多数业务场景HMAC 的性能完全够用不要过早优化。7. 我个人的几条经验总结做了这么多年接口安全和密码学相关的开发关于这三个概念我最想分享的几条经验是第一永远不要自己发明 MAC 构造。HMAC 已经被密码学界分析了二十多年安全性有充分保障。你自己拼一个H(K || M || K)看起来对称很安全实际上可能有意想不到的弱点。用标准构造别秀操作。第二比较 tag 一定用恒定时间函数。这个坑太隐蔽了代码 review 时很容易漏掉。我现在养成的习惯是只要看到比较签名/tag 的代码第一反应就是看用的什么比较方式。第三密钥管理比算法选择更重要。HMAC-SHA256 用得好是铜墙铁壁密钥泄露了就是纸糊的。我见过太多项目算法选得很讲究结果密钥硬编码在 GitHub 公开仓库里。第四理解层级关系能帮你做对选型。Hash 是原语MAC 是功能HMAC 是实现。想清楚你要的是完整性还是认证选型就不会错。最后分享一个我常用的调试技巧写一个最小的 HMAC 验证脚本把密钥、消息、tag 都硬编码进去跑通了再往业务代码里集成。这样能把密码学问题和业务逻辑问题隔离开排查效率高很多。我当年那个支付接口的坑如果一开始就这么做能省下大半天时间。
返回列表