【问题标题】:How to verify Stripe's webhook signature?如何验证 Stripe 的 webhook 签名?
【发布时间】:2021-09-18 03:46:08
【问题描述】:

感谢@user9014097 的广泛回答,我已经找到了问题所在。本节特别描述了我的错误/疏忽:

消息的格式在确定 MAC 方面起着重要作用。 每个差异,例如换行符,空白等更改 签名并导致验证失败。检查你是否可以 稍微改变了消息或其格式。

对请求正文进行字符串化之后,它就像一个魅力! 使用 cloudflare 工作者,您可以像这样以纯文本形式获取原始正文:const payload = await event.request.text();

原帖:

我正在尝试手动验证 Stripe webhook 的签名。我不在 node.js 中工作,所以不幸的是,stripe-node 包不是我的选择。我已按照https://stripe.com/docs/webhooks/signatures#verify-manually 上的“手动验证签名”步骤进行操作。到目前为止,我已经制作了以下内容:

  • body:event.request.body(来自 cloudflare worker 的 fetch 事件)
  • 标题:event.request.headers.get('Stripe-Signature')
const hexStringToUint8Array = hexString => {
  const bytes = new Uint8Array(Math.ceil(hexString.length / 2));
  for (let i = 0; i < bytes.length; i++)
    bytes[i] = parseInt(hexString.substr(i * 2, 2), 16);
  return bytes;
};

export const verifySignature = async (body, header, tolerance = 300) => {
  header = header.split(',').reduce((accum, x) => { 
    const [k, v] = x.split('=');
    return { ...accum, [k]: v };
  }, {});
  
  const encoder = new TextEncoder();
  
  const key = await crypto.subtle.importKey(
    "raw",
    encoder.encode(STRIPE_WEBHOOK_SECRET),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["verify"]
  );

  const verified = await crypto.subtle.verify(
    "HMAC",
    key,
    hexStringToUint8Array(header.v1),
    encoder.encode(`${header.t}.${body}`)
  );

  const elapsed = Math.floor(Date.now() / 1000) - Number(header.t);
  return verified && !(tolerance && elapsed > tolerance)
}; 

但是,验证函数总是返回 false。谁能在这里发现问题?

谢谢你,贾科

编辑:以下是测试数据。感谢@user9014097 的请求:

body 和 header 应该用作 verifySignature 的参数。

正文

{
  "id": "evt_1JAc3EFj0bmn6HyUJpVSB1hC",
  "object": "event",
  "api_version": "2020-08-27",
  "created": 1625669316,
  "data": {
    "object": {
      "id": "prod_Jkre4DaakpOaCt",
      "object": "product",
      "active": true,
      "attributes": [

      ],
      "created": 1624892313,
      "description": null,
      "images": [
        "https://files.stripe.com/links/MDB8YWNjdF8xSXR3Y3BGajBibW42SHlVfGZsX3Rlc3RfaHlGSVhUVHFmOHVLSzFhUUVGV0FWNlc300bY6GO8NE"
      ],
      "livemode": false,
      "metadata": {
        "brand": "DOM",
        "series": "1D",
        "key_codes_start": "1",
        "key_codes_end": "114"
      },
      "name": "DOM 1D serie 1-114",
      "package_dimensions": null,
      "shippable": null,
      "statement_descriptor": null,
      "type": "service",
      "unit_label": "sleutel",
      "updated": 1625669316,
      "url": null
    },
    "previous_attributes": {
      "description": "test",
      "updated": 1625665952
    }
  },
  "livemode": false,
  "pending_webhooks": 1,
  "request": {
    "id": "req_SxhB93mIUlcaKW",
    "idempotency_key": null
  },
  "type": "product.updated"
}

标题

t=1625700981,v1=08a60e9f42416808d1fbd3efb852695830af8f7e0da71d351dd5fbbf135d7974,v0=3ff951917ae810ac14236a0db7ce046011a1ce4d949f267650766b1c9bb1b3e2

verifySignature 函数中的 STRIPE_WEBHOOK_SECRET 变量用于导入/创建密钥。然后用于验证有效负载/正文。为了测试它,您可以将变量名换成下面的秘密字符串。

STRIPE_WEBHOOK_SECRET

whsec_bPYHe4WqVVG9jri3FwIHBLZgQSHTIS6j

【问题讨论】:

  • 能否提供测试数据?
  • 当你说你不在 Node 中工作时,这是什么意思?看起来您正在使用 Node 代码、库等。至于解决我们的问题,您如何获得请求的正文?它必须完全未更改,否则签名检查将失败。
  • @JustinMichael 我在 Cloudflare 的工作人员基础上进行构建,它提供了自己的一套工具来完成节点所做的一些基本工作。 FetchEvent 例如提供事件对象,该事件对象包含请求对象(及其主体)。正文作为第一个参数原封不动地传递给 verifySignature 函数。
  • @user9014097 我对原始帖子进行了编辑以包含测试数据。谢谢。

标签: javascript cryptography stripe-payments cloudflare-workers


【解决方案1】:

虽然您不能在 Cloudflare Worker you can use the webhook signature piece 中使用整个 Stripe 节点库。

【讨论】:

    【解决方案2】:

    在我的机器上,实际验证签名成功!

    但是,您的验证也会考虑时间戳。如果验证时间与此时间戳的差异超过给定的容差值(默认 300 秒),则验证失败。 最后一个条件导致验证失败。

    如果容差足够或消息时间戳在容差内,则验证成功:

    (async () => {
            
    const hexStringToUint8Array = hexString => {
      const bytes = new Uint8Array(Math.ceil(hexString.length / 2));
      for (let i = 0; i < bytes.length; i++)
        bytes[i] = parseInt(hexString.substr(i * 2, 2), 16);
      return bytes;
    };
    
    const verifySignature = async (body, header, tolerance = 300) => {
      header = header.split(',').reduce((accum, x) => { 
        const [k, v] = x.split('=');
        return { ...accum, [k]: v };
      }, {});
      
      const encoder = new TextEncoder();
      
      const key = await crypto.subtle.importKey(
        "raw",
        encoder.encode('whsec_bPYHe4WqVVG9jri3FwIHBLZgQSHTIS6j'),
        { name: "HMAC", hash: "SHA-256" },
        false,
        ["verify"]
      );
    
      const verified = await crypto.subtle.verify(
        "HMAC",
        key,
        hexStringToUint8Array(header.v1),
        encoder.encode(`${header.t}.${body}`)
      );
    
      const elapsed = Math.floor(Date.now() / 1000) - Number(header.t);
      return verified && !(tolerance && elapsed > tolerance)
    }; 
                
    var body = `{
      "id": "evt_1JAc3EFj0bmn6HyUJpVSB1hC",
      "object": "event",
      "api_version": "2020-08-27",
      "created": 1625669316,
      "data": {
        "object": {
          "id": "prod_Jkre4DaakpOaCt",
          "object": "product",
          "active": true,
          "attributes": [
    
          ],
          "created": 1624892313,
          "description": null,
          "images": [
            "https://files.stripe.com/links/MDB8YWNjdF8xSXR3Y3BGajBibW42SHlVfGZsX3Rlc3RfaHlGSVhUVHFmOHVLSzFhUUVGV0FWNlc300bY6GO8NE"
          ],
          "livemode": false,
          "metadata": {
            "brand": "DOM",
            "series": "1D",
            "key_codes_start": "1",
            "key_codes_end": "114"
          },
          "name": "DOM 1D serie 1-114",
          "package_dimensions": null,
          "shippable": null,
          "statement_descriptor": null,
          "type": "service",
          "unit_label": "sleutel",
          "updated": 1625669316,
          "url": null
        },
        "previous_attributes": {
          "description": "test",
          "updated": 1625665952
        }
      },
      "livemode": false,
      "pending_webhooks": 1,
      "request": {
        "id": "req_SxhB93mIUlcaKW",
        "idempotency_key": null
      },
      "type": "product.updated"
    }`                  
    
    var header = `t=1625700981,v1=08a60e9f42416808d1fbd3efb852695830af8f7e0da71d351dd5fbbf135d7974,v0=3ff951917ae810ac14236a0db7ce046011a1ce4d949f267650766b1c9bb1b3e2`;
             
    const elapsed = Math.floor(Date.now() / 1000) - Number(1625700981);
    console.log("Elapsed time in s:", elapsed)
    console.log("Verification without considering tolerance:", await verifySignature(body, header, null));
    console.log("Verification with enough tolerance:        ", await verifySignature(body, header, elapsed));
    console.log("Verification with default tolerance:       ", await verifySignature(body, header)); // default: tolerance = 300
    
    })();

    您的环境中的验证失败可能有例如原因如下:

    • 容差太小(默认值 300 秒同时 (!) 对于发布的 nessage 的时间戳来说太小了)。
    • 消息的格式在确定 MAC 中起作用。每个差异,例如换行符、空白等会更改签名并导致验证失败。检查您是否稍微更改了消息或其格式。

    【讨论】:

    • 感谢您的广泛回答!最后一段描述了我的问题,我需要对请求正文进行字符串化。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-01-14
    • 2020-08-22
    • 2016-03-16
    • 1970-01-01
    • 2021-01-25
    • 2020-07-17
    • 2021-12-25
    相关资源
    最近更新 更多