HMAC es una construcción criptográfica que combina una clave secreta con una función hash para producir un código de autenticación de mensaje. A diferencia de un hash normal, solo quien conoce la clave puede recalcular el valor correcto. Un HMAC válido indica que el contenido no fue modificado y que fue generado por una parte que poseía el secreto compartido.
El módulo hmac de la biblioteca estándar implementa el algoritmo definido en RFC 2104. Se utiliza para validar webhooks, autenticar mensajes entre servicios, proteger configuraciones distribuidas, firmar cookies sencillas y verificar payloads de colas o APIs. Esta guía cubre generación, validación segura, canonicalización, replay attacks y rotación de claves.
Hash normal o HMAC
Un digest SHA-256 no contiene ningún secreto. Cualquier persona que cambie el mensaje puede calcular otro hash. Un checksum confiable detecta corrupción, pero no autentica por sí solo al remitente.
HMAC incorpora una clave mediante una construcción estandarizada. No inventes alternativas como sha256(secret + message). Las combinaciones caseras pueden introducir problemas de extensión, límites ambiguos o gestión incorrecta de la clave.
Consulta la guía de hashlib en Python para hashes generales y el artículo de contraseñas y login seguro para almacenamiento de credenciales. HMAC resuelve un problema diferente: autenticación de mensajes.
Crear HMAC-SHA256
import hmac
import hashlib
key = b"secreto-aleatorio-del-servidor"
message = b"pedido=123&importe=49.90"
mac = hmac.new(key, message, digestmod=hashlib.sha256)
print(mac.hexdigest())
Clave y mensaje deben ser objetos bytes-like. Para texto, define UTF-8 de forma explícita:
key = "secreto del servidor".encode("utf-8")
message = "acción=confirmar".encode("utf-8")
digestmod es obligatorio. SHA-256 es una elección habitual. HMAC requiere un hash de longitud fija, por lo que SHAKE-128 y SHAKE-256 no son compatibles.
Atajo hmac.digest()
Cuando todo el mensaje ya está en memoria, hmac.digest() ofrece una llamada compacta y puede usar una implementación optimizada.
signature = hmac.digest(key, message, "sha256")
print(signature.hex())
Para streams o archivos grandes, crea un objeto HMAC y llama a update() por bloques.
Procesamiento incremental
from pathlib import Path
import hmac
import hashlib
def hmac_file(path: Path, key: bytes) -> str:
mac = hmac.new(key, digestmod=hashlib.sha256)
with path.open("rb") as file:
while chunk := file.read(1024 * 1024):
mac.update(chunk)
return mac.hexdigest()
Este patrón evita cargar archivos completos en memoria y sigue el enfoque del artículo sobre leer archivos gigantes con Python.
Verificación con compare_digest()
No compares tags de autenticación con ==. Una comparación normal puede detenerse en la primera diferencia, generando variaciones de tiempo. Muchas mediciones podrían revelar información.
def verify_signature(
key: bytes,
message: bytes,
supplied_hex: str,
) -> bool:
expected = hmac.new(
key,
message,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, supplied_hex)
Los valores deben tener el mismo tipo. Para strings usa solo ASCII, como un digest hexadecimal. Las diferencias de longitud pueden revelar el tamaño, así que valida el formato antes de comparar.
Validar un webhook
Un webhook normalmente envía el cuerpo bruto y una firma en una cabecera. Calcula HMAC sobre exactamente los bytes definidos por el proveedor.
def verify_webhook(body: bytes, header: str, secret: bytes) -> None:
if not header.startswith("sha256="):
raise ValueError("Formato de firma inválido")
supplied = header.removeprefix("sha256=")
if len(supplied) != 64:
raise ValueError("Longitud de firma inválida")
expected = hmac.new(secret, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, supplied):
raise PermissionError("Firma inválida")
No conviertas JSON a objetos y lo serialices de nuevo antes de verificar. Espacios, orden de claves, escapes y saltos de línea modifican los bytes. Lee el cuerpo bruto, valida la firma y después procesa el JSON.
Canonicalización del mensaje
Cuando defines un protocolo, especifica campos, orden, encoding y separadores. Una concatenación ingenua puede ser ambigua: ("ab", "c") y ("a", "bc") producen lo mismo sin límites.
def canonical_message(timestamp: int, method: str, path: str, body: bytes) -> bytes:
prefix = f"v1\n{timestamp}\n{method.upper()}\n{path}\n".encode("utf-8")
return prefix + body
Incluye una versión para permitir que el esquema evolucione sin confundir firmas nuevas y antiguas.
Protección contra replay
Un HMAC válido no impide que un atacante capture y reenvíe la misma solicitud. Los comandos sensibles deben autenticar timestamp, nonce o identificador de evento.
import time
MAX_AGE = 300
if abs(time.time() - timestamp) > MAX_AGE:
raise PermissionError("Mensaje expirado")
Guarda los nonces aceptados durante la ventana. El orden seguro es: validar estructura y límites, comprobar tiempo, calcular HMAC, comparar, registrar el nonce y finalmente ejecutar la acción.
Generación y almacenamiento de claves
Una clave HMAC necesita suficiente entropía y debe proceder de una fuente criptográficamente segura.
import secrets
key = secrets.token_bytes(32)
No derives la clave de una frase corta, no la incluyas en el repositorio ni la muestres en logs. Usa un secret manager, una variable de entorno protegida o un archivo con permisos restrictivos.
Rotación de claves
Asocia cada clave a un identificador público como key_id. Durante la rotación firma solo con la nueva, pero acepta temporalmente la anterior para mensajes creados antes del cambio.
KEYS = {
"2026-08": b"nueva-clave...",
"2026-07": b"clave-anterior...",
}
Valida y limita el identificador. No pruebes cientos de claves para cada solicitud controlada por un atacante.
Truncamiento de la firma
Algunos protocolos transmiten solo una parte del HMAC para ahorrar espacio. Esto reduce la resistencia frente a falsificación. Prefiere el digest completo salvo que un estándar revisado defina un mínimo aceptable. Nunca aceptes prefijos de longitud arbitraria.
Firmas de API
Los esquemas de firma suelen incluir método HTTP, ruta canónica, query ordenada, timestamp y hash del cuerpo. Cliente y servidor deben normalizar todo de forma idéntica. La guía para integrar APIs con Python complementa estas prácticas con timeouts y validación.
HMAC no cifra
El mensaje sigue siendo visible. HMAC proporciona integridad y autenticidad, no confidencialidad. Usa TLS para transporte y cifrado autenticado para datos secretos almacenados. No reutilices una clave para HMAC y cifrado; deriva claves separadas.
Errores frecuentes
Los fallos más comunes son comparar con ==, firmar JSON reserializado, omitir timestamp, aceptar tags de longitud variable, usar claves predecibles, registrar secretos, reutilizar una clave para varios propósitos y ejecutar la acción antes de verificar.
Buenas prácticas
Usa HMAC-SHA256, una clave aleatoria de al menos 32 bytes, un mensaje canónico versionado y compare_digest(). Autentica timestamp y nonce, limita tamaños, rota claves por identificador y registra únicamente metadatos no sensibles. Prefiere la biblioteca oficial del proveedor cuando su esquema sea complejo.
Conclusión
El módulo hmac proporciona una forma estándar de autenticar mensajes con una clave secreta y una función hash. La llamada es sencilla; la seguridad depende de los bytes firmados, la comparación, la gestión de claves y la protección contra replay. Con una especificación clara, HMAC es robusto para webhooks y comunicación entre servicios.
Consulta la documentación oficial de hmac y el RFC 2104.







