La lección anterior terminó con una pregunta trampa: si la huella SHA-256 viaja junto al mensaje, ¿qué impide que un atacante sustituya las dos cosas? Nada — y esa es exactamente la diferencia entre integridad (los bytes no se corrompieron) y autenticidad (los bytes vienen de quien dice enviarlos). Esta lección presenta la herramienta que da las dos a la vez cuando emisor y receptor comparten una clave: el MAC (Message Authentication Code), y su encarnación estándar, HMAC. Por el camino entenderás por qué el atajo intuitivo hash(clave + mensaje) está roto (el length extension attack), y saldaremos dos deudas concretas de MediNube: firmar los webhooks que el portal envía a Clínica Sol y Centro Médico Luna, y dejar de guardar en claro el token de recuperación de contraseña que generamos en 01-03. Es la pieza que convierte el hash de la lección anterior en una garantía frente a enemigos, no solo frente a discos que corrompen bits.
Contenido
- La demostración: un hash a secas no autentica
- Qué es un MAC y qué garantiza (y qué no)
- Por qué
hash(clave + mensaje)está roto: length extension - HMAC: la construcción correcta
- HMAC en Python: el módulo
hmac - Caso central: firmar los webhooks de MediNube
- Deuda saldada: el hash del token de recuperación
- El MAC que ya usabas sin saberlo: el tag de AES-GCM
- Hash vs MAC vs firma digital
La demostración: un hash a secas no autentica
MediNube notifica por HTTP a las clínicas cuando hay novedades ("nueva cita creada", "informe disponible"). Primer intento heredado, encontrado en el código del portal:
# codigo heredado - notificador de webhooks
import hashlib, json
def enviar_webhook_MAL(evento: dict) -> dict:
cuerpo = json.dumps(evento).encode("utf-8")
huella = hashlib.sha256(cuerpo).hexdigest()
return {"cuerpo": cuerpo, "cabecera_integridad": huella}El receptor (Clínica Sol) recalcula el SHA-256 del cuerpo y lo compara con la cabecera. Parece razonable... hasta que lo miras como atacante. Supón que controlas un proxy entre MediNube y la clínica:
import hashlib, json
# El mensaje legitimo interceptado:
paquete = enviar_webhook_MAL({"evento": "cita_creada", "paciente": "ana.perez"})
# El atacante fabrica OTRO mensaje y recalcula el hash el mismo:
falso = json.dumps({"evento": "cita_cancelada", "paciente": "ana.perez"}).encode()
paquete["cuerpo"] = falso
paquete["cabecera_integridad"] = hashlib.sha256(falso).hexdigest()
# La verificacion del receptor... pasa sin problemas:
ok = hashlib.sha256(paquete["cuerpo"]).hexdigest() == paquete["cabecera_integridad"]
print(ok) # True - la clinica acepta el mensaje falsoLa verificación es matemáticamente correcta y completamente inútil: SHA-256 es una función pública (Kerckhoffs, regla de oro 2) que no requiere nada que el atacante no tenga. El hash solo liga la cabecera al cuerpo; no liga nada a MediNube. Para eso, en el cálculo tiene que intervenir algo que el atacante no conoce: una clave secreta compartida.
Qué es un MAC y qué garantiza (y qué no)
Un MAC es una función que toma una clave secreta y un mensaje y produce una etiqueta corta (el tag):
El receptor, que comparte la clave, recalcula el tag y lo compara. La propiedad de seguridad se llama resistencia a la falsificación existencial: un atacante que vea tantos pares (mensaje, tag) como quiera no puede producir un tag válido para ningún mensaje nuevo sin la clave. Con eso, un tag correcto demuestra dos cosas a la vez:
- Integridad: el mensaje no cambió ni un bit desde que se calculó el tag (el efecto avalancha del hash subyacente sigue trabajando para nosotros).
- Autenticidad: lo generó alguien que conoce la clave. Si solo la conocen MediNube y Clínica Sol, y la clínica no se lo envió a sí misma, vino de MediNube.
Igual de importante es lo que un MAC no da:
- No da confidencialidad. El mensaje viaja como esté (en claro, en nuestro webhook); el MAC solo lo autentica. Si además hay que ocultarlo, se cifra (con AEAD, que como veremos ya incluye su propio MAC).
- No da no repudio. La clave es compartida: cualquier mensaje que MediNube pudo autenticar, Clínica Sol también pudo fabricarlo (y viceversa). Ante un juez, "tiene un MAC válido" no prueba cuál de las dos partes lo generó. Cuando haga falta que solo una parte pueda generar la prueba y cualquiera verificarla — las recetas electrónicas que tenemos pendientes — necesitaremos firmas digitales con clave asimétrica: lección 04-03.
Por qué hash(clave + mensaje) está roto: length extension
Con la idea de "mezclar una clave", el siguiente código heredado casi parece la solución:
def tag_MAL(clave: bytes, mensaje: bytes) -> str:
return hashlib.sha256(clave + mensaje).hexdigest() # NO: length extensionEstá roto, y la razón enseña mucho sobre cómo funcionan los hashes por dentro. Recuerda la "picadora" Merkle–Damgård de 03-01: SHA-256 procesa el mensaje por bloques, actualizando un estado interno, y el hash final es, literalmente, el estado interno tras el último bloque. No hay ningún paso final que lo disfrace.
Consecuencia: si un atacante conoce sha256(clave + mensaje), conoce el estado interno de la picadora en ese punto. Puede cargar ese estado y seguir picando: calcular sha256(clave + mensaje + relleno + extra) para un extra de su elección, sin conocer la clave (solo necesita su longitud, que se adivina probando). Esto es el length extension attack:
flowchart LR
A[estado inicial] -->|clave + mensaje| B[estado S]
B -->|"= tag publicado"| C[el atacante LEE S del tag]
C -->|"+ relleno + extra"| D[tag valido del mensaje extendido]
Traducido a MediNube: del webhook legítimo {"evento": "cita_creada"...} con su tag_MAL, el atacante deriva un tag válido para ese mismo cuerpo con contenido extra añadido — sin la clave. En APIs reales (parámetros de URL firmados, por ejemplo) este ataque se ha explotado durante años; el caso célebre fue la API de Flickr en 2009.
Apuntes para completar el mapa:
- Afecta a los hashes Merkle–Damgård "puros": MD5, SHA-1, SHA-256, SHA-512. No afecta a SHA-3 (la esponja no expone su estado completo), ni a BLAKE2, ni a SHA-384 (salida truncada: faltan bits del estado).
hash(mensaje + clave)(la clave al final) evita este ataque pero hereda otros defectos (una colisión del hash se convierte en falsificación del MAC). Los apaños caseros siempre acaban así — regla de oro 1: no inventes tu propia criptografía, ni siquiera "combinaciones" propias de piezas buenas.
HMAC: la construcción correcta
HMAC (1996, RFC 2104) es la forma estandarizada y demostrada de construir un MAC a partir de cualquier función hash. Su esquema:
No hace falta memorizarlo, pero sí entender la jugada:
- La clave se mezcla dos veces, con dos constantes distintas (
ipad, relleno interno;opad, externo), y hay dos pasadas de hash anidadas. - La pasada externa es lo que mata el length extension: el estado interno del hash que procesó el mensaje ya no es el tag — el tag es el hash de ese resultado junto a la clave otra vez. El atacante no puede "seguir picando" porque el resultado intermedio nunca se publica.
- HMAC tiene prueba de seguridad: si el hash subyacente cumple propiedades razonables, HMAC es un MAC seguro. Tan robusto es el diseño que HMAC-MD5 sigue sin ataques prácticos pese a estar MD5 roto en colisiones — lo cual no es excusa para usarlo, pero da idea del margen.
HMAC-SHA256produce tags de 32 bytes y es el estándar de facto en APIs. Ya lo has usado sin mirarlo: es la pieza que PBKDF2 itera miles de veces y el motor interno de HKDF (02-04).
HMAC en Python: el módulo hmac
La biblioteca estándar lo trae todo:
import hmac, hashlib
clave = bytes.fromhex(
"8f4e2a11c96d3b70e5a8d4c2f01b9e6633d7a0558c1f4b2e9d60a3517c8e4f0b"
) # 32 bytes del CSPRNG, compartidos con la clinica (ficticia, por supuesto)
mensaje = b'{"evento": "cita_creada", "paciente": "ana.perez"}'
tag = hmac.new(clave, mensaje, hashlib.sha256).hexdigest()
print(tag) # p. ej. 3e19c19d...Desglose:
hmac.new(clave, mensaje, digestmod)crea y alimenta el HMAC. El tercer argumento (la función hash) es obligatorio especificarlo; usahashlib.sha256. Como los objetos dehashlib, admite.update()para mensajes por trozos y.digest()/.hexdigest()para el tag.- La clave debe salir del CSPRNG (regla de oro 3): 32 bytes de
secrets.token_bytes(32)al darla de alta. Nunca una contraseña ni un valor "memorable". - Para verificar, nunca
==:
def verificar(clave: bytes, mensaje: bytes, tag_recibido: str) -> bool:
esperado = hmac.new(clave, mensaje, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperado, tag_recibido)Aquí sí estamos comparando un secreto: el tag correcto es algo que el atacante no conoce y quiere adivinar. Un == normal se rinde en el primer byte distinto, y esa diferencia de tiempos, medida con paciencia, permite adivinar el tag byte a byte — el timing attack de 01-04. hmac.compare_digest (equivalente a secrets.compare_digest) tarda lo mismo coincida lo que coincida: regla de oro 6 aplicada donde toca. Contrasta con la huella pública de 03-01, donde == era legítimo — el criterio es siempre "¿es secreto lo que comparo?".
Caso central: firmar los webhooks de MediNube
Vamos a arreglar de verdad el notificador. El diseño sigue el patrón que usan Stripe, GitHub o Slack para sus webhooks, adaptado a nuestra casa:
- Cada clínica receptora tiene una clave de webhook propia (32 bytes de
secrets.token_bytes(32)), entregada al configurar la integración. Clave por clínica: si la de Centro Médico Luna se filtra, la de Clínica Sol sigue a salvo, y se puede rotar una sin tocar la otra (criptoagilidad, regla 8). - MediNube envía dos cabeceras:
X-MediNube-Timestamp(segundos Unix del envío) yX-MediNube-Firma(el tag HMAC, prefijado con la versión del esquema:v1=<hex>). - El HMAC no cubre solo el cuerpo, sino
timestamp || "." || cuerpo. El punto de separar con.y de incluir el timestamp es defenderse del replay: sin él, un atacante que capture un webhook legítimo ("informe disponible") podría reenviarlo mil veces días después, con firma perfectamente válida. Al firmar el timestamp, el receptor puede rechazar mensajes viejos.
# medinube/webhooks.py - lado emisor (MediNube)
import hmac, hashlib, time
def firmar_webhook(clave_clinica: bytes, cuerpo: bytes) -> dict:
"""Genera las cabeceras de autenticacion de un webhook saliente."""
timestamp = str(int(time.time()))
base = timestamp.encode("utf-8") + b"." + cuerpo
tag = hmac.new(clave_clinica, base, hashlib.sha256).hexdigest()
return {
"X-MediNube-Timestamp": timestamp,
"X-MediNube-Firma": f"v1={tag}",
}Y la verificación, que MediNube documenta para los desarrolladores de las clínicas:
# lado receptor (p. ej. el sistema de Clinica Sol)
import hmac, hashlib, time
TOLERANCIA = 300 # segundos: 5 minutos de reloj de margen
def verificar_webhook(clave: bytes, cuerpo: bytes,
timestamp: str, firma: str) -> bool:
# 1. Frescura: rechazar mensajes viejos (anti-replay)
if abs(time.time() - int(timestamp)) > TOLERANCIA:
return False
# 2. Version del esquema
if not firma.startswith("v1="):
return False
# 3. Recalcular y comparar en tiempo constante
base = timestamp.encode("utf-8") + b"." + cuerpo
esperado = hmac.new(clave, base, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperado, firma[3:])Detalles que marcan la diferencia entre este código y uno vulnerable:
- El timestamp verificado es el firmado. El atacante no puede "refrescar" un mensaje capturado cambiando la cabecera de timestamp, porque el tag dejaría de cuadrar. Replay bloqueado más allá de la ventana de 5 minutos (ventana que existe solo para tolerar relojes desincronizados). Si tu aplicación no tolera ningún replay ni dentro de la ventana, añade un identificador único de evento y deduplica — capa de aplicación, no de criptografía (regla 9).
- Se verifica sobre el cuerpo crudo (los bytes recibidos), no sobre el JSON re-serializado: dos serializadores pueden ordenar claves o espaciar distinto, y la firma no coincidiría aun siendo el mensaje legítimo. Firma bytes, verifica bytes.
- El prefijo
v1=es la misma jugada que el byte de versión del formato de historiales de 02-03: si mañana migramos el esquema de firma, ambos lados pueden convivir conv1yv2durante la transición. - Nota de contexto: los webhooks viajan además por HTTPS (Módulo 5), que ya cifra y autentica el canal. La firma HMAC autentica el mensaje de extremo a extremo: sobrevive a proxies, colas, reintentos y logs intermedios, y permite a la clínica validar el webhook aunque termine el TLS un balanceador ajeno.
Deuda del curso saldada: los webhooks salientes de MediNube van firmados.
Deuda saldada: el hash del token de recuperación
Segunda deuda, esta viene de 01-03. Allí generamos tokens de recuperación de contraseña con secrets.token_urlsafe(32) y los guardábamos tal cual en la base de datos — con la promesa de arreglarlo. El problema: si un atacante lee la BD (inyección SQL, backup filtrado), obtiene tokens válidos y utilizables para resetear la contraseña de cualquier usuario, incluida ana.perez.
La solución es la misma idea que aplicaremos a las contraseñas en 03-03, pero con una diferencia crucial que conviene entender ya: guardar el hash del token, no el token.
# medinube/recuperacion.py
import hashlib, secrets
def crear_token_recuperacion(usuario: str) -> str:
token = secrets.token_urlsafe(32) # 32 bytes de entropia = 256 bits
indice = hashlib.sha256(token.encode("utf-8")).hexdigest()
guardar_en_bd(usuario, indice) # se guarda el hash, no el token
return token # el token solo viaja al email
def canjear_token(token_presentado: str):
indice = hashlib.sha256(token_presentado.encode("utf-8")).hexdigest()
return buscar_en_bd(indice) # busqueda exacta por el hash- El token real solo existe en el email del usuario y en memoria durante la petición. La BD guarda
sha256(token): si se filtra, el atacante tiene huellas de las que no puede recuperar los tokens (resistencia a preimagen sobre una entrada de 256 bits de entropía — aquí sí es impredecible de verdad). - ¿Por qué basta un SHA-256 rápido, si en 03-01 dijimos que "hash rápido" y "secreto" no se llevan bien? Por la entropía de la entrada. Una contraseña humana se ataca por diccionario: el espacio de candidatos es pequeño y un hash rápido lo recorre volando. Este token tiene 256 bits de entropía del CSPRNG: no hay diccionario posible, el ataque de fuerza bruta es 2^256. La lentitud de Argon2 es un parche para la baja entropía humana; con entropía plena, sobra. Este contraste es exactamente la puerta de entrada de 03-03.
- Bonus de diseño: al ser el hash determinista, sirve como índice de búsqueda en la BD (búsqueda exacta por
indice). Una variante igual de válida es guardarHMAC(clave_servidor, token), que además exige comprometer la clave del servidor para siquiera verificar candidatos; para un token de 256 bits, SHA-256 a secas ya es suficiente y más simple. - La comparación se hace implícitamente al buscar el índice en la BD; si tu implementación compara el hash "a mano", vuelve a ser un secreto derivado:
compare_digest. Y recuerda darle al token una caducidad corta y un solo uso — de nuevo, capa de aplicación.
El MAC que ya usabas sin saberlo: el tag de AES-GCM
Si HMAC te suena conceptualmente familiar es porque el Módulo 2 ya te hizo usar un MAC a diario: el tag de autenticación de AES-GCM (02-03) es exactamente esto — un MAC (GMAC, basado en aritmética de polinomios en vez de hashes) calculado sobre el cifrado y el AAD, que es lo que hace saltar nuestra excepción HistorialManipulado cuando alguien toca un historial cifrado. Y Poly1305, la mitad de ChaCha20-Poly1305, es otro MAC de esa familia de "un solo uso por clave derivada", diseñado para ser rapidísimo. La jerarquía queda así:
- ¿Cifras y autenticas a la vez? → AEAD (el MAC va incluido; no le añadas un HMAC encima).
- ¿Autenticas sin cifrar (el receptor necesita leer el mensaje, como nuestros webhooks)? → HMAC.
Hash vs MAC vs firma digital
La tabla que ordena este módulo y anticipa el siguiente:
| Hash (03-01) | MAC/HMAC (esta lección) | Firma digital (04-03) | |
|---|---|---|---|
| Necesita clave | No | Sí, compartida (simétrica) | Sí, par pública/privada |
| Integridad | Sí | Sí | Sí |
| Autenticidad | No | Sí (entre quienes comparten la clave) | Sí (ante cualquiera) |
| No repudio | No | No (ambas partes pueden generar el tag) | Sí (solo el dueño de la privada firma) |
| Quién puede verificar | Cualquiera | Solo quien tiene la clave | Cualquiera, con la clave pública |
| Coste | Muy bajo | Muy bajo | Alto (mil veces más lento) |
| En MediNube | Huella de exports | Webhooks, índice de tokens | Recetas electrónicas (pendiente) |
Errores Comunes y Consejos
hash(clave + mensaje)casero. Length extension sobre SHA-256. Existehmac.new; úsalo. Si algún día vessha256(secreto + datos)en un código heredado, es un hallazgo de auditoría, no una peculiaridad de estilo.- Verificar el tag con
==. Timing attack sobre un secreto.hmac.compare_digest, siempre, en los dos lados de la integración (regla de oro 6). - Firmar el JSON re-serializado en vez de los bytes recibidos. La firma debe calcularse y verificarse sobre los bytes crudos del cuerpo; re-serializar introduce diferencias invisibles (orden de claves, espacios, unicode) que rompen firmas legítimas — y los "arreglos" apresurados de eso suelen acabar debilitando la verificación.
- Olvidar el timestamp (o no incluirlo en lo firmado). Sin él, cualquier webhook capturado es reproducible para siempre. Y si el timestamp viaja pero no está dentro del HMAC, el atacante lo actualiza y el replay revive.
- Reutilizar la misma clave de webhook para todas las clínicas o derivar la clave de algo adivinable. Clave por receptor, del CSPRNG, rotable. Si necesitas varias claves de un secreto maestro, ya conoces la herramienta: HKDF con
infodistinto (02-04). - Añadir HMAC encima de AES-GCM "para más seguridad". El tag de GCM ya es un MAC sobre cifrado y AAD; duplicarlo añade complejidad y superficie de error, no seguridad.
- Consejo: cuando diseñes qué bytes firmar, pregúntate qué podría cambiar un atacante sin invalidar el tag. Todo lo que deba ser inmutable (timestamp, versión, destinatario) tiene que estar dentro de lo firmado — la misma lógica que el AAD de 02-03.
Ejercicios
Ejercicio 1. Este endpoint heredado verifica webhooks entrantes en el sistema de Centro Médico Luna. Encuentra los tres fallos y explica el ataque que permite cada uno:
def verificar_MAL(clave, cuerpo, cabeceras):
tag = hashlib.sha256(clave + cuerpo).hexdigest()
return tag == cabeceras["X-MediNube-Firma"]Ejercicio 2. MediNube quiere añadir a cada webhook la clínica de destino, de forma que un webhook firmado para Clínica Sol no pueda presentarse ante Centro Médico Luna ni siquiera si ambas compartieran clave por un error de configuración. Modifica firmar_webhook/verificar_webhook para incluir el identificador de la clínica en el material firmado, sin añadir cabeceras nuevas que el receptor deba leer (pista: el receptor ya sabe quién es).
Ejercicio 3. Un compañero propone guardar los tokens de recuperación con Argon2 "porque es lo más seguro para secretos, mejor que SHA-256". Explica (a) por qué no aporta seguridad real en este caso, y (b) qué coste operativo introduce que SHA-256 no tiene (pista: piensa en cómo se busca el token en la BD, sabiendo que Argon2 usa salt aleatorio).
Soluciones
Solución 1. (1) sha256(clave + cuerpo) es la construcción vulnerable a length extension: un atacante puede extender un webhook legítimo con contenido extra y calcular un tag válido sin la clave — debe ser hmac.new(clave, cuerpo, hashlib.sha256). (2) tag == ... compara un secreto con ==: timing attack que permite reconstruir el tag válido byte a byte — debe ser hmac.compare_digest. (3) No hay timestamp ni comprobación de frescura: cualquier webhook capturado puede reproducirse indefinidamente — hay que firmar timestamp || "." || cuerpo y rechazar mensajes fuera de la ventana de tolerancia.
Solución 2. Se incluye el identificador en la base firmada; cada receptor verifica con su propio identificador, que ya conoce, sin cabecera nueva:
def firmar_webhook(clave: bytes, cuerpo: bytes, clinica: str) -> dict:
timestamp = str(int(time.time()))
base = f"{timestamp}.{clinica}.".encode("utf-8") + cuerpo
tag = hmac.new(clave, base, hashlib.sha256).hexdigest()
return {"X-MediNube-Timestamp": timestamp, "X-MediNube-Firma": f"v1={tag}"}
def verificar_webhook(clave: bytes, cuerpo: bytes, timestamp: str,
firma: str, mi_id: str = "clinica-sol") -> bool:
if abs(time.time() - int(timestamp)) > 300 or not firma.startswith("v1="):
return False
base = f"{timestamp}.{mi_id}.".encode("utf-8") + cuerpo
esperado = hmac.new(clave, base, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperado, firma[3:])Si el webhook iba dirigido a otra clínica, la base reconstruida difiere y el tag no cuadra. Es el mismo principio que el AAD paciente=... de 02-03: atar el contexto al material autenticado. (En un cambio real habría que subir el esquema a v2=, porque los tags de v1 ya no serían compatibles.)
Solución 3. (a) Argon2 es lento a propósito para compensar la baja entropía de las contraseñas humanas (espacio de candidatos pequeño → hay que encarecer cada intento). El token tiene 256 bits de entropía del CSPRNG: el espacio de candidatos es 2^256 y ningún atacante puede enumerarlo, a cualquier velocidad. SHA-256 ya deja la fuerza bruta en "imposible"; Argon2 la dejaría en "imposible pero gastando tu RAM". (b) Argon2 genera un salt aleatorio por hash: el mismo token produce hashes distintos cada vez, así que no puedes localizar el registro calculando el hash del token presentado — tendrías que ejecutar verify (lento y con memoria alta) contra cada fila candidata de la tabla. SHA-256, determinista, funciona como índice de búsqueda exacta e instantánea. Herramienta según el problema: regla de oro 7.
Conclusión
El hueco que dejó 03-01 está cerrado: cuando huella y mensaje viajan juntos, la respuesta es mezclar una clave secreta compartida en el cálculo — un MAC —, y la forma correcta de hacerlo con hashes es HMAC, cuyas dos pasadas anidadas neutralizan el length extension que rompe el atajo hash(clave + mensaje). En MediNube eso se tradujo en dos deudas saldadas: los webhooks salientes viajan con X-MediNube-Firma: v1=<hmac> sobre timestamp.cuerpo (anti-replay incluido, verificación con compare_digest), y la BD ya no guarda tokens de recuperación sino su SHA-256 — hash rápido que aquí sí basta, porque el token tiene 256 bits de entropía y ningún diccionario lo alcanza.
Esa última frase es el trampolín de la próxima lección: ¿y cuando el secreto no tiene entropía de sobra? Las contraseñas de ana.perez y del resto de usuarios del portal son palabras humanas, predecibles, reutilizadas — y guardarlas bien es el problema de seguridad más frecuente (y más frecuentemente mal resuelto) del desarrollo web. Hemos rozado la respuesta dos veces: las KDFs lentas de 02-04 y el "el hash rápido no sirve" de 03-01. En 03-03: Almacenamiento Seguro de Contraseñas juntamos las piezas: del desastre histórico (texto claro, MD5 sin salt, rainbow tables) a Argon2id con argon2-cffi, con el flujo completo de registro y login de MediNube y la migración de hashes antiguos. Es la última deuda pendiente de este módulo. Vamos a saldarla.
Curso de Criptografía Aplicada
Módulo 1: Fundamentos de la Criptografía
- Qué es la criptografía y para qué sirve
- Codificación, ofuscación y cifrado
- Aleatoriedad y entropía
- El principio de Kerckhoffs y las reglas de oro
Módulo 2: Criptografía Simétrica
- Cifrado simétrico: AES y ChaCha20
- Modos de operación
- Cifrado autenticado (AEAD)
- Derivación de claves (KDF)
Módulo 3: Hashes, MAC y Contraseñas
- Funciones hash criptográficas
- Autenticación de mensajes con HMAC
- Almacenamiento seguro de contraseñas
Módulo 4: Criptografía Asimétrica
- Fundamentos de clave pública y RSA
- Criptografía de curva elíptica
- Firmas digitales
- Intercambio de claves: Diffie-Hellman
- Cifrado híbrido
Módulo 5: PKI, Certificados y TLS
- Certificados X.509 y autoridades de certificación
- TLS en la práctica
- Gestión del ciclo de vida de certificados
