Saltar al contenido
nvíame
Capítulo 2 de 513 min11 oct 2026

Firma y verificación de webhooks

Calcula firmas HMAC reales con WebCrypto y entiende cada paso de la verificación.

Tu endpoint de webhooks es una URL pública. Cualquiera puede enviarle un POST que diga "status": "delivered". La firma es lo único que distingue un evento real de uno fabricado.

Los pasos de la verificación

  • Lee el cuerpo crudo (bytes), no el objeto parseado.
  • Lee el timestamp y la firma de los headers.
  • Rechaza si el timestamp tiene más de 5 minutos.
  • Calcula HMAC(secret, timestamp + "." + rawBody).
  • Compara en tiempo constante con la firma recibida.

Firma real con WebCrypto

mx.webhook.sign(payload, secret) usa crypto.subtle del navegador: es HMAC-SHA256 real. Cambia un solo carácter del payload y observa cómo cambia toda la firma.

// HMAC-SHA256 real con WebCrypto dentro del worker
const secret = "whsec_demo_9f2c";
const ts = "1760536800";
const body = JSON.stringify({ id: "evt_91", type: "sms.delivered", to: "+56912345678" });

const firma = await mx.webhook.sign(ts + "." + body, secret);
console.log("firma:", firma);

// Simula la verificación en tu servidor
const recalculada = await mx.webhook.sign(ts + "." + body, secret);
console.log("válida:", firma === recalculada);

// Un atacante altera el cuerpo
const alterado = body.replace("delivered", "failed");
const falsa = await mx.webhook.sign(ts + "." + alterado, secret);
console.log("alterado válido:", firma === falsa);

Comparación en tiempo constante

Comparar strings con === termina en el primer carácter distinto, lo que filtra información por tiempo de respuesta. En Node usa crypto.timingSafeEqual; en otros entornos, compara byte a byte acumulando con XOR.

function safeEqual(a, b) {
  if (a.length !== b.length) return false;
  let diff = 0;
  for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
  return diff === 0;
}

Además de la firma, incluye una marca de tiempo en el contenido firmado y rechaza las peticiones con más de cinco minutos de antigüedad. Así un atacante que capture un webhook válido no puede reenviarlo días después.

Se completa automáticamente al leer el 80%.

Comentarios (3)

Diego Fernández11 oct 2026

¿Por qué mi firma no coincide si el JSON es idéntico?

Equipo nvíameAutor11 oct 2026

Porque probablemente lo re-serializaste. El orden de claves y los espacios cambian los bytes. Verifica siempre sobre el cuerpo crudo, tal como llegó, antes de parsearlo.

Sofía Castillo11 oct 2026

Lo apliqué en el proyecto del trabajo. La sección «Firma y verificación de webhooks» me ahorró una discusión larga con el equipo: ahora tenemos un criterio compartido.

Valentina Muñoz11 oct 2026

¿Hay alguna diferencia importante si lo implemento en un servicio con varias instancias detrás de un balanceador?

Equipo nvíameAutor11 oct 2026

Sí: todo estado que hoy vive en memoria (contadores, sesiones, claves vistas) debe moverse a un almacén compartido con TTL, como Redis. Si no, cada instancia ve solo una parte de la historia.

Los comentarios se guardan solo en este navegador.