Ir al contenido

Webhooks e integraciones

Cómo verificar una firma HMAC-SHA256

Guía general para validar firmas HMAC-SHA256 sobre webhooks, evitar errores comunes y adaptar el proceso al contrato de cada proveedor.

Por Equipo AltokePayPublicado Actualizado 9 min de lectura

HMAC permite comprobar que un mensaje fue firmado por alguien que conoce un secreto compartido y que los bytes protegidos no cambiaron después de la firma. En integraciones HTTP, algunos proveedores publican un valor HMAC en un encabezado y esperan que el receptor calcule el suyo sobre el cuerpo recibido. Si ambos coinciden, el mensaje supera esa comprobación de autenticidad e integridad.

Eso no demuestra que el contenido sea correcto en términos de negocio, no cifra la carga y no garantiza seguridad absoluta. Tampoco existe un formato universal. Cada proveedor define qué bytes se firman, cómo codifica el resultado, qué encabezado utiliza, si incorpora marca temporal y cómo rota secretos. Hay que seguir su documentación literalmente; el ejemplo de este artículo describe un contrato hipotético y autocontenido.

Qué aporta HMAC y qué no

HMAC combina una función hash con una clave secreta. El RFC 2104 especifica la construcción HMAC y explica su propósito. SHA-256 puede actuar como función hash dentro de esa construcción. Es importante distinguir HMAC-SHA256 de calcular simplemente SHA-256(secreto + mensaje): no son operaciones equivalentes ni deben intercambiarse.

Una verificación correcta puede aportar dos señales:

  • el firmante probablemente conocía el secreto compartido;
  • el mensaje firmado conserva los mismos bytes usados al producir la firma.

No aporta confidencialidad. Si la carga contiene información sensible, HTTPS sigue siendo necesario durante el transporte y deben aplicarse controles de acceso, minimización y retención. HMAC tampoco impide por sí solo que un mensaje válido sea reenviado. La defensa frente a repeticiones suele combinar identidad de evento, reglas temporales definidas por el contrato e idempotencia.

El contrato hipotético del ejemplo

Supongamos que un proveedor ficticio documenta lo siguiente:

  • el secreto está disponible como GENERIC_WEBHOOK_SECRET;
  • se firma el cuerpo HTTP exacto, antes de analizar JSON;
  • el algoritmo elegido para este caso ficticio es HMAC-SHA256;
  • el resultado se codifica en hexadecimal minúsculo;
  • el encabezado es x-generic-signature;
  • su valor tiene el prefijo sha256=.

Estas decisiones solo pertenecen al ejemplo. Otro proveedor puede usar Base64, firmar una concatenación de campos o publicar varias firmas para facilitar una rotación. Copiar el procedimiento sin confirmar el contrato produce rechazos o, peor, verificaciones incompletas.

Verificación en TypeScript con Node.js

La función siguiente recibe el cuerpo como Buffer para conservar sus bytes. Usa únicamente node:crypto, obtiene el secreto de una variable de entorno y compara valores binarios de igual longitud con timingSafeEqual.

import { createHmac, timingSafeEqual } from "node:crypto";

type VerificationInput = {
  rawBody: Buffer;
  signatureHeader: string | undefined;
};

export function hasValidGenericSignature({
  rawBody,
  signatureHeader,
}: VerificationInput): boolean {
  const secret = process.env.GENERIC_WEBHOOK_SECRET;
  if (!secret || !signatureHeader?.startsWith("sha256=")) return false;

  const providedHex = signatureHeader.slice("sha256=".length);
  if (!/^[0-9a-f]{64}$/.test(providedHex)) return false;

  const expected = createHmac("sha256", secret).update(rawBody).digest();
  const provided = Buffer.from(providedHex, "hex");

  return provided.length === expected.length && timingSafeEqual(provided, expected);
}

La documentación oficial de Node.js detalla createHmac y timingSafeEqual. Esta última requiere entradas con la misma longitud; por eso el código valida el formato y la longitud antes de comparar. Una comparación constante reduce filtraciones asociadas a abandonar la operación en el primer byte diferente, aunque no vuelve constante todo el comportamiento del endpoint.

El uso dentro de un manejador debe respetar un orden estricto:

  1. leer el cuerpo con un límite de tamaño;
  2. conservar los bytes originales;
  3. obtener el encabezado según el runtime;
  4. verificar la firma;
  5. rechazar sin procesar si falla;
  6. analizar y validar el JSON solo después;
  7. registrar el identificador e iniciar procesamiento idempotente.

Por qué el cuerpo original es decisivo

Dos textos JSON pueden representar el mismo objeto y tener bytes distintos. Cambiar espacios, saltos de línea, orden de propiedades o escapes modifica el resultado HMAC. Este patrón es incorrecto cuando el contrato firma el cuerpo original:

const parsed = JSON.parse(rawBody.toString("utf8"));
const reconstructed = JSON.stringify(parsed);

reconstructed no tiene por qué coincidir con la entrada. Algunos frameworks analizan el cuerpo automáticamente y descartan la representación original. En ese caso, configura una ruta que entregue bytes sin transformar o utiliza la capacidad oficial del framework para acceder al cuerpo crudo. No improvises reconstrucciones.

También importa dónde ocurre la descompresión o transformación. Un proxy podría alterar la representación antes de que la aplicación la vea. El contrato debe indicar qué representación está protegida y la arquitectura de despliegue debe conservarla hasta la verificación.

Manejo de marcas temporales y repeticiones

Algunos contratos firman una marca temporal junto con el cuerpo y piden rechazar mensajes demasiado antiguos. Otros no lo hacen. No inventes un separador, una unidad de tiempo ni una ventana de tolerancia: una diferencia mínima rompe la interoperabilidad. Sigue el algoritmo y la tolerancia publicados por el proveedor.

Una regla temporal puede reducir cierto riesgo de reproducción, pero introduce dependencia del reloj y no sustituye una identidad única. Un evento legítimo podría volver a entregarse dentro del periodo permitido. Guarda y reclama atómicamente su identificador antes de producir efectos irreversibles.

La firma tampoco establece el orden de eventos. Si record.updated llega antes de record.created, la lógica debe decidir si consulta el estado actual, aplaza el evento o aplica una versión monotónica. Esa decisión pertenece al dominio, no a HMAC.

Rotación y custodia del secreto

Los secretos deben generarse con suficiente entropía, mantenerse fuera del repositorio y limitarse al servicio que verifica. Las variables de entorno son un placeholder razonable para ejemplos; en producción puede ser preferible un gestor de secretos con auditoría y permisos restringidos.

Durante una rotación, el proveedor puede permitir dos secretos activos o enviar múltiples firmas. El receptor debe implementar exactamente ese mecanismo y retirar el secreto anterior cuando termine el periodo documentado. Nunca registres el secreto, la cadena usada para firmar ni encabezados sensibles completos. Si necesitas diagnóstico, registra el identificador del evento, el resultado de la validación y una razón general.

El OWASP Cryptographic Storage Cheat Sheet ofrece criterios generales sobre gestión de claves y algoritmos. No reemplaza el modelo de amenazas ni las instrucciones del proveedor.

Errores frecuentes

Una revisión rápida debería buscar estos fallos:

  • calcular un hash simple en lugar de HMAC;
  • analizar y volver a serializar el JSON antes de firmar;
  • confundir hexadecimal con Base64;
  • omitir el prefijo o firmarlo cuando no corresponde;
  • comparar cadenas con ===;
  • aceptar la solicitud si falta el secreto local;
  • compartir un secreto entre entornos sin necesidad;
  • registrar credenciales para depurar;
  • ejecutar la acción antes de verificar;
  • asumir que firma válida significa evento no duplicado.

Al fallar la verificación, responde con el código que indique el contrato y evita revelar si falló el formato, el secreto o un byte concreto. Para operación interna sí conviene distinguir errores de configuración, encabezados ausentes y firmas inválidas mediante métricas protegidas.

Cómo probar sin exponer credenciales

Crea fixtures locales con un secreto exclusivo de pruebas y cuerpos conocidos, incluidos espacios y caracteres Unicode. Comprueba al menos: firma válida, cuerpo alterado, firma truncada, codificación inválida, encabezado ausente y secreto no configurado. Si el contrato incorpora tiempo, fija el reloj de la prueba y cubre sus límites documentados.

Una prueba de integración debe enviar bytes reales al endpoint, no llamar solo a la función. Así detecta middleware que transforma el cuerpo. Añade luego casos repetidos para confirmar que una segunda entrega válida no repite la acción.

Comprender qué es un webhook y cuándo utilizarlo da contexto al mecanismo. La verificación HMAC es una capa concreta dentro de un receptor más amplio: HTTPS, validación de esquema, límites, autorización, idempotencia, observabilidad y recuperación. Su valor depende de implementar el contrato exacto, conservar los bytes correctos y proteger el secreto durante todo su ciclo de vida.

Lecturas relacionadas