如何验证 Stripe 的 webhook 签名?
How to verify Stripe's webhook signature?
感谢@user9014097 的广泛回答,我已经解决了这个问题。本节特别描述了我的 mistake/oversight:
The formatting of the message plays a role in determining the MAC.
Every difference, e.g. a line break, a blank etc. changes the
signature and results in a failed verification. Check if you might
have changed the message or its format a little bit.
在 将请求字符串化 body 之后,它就像一个魅力!
使用 cloudflare worker,您可以获得原始的 body 纯文本格式,如下所示:const payload = await event.request.text();
原文post:
我正在尝试手动验证 Stripe webhook 的签名。我不在 node.js 工作,所以不幸的是 stripe-node 包不适合我。我已遵循 https://stripe.com/docs/webhooks/signatures#verify-manually 上的“手动验证签名”步骤。到目前为止,我制作了以下内容:
- body:event.request.body(来自 cloudflare worker 的 fetch 事件)
- header: 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)
};
但是验证函数总是 returns 错误。有人能在这里发现问题吗?
谢谢你,杰科
编辑: 下面是测试数据。感谢@user9014097 的请求:
body 和 header 应该用作 verifySignature 的参数。
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"
}
header
t=1625700981,v1=08a60e9f42416808d1fbd3efb852695830af8f7e0da71d351dd5fbbf135d7974,v0=3ff951917ae810ac14236a0db7ce046011a1ce4d949f267650766b1c9bb1b3e2
verifySignature 函数中的 STRIPE_WEBHOOK_SECRET 变量用于 import/create 密钥。然后用来验证payload/body。为了测试它,您可以将变量名称换成下面的秘密字符串。
STRIPE_WEBHOOK_SECRET
whsec_bPYHe4WqVVG9jri3FwIHBLZgQSHTIS6j
虽然您不能在 Cloudflare Worker 中使用整个 Stripe Node 库 you can use the webhook signature piece。
在我的机器上,实际签名验证成功!
但是,您的验证还考虑了时间戳。如果验证时间与此时间戳的差异超过给定的容差值(默认 300s),则验证失败。 这最后一个条件导致验证失败。
如果容差足够或者消息时间戳在容差范围内,则验证成功:
(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
})();
您环境中的验证失败可能导致例如以下原因:
- 公差太小(默认值 300s 同时(!)对于发布消息的时间戳来说太小了)。
- 邮件的格式在确定 MAC 方面发挥着重要作用。每个差异,例如换行符、空白等更改签名并导致验证失败。检查您是否稍微更改了邮件或其格式。
感谢@user9014097 的广泛回答,我已经解决了这个问题。本节特别描述了我的 mistake/oversight:
The formatting of the message plays a role in determining the MAC. Every difference, e.g. a line break, a blank etc. changes the signature and results in a failed verification. Check if you might have changed the message or its format a little bit.
在 将请求字符串化 body 之后,它就像一个魅力!
使用 cloudflare worker,您可以获得原始的 body 纯文本格式,如下所示:const payload = await event.request.text();
原文post:
我正在尝试手动验证 Stripe webhook 的签名。我不在 node.js 工作,所以不幸的是 stripe-node 包不适合我。我已遵循 https://stripe.com/docs/webhooks/signatures#verify-manually 上的“手动验证签名”步骤。到目前为止,我制作了以下内容:
- body:event.request.body(来自 cloudflare worker 的 fetch 事件)
- header: 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)
};
但是验证函数总是 returns 错误。有人能在这里发现问题吗?
谢谢你,杰科
编辑: 下面是测试数据。感谢@user9014097 的请求:
body 和 header 应该用作 verifySignature 的参数。
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"
}
header
t=1625700981,v1=08a60e9f42416808d1fbd3efb852695830af8f7e0da71d351dd5fbbf135d7974,v0=3ff951917ae810ac14236a0db7ce046011a1ce4d949f267650766b1c9bb1b3e2
verifySignature 函数中的 STRIPE_WEBHOOK_SECRET 变量用于 import/create 密钥。然后用来验证payload/body。为了测试它,您可以将变量名称换成下面的秘密字符串。
STRIPE_WEBHOOK_SECRET
whsec_bPYHe4WqVVG9jri3FwIHBLZgQSHTIS6j
虽然您不能在 Cloudflare Worker 中使用整个 Stripe Node 库 you can use the webhook signature piece。
在我的机器上,实际签名验证成功!
但是,您的验证还考虑了时间戳。如果验证时间与此时间戳的差异超过给定的容差值(默认 300s),则验证失败。 这最后一个条件导致验证失败。
如果容差足够或者消息时间戳在容差范围内,则验证成功:
(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
})();
您环境中的验证失败可能导致例如以下原因:
- 公差太小(默认值 300s 同时(!)对于发布消息的时间戳来说太小了)。
- 邮件的格式在确定 MAC 方面发挥着重要作用。每个差异,例如换行符、空白等更改签名并导致验证失败。检查您是否稍微更改了邮件或其格式。