跳至主要内容

验证 Webhook 传递

您可以使用 Webhook 密钥来验证 Webhook 传递是否来自 GitHub。

关于验证 Webhook 传递

配置好服务器接收有效负载后,它将监听发送到您配置的端点的任何传递。为了确保您的服务器只处理由 GitHub 发送的 Webhook 传递,并确保传递没有被篡改,您应该在进一步处理传递之前验证 Webhook 签名。这将帮助您避免将服务器时间浪费在处理非来自 GitHub 的传递上,并有助于避免中间人攻击。

为此,您需要

  1. 为 Webhook 创建密钥令牌。
  2. 将令牌安全地存储在您的服务器上。
  3. 根据令牌验证传入的 Webhook 有效负载,以验证它们是否来自 GitHub 且未被篡改。

创建密钥令牌

您可以使用密钥令牌创建新的 Webhook,也可以将密钥令牌添加到现有 Webhook。创建密钥令牌时,您应该选择一个具有高熵的随机文本字符串。

  • 要使用密钥令牌创建新的 Webhook,请参阅“创建 Webhook”。
  • 要将密钥令牌添加到现有 Webhook,请编辑 Webhook 的设置。在“密钥”下,键入一个字符串作为secret密钥。有关更多信息,请参阅“编辑 Webhook”。

安全存储密钥令牌

创建密钥令牌后,您应该将其存储在您的服务器可以访问的安全位置。切勿将令牌硬编码到应用程序中或将令牌推送到任何存储库。有关如何在代码中安全使用身份验证凭据的更多信息,请参阅“保护您的 API 凭据”。

验证 Webhook 传递

GitHub 将使用您的密钥令牌创建一个哈希签名,该签名将与每个有效负载一起发送给您。哈希签名将作为X-Hub-Signature-256标头的值出现在每个传递中。有关更多信息,请参阅“Webhook 事件和有效负载”。

在处理 webhook 传递的代码中,您应该使用您的密钥令牌计算哈希值。然后,将 GitHub 发送的哈希值与您计算出的预期哈希值进行比较,并确保它们匹配。有关在各种编程语言中验证哈希值的示例,请参见“示例”。

在验证 webhook 负载时,有一些重要事项需要牢记。

  • GitHub 使用 HMAC 十六进制摘要来计算哈希值。
  • 哈希签名始终以 sha256= 开头。
  • 哈希签名是使用您的 webhook 密钥令牌和负载内容生成的。
  • 如果您的语言和服务器实现指定了字符编码,请确保您将负载处理为 UTF-8。Webhook 负载可能包含 Unicode 字符。
  • 切勿使用普通的 == 运算符。相反,请考虑使用类似于 secure_comparecrypto.timingSafeEqual 的方法,这些方法执行“恒定时间”字符串比较,以帮助减轻针对常规等式运算符或 JIT 优化语言中的常规循环的某些计时攻击。

测试 webhook 负载验证

您可以使用以下 secretpayload 值来验证您的实现是否正确。

  • 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 负载时调用它。

JavaScript
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 配置了密钥。如果您没有为您的 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 字符。

进一步阅读