关于验证 Webhook 交付
一旦服务器配置为接收有效负载,它将侦听发送到你配置的端点的任何交付。 为了确保服务器仅处理 GitHub 发送的 Webhook 交付,并确保交付未被篡改,应在进一步处理交付之前验证 Webhook 签名。 这有助于避免花费服务器时间来处理并非来自 GitHub 的交付,同时有助于避免中间人攻击。
若要实现此目的,需要:
- 为 Webhook 创建机密令牌。
- 将令牌安全存储在服务器上。
- 根据令牌来验证传入的 Webhook 有效负载,以确认其来自 GitHub 且未被篡改。
创建机密令牌
可以使用机密令牌来创建新的 Webhook,也可以为现有 Webhook 添加机密令牌。 在创建机密令牌时,应选择高熵的随机文本字符串。
- 若要使用机密令牌创建新的 Webhook,请参阅“创建 web 挂钩”。
- 若要为现有 Webhook 添加机密令牌,请编辑 Webhook 的设置。 在“机密”下,键入用作
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 Everybody"payload
: "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
函数,并在接收 Webhook 有效负载时在任何 JavaScript 环境中进行调用:
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 * as crypto from "crypto"; const WEBHOOK_SECRET: string = process.env.WEBHOOK_SECRET; const verify_signature = (req: Request) => { const signature = crypto .createHmac("sha256", WEBHOOK_SECRET) .update(JSON.stringify(req.body)) .digest("hex"); let trusted = Buffer.from(`sha256=${signature}`, 'ascii'); let untrusted = Buffer.from(req.headers.get("x-hub-signature-256"), 'ascii'); return crypto.timingSafeEqual(trusted, untrusted); }; const handleWebhook = (req: Request, res: Response) => { if (!verify_signature(req)) { res.status(401).send("Unauthorized"); return; } // The rest of your logic here };
import * as crypto from "crypto";
const WEBHOOK_SECRET: string = process.env.WEBHOOK_SECRET;
const verify_signature = (req: Request) => {
const signature = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(JSON.stringify(req.body))
.digest("hex");
let trusted = Buffer.from(`sha256=${signature}`, 'ascii');
let untrusted = Buffer.from(req.headers.get("x-hub-signature-256"), 'ascii');
return crypto.timingSafeEqual(trusted, untrusted);
};
const handleWebhook = (req: Request, res: Response) => {
if (!verify_signature(req)) {
res.status(401).send("Unauthorized");
return;
}
// The rest of your logic here
};
疑难解答
如果确定有效负载来自 GitHub 但签名验证失败:
- 请确保已为 Webhook 配置机密。 如果尚未为 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 字符。