HMAC (Hash-based Message Authentication Code)

Authentication Security Notes Jan 6, 2025 JAVASCRIPT

Definition

Imagina que estás enviando una carta sellada a alguien importante. Quieres que estén absolutamente seguros de que la carta proviene de ti y no fue manipulada durante la entrega. En los viejos tiempos, los nobles usaban sellos de cera con su anillo de sello único: solo alguien con ese anillo específico podía crear ese sello exacto. Si el sello estaba roto o se veía diferente, el destinatario sabía que algo andaba mal. HMAC (Hash-based Message Authentication Code) es la versión digital de este sello de cera: crea una “firma” única para cualquier mensaje utilizando una clave secreta que solo tú y el destinatario conocen.

HMAC funciona combinando tu mensaje con una clave secreta y procesándolos a través de una función hash criptográfica (como SHA-256). El resultado es una cadena de caracteres de longitud fija que es única tanto para el contenido del mensaje COMO para la clave secreta. Si incluso un solo carácter del mensaje cambia, el HMAC cambia completamente. Si alguien no conoce la clave secreta, no puede generar un HMAC válido. Cuando el destinatario recibe tu mensaje, realiza el mismo cálculo con la misma clave secreta. Si su resultado coincide con el HMAC que enviaste, saben dos cosas: el mensaje definitivamente proviene de alguien que conoce la clave secreta (autenticación), y el mensaje no ha sido modificado desde que fue firmado (integridad).

HMAC está en todas partes en las APIs modernas, incluso si no lo ves directamente. Cuando usas las APIs de AWS, cada solicitud es firmada con HMAC-SHA256 usando tu clave de acceso secreta. Cuando llega un webhook de Stripe, incluye una firma HMAC para que puedas verificar que realmente proviene de Stripe. Cuando tu navegador establece una conexión HTTPS, HMAC es parte del handshake de seguridad. Es uno de los bloques fundamentales de la comunicación segura: un concepto simple (combinar mensaje + secreto + hash) que proporciona poderosas garantías de seguridad.

Example

Escenario Real 1: Firma de Solicitudes API de AWS Cuando realizas cualquier solicitud a AWS (subir a S3, lanzar instancias EC2, etc.), tu solicitud es firmada con HMAC-SHA256. Tu SDK de AWS toma los detalles de la solicitud (método HTTP, headers, URL, body), los combina con tu Clave de Acceso Secreta, y genera una firma. AWS recibe la solicitud, realiza el mismo cálculo con tu clave desde su base de datos, y verifica que las firmas coincidan. Esto demuestra que la solicitud provino de alguien con credenciales válidas Y que la solicitud no fue modificada en tránsito.

Escenario Real 2: Verificación de Webhooks Cuando Stripe envía un webhook a tu servidor notificándote de un pago, ¿cómo sabes que realmente es de Stripe y no de un atacante? Stripe incluye una firma HMAC en el header Stripe-Signature, calculada usando tu secreto de webhook. Tu servidor calcula el HMAC del body del webhook usando el mismo secreto. Si coinciden, sabes que el webhook es auténtico. Si no coinciden, lo rechazas. Esto evita que atacantes falsifiquen notificaciones de pago.

Escenario Real 3: Seguridad de API Key En lugar de enviar API keys en texto plano con cada solicitud (que podría ser interceptado), algunas APIs usan solicitudes firmadas con HMAC. La API de Coinbase Pro requiere que firmes cada solicitud con HMAC-SHA256 usando tu secreto de API, incluyendo un timestamp. La firma demuestra que tienes el secreto sin transmitirlo. El timestamp evita ataques de repetición: las solicitudes firmadas antiguas no pueden reutilizarse porque el timestamp estaría obsoleto.

Escenario Real 4: Autenticación Segura con Cookies Cuando inicias sesión en un sitio web y obtienes una cookie de “recordarme”, esa cookie a menudo contiene una firma HMAC. El servidor crea un valor de cookie como user_id=12345|timestamp=1699000000|signature=abc123. La firma es un HMAC de los otros campos usando un secreto del lado del servidor. Cuando regresas, el servidor verifica la firma: si alguien modificó el user_id para hacerse pasar por otro usuario, la firma no coincidiría.

Analogy

La Analogía del Sello de Cera: En tiempos históricos, las cartas importantes se sellaban con cera y se estampaban con un anillo de sello único. Solo el remitente tenía ese anillo específico, por lo que el destinatario podía verificar quién lo envió. Si el sello estaba roto o no coincidía con el patrón esperado, la carta había sido manipulada. HMAC es este anillo de sello para mensajes digitales: un sello único que prueba autenticidad e integridad.

La Verificación de la Receta Secreta: Imagina que un panadero envía un pastel a una competencia con una tarjeta sellada que contiene un HMAC. La tarjeta dice “Este es un pastel de zanahoria hecho con nuestro ingrediente secreto”. El HMAC se calcula a partir de los detalles de la receta más un secreto que solo el panadero y el juez comparten. El juez puede verificar que el HMAC sellado coincide con lo que calculan, demostrando que el pastel realmente proviene de ese panadero y que la descripción no fue intercambiada por un competidor.

El Desafío-Respuesta Militar: Las unidades militares usan códigos de desafío-respuesta: “¿Cuál es la contraseña?” “Thunderbird”. Pero con HMAC, es más sofisticado: “Aquí está el desafío aleatorio de hoy: 73829”. Combinas el desafío con tu contraseña secreta, lo hasheas, y respondes con el resultado. Solo alguien que conoce la contraseña real podría generar la respuesta correcta para ese desafío específico, y el desafío de mañana tendrá una respuesta correcta diferente.

El Sello Anti-manipulación con Número de Serie: Los paquetes modernos tienen sellos anti-manipulación con números de serie únicos. El fabricante registra cada número de serie y lo asocia con el contenido del paquete. Si alguien abre el paquete e intenta resellarlo con un sello falso, el número de serie no coincidirá con los registros del fabricante. HMAC funciona de manera similar: el “número de serie” (firma) está matemáticamente vinculado al contenido exacto del paquete (mensaje).

Code Example


// Generación de firma HMAC
const crypto = require('crypto');

function signRequest(message, secretKey) {
  return crypto
    .createHmac('sha256', secretKey)
    .update(message)
    .digest('hex');
}

// Ejemplo: Firmar una solicitud API
const method = 'POST';
const path = '/api/users';
const timestamp = Date.now();
const body = JSON.stringify({ name: 'John' });

// Crear el string de firma (formato canónico)
const message = `${method}\n${path}\n${timestamp}\n${body}`;
const signature = signRequest(message, 'your-secret-key');

// Enviar con la solicitud
fetch('https://api.example.com/api/users', {
  method: 'POST',
  headers: {
    'X-Signature': signature,
    'X-Timestamp': timestamp,
    'Content-Type': 'application/json'
  },
  body: body
});

// Verificar en el servidor
function verifySignature(req, secretKey) {
  const receivedSig = req.headers['x-signature'];
  const timestamp = req.headers['x-timestamp'];

  // Verificar frescura del timestamp (prevenir ataques de repetición)
  const now = Date.now();
  const requestTime = parseInt(timestamp);
  if (now - requestTime > 300000) { // 5 minutos
    return false; // Solicitud demasiado antigua
  }

  const message = `${req.method}\n${req.path}\n${timestamp}\n${req.body}`;
  const expectedSig = signRequest(message, secretKey);

  // Usar comparación timing-safe para prevenir ataques de timing
  return crypto.timingSafeEqual(
    Buffer.from(receivedSig),
    Buffer.from(expectedSig)
  );
}

// Verificación de webhook (estilo Stripe)
function verifyWebhook(payload, signatureHeader, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signatureHeader),
    Buffer.from(`sha256=${expectedSignature}`)
  );
}

Diagram

sequenceDiagram
    participant C as Cliente
    participant S as Servidor

    Note over C,S: Ambos comparten la misma CLAVE SECRETA
(establecida de forma segura previamente) Note over C: Construir mensaje a firmar:
método + ruta + timestamp + body Note over C: Calcular HMAC:
firma = HMAC-SHA256(mensaje, clave_secreta) C->>S: Solicitud con:
X-Signature: abc123...
X-Timestamp: 1699000000
Body: {"name": "John"} Note over S: Reconstruir el mismo mensaje desde la solicitud Note over S: Recalcular HMAC usando
la misma clave secreta alt Las Firmas Coinciden Note over S: Verificar que el timestamp es reciente
(prevenir ataques de repetición) S-->>C: 200 OK - Solicitud Auténtica else Las Firmas Difieren S-->>C: 401 Unauthorized
Firma Inválida else Timestamp Demasiado Antiguo S-->>C: 401 Unauthorized
Solicitud Expirada end Note over C,S: Proporciona INTEGRIDAD + AUTENTICACIÓN
La clave secreta nunca se transmite

Security Notes

SECURITY NOTES

CRÍTICO: La seguridad de HMAC depende de secretos fuertes e implementación adecuada. Vulnerable a ataques de repetición.

Gestión de Claves Secretas:

  • Criptográficamente aleatorio: Las claves deben ser aleatorias y suficientemente largas (32+ bytes)
  • Almacenamiento seguro: Usar sistemas de gestión de secretos (vault, AWS Secrets Manager)
  • Nunca registrar claves: Nunca registrar o exponer claves secretas en logs
  • Rotación de claves: Rotar claves regularmente (se recomienda cada 90 días)
  • Separar por cliente: Usar diferentes claves para cada cliente/servicio

Selección de Algoritmo Hash:

  • Usar SHA-256 o mejor: Usar SHA-256, SHA-384 o SHA-512
  • Evitar algoritmos débiles: Nunca usar MD5, SHA-1 o CRC32
  • Algoritmo consistente: Usar siempre el mismo algoritmo para verificación

Prevención de Ataques de Repetición:

  • Incluir timestamp: Añadir timestamp de creación del mensaje; rechazar solicitudes antiguas
  • Ventana de tiempo: Rechazar mensajes más antiguos de 5-15 minutos
  • Valor nonce: Incluir nonce único para protección adicional contra repetición
  • ID de solicitud: Prevenir reutilización del mismo ID de solicitud dentro de ventana de tiempo

Cálculo de Firma:

  • Incluir todos los datos relevantes: Firmar método, ruta, headers, body, timestamp
  • Formato canónico: Usar formato consistente para firma (sin espacios extra)
  • No firmar parcialmente: Incluir todo lo que podría ser manipulado
  • Comparación timing-safe: Usar comparación de tiempo constante para prevenir ataques de timing

Mejores Prácticas de Implementación:

  • HTTPS requerido: Siempre transmitir sobre HTTPS a pesar de la protección HMAC
  • Clave de firma separada: No mezclar clave HMAC con clave de cifrado
  • Versionar tu esquema: Soportar múltiples algoritmos para rotación
  • Manejo de errores: No filtrar validez de firma en mensajes de error
  • Monitorear: Registrar y alertar sobre fallos de verificación de firma

Standards & RFCs