Assinatura
Toda entrega é assinada. Validar a assinatura é obrigatório.
Formato
Pitra-Signature: t=1735689600,v1=<hmac_sha256_hex>v1 é HMAC-SHA256(secret, "<t>.<corpo bruto>") em hexadecimal, onde t é o timestamp Unix em segundos presente no próprio header e secret é o whsec_... devolvido na criação do endpoint.
Calcule o HMAC sobre o corpo bruto da requisição. Nunca faça
JSON.parse e reserialize antes de assinar — qualquer diferença de espaçamento invalida a comparação.Verificação
import crypto from "node:crypto";
export function verifyPitraSignature(rawBody, header, secret, toleranceSec = 300) {
if (!header) return false;
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const ts = Number(parts.t);
if (!ts || Math.abs(Date.now() / 1000 - ts) > toleranceSec) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 ?? "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express: use express.raw({ type: "application/json" })
app.post("/webhooks/pitra", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
if (!verifyPitraSignature(raw, req.header("Pitra-Signature"), process.env.PITRA_WEBHOOK_SECRET)) {
return res.status(400).send("assinatura inválida");
}
res.status(200).send("ok"); // responda rápido
queue.push(JSON.parse(raw)); // processe de forma assíncrona
});Proteção contra replay
Rejeite eventos cujo t esteja fora de uma janela de tolerância — 300 segundos é um valor recomendado. Combine isso com a deduplicação por Pitra-Event-Id para que um evento capturado por um atacante não possa ser reprocessado.
Sempre compare hashes com função de tempo constante (timingSafeEqual, compare_digest, hash_equals).