HMAC 是什么?API 签名从这里开始

· 约 3 分钟 #️⃣ Hash

你把 Webhook 接口暴露出去,怎么确认发过来的请求真的来自 GitHub 而不是攻击者伪造?答案是 HMAC 签名。HMAC 是 API 鉴权的基石,从 Webhook 验签到 JWT 到 AWS 请求都在用。

HMAC = 哈希 + 共享密钥

普通哈希 sha256(msg) 谁都能算——收到消息和哈希后没法判断发送者是谁。HMAC 要求双方共享一个 secret key

signature = HMAC-SHA256(key, message)

只有知道 key 的人才能算出正确 signature。收方用同一 key 对 message 重算,相等就说明来自可信方且中间没被篡改

GitHub Webhook 实例

GitHub 推送事件时会在 HTTP header 里带:

X-Hub-Signature-256: sha256=7d38cdd689735b008b3c702edd92eea23791c5f6

接收端验签:

const expected = 'sha256=' +
  crypto.createHmac('sha256', WEBHOOK_SECRET)
    .update(rawBody)
    .digest('hex');

if (!crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(received))) {
  return res.status(401).end();
}

两个容易踩的细节:

  1. update(rawBody) 必须用原始字节,不能 JSON.parse 后再 JSON.stringify——字段顺序、空格、数字精度可能变化,哈希就不一样了。Express 里要用 express.raw({type: 'application/json'}),别用默认的 express.json()
  2. timingSafeEqual,不要用 ===

为什么不能直接 SHA256(key + message)

看起来 sha256(key + message) 也能校验身份,但它有长度扩展攻击漏洞。

SHA-256 等 Merkle–Damgård 结构的哈希会把当前内部状态作为下一轮输入。攻击者已知 h = sha256(key + data)len(key + data) 后,不用知道 key 就能计算:

h' = sha256(key + data + padding + extra)

相当于白嫖一个合法签名。Flickr API 2009 年就被这样打过。

HMAC 通过双轮结构堵死这个漏洞

HMAC(key, msg) = H((key ⊕ opad) ‖ H((key ⊕ ipad) ‖ msg))

两个不同的常量 pad(ipad = 0x36…opad = 0x5C…)+ 两次哈希嵌套,攻击者无法从外层哈希反推内层状态。

SHA-3 和 BLAKE2/3 没有长度扩展漏洞,理论上可以直接 sha3_256(key + msg),但工程上仍推荐 HMAC——协议层一致性比”少一层包装”重要。

四个常见用途

1. Webhook 验签

GitHub、Stripe、Shopify、飞书、微信公众号、钉钉,全是 HMAC。每家 header 名字不同,逻辑一模一样。

2. API 请求签名(AWS SigV4、微信支付 v3)

每个请求拼一个规范化字符串再签:

signature = HMAC(secret,
  method + '\n' +
  path + '\n' +
  query + '\n' +
  timestamp + '\n' +
  body_sha256)

带在 header 里传。服务端同样拼 + 重算 + 对比。

3. JWT(HS256

HS256 的 JWT 就是 HMAC-SHA256。header 和 payload base64 拼接当 message:

signature = HMAC-SHA256(secret,
  base64url(header) + '.' + base64url(payload))

注意:JWT 是签名,不是加密——payload 任何人都能 base64 解出来。别把密码、手机号、身份证明文放 payload。

4. 令牌哈希存储

服务端把 API Token 写数据库前用 HMAC 处理,存的是 HMAC(server_key, token)。泄库时黑客拿到的是哈希,没有 server_key 无法反推回原 token。

典型坑清单

  • 密钥硬编码在前端:HMAC 的前提是 key 不外泄,放前端 = 废了,签名和身份证明等于作废
  • 拼 message 忘了分隔符HMAC(k, user + action)("ali", "ce_pay")("alice", "_pay") 算出同一签名。用不会出现在字段值里的分隔符(\n / |)或直接 JSON 规范化
  • 用普通 == 比签名:时序攻击漏洞,必须 timing-safe 对比
  • 没带时间戳:重放攻击,攻击者截一次包能无限复用
  • Base64 / Hex 混用:对方期望 hex、你发 base64,永远验不过。在协议里写死
  • 签名覆盖不全:只签 body 不签 path / query,攻击者改 URL 仍能通过
  • 用弱哈希:HMAC-MD5 / HMAC-SHA1 在实践中仍然安全(HMAC 不依赖底层哈希抗碰撞),但新项目没理由不用 SHA-256 起步

小结

  • 要签名用 HMAC,不要自己拼 hash(key + msg)
  • key ≥ 32 字节,来自密码学 RNG
  • 对比用 timingSafeEqual
  • 带 timestamp + nonce 防重放
  • 编码格式(hex / base64)在协议里明确约定

SHA-256 是原材料,HMAC 是把它变成可用签名的必要加工。API 鉴权必备。

❓ 常见问题

HMAC 和普通哈希加盐有什么区别?

形式上像(都是 `hash(key + data)`),但 HMAC 有防长度扩展攻击的设计。MD5 / SHA-1 / SHA-256 这类 Merkle–Damgård 结构的哈希在攻击者已知 `h = sha256(key + data)` 和 `len(data)` 时,不需要知道 key 就能构造出 `sha256(key + data + extra)` 的合法哈希。HMAC 用内外两轮哈希 + 两个固定 pad 堵上这个洞。所以签名一律用 HMAC,不要自己拼 key。

HMAC 的 key 要多长?

至少和哈希输出一样长。HMAC-SHA256 的 key 推荐 32 字节(256 位),太短抗暴力差;超过 block size(SHA-256 是 64 字节)会被先做一次哈希反而略慢。用 `crypto.randomBytes(32).toString('hex')` 生成,不要自己拍几位短字符串。

为什么比较签名不能用 `===`?

时序攻击——`===` 或 `strcmp` 遇到不相等就提前返回,攻击者能通过计时测出前 N 位是否正确,逐位猜出签名。要用 `crypto.timingSafeEqual`(Node)、`hmac.compare_digest`(Python)、`hash_equals`(PHP)强制全长度对比,每次耗时恒定。

HMAC 签名能防重放攻击吗?

单独不能。签名只证明"这个请求来自持有 key 的人",攻击者截获一个合法请求后可原样重发。防重放要在签名字段里加时间戳和随机 nonce,服务端拒绝超过 5 分钟的请求和重复 nonce。AWS SigV4、微信/支付宝 API 都是这个套路。

#️⃣ 打开 Hash MD5/SHA · HMAC · 文件校验

📖 同一工具的其他教程