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:
- Faça o parse do header em
{t, v1}. - Recalcule o HMAC sobre
<t>.<raw_body>usando o signing secret do endpoint. - Compare
v1com o seu HMAC calculado em tempo constante. Rejeite se não baterem, ou setestiver 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-parserdo Express altera o corpo da requisição. Verifique contra o corpo raw, não o JSON parseado. Passeverify: (req, res, buf) => { req.rawBody = buf }ao parser de JSON e depois verifique contrareq.rawBody. - Comparar strings com
==ou===é um oráculo de timing. Usecrypto.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.