dev

HMAC 签名校验失败排查:Webhook、API Header 和原始 Body 怎么对齐

用 HMAC Generator 排查 webhook 和 API 签名校验失败,逐步确认 secret、原始 payload 字节、canonical string、算法、hex/Base64 输出、时间戳窗口和 header 解析边界,避免随机试错。并说明格式化 JSON、重放保护和安全比较为什么会影响验证结果。

HMAC 校验失败的错误通常很短:signature mismatchinvalid webhook401 unauthorized。但真正原因可能在任何一层:raw body 被框架消费了、secret 拿错环境、timestamp 拼接顺序不对、输出编码不一致,或者 header 解析时多比较了 sha256= 前缀。

排查时可以用 HMAC Generator 复现单个样例,但前提是你拿到的输入、secret、算法和输出编码都与线上一致。否则工具算得再快,也只是在验证另一套条件。

第一步:拿到真正参与签名的输入

Webhook 最常见的坑是 JSON 解析。很多平台签的是原始请求 body,也就是网络上传来的字节;而应用代码拿到的是已经被框架解析成对象、再重新序列化的 JSON。字段顺序、空格、换行、转义方式一变,HMAC 就完全不同。

例如原始 body 是:

{"event":"paid","amount":100}

你的调试代码却签了格式化后的版本:

{
  "amount": 100,
  "event": "paid"
}

这两段 JSON 语义相同,但字节不同。正确做法是在 body parser 改写之前保存 raw body,或按平台 SDK 要求配置 raw buffer。

API 请求签名还可能不是只签 body。文档可能要求拼接:

METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + BODY

这种 canonical string 必须逐字符一致。路径是否包含 query、query 是否排序、URL 是否解码、时间戳单位是秒还是毫秒,都会影响结果。

第二步:确认 secret 来源,而不是盲换算法

同一个平台常有多个 secret:sandbox、production、不同 workspace、不同 webhook endpoint、旧版本和轮换后的新版本。拿错 secret 时,本地 HMAC 会稳定地产生“看起来正确但永远不匹配”的结果。

排查时先列出 secret 维度:

  • 这是测试环境还是生产环境。
  • 这个 webhook endpoint 是否有独立 signing secret。
  • secret 是否最近轮换,平台是否同时发送多版本签名。
  • 环境变量里存的是原始文本、hex,还是 Base64。
  • 代码是否额外 trim、转码或读取了错误配置项。

每次只改一个变量。不要同时换 secret、算法和 body,否则匹配了也不知道真正原因。

第三步:算法和输出编码要按 header 对齐

HMAC-SHA256 很常见,但也有平台使用 SHA1、SHA384 或 SHA512。算法必须和文档一致。更隐蔽的是输出编码:同一段 HMAC 字节可以显示为 hex、Base64 或 Base64URL。

如果 header 是:

X-Signature: sha256=4f2c...

通常要比较 sha256= 后面的 hex 值,而不是把整个 header 值拿来比较。如果 header 是 Base64 digest,就不要把本地 hex 直接拿去比。需要检查表示层时,可以用 Base64 Encoder 辅助转换,但它不能替代 HMAC 计算。

如果你只是要计算没有 secret 的摘要,例如平台文档要求 body_sha256 字段,再使用 Hash Generator。hash 和 HMAC 的输入可能相同,但安全含义不同。

第四步:timestamp 和重放保护放在签名匹配之后查

很多 webhook 会把 timestamp 纳入签名,并设置 3 分钟或 5 分钟容忍窗口。排查时先让本地计算出的签名与 header 匹配,再查 timestamp 是否过期。否则你可能把时间窗口问题误判成 HMAC 算法问题。

典型顺序是:

  1. 取原始 body 或 canonical string。
  2. 使用收到的 timestamp,而不是当前时间。
  3. 使用对应 endpoint 的 secret。
  4. 按文档算法生成 HMAC。
  5. 转成 header 使用的编码。
  6. 去掉 header 前缀后比较。
  7. 签名匹配后,再检查 timestamp 容忍窗口和 replay id。

如果第 6 步匹配,但应用仍然拒绝,请看 header 提取、时间解析、重复事件缓存和权限逻辑。

第五步:比较方式也可能出 bug

生产代码里不要用普通字符串早退比较来处理签名,因为它可能泄露时间侧信道。应使用语言或框架提供的 constant-time comparison,并在比较前确保两边是同一种字节或同一种字符串编码。

还有一个现实 bug:大小写。hex digest 有的库输出小写,有的输出大写。通常可以统一大小写后比较,但不要对 Base64 做类似处理,因为 Base64 大小写敏感。

日志怎么打才有用

不要把完整 secret 或真实 payload 打进日志。可以记录这些安全字段:算法名、body 字节长度、canonical string 的结构描述、timestamp、签名 header 是否存在、前缀类型、digest 长度、使用的 endpoint id。必要时在安全环境里保存脱敏样例,确保还能复现字节边界。

一条有用的排查记录应该能回答:我签的到底是什么、用哪个 secret、用什么算法、输出成什么编码、和 header 的哪一段比较。

FAQ

为什么我用同一个 JSON 算 HMAC 还是不匹配?

你看到的“同一个 JSON”可能不是同一段字节。字段顺序、空格、换行、Unicode 转义和字符编码都会改变签名。优先使用 raw body。

header 里有多个签名怎么办?

按平台文档选择当前版本或指定算法的签名。有些平台轮换 secret 时会同时发送多个值,你需要逐个按规则验证,而不是随便取第一个。

HMAC 签名匹配后还需要检查什么?

还要检查 timestamp 窗口、事件 id 是否重复、来源 endpoint 是否正确,以及业务权限。HMAC 只证明消息和 secret 匹配,不代表业务上一定应该接受。

继续阅读