feat: add HMAC webhook signature verification utilities

This commit is contained in:
Codex
2026-02-18 12:33:58 +00:00
parent 3c0584a057
commit 53e0daecaf
2 changed files with 73 additions and 0 deletions

40
src/lib/signature.js Normal file
View File

@@ -0,0 +1,40 @@
"use strict";
const crypto = require("node:crypto");
function hmacSHA256Hex(payload, secret) {
return crypto
.createHmac("sha256", secret)
.update(payload, "utf8")
.digest("hex");
}
function verifySignature({ payload, secret, signature }) {
if (!secret) {
throw new Error("webhook_secret_required");
}
if (!signature) {
return false;
}
const normalized = signature.startsWith("sha256=")
? signature.slice("sha256=".length)
: signature;
const expected = hmacSHA256Hex(payload, secret);
const expectedBuf = Buffer.from(expected, "hex");
const givenBuf = Buffer.from(normalized, "hex");
if (expectedBuf.length !== givenBuf.length) {
return false;
}
return crypto.timingSafeEqual(expectedBuf, givenBuf);
}
module.exports = {
hmacSHA256Hex,
verifySignature,
};

33
test/signature.test.js Normal file
View File

@@ -0,0 +1,33 @@
"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const { hmacSHA256Hex, verifySignature } = require("../src/lib/signature");
test("verifies valid signature", () => {
const payload = JSON.stringify({ hello: "world" });
const secret = "topsecret";
const sig = hmacSHA256Hex(payload, secret);
assert.equal(
verifySignature({ payload, secret, signature: `sha256=${sig}` }),
true,
);
});
test("rejects invalid signature", () => {
const payload = JSON.stringify({ hello: "world" });
const secret = "topsecret";
assert.equal(
verifySignature({ payload, secret, signature: "sha256=deadbeef" }),
false,
);
});
test("rejects missing signature", () => {
const payload = "abc";
const secret = "topsecret";
assert.equal(verifySignature({ payload, secret, signature: "" }), false);
});