Skip to main content

Enterprise Server 3.15 目前作为候选发布提供。

验证 Webhook 交付

可以使用 Webhook 机密来验证 Webhook 交付是否来自 GitHub。

关于验证 Webhook 交付

一旦服务器配置为接收有效负载,它将侦听发送到你配置的端点的任何交付。 为了确保服务器仅处理 GitHub 发送的 Webhook 交付,并确保交付未被篡改,应在进一步处理交付之前验证 Webhook 签名。 这有助于避免花费服务器时间来处理并非来自 GitHub 的交付,同时有助于避免中间人攻击。

若要实现此目的,需要:

  1. 为 Webhook 创建机密令牌。
  2. 将令牌安全存储在服务器上。
  3. 根据令牌来验证传入的 Webhook 有效负载,以确认其来自 GitHub 且未被篡改。

创建机密令牌

可以使用机密令牌来创建新的 Webhook,也可以为现有 Webhook 添加机密令牌。 在创建机密令牌时,应选择高熵的随机文本字符串。

  • 若要使用机密令牌创建新的 Webhook,请参阅“创建 web 挂钩”。
  • 若要为现有 Webhook 添加机密令牌,请编辑 Webhook 的设置。 在“机密”下,键入用作 secret 密钥的字符串。 有关详细信息,请参阅“测试 Webhook”。

以安全的方式存储机密令牌

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

验证 Webhook 交付

对于每个有效负载,GitHub Enterprise Server 将使用你的机密令牌来创建一个哈希签名并发送给你。 哈希签名将作为 X-Hub-Signature-256 标头的值出现在每个交付中。 有关详细信息,请参阅“Webhook 事件和有效负载”。

在处理 Webhook 交付的代码中,应使用机密令牌计算哈希。 然后,将 GitHub 发送的哈希与你计算的预期哈希进行比较,并确保它们匹配。 有关如何在各种编程语言中验证哈希的示例,请参阅“示例”。

验证 Webhook 有效负载时,必须记住一些重要事项:

  • GitHub Enterprise Server 使用 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 字符。

延伸阅读