WhatIsUp.dev
Começar
Esta página ainda só está em inglês.

Webhook signature verification

Every webhook WhatIsUp.dev POSTs carries an X-WhatIsUp-Signature header in this format:

X-WhatIsUp-Signature: t=1700000000,v1=<hex_hmac>

The signature is computed as:

HMAC_SHA256(<endpoint_signing_secret>, "<unix_timestamp>.<raw_request_body>")

…where <raw_request_body> is the exact bytes sent on the wire. Do not re-serialize the JSON before verifying — pretty-printing, key-order canonicalization, anything like that breaks the signature.

Verification recipe

Three steps:

  1. Parse the header into {t, v1}.
  2. Recompute the HMAC over <t>.<raw_body> using the endpoint's signing secret.
  3. Compare v1 to your computed HMAC in constant time. Reject if they don't match, or if t is more than 5 minutes off from your wall clock (replay defense).

Code

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(
rawBody: Buffer,
header: string,
secret: string,
toleranceSec = 300,
): boolean {
const parts = Object.fromEntries(
  header.split(',').map((p) => p.split('=', 2)),
) as { t?: string; v1?: string };
if (!parts.t || !parts.v1) return false;

const ts = Number(parts.t);
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - ts) > toleranceSec) return false;

const expected = createHmac('sha256', secret)
  .update(`${ts}.${rawBody.toString('utf8')}`)
  .digest();
const provided = Buffer.from(parts.v1, 'hex');
if (provided.length !== expected.length) return false;
return timingSafeEqual(expected, provided);
}

Common pitfalls

  • Express's body-parser mutates the request body. Verify against the raw body, not the parsed JSON. Pass verify: (req, res, buf) => { req.rawBody = buf } to the JSON parser, then verify against req.rawBody.
  • Comparing strings with == or === is a timing oracle. Use crypto.timingSafeEqual (Node), hmac.compare_digest (Python), hmac.Equal (Go).
  • Wide tolerance. Don't bump the 5-minute tolerance to mask a clock-drift problem. Run NTP.
  • Logging the secret. Don't. We HMAC it on our side; you should store it like a password — env-var only, never in your source tree.

Replay defense

The 5-minute timestamp tolerance protects against an attacker replaying a captured request. For belt-and-suspenders: store event_id in a dedupe table on your side. Idempotency makes replays no-ops even if the signature window is wide open.