GitHub 签名是如何生成的

GitHub 使用 Webhook Secret 作为 HMAC 密钥,对未经修改的请求 Body 计算 SHA-256,并把十六进制结果放在 X-Hub-Signature-256 请求头中。该值以 sha256= 开头。

import crypto from "node:crypto";

const expected = "sha256=" + crypto
  .createHmac("sha256", process.env.GITHUB_WEBHOOK_SECRET)
  .update(rawBody)
  .digest("hex");

const valid = crypto.timingSafeEqual(
  Buffer.from(expected),
  Buffer.from(request.headers["x-hub-signature-256"] || "")
);

签名不匹配的常见原因

  • 框架先把 JSON 解析为对象,再重新序列化。
  • 使用了错误仓库或组织的 Webhook Secret。
  • 比较时遗漏 sha256= 前缀。
  • 读取 Body 时改变了编码、空格或换行。
  • 直接用普通字符串比较,未使用恒定时间比较。

正确的排查顺序

  1. 保存收到的原始 Body 字节,不要先调用 JSON 解析器。
  2. 确认读取的是 X-Hub-Signature-256,而不是旧的 SHA-1 请求头。
  3. 在本地验签工具中输入相同 Secret、签名和 Body。
  4. 如果本地通过而应用失败,检查框架的 Body 中间件顺序。