Hashing y autenticación de mensajes: integridad real sin confusiones

Capítulo 5

Tiempo estimado de lectura: 9 minutos

+ Ejercicio

Hashing vs autenticación: dos objetivos distintos

Un hash criptográfico (p. ej., SHA-256) toma un mensaje y produce una huella fija. Sirve para detectar cambios cuando ya confías en el canal o en la referencia (el “valor esperado”). No usa clave.

La autenticación de mensajes (p. ej., HMAC o un esquema AEAD que autentica) busca integridad + autenticidad: que el mensaje no fue modificado y que proviene de quien conoce una clave. Aquí sí hay secreto compartido (HMAC) o clave de cifrado (AEAD).

  • Hash: “¿Este archivo cambió respecto a la huella que ya tengo?”
  • HMAC/AEAD: “¿Este mensaje lo generó alguien con la clave y no fue alterado?”

Propiedades clave: colisiones y preimagen (y por qué importan)

Colisiones

Una colisión ocurre si dos entradas distintas producen el mismo hash. En hashes modernos (SHA-256/512) es computacionalmente inviable encontrar colisiones prácticas, pero el concepto es importante porque muchos usos inseguros se basan en asumir “imposible” donde solo es “difícil”.

Riesgo típico: usar hash como “identificador único” en un contexto adversarial (por ejemplo, aceptar un documento si su hash coincide con uno permitido). Si un atacante pudiera fabricar una colisión, podría sustituir contenido sin cambiar la huella.

Preimagen y segunda preimagen

  • Resistencia a preimagen: dado un hash h, es inviable encontrar un mensaje m tal que hash(m)=h.
  • Resistencia a segunda preimagen: dado un mensaje m, es inviable encontrar otro m2 distinto con el mismo hash.

Estas propiedades sostienen usos como fingerprints y verificación de integridad, pero no sustituyen autenticación: un atacante puede modificar el mensaje y recalcular el hash, porque no hay clave que lo impida.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

Usos correctos de hashes (y límites prácticos)

1) Fingerprints (huellas) para verificación manual o fuera de banda

Escenario: publicas un instalador y también publicas su SHA-256 en un canal más confiable (por ejemplo, una página con TLS y un segundo canal como un repositorio verificado). El usuario descarga el archivo y compara.

# Ejemplo conceptual (Linux/macOS): calcular huella SHA-256 de un archivo descargado sha256sum producto_v1.2.3.zip # Comparar con el valor publicado por el proveedor

Clave: la seguridad depende de que el valor esperado llegue por un canal que el atacante no pueda modificar al mismo tiempo que el archivo.

2) Deduplicación y direccionamiento por contenido (no seguro)

Usar hashes para detectar duplicados o como clave de caché es válido si no hay adversario intentando forzar colisiones o inferir contenido. Ejemplos: almacenamiento interno, cachés, pipelines de build.

  • Correcto: key = SHA-256(bytes) para identificar blobs en un sistema interno.
  • Precaución: si el sistema es multi-tenant o expuesto, el hash puede filtrar información (p. ej., “¿este archivo existe?”) o permitir ataques de enumeración.

3) Verificación de integridad en canales confiables

Si el canal ya es autenticado (por ejemplo, un enlace interno con autenticación fuerte y controles), un hash puede servir como checksum robusto para detectar corrupción accidental.

Pero si el canal es hostil o el atacante puede modificar tanto el mensaje como el hash, el hash no aporta integridad real.

Cuándo necesitas HMAC o AEAD (integridad con clave)

Necesitas HMAC cuando:

  • El mensaje viaja por un canal donde un atacante puede modificarlo.
  • Quieres que solo quien conoce la clave pueda producir un tag válido.
  • No necesitas confidencialidad, solo autenticidad/integridad (o ya cifras por otro lado).

Necesitas AEAD cuando:

  • Quieres confidencialidad + integridad en un solo esquema.
  • Quieres autenticar también datos asociados (headers, metadatos) sin cifrarlos.

Regla práctica: hash ≠ MAC. Si hay adversario activo, usa HMAC/AEAD.

Por qué “hash + sal” no reemplaza cifrado (ni autenticación)

“Sal” (salt) es un valor aleatorio que se concatena al dato antes de hashearlo. Se usa principalmente para contraseñas (para evitar tablas arcoíris y hacer únicos los hashes). Fuera de ese contexto, se malinterpreta.

Malentendido 1: “Si le pongo sal, ya está protegido”

Un hash con sal sigue siendo determinista dado (dato, sal). Si el atacante ve salt y hash(salt||dato), puede probar candidatos (ataque de diccionario/fuerza bruta) si el dato tiene baja entropía (IDs, emails, tokens cortos).

Malentendido 2: “Hash con sal es como cifrar”

El cifrado es reversible con clave; el hash no. Para “ocultar” datos, el hash no sirve: solo los destruye. Si necesitas recuperar el dato original, necesitas cifrado. Si necesitas integridad/autenticidad, necesitas MAC/AEAD.

Malentendido 3: “Hash con sal autentica”

Si la sal no es secreta, cualquiera puede recalcular el hash tras modificar el mensaje. Si la “sal” se mantiene secreta, ya no es una sal: es una clave. En ese caso, lo correcto es usar HMAC (diseñado y analizado para eso), no inventar hash(secret||message).

Patrones prácticos para APIs: firmar y validar payloads

Diseño base: firma con HMAC sobre una representación canónica

Objetivo: que el servidor pueda verificar que el cliente (o un intermediario autorizado) generó la solicitud y que no fue alterada.

Componentes típicos a firmar:

  • Método HTTP (GET/POST…)
  • Ruta (path) y query
  • Timestamp
  • Nonce (opcional pero recomendado)
  • Body (o su hash)
  • Identificador de clave (key id) para rotación

Ejemplo de “string to sign” (conceptual):

METHOD
/path
canonical_query
timestamp
nonce
SHA256(body)

Luego:

tag = HMAC-SHA256(secret_key, string_to_sign) signature = base64(tag)

El cliente envía timestamp, nonce, key_id y signature en headers. El servidor reconstruye exactamente el mismo string_to_sign y verifica.

Guía paso a paso (implementación mental, independiente de lenguaje)

  1. Define qué se firma: método, path, query, headers relevantes, body. Evita firmar headers volátiles (p. ej., User-Agent).
  2. Normaliza (canoniza) cada parte: ver sección de normalización. Esto evita discrepancias entre cliente y servidor.
  3. Calcula hash del body (si el body puede ser grande): body_hash = SHA-256(body_bytes).
  4. Construye el string to sign con separadores inequívocos (por ejemplo \n) y un orden fijo.
  5. Calcula HMAC con una clave por cliente/servicio: HMAC-SHA-256.
  6. Envía la firma en un header estándar interno, por ejemplo: X-Signature, junto con X-Timestamp, X-Nonce, X-Key-Id.
  7. En el servidor: valida formato, reconstruye el string, verifica HMAC en tiempo constante, valida anti-replay (timestamp/nonce), y solo entonces procesa.

Errores comunes al firmar APIs

  • Firmar JSON “tal cual”: el mismo objeto puede serializarse con distinto orden de claves o espacios. Resultado: firmas que no verifican.
  • Olvidar incluir el método o la ruta: permite reusar una firma válida en otro endpoint (confusión de contexto).
  • Usar hash(secret||message): diseño casero; usa HMAC.
  • No proteger contra replay: una solicitud capturada puede repetirse.

Protección contra replay: timestamps, nonces y ventanas

Una firma válida puede ser capturada y reenviada. Para mitigarlo, combina:

Timestamps + ventana de tolerancia

  • El cliente envía timestamp (UTC, epoch seconds o ms).
  • El servidor acepta solo si abs(now - timestamp) <= window (por ejemplo, 300s).

Esto limita el tiempo de reutilización, pero no evita replays dentro de la ventana.

Nonces (uso único) + almacenamiento

  • El cliente genera un nonce aleatorio por solicitud.
  • El servidor guarda los nonces vistos por key_id durante la ventana y rechaza repetidos.

Implementación típica: almacenar (key_id, nonce) en Redis con TTL igual a la ventana. Rechazar si ya existe.

Ventanas y relojes desincronizados

Si hay riesgo de relojes desincronizados, usa una ventana razonable y monitorea rechazos por skew. Alternativa: un endpoint de “time sync” autenticado o usar tiempos del servidor en un desafío previo, pero eso complica el flujo.

Normalización antes de hashear o firmar: evitar discrepancias

La causa más común de fallos de verificación no es la criptografía, sino que cliente y servidor no firman exactamente los mismos bytes. La regla: firma bytes, no estructuras. Para lograrlo, define una representación canónica.

Canonización de query strings

  • Ordena parámetros por nombre y luego por valor.
  • Usa un esquema de encoding único (por ejemplo, percent-encoding consistente).
  • Define cómo tratar parámetros repetidos (a=1&a=2).

Ejemplo conceptual: canonical_query = sort_and_encode(params).

Canonización de JSON

Opciones:

  • JSON canónico: ordenar claves, sin espacios, números normalizados, UTF-8 estricto.
  • Firmar el hash del body raw: si controlas que el body se envía como bytes exactos (p. ej., el cliente firma el body ya serializado y el servidor verifica contra el body recibido). Esto evita reserializar, pero exige que proxies no reescriban el body.

Si eliges JSON canónico, documenta reglas exactas (orden de claves, tratamiento de null, floats, Unicode normalizado). Evita floats si puedes: diferencias de representación rompen firmas.

Normalización de Unicode

Dos cadenas visualmente iguales pueden tener bytes distintos (composición vs descomposición). Si firmas texto introducido por usuarios, define una normalización (por ejemplo, NFC) antes de construir el payload a firmar.

Separadores y ambigüedad

Evita concatenar campos sin delimitadores claros. Mejor:

  • Usar \n entre campos y escapar \n dentro de campos, o
  • Usar un formato estructurado canónico (por ejemplo, un objeto con claves fijas) y luego canonizarlo.

Tabla rápida: qué usar según el problema

NecesitasHerramientaNotas
Detectar corrupción accidentalHash (SHA-256)Canal ya confiable o referencia fuera de banda
Comparar/identificar contenidoHashNo asumir seguridad ante adversarios
Integridad + autenticidad sin cifrarHMACIncluye anti-replay si aplica
Confidencialidad + integridadAEADAutentica también datos asociados
“Proteger” datos de baja entropíaNo usar hash simplePara contraseñas: KDF con sal; para datos recuperables: cifrado

Checklist operativo para firmar requests en producción

  • Define un esquema de firma versionado (p. ej., v1) para poder evolucionar.
  • Incluye contexto en lo firmado: método, ruta, query, timestamp, nonce, body_hash.
  • Canoniza query y define reglas para JSON (o firma bytes exactos del body recibido).
  • Verifica HMAC en tiempo constante y rechaza si falta cualquier campo.
  • Implementa anti-replay con timestamp + nonce y almacenamiento con TTL.
  • Soporta rotación de claves con key_id y múltiples claves activas.
  • Registra métricas: fallos por skew, nonces repetidos, firmas inválidas (sin loguear secretos ni payloads sensibles).

Ahora responde el ejercicio sobre el contenido:

En una API expuesta a un adversario activo que puede modificar solicitudes en tránsito, ¿qué enfoque proporciona integridad y autenticidad reales del mensaje y por qué?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

La autenticación requiere una clave: HMAC/AEAD permiten verificar que el mensaje no fue alterado y que lo generó alguien con la clave. Un hash (con o sin sal pública) no impide que un atacante modifique el mensaje y vuelva a calcular la huella.

Siguiente capítulo

Contraseñas y derivación de claves: almacenamiento seguro y políticas técnicas

Arrow Right Icon
Portada de libro electrónico gratuitaCriptografía aplicada para profesionales: qué usar, cuándo y por qué.
38%

Criptografía aplicada para profesionales: qué usar, cuándo y por qué.

Nuevo curso

13 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.