From 53e0daecafcd3d9608adfd888b72835d809fdf8d Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 18 Feb 2026 12:33:58 +0000 Subject: [PATCH] feat: add HMAC webhook signature verification utilities --- src/lib/signature.js | 40 ++++++++++++++++++++++++++++++++++++++++ test/signature.test.js | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 src/lib/signature.js create mode 100644 test/signature.test.js diff --git a/src/lib/signature.js b/src/lib/signature.js new file mode 100644 index 0000000..21f10d7 --- /dev/null +++ b/src/lib/signature.js @@ -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, +}; diff --git a/test/signature.test.js b/test/signature.test.js new file mode 100644 index 0000000..215177f --- /dev/null +++ b/test/signature.test.js @@ -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); +});