Verificar la firma

Comprueba que cada webhook viene de Caibo y no fue alterado. Código listo en Node.js, Python y Go.

Cada webhook trae Psp-Signature:

Psp-Signature: t=1790769600,v1=c0c9eaafdea23b0f8c992b380b286917c6459b0729b2496b8452fa695215e6b8
  • t es el momento de la firma, en segundos Unix.
  • v1 es HMAC-SHA256 en hexadecimal, con tu secreto como clave, sobre t + . + el cuerpo exacto que recibiste.
  • La clave es el secreto completo, incluido el prefijo whsec_.
  • Puede venir más de un v1 (durante una rotación). Basta con que uno coincida.

Para aceptar un webhook:

  1. Calcula el HMAC con el cuerpo sin tocar: no lo parsees ni lo vuelvas a serializar antes.
  2. Compara con cada v1 en tiempo constante.
  3. Rechaza si t difiere de tu hora en más de 5 minutos: así nadie puede reenviarte un aviso viejo.
const crypto = require("crypto");

// rawBody: the exact bytes received (a Buffer).
function verifyCaiboSignature(secret, header, rawBody, toleranceSeconds = 300) {
  let t = null;
  const signatures = [];
  for (const part of String(header || "").split(",")) {
    const [key, value] = part.trim().split("=", 2);
    if (key === "t") {
      if (t !== null || !/^\d+$/.test(value)) return false;
      t = value;
    } else if (key === "v1" && /^[0-9a-f]{64}$/i.test(value)) {
      signatures.push(Buffer.from(value, "hex"));
    }
  }
  if (t === null || signatures.length === 0) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest();
  if (!signatures.some((s) => crypto.timingSafeEqual(s, expected))) return false;
  return Math.abs(Date.now() / 1000 - Number(t)) <= toleranceSeconds;
}

// Express: read the body raw, verify, then parse.
app.post("/webhooks/caibo", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyCaiboSignature(process.env.CAIBO_WEBHOOK_SECRET, req.get("Psp-Signature"), req.body)) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(req.body);
  queue.push(event); // process later; dedupe on event.id
  res.sendStatus(200);
});

Prueba tu código

Con estos valores tu función debe aceptar la firma (ignorando la tolerancia, porque t es una fecha pasada):

Secretowhsec_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
Cuerpo (exacto, sin salto de línea final){"id":"evt_1","type":"payout.completed","data":{"amount":"10.00"}}
t1790769600
Firma esperadat=1790769600,v1=c0c9eaafdea23b0f8c992b380b286917c6459b0729b2496b8452fa695215e6b8

Para probar contra tu servidor real, usa POST/v1/webhook-endpoints/{id}/ping.

El error más común

Muchos frameworks parsean el JSON antes de que lo veas. Si firmas JSON.stringify(req.body) en lugar del cuerpo original, la firma no coincide. Lee el cuerpo crudo.

En esta página