Cómo funcionan las firmas de webhook
Un webhook no es más que un POST HTTP desde el servidor de otro, y cualquiera puede hacer POST a tu URL. Para demostrar que la petición viene del proveedor, este calcula un HMAC sobre el cuerpo con un secreto que solo conocéis los dos y lo envía en una cabecera. Tú lo recalculas y comparas. El secreto nunca viaja; solo el hash.
Por qué ocurre «invalid signature»
- Hasheaste el cuerpo analizado, no los bytes en bruto. Los frameworks que analizan JSON antes de que se ejecute tu manejador (Express con
express.json(), Rails, Django) te entregan un objeto; volver a serializarlo cambia los espacios y el orden de las claves y el hash ya no coincide. Captura el cuerpo en bruto. - Secreto equivocado. Stripe tiene un
whsec_distinto por endpoint y por entorno; el secreto de GitHub es por webhook; el signing secret de Slack no es el token del bot. - El proveedor firma más que el cuerpo. Stripe firma
timestamp.body; Slack firmav0:timestamp:body; Svix firmaid.timestamp.body; Twilio firma la URL más los campos del formulario ordenados. La receta de cada uno se muestra arriba al elegirlo. - Codificación. Algunos proveedores envían hex, otros Base64; algunos anteponen
sha256=. Compara las formas. - Proxies. Un proxy o CDN que recodifica el cuerpo (gzip, juego de caracteres) o un límite de tamaño que lo trunca romperá el hash.
Comparar con seguridad
En producción, compara con una función de tiempo constante (crypto.timingSafeEqual, hmac.compare_digest, hash_equals) y comprueba que la marca de tiempo es reciente (Stripe y Slack permiten cinco minutos) para detener los replays. Esta página es un depurador, no una implementación de referencia; usa una comparación normal porque los ataques de temporización contra tu propio navegador no son una preocupación.
Preguntas
¿Mi secreto está seguro aquí?
El HMAC se calcula en tu navegador con WebCrypto y la página no hace peticiones de red. Aun así, usa un secreto de endpoint de prueba y rota cualquier cosa que pegues en una página web si quieres ser estricto.
¿Por qué GitHub envía X-Hub-Signature y X-Hub-Signature-256?
La primera es HMAC-SHA1, conservada para integraciones antiguas; la segunda es HMAC-SHA256 y es la que hay que verificar. Ambas se calculan sobre el cuerpo en bruto con el mismo secreto.
El valor esperado coincide salvo en mayúsculas. ¿Es un fallo?
El hex no distingue mayúsculas en el código de verificación de ningún proveedor, así que no; esta herramienta también compara el hex sin distinguir mayúsculas. Base64 sí distingue mayúsculas.
Stripe dice que la marca de tiempo es demasiado antigua.
Las bibliotecas de Stripe rechazan las firmas cuyo t= está a más de 300 segundos de la hora actual, para evitar replays. Reproducir una petición capturada más tarde fallará por esa razón aunque el HMAC sea correcto; la herramienta indica la antigüedad.