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).

API v1 · OpenAPI 1.0.0 · SDK 0.1.0