Saltar al contenido principal
Listo manda los webhooks a una URL que tú le diste, sin ninguna credencial en la petición. La firma es la credencial. Es lo que te dice que un evento vino de Listo, llegó sin alteraciones, y no es uno que alguien capturó antes y volvió a mandar. Cada evento lleva un encabezado:
  • t es la hora Unix, en segundos, en la que se firmó el evento.
  • v1 es un HMAC-SHA256 en hexadecimal, con tu secreto de firma como llave, sobre la cadena "<t>.<raw body>": el valor de t, un punto, y el cuerpo de la petición tal como llegó.
Fíjate en que t va dentro de la cadena que se firma, no nada más al lado. Ahí está toda la diferencia contra un HMAC simple del cuerpo: uno simple sigue siendo válido para siempre, así que cualquiera que haya visto una sola petición válida podría reenviarla cuando se le antojara. Meter la marca de tiempo dentro del resumen obliga a que una repetición o conserve el t original, y caiga fuera de la ventana de abajo, o lo cambie, e invalide la firma.

Cómo verificar una

verify_webhook_signature.rb
Cuatro reglas, y cada una es una forma de que la verificación esté mal mientras aparenta estar bien. Firma los bytes tal como llegaron. Sobre lo que llegó por el cable, antes de cualquier parseo de JSON. Parsear y volver a serializar cambia los bytes, porque tu librería de JSON tiene sus propias ideas sobre el orden de las llaves, las secuencias de escape y los espacios, y la firma cubre lo que se mandó, no lo que tu librería habría mandado. Casi cualquier framework web te da el cuerpo sin procesar si se lo pides; averigua cómo lo hace el tuyo antes de escribir la verificación. Revisa la marca de tiempo en las dos direcciones. Rechaza cualquier cosa con más de cinco minutos de antigüedad, que es la repetición que este esquema existe para frenar, y cualquier cosa con más de cinco minutos hacia el futuro. Saltarte la segunda mitad le regala a un atacante una ventana arbitrariamente larga con solo escoger un t grande. Compara en tiempo constante. Usa la comparación de tiempo constante de tu lenguaje, no ==. Una comparación que se corta en el primer byte distinto filtra qué tanto acertó el intento, y con eso basta para ir sacando una firma válida byte por byte. Falla cerrado. Un encabezado mal formado, un encabezado ausente, una marca de tiempo que no se puede leer y una firma que no coincide son todos la misma respuesta: no es auténtico. Rechaza con un 400, y no parsees ni actúes sobre el cuerpo. Nunca dejes que el verificador lance una excepción; una excepción en un verificador es un 500 donde iba un rechazo.

Cómo rotar un secreto

Puedes tener dos secretos vivos al mismo tiempo, y eso es lo que hace posible una rotación sin perder eventos. Listo firma con el principal; tú aceptas cualquiera de los dos.
  1. Agrega un segundo secreto en el panel de socios. Todavía no cambia nada: Listo sigue firmando con el primero.
  2. Enséñale los dos a tu verificador. Por eso la verificación de arriba recibe una lista: acepta un evento que valide con cualquiera de los dos.
  3. Promueve el nuevo secreto. Listo empieza a firmar con él. Tu endpoint sigue funcionando durante el cambio, porque ya acepta los dos.
  4. Quita el viejo, en el panel y en tu propia configuración.
Hazlo en ese orden. Si promueves antes del paso 2, cada evento en vuelo lo rechaza un endpoint que nunca ha visto el secreto nuevo.
Un secreto es un secretoGuarda los secretos de firma como guardas las contraseñas: del lado del servidor, fuera de tu repositorio, fuera de cualquier cosa que alcance un navegador. Quien tenga uno puede falsificar un evento que tu verificador va a aceptar, que es lo mismo que poder decirle a tu sistema que un pedido se entregó.

Cada modo tiene sus propios secretos

El mundo real y el de prueba tienen cada uno su URL de endpoint y su secreto de firma. Un secreto de un modo nunca valida un evento del otro, y eso es lo que impide que un despliegue de staging pueda aceptar tráfico de producción. Rótalos por separado. Poner todo esto a trabajar, incluyendo cómo responder, cómo deduplicar y cómo funcionan los reintentos, es recibe y verifica webhooks.