Verificación de firma de webhook
Esta guía explica cómo Fluxis firma los webhooks salientes y cómo verificar que (1) la solicitud provino de Fluxis y (2) el cuerpo JSON no fue alterado.
Referencia de implementación: src/pkg/utils/crypto_signature.go (GenerateRequestSignature, VerifyRequestSignature, toCanonicalJSON).
Relacionado: Resumen de webhooks · Guía de integración API
::::note Webhooks a nivel de cuenta
Las URLs de webhook y los secretos de firma están vinculados a una cuenta (POST /v1/account/{accountID}/webhook), no a Puntos de Venta individuales. Los eventos de pago incluyen point_of_sale_id en el payload cuando corresponde.
::::
Qué demuestra la verificación
| Propiedad | Significado |
|---|---|
| Autenticidad | Solo alguien con tu secret de webhook podría producir una firma válida |
| Integridad | El cuerpo JSON no fue modificado en tránsito |
Rechaza solicitudes que fallen la verificación. No proceses el cumplimiento de pagos hasta que pasen las comprobaciones de firma y marca de tiempo.
Requisitos previos
- Secreto de webhook — Devuelto una vez en la respuesta a
POST /v1/account/{accountID}/webhook. Guárdalo de forma segura; no puede recuperarse de nuevo. - Endpoint HTTPS — Tu servidor debe aceptar
POSTconContent-Type: application/json. - Cuerpo parseado — Parsea el JSON primero, luego re-canonicaliza para firmar. Nunca firmes bytes crudos de la solicitud.
Algoritmo
Fluxis usa HMAC-SHA256 sobre una cadena determinista construida a partir de la marca de tiempo y el cuerpo del webhook:
Code
Encabezados de entrega:
| Encabezado | Valor |
|---|---|
X-Request-Signature | HMAC-SHA256 codificado en hexadecimal |
X-Request-Timestamp | Segundos Unix (cadena, p. ej. "1712345678") |
Content-Type | application/json |
Validación de marca de tiempo: Rechaza si now - timestamp > 300 segundos (WEBHOOK_MAX_AGE_SECONDS). Las marcas de tiempo futuras no se rechazan en la implementación actual del servidor — puedes agregar comprobaciones más estrictas en tu manejador si lo deseas.
Objeto firmado
Firma el cuerpo completo del webhook como objeto JSON, típicamente:
Code
No firmes solo data, y no firmes la cadena del cuerpo HTTP crudo antes de parsear.
Reglas de JSON canónico
Fluxis construye JSON canónico vía toCanonicalJSON:
- Serializa el valor a JSON (las etiquetas de struct y
omitemptyaplican — campos nil/cero se omiten). - Deserializa en
map[string]any/ arrays para que objetos anidados se conviertan en maps. - Vuelve a serializar —
json.Marshalde Go en maps produce JSON compacto (sin espacios) con claves ordenadas alfabéticamente en cada nivel de objeto.
Tu verificador debe reproducir este comportamiento:
| Regla | Detalle |
|---|---|
| Compacto | Sin espacios: {"a":1} no { "a": 1 } |
| Claves ordenadas | En cada nivel de objeto, incluido data anidado |
| Tal como se recibió | Campos opcionales ausentes en el payload deben permanecer ausentes en forma canónica |
| No bytes crudos | Middleware que reformatea JSON antes de verificar romperá las firmas |
::::note omitempty
Si external_reference_id o account_id no está definido, esas claves se omiten del JSON que Fluxis firma. Reconstruir el cuerpo desde un esquema fijo que siempre incluye esas claves producirá una firma diferente.
::::
Lista de verificación
- Lee
X-Request-SignatureyX-Request-Timestampde los encabezados. - Parsea el cuerpo de la solicitud en un objeto genérico (
map/dict). - Rechaza si
now - timestamp > 300. - Serializa el objeto parseado a JSON canónico (claves ordenadas, compacto).
- Construye
signed_string = timestamp + "." + canonical_json. - Calcula
HMAC-SHA256(secret, signed_string), codifica en hex y compara con igualdad de tiempo constante (tras verificar longitud).
Ejemplo trabajado
Usa estos valores fijos para probar tu implementación:
| Entrada | Valor |
|---|---|
| Secret | whsec_example_for_documentation_only |
| Timestamp | 1712345678 |
| Body | Ver JSON abajo |
Cuerpo (objeto parseado):
Code
JSON canónico (una línea):
Code
Cadena firmada:
Code
Firma esperada (hex):
Code
Si tu implementación produce esta firma para las entradas anteriores, coincide con la firma de producción de Fluxis.
Ejemplos de código
Node.js / TypeScript
Code
::::warning timingSafeEqual en Node.js
crypto.timingSafeEqual lanza error si las longitudes de buffer difieren. Compara longitudes primero (como arriba), o usa hmac.compare_digest en Python.
::::
Python
Code
Go
Coincide con toCanonicalJSON y VerifyRequestSignature de producción:
Code
Probar tu manejador
- Registra un webhook:
POST /v1/account/{accountID}/webhookcon tu URL pública yevent_type: "payment_request". - Guarda el
secretde la respuesta de creación. - Llama
POST /v1/account/{accountID}/webhook/{webhookID}/test— Fluxis hace POST de un payload de muestra firmado a tu URL usando la misma ruta de código que producción (GenerateRequestSignatureen{ "data": … }). - Verifica la firma en tu manejador y devuelve HTTP
200. - Opcionalmente compara con el cálculo manual usando el ejemplo trabajado arriba.
Consulta los intentos de entrega en GET /v1/account/{accountID}/webhook/logs?page=1&page_size=20.
Fallos comunes
| Síntoma | Causa probable |
|---|---|
| Firma siempre inválida | Secreto incorrecto (no el de la creación); firmar bytes crudos en lugar de JSON parseado |
| Inválida intermitente | Campos opcionales agregados/eliminados vs lo firmado; middleware mutando req.body |
| Firma válida, rechazada igual | Desfase de reloj mayor a 300 s; usar marca de tiempo incorrecta (debe coincidir con el encabezado X-Request-Timestamp) |
| El manejador Node falla al comparar | timingSafeEqual llamado con cadenas hex de longitud distinta — agrega verificación de longitud primero |
| Desajuste en Go con structs anidados | Firmar struct sin round-trip por any — usa el patrón canonicalJSON arriba |
| La prueba funciona, producción falla | Payload de producción sin campos que tu vector de prueba siempre incluye |
Notas de seguridad
- Usa comparación de tiempo constante (
hmac.Equal,hmac.compare_digest,timingSafeEqualcon buffers de igual longitud). - Rechaza marcas de tiempo obsoletas (mayores a 300 segundos).
- Procesa la lógica de negocio solo después de que la verificación de firma tenga éxito.
- Guarda el secreto de webhook en un gestor de secretos, no en el control de versiones.
- Usa HTTPS para tu endpoint; trata el secreto como una credencial API.
¿Necesitas ayuda?
Contacta [email protected] con una copia redactada del cuerpo recibido, encabezados (omite el secreto) y tu JSON canónico calculado si la verificación falla.