Ha llegado el momento de saldar la deuda más vieja del curso. Desde 02-01, cada vez que usábamos AES-GCM o HMAC aparcábamos la misma pregunta: ¿y cómo llegó la clave secreta a ambos lados sin que nadie la interceptara? En 04-01 la criptografía asimétrica dio una respuesta parcial (cifrar hacia una clave pública), pero había otra idea, anterior y más elegante, publicada por Diffie y Hellman en 1976: dos personas que nunca se han visto pueden acordar un secreto compartido hablando por un canal público donde todo el mundo escucha, y aun así el espía no se entera del secreto. Suena a magia; es aritmética. En esta lección verás la intuición (una mezcla de colores), un Diffie-Hellman de juguete con enteros pequeños, su versión moderna con X25519, cómo convertir el secreto en una clave AES real con HKDF, y qué son la efímeridad y la forward secrecy. Y descubrirás el fallo fatal que arrastra todo esto — un ataque con nombre propio — que solo las firmas de 04-03 y los certificados del módulo 5 podrán cerrar.
Contenido
- El problema, planteado con precisión
- La intuición: mezclar colores
- Diffie-Hellman de juguete con enteros pequeños
- ECDH moderno: X25519 en pyca/cryptography
- Del secreto compartido a la clave: HKDF
- Demostración completa: MediNube ↔ Clínica Sol
- Claves efímeras y forward secrecy
- El fallo fatal: man-in-the-middle
- Cómo se cierra el agujero (y dónde)
El problema, planteado con precisión
Precisemos el reto, porque su solución es contraintuitiva. Dos partes — MediNube y la Clínica Sol — quieren acordar una clave secreta común para luego cifrar con AES-256-GCM (módulo 2). Las condiciones:
- Nunca han compartido nada antes: no hay secreto previo del que partir.
- El único canal es público: una atacante, que llamaremos siempre por su papel, puede leer todo lo que se transmite.
- Al final, ambos deben tener el mismo secreto, y la atacante — que ha visto pasar cada byte — no debe poder calcularlo.
Que esto sea posible parece violar el sentido común: si la espía ve todo lo que se dice, ¿cómo no va a poder repetir el cálculo? La respuesta está en las funciones de un solo sentido que ya conoces: se intercambian datos con los que cada parte puede terminar el cálculo usando su secreto privado, pero con los que la espía, sin ningún secreto privado, se queda a las puertas.
La intuición: mezclar colores
La analogía clásica, y la mejor. Imagina que mezclar pinturas es fácil, pero separar una mezcla en sus colores originales es prácticamente imposible (esa es la "función de un solo sentido"):
- MediNube y la Clínica Sol acuerdan en público un color base, digamos amarillo. La atacante lo ve: da igual.
- Cada parte elige en secreto un color propio y no se lo dice a nadie. MediNube elige rojo; la Clínica, azul.
- Cada parte mezcla su secreto con el amarillo público y envía la mezcla por el canal. MediNube manda naranja (amarillo+rojo); la Clínica manda verde (amarillo+azul). La atacante ve el naranja y el verde volar por la red.
- Cada parte añade su propio secreto a la mezcla que ha recibido. MediNube coge el verde y le añade su rojo; la Clínica coge el naranja y le añade su azul. Ambos llegan al mismo color final (amarillo+rojo+azul): un marrón compartido.
flowchart TD
Base["Color base público: AMARILLO (todos lo ven)"]
Base --> MN1["MediNube: amarillo + ROJO secreto = NARANJA"]
Base --> CS1["Clínica Sol: amarillo + AZUL secreto = VERDE"]
MN1 -->|envía NARANJA por el canal público| CS2
CS1 -->|envía VERDE por el canal público| MN2
MN2["MediNube: VERDE recibido + su ROJO"] --> Final["= MARRÓN (amarillo+rojo+azul)"]
CS2["Clínica Sol: NARANJA recibido + su AZUL"] --> Final
Espia["Atacante: ve amarillo, naranja y verde... pero separar pinturas es inviable"] -.no puede.-> Final
La atacante tiene el amarillo, el naranja y el verde. Para llegar al marrón necesitaría el rojo o el azul, y esos nunca viajaron: para extraerlos tendría que "separar" una mezcla, que es justo lo inviable. Los secretos privados (rojo, azul) jamás salen de sus dueños; solo viajan las mezclas. Cambia "mezclar colores" por una operación matemática de un solo sentido y tienes Diffie-Hellman.
Diffie-Hellman de juguete con enteros pequeños
Aviso de juguete (regla de oro 1). Números ridículamente pequeños, sin ninguna de las comprobaciones que exige un DH real. Sirve para ver el mecanismo, nada más. En producción, X25519 de
pyca/cryptography.
La operación de un solo sentido del DH clásico es la exponenciación modular: pow(g, x, p) es fácil; recuperar x a partir del resultado (el logaritmo discreto, pariente del elíptico de 04-02) es inviable con p grande. El "amarillo" son dos números públicos: un primo p y una base g.
# --- DIFFIE-HELLMAN DE JUGUETE: solo para aprender ---
# Parámetros PÚBLICOS, acordados a la vista de todos (el "amarillo").
p = 23 # primo (en real: >= 2048 bits)
g = 5 # base
# --- Secretos PRIVADOS: cada parte elige el suyo del CSPRNG (regla de oro 3) ---
a = 6 # secreto de MediNube (el "rojo"; nunca se transmite)
b = 15 # secreto de la Clínica (el "azul"; nunca se transmite)
# --- Cada parte publica su "mezcla" (clave pública DH) ---
A = pow(g, a, p) # MediNube envía A = 5^6 mod 23 = 8 (el "naranja")
B = pow(g, b, p) # Clínica envía B = 5^15 mod 23 = 19 (el "verde")
print(f"Viajan por el canal público: A={A}, B={B}")
# --- Cada parte combina lo recibido con SU secreto ---
secreto_medinube = pow(B, a, p) # (g^b)^a mod p
secreto_clinica = pow(A, b, p) # (g^a)^b mod p
print(secreto_medinube, secreto_clinica) # 2 y 2: ¡el mismo!
assert secreto_medinube == secreto_clinicaEl corazón es una igualdad de las potencias: (g^b)^a = g^(b·a) = g^(a·b) = (g^a)^b (mod p). Cada parte llega a g^(a·b) mod p por un camino distinto, usando su propio exponente secreto. La atacante ve p, g, A=8 y B=19, pero para calcular g^(a·b) necesitaría a o b, y extraerlos de A o B es el logaritmo discreto — inviable con parámetros grandes.
# Lo que la atacante NO puede hacer eficientemente con p grande:
# recuperar 'a' desde A. Con p=23 sí (fuerza bruta); con p de 2048 bits, no.
for candidato_a in range(p):
if pow(g, candidato_a, p) == A:
print(f"(solo con juguete) a = {candidato_a}") # a = 6
breakEse bucle que rompe el juguete en microsegundos es exactamente lo que se vuelve imposible al crecer p. El DH clásico sigue vivo (en su variante de curva elíptica), pero con números enormes y comprobaciones que aquí omitimos. Por eso pasamos a la versión moderna.
ECDH moderno: X25519 en pyca/cryptography
ECDH (Elliptic Curve Diffie-Hellman) es el mismo protocolo sobre la aritmética de curvas de 04-02: los secretos son escalares, las claves públicas son puntos, y la "combinación" es multiplicación escalar. Con X25519 (la curva que MediNube eligió en 04-02 para intercambio) la API esconde toda la aritmética tras un solo método, exchange():
from cryptography.hazmat.primitives.asymmetric import x25519 # Cada parte genera su par EFÍMERO (privada = escalar aleatorio; pública = punto). priv_medinube = x25519.X25519PrivateKey.generate() priv_clinica = x25519.X25519PrivateKey.generate() # Se intercambian SOLO las públicas por el canal (serializadas a bytes/PEM). pub_medinube = priv_medinube.public_key() pub_clinica = priv_clinica.public_key() # Cada parte combina SU privada con la pública AJENA -> mismo secreto. secreto_mn = priv_medinube.exchange(pub_clinica) secreto_cs = priv_clinica.exchange(pub_medinube) print(secreto_mn == secreto_cs) # True print(len(secreto_mn)) # 32 bytes de secreto compartido
Mapea cada línea contra el juguete y contra los colores: generate() es elegir el secreto (rojo/azul) más publicar la mezcla (naranja/verde); exchange(pública_ajena) es el paso final de combinar. Nada del secreto privado sale nunca del objeto. Y como generar un par X25519 es prácticamente gratis (04-02), no hay problema en crear uno nuevo por cada intercambio — la base de la forward secrecy del apartado 7.
Del secreto compartido a la clave: HKDF
Tentación fatal: usar secreto_mn (esos 32 bytes) directamente como clave AES-256. No lo hagas. El secreto de un intercambio DH/ECDH no es una clave uniforme: es un elemento matemático de la curva con posible estructura y sesgo, no una cadena de bits uniformemente aleatoria. Meterlo tal cual en AES viola el principio de que las claves deben ser uniformes.
La herramienta correcta ya la conoces de 02-04: HKDF (HMAC-based Key Derivation Function). HKDF "condensa" ese secreto en una clave criptográficamente uniforme del tamaño que quieras, y de paso permite atar la clave a un contexto con el parámetro info — igual que hacíamos con b"medinube:historiales:v1":
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
def derivar_clave_canal(secreto_compartido: bytes) -> bytes:
"""Convierte el secreto ECDH crudo en una clave AES-256 uniforme."""
return HKDF(
algorithm=hashes.SHA256(),
length=32, # 256 bits para AES-256-GCM
salt=None, # opcional; en TLS se usan nonces del handshake
info=b"medinube:canal-clinica:v1", # ata la clave a ESTE propósito (02-04)
).derive(secreto_compartido)
clave_canal = derivar_clave_canal(secreto_mn)
print(len(clave_canal)) # 32 bytes, listos para AES-256-GCMEl info con etiqueta versionada (:v1) es el mismo patrón del curso: si una clave derivada para el "canal-clinica" nunca colisiona con otra derivada para, digamos, b"medinube:backup:v1", aunque ambas partan del mismo secreto. Criptoagilidad (regla de oro 8) integrada en el nombre.
Demostración completa: MediNube ↔ Clínica Sol
Juntemos las tres piezas — ECDH + HKDF + AES-GCM — para lo que llevábamos prometiendo desde 02-01: enviar un fragmento de historial de la Clínica Sol a MediNube estableciendo la clave sobre la marcha, sin secreto previo. Reutilizamos el formato v1 del módulo 2 (versión(0x01)||nonce(12)||cifrado+tag) y su AAD por paciente:
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.asymmetric import x25519
# ---- FASE 1: intercambio de claves (canal público) ----
# Cada parte genera su par efímero y se envían las públicas serializadas.
priv_cs = x25519.X25519PrivateKey.generate()
priv_mn = x25519.X25519PrivateKey.generate()
pub_cs_bytes = priv_cs.public_key().public_bytes_raw() # 32 bytes por el canal
pub_mn_bytes = priv_mn.public_key().public_bytes_raw() # 32 bytes por el canal
# ---- FASE 2: cada parte deriva LA MISMA clave de canal ----
pub_mn = x25519.X25519PublicKey.from_public_bytes(pub_mn_bytes)
pub_cs = x25519.X25519PublicKey.from_public_bytes(pub_cs_bytes)
clave_cs = derivar_clave_canal(priv_cs.exchange(pub_mn)) # lado Clínica
clave_mn = derivar_clave_canal(priv_mn.exchange(pub_cs)) # lado MediNube
assert clave_cs == clave_mn # mismo secreto
# ---- FASE 3: la Clínica cifra un historial con AES-256-GCM (módulo 2) ----
paciente = "ana.perez"
aad = f"paciente={paciente};formato=v1".encode() # AAD del curso
historial = b"Alergias: penicilina. Grupo sanguineo: A+. Ultima visita: 2026-07-01"
nonce = os.urandom(12) # nonce fresco (regla de oro 3)
sobre = bytes([0x01]) + nonce + AESGCM(clave_cs).encrypt(nonce, historial, aad)
# ---- FASE 4: MediNube descifra ----
assert sobre[0] == 0x01 # versión de formato
nonce_recibido, cifrado = sobre[1:13], sobre[13:]
descifrado = AESGCM(clave_mn).decrypt(nonce_recibido, cifrado, aad)
print(descifrado.decode())sequenceDiagram
participant CS as Clínica Sol
participant MN as MediNube
CS->>MN: pública efímera X25519 de CS (32 bytes, en claro)
MN->>CS: pública efímera X25519 de MN (32 bytes, en claro)
Note over CS,MN: cada uno: exchange() -> secreto común -> HKDF -> clave AES-256
CS->>MN: sobre v1 = 0x01 || nonce || AES-GCM(historial, AAD)
Note over MN: AES-GCM.decrypt -> historial de ana.perez
Lo hemos conseguido: dos partes sin secreto previo, un canal donde todo se ve, y aun así un historial cifrado con una clave que nunca viajó. Cada primitiva del curso ocupa su lugar: X25519 acuerda, HKDF condensa, AES-GCM protege, la AAD ata el cifrado al paciente. Detalle de API: public_bytes_raw() / from_public_bytes() son la forma compacta (32 bytes crudos) de X25519; también puedes usar PEM (SubjectPublicKeyInfo) como en 04-01/04-02 si prefieres un formato autodescriptivo.
Claves efímeras y forward secrecy
Fíjate en que en la demo cada par X25519 se generó con generate() en el momento y no se guardó en ningún sitio. Eso es una clave efímera: se usa para un intercambio y se descarta. La alternativa serían claves estáticas (fijas, reutilizadas en cada conexión). La diferencia tiene un nombre y consecuencias enormes: la forward secrecy (secreto hacia adelante, o perfect forward secrecy, PFS).
Imagina que la atacante graba hoy todo el tráfico cifrado entre la Clínica Sol y MediNube — no puede leerlo, pero lo archiva. Y mañana, por un descuido, un robo o una orden judicial, consigue la clave privada de una de las partes. ¿Puede ahora descifrar lo que grabó?
| DH con claves estáticas | DH con claves efímeras (con PFS) | |
|---|---|---|
| Se genera un par nuevo... | Una vez, se reutiliza siempre | En cada sesión/conexión |
| Si roban la privada hoy... | Descifra todo el tráfico pasado grabado | No descifra nada del pasado |
| Coste | Menor (no se regeneran claves) | Mínimo con X25519 (generar es casi gratis, 04-02) |
| Recomendación | Evitar para canales | Estándar moderno |
La clave de la forward secrecy: como las privadas efímeras se destruyen tras cada intercambio, comprometer la clave de largo plazo de una parte no revela los secretos de sesión ya negociados — esos secretos ya no existen en ningún sitio. Cada conversación queda protegida "hacia adelante" frente a futuros compromisos. Aquí se cobra la promesa de 04-02: generar un par EC es tan barato que hacerlo por conexión no cuesta nada, y por eso la forward secrecy es hoy el estándar (el "E" final de ECDHE en TLS significa justo ephemeral).
El fallo fatal: man-in-the-middle
Y ahora la mala noticia, la misma sombra que persigue al módulo entero. Vuelve a la demostración y hazte la pregunta de 04-01 y 04-03: cuando MediNube recibe pub_cs_bytes, ¿cómo sabe que esos 32 bytes son de verdad la clave pública de la Clínica Sol? No lo sabe. Son bytes que llegaron por un canal público. Y ahí se cuela el ataque.
Aparece MalloryClinic, una atacante situada en medio del canal (un router comprometido, una wifi hostil, un DNS envenenado). En lugar de limitarse a escuchar (contra lo cual DH es inmune), intercepta y sustituye las claves públicas: hace dos intercambios, uno con cada parte, haciéndose pasar por la otra.
sequenceDiagram
participant CS as Clínica Sol
participant M as MalloryClinic (en medio)
participant MN as MediNube
CS->>M: pública de CS
M->>MN: pública de MALLORY (se hace pasar por CS)
MN->>M: pública de MN
M->>CS: pública de MALLORY (se hace pasar por MN)
Note over CS,M: CS acuerda una clave... con MALLORY
Note over M,MN: MN acuerda otra clave... con MALLORY
CS->>M: sobre cifrado (clave CS-Mallory)
Note over M: descifra, LEE el historial, recifra
M->>MN: sobre cifrado (clave Mallory-MN)
El resultado es devastador y silencioso: la Clínica Sol establece un canal cifrado con Mallory creyendo hablar con MediNube; MediNube establece otro con Mallory creyendo hablar con la Clínica. Mallory descifra cada mensaje con una clave, lo lee (el historial de ana.perez, en claro, ante sus ojos), lo vuelve a cifrar con la otra y lo reenvía. Ambos extremos ven un canal que "funciona": los mensajes llegan, se descifran, todo parece correcto. La matemática de Diffie-Hellman es perfecta; el eslabón roto es, una vez más, la autenticidad de las claves públicas.
Diffie-Hellman, por sí solo, garantiza confidencialidad frente a un espía pasivo, pero no frente a un atacante activo que pueda modificar el tráfico. DH sin autenticar es una casa sin cerradura: preciosa por dentro, abierta a quien empuje la puerta.
Cómo se cierra el agujero (y dónde)
La cura es autenticar las claves públicas del intercambio: que cada parte pueda demostrar que la clave que envía es suya. Y para "demostrar autoría" ya tienes la herramienta de la lección anterior: firmas digitales. Si la Clínica Sol firma su clave pública efímera con su clave Ed25519 de largo plazo, MalloryClinic no podrá sustituirla — no sabe falsificar la firma de la Clínica. Esta combinación (DH efímero para el secreto + firma de largo plazo para autenticar) es exactamente la que usa TLS moderno, y tiene nombre: DH efímero autenticado.
Pero fíjate en que esto solo traslada el problema: para verificar la firma de la Clínica, MediNube necesita su clave pública Ed25519 auténtica... y ¿cómo sabe esa que es de la Clínica? Es la pregunta incómoda de 04-01 y 04-03, ahora ineludible: hace falta alguien de confianza que certifique qué clave pública pertenece a quién. Esa infraestructura — certificados X.509 y autoridades de certificación — es el módulo 5, y TLS (que combina ECDHE + certificados + AES-GCM exactamente como aquí, y que verás en 05-02) es su aplicación estrella. Lo dejamos, por última vez, explícitamente abierto: tenemos ya casi todas las piezas; falta la que da confianza a las claves públicas.
Antes de ir allí, queda una lección que une los dos hilos de este módulo — cifrar hacia alguien (04-01) y acordar una clave (esta) — en el patrón que usa el mundo real para cifrar de verdad: el cifrado híbrido, en 04-05.
Errores Comunes y Consejos
- Usar el secreto ECDH crudo como clave. El error número uno.
exchange()no devuelve una clave lista: devuelve material que debe pasar por HKDF. Saltarse la KDF es un fallo de seguridad real, no un tecnicismo. - Creer que Diffie-Hellman autentica. No lo hace: da confidencialidad frente a un espía pasivo y nada frente a un man-in-the-middle activo. DH acuerda un secreto con quienquiera que esté al otro lado, sea quien sea. Autenticar las claves es un paso aparte (firmas + certificados).
- Reutilizar claves DH estáticas cuando podrías usar efímeras. Renuncias a la forward secrecy: un compromiso futuro de la clave descifra todo el pasado grabado. Con X25519, generar un par por sesión es casi gratis — hazlo.
- Confundir X25519 con Ed25519 (otra vez). X25519 es solo intercambio (
exchange); Ed25519 es solo firma (sign). No comparten API ni se sustituyen. Cada propósito, su par. - Reutilizar el nonce de AES-GCM entre mensajes del canal. Que la clave venga de un DH no exime de la regla del módulo 2: nonce fresco por mensaje. Una clave de sesión no autoriza a repetir nonces.
- Implementar DH clásico a mano con
pow. El juguete omite validar los parámetros, comprobar que las claves recibidas están en el grupo correcto y defenderse de subgrupos pequeños. X25519 fue diseñada para que nada de eso te explote: úsala. - Olvidar el
infode HKDF o reutilizarlo entre propósitos distintos. La etiquetab"medinube:canal-clinica:v1"es lo que separa criptográficamente esta clave de cualquier otra derivada del mismo secreto. Una etiqueta clara y versionada por cada uso.
Ejercicios
-
Juguete a mano. Con
p = 23,g = 5, secreto de MediNubea = 4y secreto de la Clínicab = 3: calculaA,By comprueba que ambas partes llegan al mismo secreto. Luego ponte en la piel de la atacante: recuperaapor fuerza bruta desdeA. Explica en una frase por qué esa fuerza bruta es el ataque que se vuelve imposible conpde 2048 bits. -
Canal completo con forward secrecy. Escribe
abrir_canal()que devuelva la clave AES-256 del canal derivada por ambas partes con X25519 efímero + HKDF (info=b"medinube:canal-clinica:v1"), y usa esa clave para que MediNube envíe a la Clínica el sobrev1de un mensaje corto con AADpaciente=ana.perez;formato=v1. Después, genera un segundo canal conabrir_canal()y comprueba que la clave es distinta de la primera. ¿Qué propiedad de seguridad acabas de demostrar y por qué protege el tráfico ya grabado? -
El ataque en código. Simula a MalloryClinic sin cifrado, solo con las claves: genera pares efímeros para la Clínica, para MediNube y dos para Mallory. Calcula (a) el secreto que creería compartir la Clínica (su privada × la pública que le llegó, que es la de Mallory) y (b) el que creería compartir MediNube. Comprueba que Clínica y MediNube no comparten secreto entre sí, pero que Mallory comparte uno con cada uno. Explica qué habría impedido el ataque.
Soluciones
p, g, a, b = 23, 5, 4, 3 A = pow(g, a, p) # 5^4 mod 23 = 4 B = pow(g, b, p) # 5^3 mod 23 = 10 assert pow(B, a, p) == pow(A, b, p) # ambos: 5^12 mod 23 = 18 # La atacante recupera 'a' desde A por fuerza bruta: a_roto = next(x for x in range(p) if pow(g, x, p) == A) # 4 print(a_roto)
Esa fuerza bruta —probar todos los exponentes hasta reproducir A— es el logaritmo discreto. Con p de 2048 bits el número de exponentes posibles es astronómico y no existe atajo eficiente, así que el ataque que aquí tarda microsegundos pasa a tardar más que la edad del universo.
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.asymmetric import x25519
def abrir_canal():
priv_a, priv_b = x25519.X25519PrivateKey.generate(), x25519.X25519PrivateKey.generate()
clave_a = derivar_clave_canal(priv_a.exchange(priv_b.public_key()))
clave_b = derivar_clave_canal(priv_b.exchange(priv_a.public_key()))
assert clave_a == clave_b
return clave_a
clave1 = abrir_canal()
aad = b"paciente=ana.perez;formato=v1"
nonce = os.urandom(12)
sobre = bytes([0x01]) + nonce + AESGCM(clave1).encrypt(nonce, b"Cita confirmada 09:00", aad)
print(AESGCM(clave1).decrypt(sobre[1:13], sobre[13:], aad).decode())
clave2 = abrir_canal()
print(clave1 == clave2) # False: cada canal, clave distintaHas demostrado forward secrecy: como cada sesión usa un par efímero que se descarta, las claves de dos sesiones no tienen relación. Si mañana la atacante roba una privada de largo plazo, no puede reconstruir la clave de una sesión pasada —esa privada efímera ya no existe—, así que el tráfico grabado ayer permanece ilegible.
from cryptography.hazmat.primitives.asymmetric import x25519 cs, mn = x25519.X25519PrivateKey.generate(), x25519.X25519PrivateKey.generate() m_cs, m_mn = x25519.X25519PrivateKey.generate(), x25519.X25519PrivateKey.generate() # A CS le llega la pública de Mallory (m_cs) creyendo que es de MN; y viceversa. secreto_cs = derivar_clave_canal(cs.exchange(m_cs.public_key())) secreto_mn = derivar_clave_canal(mn.exchange(m_mn.public_key())) # Mallory reproduce ambos con sus dos privadas: secreto_mallory_con_cs = derivar_clave_canal(m_cs.exchange(cs.public_key())) secreto_mallory_con_mn = derivar_clave_canal(m_mn.exchange(mn.public_key())) print(secreto_cs == secreto_mn) # False: CS y MN NO comparten nada print(secreto_cs == secreto_mallory_con_cs) # True: Mallory descifra a CS print(secreto_mn == secreto_mallory_con_mn) # True: Mallory descifra a MN
Clínica y MediNube creen tener un canal común, pero cada uno lo tiene con Mallory, que lee y reenvía en medio. Lo habría impedido autenticar las claves públicas: si la Clínica firmara su pública efímera con su Ed25519 de largo plazo (04-03), Mallory no podría sustituirla sin falsificar esa firma. Y para verificar esa firma hace falta conocer la Ed25519 auténtica de la Clínica —el problema de confianza del módulo 5.
Conclusión
Saldada la deuda de 02-01. Diffie-Hellman permite que dos partes sin secreto previo acuerden una clave común por un canal público: cada una elige un secreto privado, publica una "mezcla" (pow(g, a, p) en el clásico; un punto en ECDH), y combina la mezcla ajena con su propio secreto para llegar al mismo resultado —mientras el espía, que lo ha visto todo, se queda fuera por el logaritmo discreto. En la práctica se usa X25519 con exchange(), su secreto crudo jamás se usa directo sino que pasa por HKDF (info=b"medinube:canal-clinica:v1") para obtener la clave AES-256-GCM, y las claves se generan efímeras para ganar forward secrecy: robar la clave de largo plazo no descifra el pasado. Lo montaste entero —X25519 + HKDF + AES-GCM— para enviar un historial de la Clínica Sol a MediNube. Pero DH tiene un talón de Aquiles que no es matemático sino de identidad: sin autenticar las claves públicas, MalloryClinic se sienta en medio y lee todo sin que nadie lo note. La autenticación empieza con las firmas de 04-03 y se completa con los certificados del módulo 5 —y TLS (05-02) hace exactamente lo que has hecho aquí, ECDHE incluido. Antes de eso, una última pieza del módulo: unir "cifrar hacia alguien" y "acordar una clave" en el patrón con el que el mundo real cifra de verdad. Es el cifrado híbrido, en 04-05. Nos vemos allí.
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
