关于验证 webhook 交付
一旦您的服务器配置为接收负载,它将监听发送到您配置的端点的所有交付。为了确保您的服务器仅处理 GitHub 发送的 webhook 交付,并确保交付未被篡改,您应在进一步处理交付之前验证 webhook 签名。这将帮助您避免耗费服务器时间处理非 GitHub 发来的交付,并有助于防止中间人攻击。
为此,您需要
- 为 webhook 创建一个密钥令牌。
- 在服务器上安全地存储该令牌。
- 使用该令牌验证传入的 webhook 负载,以确认它们来自 GitHub 且未被篡改。
创建密钥令牌
您可以为新 webhook 创建带有密钥令牌的 webhook,亦可向已有的 webhook 添加密钥令牌。创建密钥令牌时,您应选择具有高熵的随机字符串。
- 要创建带有密钥令牌的新 webhook,请参阅 创建 webhook。
- 要向已有的 webhook 添加密钥令牌,编辑该 webhook 的设置。在 “Secret”(密钥)下,输入用作
secret键的字符串。更多信息,请参阅 编辑 webhook。
安全存储密钥令牌
创建密钥令牌后,您应将其存放在服务器能够访问的安全位置。切勿在应用程序中硬编码令牌或将令牌推送到任何仓库。有关在代码中安全使用身份验证凭据的更多信息,请参阅 安全保管 API 凭据。
验证 webhook 交付
GitHub 将使用您的密钥令牌生成哈希签名,并随每个负载一起发送给您。该哈希签名会出现在每次交付的 X-Hub-Signature-256 头部中。更多信息,请参阅 Webhook 事件和负载。
在处理 webhook 交付的代码中,您应使用密钥令牌计算哈希。然后,将 GitHub 发送的哈希与您计算的预期哈希进行比较,确保二者匹配。有关在各种编程语言中验证哈希的示例,请参阅 示例。
在验证 webhook 负载时,有几件重要事项需要记住
- GitHub 使用 HMAC 十六进制摘要来计算哈希。
- 哈希签名始终以
sha256=开头。 - 哈希签名是使用您的 webhook 密钥令牌和负载内容生成的。
- 如果您的语言和服务器实现指定了字符编码,请确保将负载按 UTF-8 处理。Webhook 负载可能包含 Unicode 字符。
- 绝不要使用普通的
==运算符。请改用诸如secure_compare或crypto.timingSafeEqual之类的方法,它们执行“常量时间”字符串比较,以帮助缓解对普通相等运算符或在 JIT 优化语言中使用的常规循环的时序攻击。
测试 webhook 负载验证
您可以使用以下 secret 和 payload 值来验证您的实现是否正确
secret:It's a Secret to Everybodypayload:Hello, World!
如果您的实现正确,您生成的签名应与以下签名值匹配
- signature:
757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17 - X-Hub-Signature-256:
sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17
示例
您可以使用您选择的编程语言在代码中实现 HMAC 验证。下面是一些展示不同编程语言实现方式的示例。
Ruby 示例
例如,您可以定义以下 verify_signature 函数
def verify_signature(payload_body)
signature = 'sha256=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), ENV['SECRET_TOKEN'], payload_body)
return halt 500, "Signatures didn't match!" unless Rack::Utils.secure_compare(signature, request.env['HTTP_X_HUB_SIGNATURE_256'])
end
然后在收到 webhook 负载时调用它
post '/payload' do
request.body.rewind
payload_body = request.body.read
verify_signature(payload_body)
push = JSON.parse(payload_body)
"I got some JSON: #{push.inspect}"
end
Python 示例
例如,您可以定义以下 verify_signature 函数,并在收到 webhook 负载时调用它
import hashlib
import hmac
def verify_signature(payload_body, secret_token, signature_header):
"""Verify that the payload was sent from GitHub by validating SHA256.
Raise and return 403 if not authorized.
Args:
payload_body: original request body to verify (request.body())
secret_token: GitHub app webhook token (WEBHOOK_SECRET)
signature_header: header received from GitHub (x-hub-signature-256)
"""
if not signature_header:
raise HTTPException(status_code=403, detail="x-hub-signature-256 header is missing!")
hash_object = hmac.new(secret_token.encode('utf-8'), msg=payload_body, digestmod=hashlib.sha256)
expected_signature = "sha256=" + hash_object.hexdigest()
if not hmac.compare_digest(expected_signature, signature_header):
raise HTTPException(status_code=403, detail="Request signatures didn't match!")
JavaScript 示例
例如,您可以定义以下 verifySignature 函数,并在任何 JavaScript 环境中收到 webhook 负载时调用它
let encoder = new TextEncoder();
async function verifySignature(secret, header, payload) {
let parts = header.split("=");
let sigHex = parts[1];
let algorithm = { name: "HMAC", hash: { name: 'SHA-256' } };
let keyBytes = encoder.encode(secret);
let extractable = false;
let key = await crypto.subtle.importKey(
"raw",
keyBytes,
algorithm,
extractable,
[ "sign", "verify" ],
);
let sigBytes = hexToBytes(sigHex);
let dataBytes = encoder.encode(payload);
let equal = await crypto.subtle.verify(
algorithm.name,
key,
sigBytes,
dataBytes,
);
return equal;
}
function hexToBytes(hex) {
let len = hex.length / 2;
let bytes = new Uint8Array(len);
let index = 0;
for (let i = 0; i < hex.length; i += 2) {
let c = hex.slice(i, i + 2);
let b = parseInt(c, 16);
bytes[index] = b;
index += 1;
}
return bytes;
}
TypeScript 示例
例如,您可以定义以下 verify_signature 函数,并在收到 webhook 负载时调用它
import { Webhooks } from "@octokit/webhooks";
const webhooks = new Webhooks({
secret: process.env.WEBHOOK_SECRET,
});
const handleWebhook = async (req, res) => {
const signature = req.headers["x-hub-signature-256"];
const body = await req.text();
if (!(await webhooks.verify(body, signature))) {
res.status(401).send("Unauthorized");
return;
}
// The rest of your logic here
};
import { Webhooks } from "@octokit/webhooks";
const webhooks = new Webhooks({
secret: process.env.WEBHOOK_SECRET,
});
const handleWebhook = async (req, res) => {
const signature = req.headers["x-hub-signature-256"];
const body = await req.text();
if (!(await webhooks.verify(body, signature))) {
res.status(401).send("Unauthorized");
return;
}
// The rest of your logic here
};
故障排除
如果您确信负载来自 GitHub,但签名验证失败
- 确保您已经为 webhook 配置了密钥。如果未配置密钥,则不会出现
X-Hub-Signature-256头部。有关为 webhook 配置密钥的更多信息,请参阅 编辑 webhook。 - 确保您使用了正确的头部。GitHub 推荐使用
X-Hub-Signature-256头部,它采用 HMAC‑SHA256 算法。X-Hub-Signature头部使用 HMAC‑SHA1 算法,仅为兼容旧版本而保留。 - 确保您使用了正确的算法。如果使用
X-Hub-Signature-256头部,则应使用 HMAC‑SHA256 算法。 - 确保您使用了正确的 webhook 密钥。如果不知道 webhook 密钥的值,您可以更新 webhook 的密钥。更多信息,请参阅 编辑 webhook。
- 确保在验证之前,负载和头部未被修改。例如,如果使用代理或负载均衡器,请确保它们不会修改负载或头部。
- 如果您的语言和服务器实现指定了字符编码,请确保将负载按 UTF-8 处理。Webhook 负载可能包含 Unicode 字符。