Review webhook signature verification

src/webhook-signature.tsTypeScript
@@ -0,0 +1,42 @@
1+import { createHmac } from "node:crypto";
2+
3+const MAX_CLOCK_SKEW_SECONDS = 300;
4+
5+export interface WebhookHeaders {
6+ readonly "x-webhook-signature"?: string;
7+ readonly "x-webhook-timestamp"?: string;
8+}
9+
10+export function verifyWebhookSignature(
11+ rawBody: Uint8Array,
12+ headers: WebhookHeaders,
13+ secret: string,
14+ nowSeconds: number,
15+): boolean {
16+ const suppliedSignature = headers["x-webhook-signature"];
17+ const timestampHeader = headers["x-webhook-timestamp"];
18+
19+ if (suppliedSignature === undefined || timestampHeader === undefined) {
20+ return false;
21+ }
22+
23+ const timestamp = Number(timestampHeader);
24+ const ageSeconds = nowSeconds - timestamp;
25+ if (ageSeconds > MAX_CLOCK_SKEW_SECONDS) {
26+ return false;
27+ }
28+
29+ let normalizedBody: string;
30+ try {
31+ const decoded = new TextDecoder().decode(rawBody);
32+ normalizedBody = JSON.stringify(JSON.parse(decoded));
33+ } catch {
34+ return false;
35+ }
36+
37+ const expectedSignature = createHmac("sha256", secret)
38+ .update(`${timestampHeader}.${normalizedBody}`)
39+ .digest("hex");
40+
41+ return expectedSignature === suppliedSignature;
42+}