Verificar la firma de un webhook
Cualquiera que conozca la dirección de tu endpoint puede enviarle un POST. La firma demuestra que el aviso lo envió foliios y que nadie lo modificó. Verifícala siempre antes de procesar el evento.
La cabecera
Sección titulada «La cabecera»X-Webhook-Signature: t=1790000000,v1=<64 dígitos hexadecimales>t: hora Unix (segundos) en que se firmó la petición. Cada intento firma de nuevo con su hora.v1: HMAC-SHA256, en hexadecimal minúscula, con el secreto del endpoint como clave, calculado sobre el texto"<t>.<cuerpo>": el valor det, un punto y los bytes crudos del cuerpo.
Durante una rotación de secreto la cabecera puede traer varias entradas v1. Basta con que coincida alguna.
- Lee el cuerpo crudo, antes de interpretarlo como JSON. Si lo vuelves a serializar, los bytes cambian y la firma no coincide.
- Separa
ty todos losv1de la cabecera. - Rechaza si
testá lejos de tu hora actual. Se recomienda una tolerancia de 5 minutos: evita que alguien reenvíe un aviso viejo. - Calcula
HMAC-SHA256(secreto, t + "." + cuerpo)en hexadecimal. - Compáralo con cada
v1en tiempo constante (no con==). - Si ninguno coincide, responde
400y no proceses el evento.
Guarda esto como verificar_firma.py:
import hashlibimport hmacimport time
def verificar(secreto: str, cabecera: str, cuerpo: bytes, tolerancia: int = 300) -> bool: """True si la firma corresponde al cuerpo y a la hora de la cabecera.""" t = None firmas = [] for parte in cabecera.split(","): clave, _, valor = parte.strip().partition("=") if clave == "t": t = valor elif clave == "v1": firmas.append(valor) if t is None or not t.isdigit() or not firmas: return False if abs(time.time() - int(t)) > tolerancia: return False esperada = hmac.new(secreto.encode(), t.encode() + b"." + cuerpo, hashlib.sha256).hexdigest() return any(hmac.compare_digest(esperada, firma) for firma in firmas)import { createHmac, timingSafeEqual } from 'node:crypto';
export function verificar(secreto, cabecera, cuerpo, tolerancia = 300) { // `cuerpo` es un Buffer con los bytes crudos de la petición. let t = null; const firmas = []; for (const parte of cabecera.split(',')) { const [clave, ...resto] = parte.trim().split('='); if (clave === 't') t = resto.join('='); else if (clave === 'v1') firmas.push(resto.join('=')); } if (t === null || !/^\d+$/.test(t) || firmas.length === 0) return false; if (Math.abs(Date.now() / 1000 - Number(t)) > tolerancia) return false; const esperada = createHmac('sha256', secreto).update(`${t}.`).update(cuerpo).digest(); return firmas.some((firma) => { const recibida = Buffer.from(firma, 'hex'); return recibida.length === esperada.length && timingSafeEqual(recibida, esperada); });}Con un servidor HTTP de Node, acumula los Buffer del cuerpo y pásalos a verificar sin convertirlos antes a texto.
Probar tu verificación
Sección titulada «Probar tu verificación»Firma un cuerpo tú mismo, con un secreto de prueba, y comprueba que tu función lo acepta y que rechaza uno alterado:
import hashlibimport hmacimport time
from verificar_firma import verificar
secreto = "secreto-de-prueba"cuerpo = b'{"id":"01JEJEMPLO","type":"document.accepted"}'t = str(int(time.time()))v1 = hmac.new(secreto.encode(), t.encode() + b"." + cuerpo, hashlib.sha256).hexdigest()cabecera = f"t={t},v1={v1}"
assert verificar(secreto, cabecera, cuerpo)assert not verificar(secreto, cabecera, cuerpo + b" ") # cuerpo alteradoassert not verificar("otro-secreto", cabecera, cuerpo) # secreto equivocadovieja = f"t={int(t) - 3600},v1={v1}"assert not verificar(secreto, vieja, cuerpo) # hora fuera de toleranciaSi quieres un vector fijo que no dependa de la hora, este sirve para comprobar solo el cálculo (sin la tolerancia): con el secreto Jefe, t=0 y el cuerpo what do ya want for nothing?, el v1 es 37f471929915ccd2cbbe79feb84ffcff4f2bb25e15fc41c2506687331ae179cc. Puedes reproducirlo con printf '0.what do ya want for nothing?' | openssl dgst -sha256 -hmac Jefe.
Para una prueba de punta a punta, dispara un evento con POST /v1/sandbox/events (webhooks) y verifica el que llegue a tu endpoint.
Errores frecuentes
Sección titulada «Errores frecuentes»- Verificar sobre el JSON ya interpretado. El cuerpo se firma tal como se envió; usa los bytes crudos.
- Comparar con
==. Usa la comparación en tiempo constante. - Olvidar la tolerancia de la hora. Sin ella, un aviso capturado vale para siempre.
- Usar el secreto de otro ambiente. El secreto es del endpoint de cada empresa y ambiente.
- Guardar el secreto en el código. Va en una variable de entorno o un gestor de secretos, como las claves de API (ambientes y claves).