# Verificación de firma de webhooks: HMAC-SHA256 y protección contra repetición

> Un webhook se autentica mediante una firma HMAC-SHA256 que el emisor calcula sobre un timestamp más el cuerpo en bruto exacto, usando un secreto compartido, y envía en un header. El receptor recalcula el HMAC, lo compara en tiempo constante y rechaza la petición si la firma no coincide o el timestamp cae fuera de una ventana de tolerancia (protección contra repetición).

- URL canónica: https://www.broadnet.me/es/reference/webhook-signature-verification
- Idioma: es
- Última actualización: 2026-07-19
- Editor: BroadNet (https://www.broadnet.me)

Cómo verificar que un webhook es auténtico y no ha sido manipulado: HMAC-SHA256 sobre el cuerpo en bruto, los headers de firma y timestamp, protección contra repetición con una ventana de tolerancia de timestamp y un nonce, comparación en tiempo constante para frustrar ataques de temporización, y rotación segura de secretos con claves solapadas.

## Por qué firmas, y no listas de IP permitidas

Un endpoint de webhook es una URL HTTPS pública, así que cualquiera puede hacerle POST. TLS prueba que el cliente alcanzó tu servidor; no prueba en absoluto que la petición provenga realmente de la plataforma de mensajería. Las listas de IP permitidas son frágiles (las IP del emisor cambian y fallan detrás de una salida compartida). La respuesta estándar es un código de autenticación de mensaje con clave: el emisor y el receptor comparten un secreto, el emisor firma cada payload con él y el receptor verifica. HMAC (RFC 2104) con SHA-256 (RFC 6234) es la construcción omnipresente porque es rápida, está estandarizada y —a diferencia de un hash simple del secreto concatenado con el mensaje— es demostrablemente resistente a los ataques de extensión de longitud. Una firma válida prueba dos cosas a la vez: el payload lo produjo un poseedor del secreto, y no se alteró en tránsito.

## Los headers de firma y timestamp

El emisor incluye al menos dos headers: un timestamp (cuándo se firmó la petición) y una firma. El contenido firmado no es el JSON por sí solo, sino una cadena canónica que liga el timestamp al cuerpo —normalmente el timestamp, un separador y el cuerpo en bruto de la petición— para que un atacante no pueda trasladar una firma válida a un payload distinto ni repetirla en otro momento. La nomenclatura varía según el proveedor (Webhook-Signature / Webhook-Timestamp, o X-Signature / X-Timestamp), pero la construcción es la misma. La firma suele ser hexadecimal o base64 y a menudo lleva un prefijo de esquema (por ejemplo v1,) para poder rotar el secreto a un nuevo esquema sin romper los verificadores antiguos.

*Contenido firmado y disposición de headers*

```text
signed_content = timestamp + '.' + raw_body
signature      = HMAC_SHA256(secret, signed_content)

Webhook-Timestamp: 1752916522
Webhook-Signature: v1,3f2b8c9a1d...   # hex or base64, scheme-prefixed
```

## Verificar contra el cuerpo en bruto

El error de verificación más común con diferencia es firmar un cuerpo reserializado. El HMAC se calcula sobre bytes exactos, así que el receptor debe verificar contra el cuerpo en bruto de la petición precisamente como se recibió, antes de cualquier parseo y reencodeado del JSON. Parsear a un objeto y volver a serializar reordena las claves, cambia los espacios en blanco y normaliza números y escapes Unicode, produciendo bytes distintos y una firma que no coincide aunque nada se haya manipulado. En la práctica esto significa capturar el cuerpo en bruto en el framework web antes de que el middleware de parseo lo consuma, verificar el HMAC sobre esos bytes y solo entonces parsear el JSON para usarlo. El juego de caracteres también importa: firma y verifica la misma secuencia de octetos (UTF-8), no una cadena decodificada.

*Verificar (pseudocódigo) — cuerpo en bruto, tiempo constante*

```text
raw   = request.raw_body            # exact bytes, pre-parse
ts    = header('Webhook-Timestamp')
sig   = header('Webhook-Signature')

if abs(now() - ts) > TOLERANCE: reject   # replay window
expected = hmac_sha256(secret, ts + '.' + raw)
if not constant_time_equal(expected, sig): reject
accept(parse_json(raw))
```

## Protección contra repetición: tolerancia de timestamp y nonce

Una firma por sí sola no detiene la repetición: una petición capturada pero válida podría reenviarse más tarde. Dos mecanismos cierran esa brecha. Primero, como el timestamp está dentro del contenido firmado, el receptor lo compara con su propio reloj y rechaza cualquier cosa más antigua que una tolerancia (comúnmente unos minutos, lo bastante amplia para absorber el desfase de reloj y el retardo de reintento, lo bastante estrecha para limitar la ventana de repetición). Segundo, para garantías más estrictas el receptor registra un nonce —el id único del evento— durante el periodo de tolerancia y rechaza una segunda petición que porte un id que ya ha visto. La tolerancia de timestamp acota la ventana; la caché de nonces hace ineficaz la repetición dentro de esa ventana. Nótese que esto es complementario a la idempotencia del consumidor, que gestiona los duplicados legítimos y no las repeticiones maliciosas.

*Controles de protección contra repetición*

| Control | Defiende contra | Ajuste |
|---|---|---|
| Marca de tiempo en el contenido firmado | Trasladar una firma a otro momento | Debe estar firmado, no un simple encabezado |
| Ventana de tolerancia de marca de tiempo | Reproducción diferida de una solicitud capturada | Pocos minutos; equilibrio entre desfase de reloj y exposición |
| Caché de nonce/ID de evento | Repetición (replay) dentro de la ventana de tolerancia | Guardar en caché los ID durante al menos la ventana de tolerancia |
| Comparación en tiempo constante | Canal lateral de temporización en la firma | Siempre, independientemente de lo anterior |

## Comparación en tiempo constante

Comparar las firmas calculada y recibida con una comprobación de igualdad ordinaria filtra información: una comparación byte a byte retorna antes ante una discrepancia temprana, y un atacante que pueda medir ese tiempo puede recuperar la firma correcta byte a byte. Por tanto, la verificación debe usar una comparación en tiempo constante (independiente de la longitud) —la comparación segura frente a temporización de la biblioteca estándar de cada plataforma— que siempre examina toda la longitud antes de retornar. Es barata e innegociable; marca la diferencia entre un esquema de firma que es sólido y uno que es vulnerable con suficientes peticiones cronometradas.

## Rotación de secretos

Los secretos de firma deben poder rotarse sin descartar webhooks legítimos a mitad de vuelo. El patrón seguro son las claves solapadas: se aprovisiona al emisor un nuevo secreto mientras el antiguo sigue siendo válido, y durante el solape el emisor o bien firma con ambos (emitiendo dos firmas) o el receptor acepta una firma que coincida con cualquiera de los secretos. Una vez confirmado que todos los productores están en el nuevo secreto, el antiguo se retira. El prefijo de esquema en la firma (v1, v2,) hace esto observable y permite acompañarlo de un cambio de algoritmo versionado. El validador de firmas de webhook alojado por BroadNet permite comprobar un payload, timestamp y secreto contra un HMAC esperado sin escribir código; la mecánica de la firma HMAC-SHA256 de DLR se trata en la biblioteca de respuestas.

## Preguntas frecuentes

### ¿Cómo se calcula la firma de un webhook?

El emisor construye una cadena canónica que liga el timestamp al cuerpo en bruto (normalmente timestamp + '.' + cuerpo), calcula HMAC-SHA256 sobre ella con un secreto compartido y coloca el resultado en un header de firma junto al timestamp. El receptor recalcula el mismo HMAC y compara. Una coincidencia prueba que el payload provino de un poseedor del secreto y no se alteró.

### ¿Por qué debo verificar contra el cuerpo en bruto de la petición?

El HMAC se calcula sobre bytes exactos. Si parseas el JSON y lo reserializas, cambian el orden de las claves, los espacios en blanco, el formato de los números y los escapes Unicode, así que los bytes difieren y la firma no coincide aunque nada se haya manipulado. Captura el cuerpo en bruto antes del middleware de parseo, verifica y luego parsea.

### ¿Cómo detiene la tolerancia de timestamp los ataques de repetición?

El timestamp forma parte del contenido firmado, así que no puede alterarse sin romper la firma. El receptor lo compara con su propio reloj y rechaza cualquier cosa más antigua que una tolerancia (comúnmente unos minutos). Eso acota cuánto tiempo puede repetirse una petición válida capturada; una caché de nonces cierra las repeticiones dentro de esa ventana.

### ¿Por qué se exige la comparación en tiempo constante?

Una comprobación de igualdad ordinaria retorna antes ante el primer byte discrepante, y un atacante que pueda cronometrar la respuesta puede recuperar la firma correcta byte a byte. Una comparación en tiempo constante siempre examina toda la longitud antes de retornar, eliminando el canal lateral de temporización. Usa la función de comparación segura frente a temporización de la plataforma.

### ¿Cómo roto de forma segura un secreto de firma de webhook?

Usa claves solapadas. Aprovisiona el nuevo secreto mientras el antiguo sigue válido; durante el solape el emisor firma con ambos o el receptor acepta cualquiera, de modo que no se descarta ningún webhook legítimo. Una vez confirmados todos los productores en el nuevo secreto, retira el antiguo. Un prefijo de esquema en la firma hace observable la transición.

### ¿No basta con HTTPS para confiar en un webhook?

No. TLS prueba que el transporte estaba cifrado y que el cliente alcanzó tu servidor, pero no que la petición provenga de la plataforma de mensajería: cualquiera puede hacer POST a una URL pública. Solo una firma HMAC con secreto compartido autentica al emisor y prueba que el payload no se modificó. Las listas de IP permitidas son un sustituto débil porque las IP de salida cambian.

## Fuentes

- [IETF RFC 2104 — HMAC: Keyed-Hashing for Message Authentication](https://datatracker.ietf.org/doc/html/rfc2104)
- [IETF RFC 6234 — US Secure Hash Algorithms (SHA and HMAC-SHA)](https://datatracker.ietf.org/doc/html/rfc6234)

## Relacionado

- [Entrega y fiabilidad de webhooks](https://www.broadnet.me/es/reference/webhook-delivery-and-reliability)
- [Estado de mensajes y webhooks entrantes](https://www.broadnet.me/es/reference/message-status-and-inbound-webhooks)
- [Autenticación y firma de la API](https://www.broadnet.me/es/reference/api-authentication-and-signing)
- [Validador de firmas de webhook](https://www.broadnet.me/es/tools/webhook-signature-validator)
- [Firma HMAC-SHA256 de DLR](https://www.broadnet.me/es/answers/hmac-sha256-dlr-signing)
