Skip to content
WhatIsUp.dev

Verificação de assinatura de webhook

Todo webhook que o WhatIsUp.dev envia via POST carrega um header X-WhatIsUp-Signature neste formato:

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

A assinatura é calculada assim:

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

…onde <raw_request_body> são os bytes exatos enviados na rede. Não re-serialize o JSON antes de verificar — pretty-print, canonicalização da ordem das chaves, qualquer coisa assim quebra a assinatura.

Receita de verificação

Três passos:

  1. Faça o parse do header em {t, v1}.
  2. Recalcule o HMAC sobre <t>.<raw_body> usando o signing secret do endpoint.
  3. Compare v1 com o seu HMAC calculado em tempo constante. Rejeite se não baterem, ou se t estiver a mais de 5 minutos do seu relógio (defesa contra replay).

Código

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);
}

Armadilhas comuns

  • O body-parser do Express altera o corpo da requisição. Verifique contra o corpo raw, não o JSON parseado. Passe verify: (req, res, buf) => { req.rawBody = buf } ao parser de JSON e depois verifique contra req.rawBody.
  • Comparar strings com == ou === é um oráculo de timing. Use crypto.timingSafeEqual (Node), hmac.compare_digest (Python), hmac.Equal (Go).
  • Tolerância ampla. Não aumente a tolerância de 5 minutos para mascarar um problema de desvio de relógio. Rode NTP.
  • Logar o secret. Não faça isso. Nós aplicamos HMAC do nosso lado; você deve guardá-lo como uma senha — só em variável de ambiente, nunca na sua árvore de código.

Defesa contra replay

A tolerância de 5 minutos no timestamp protege contra um atacante reproduzindo uma requisição capturada. Para garantia extra: armazene o event_id numa tabela de deduplicação do seu lado. A idempotência transforma replays em no-ops mesmo que a janela de assinatura esteja totalmente aberta.