Firma de webhooks
Cada delivery incluye una firma HMAC-SHA256. Verificarla evita aceptar requests falsificados o alterados.
OrderFast-Event-Id: evt_019f7067f8b47b4e
OrderFast-Timestamp: 1784294108
OrderFast-Signature: v1=0c240b7f...Contenido firmado
El mensaje se construye sin espacios adicionales:
{timestamp}.{raw_body}raw_body son exactamente los bytes recibidos. Serializar otra vez el JSON puede cambiar espacios, escapes u orden de claves y producir una firma distinta.
Algoritmo
- Leer
OrderFast-TimestampyOrderFast-Signature. - Rechazar headers faltantes o mal formados.
- Convertir el timestamp Unix y comprobar que está dentro de la tolerancia acordada.
- Concatenar el timestamp, un punto y el body crudo.
- Calcular HMAC-SHA256 con el secreto del endpoint.
- Codificar el digest en hexadecimal y anteponer
v1=. - Comparar los bytes en tiempo constante.
- Solo entonces parsear y procesar el JSON.
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWebhook({ rawBody, timestamp, signature, secret }) {
const signed = Buffer.concat([Buffer.from(`${timestamp}.`, "utf8"), rawBody]);
const expected = `v1=${createHmac("sha256", secret)
.update(signed)
.digest("hex")}`;
const receivedBytes = Buffer.from(signature, "utf8");
const expectedBytes = Buffer.from(expected, "utf8");
return (
receivedBytes.length === expectedBytes.length &&
timingSafeEqual(receivedBytes, expectedBytes)
);
}Configurá el framework para conservar rawBody como Buffer. No pases primero por JSON.parse().
Python
import hashlib
import hmac
def verify_webhook(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
signed = timestamp.encode("utf-8") + b"." + raw_body
digest = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
expected = f"v1={digest}"
return hmac.compare_digest(expected, signature)Tolerancia temporal
La comprobación del timestamp limita ataques de replay. Usá la tolerancia publicada por el ambiente; si todavía no está definida en el contrato, acordala antes de producción y mantené los relojes sincronizados. No aceptes timestamps arbitrariamente antiguos solo porque la firma es válida.
Rotación de secretos
Durante una rotación pueden coexistir temporalmente dos secretos:
- verificar primero con el secreto nuevo;
- si falla, verificar con el anterior mientras dure la ventana;
- registrar qué versión validó, sin registrar el secreto ni la firma completa;
- retirar el secreto anterior al cerrar la rotación.
Fallas comunes
| Problema | Resultado |
|---|---|
| Parsear y serializar JSON antes de verificar | Cambian los bytes y falla la firma. |
| Proxy que transforma el body | La aplicación ya no recibe el contenido firmado. |
Comparación con == |
Puede filtrar información temporal. |
| Reloj desincronizado | Se rechazan deliveries válidas o se amplía la ventana de replay. |
Secreto de test usado en live |
Todas las firmas fallan. |
Respondé 401 ante una firma inválida y no modifiques ningún recurso de negocio. Nunca registres el secreto ni el header completo de firma.