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=c0c9eaafdea23b0f8c992b380b286917c6459b0729b2496b8452fa695215e6b8tes el momento de la firma, en segundos Unix.v1esHMAC-SHA256en hexadecimal, con tu secreto como clave, sobret+.+ 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:
- Calcula el HMAC con el cuerpo sin tocar: no lo parsees ni lo vuelvas a serializar antes.
- Compara con cada
v1en tiempo constante. - Rechaza si
tdifiere 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):
| Secreto | whsec_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef |
| Cuerpo (exacto, sin salto de línea final) | {"id":"evt_1","type":"payout.completed","data":{"amount":"10.00"}} |
t | 1790769600 |
| Firma esperada | t=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.