TLS 1.3 + Post-Quantum: Intercambio de Claves Híbrido ML-KEM (Kyber) en el Mundo Real
El Modelo de Amenaza: Por Qué No Podemos Esperar
El argumento para la criptografía post-cuántica ya no es especulativo. Es matemática de riesgo directa:
- Los datos cifrados transitan por internet
- Los adversarios graban ese tráfico cifrado (el almacenamiento barato lo hace trivial)
- Eventualmente aparece una computadora cuántica criptográficamente relevante (CRQC)
- Todo el tráfico grabado cifrado con intercambio de claves clásico se vuelve legible
Este es el ataque "harvest now, decrypt later" (HNDL). Los datos que se está protegiendo con TLS hoy podrían necesitar permanecer confidenciales por 10, 20 o 30 años. Si aparece un CRQC dentro de esa ventana, tu intercambio de claves clásico no sirvió para nada.
El cálculo de urgencia: si tus datos necesitan N años de confidencialidad, y estima T años hasta un CRQC, y lleva M años migrar tu infraestructura, se necesita empezar cuando N + M > T. Para la mayoría de las organizaciones con datos sensibles, esa desigualdad ya se cumple.
ML-KEM: Lo que NIST Realmente Estandarizó
FIPS 203, publicado en agosto 2024, estandarizó ML-KEM en tres conjuntos de parámetros:
| Conjunto de Parámetros | Nivel de Seguridad | Tamaño Clave Pública | Tamaño Ciphertext | Secreto Compartido | |------------------------|-------------------|----------------------|--------------------|--------------------| | ML-KEM-512 | NIST Nivel 1 | 800 bytes | 768 bytes | 32 bytes | | ML-KEM-768 | NIST Nivel 3 | 1,184 bytes | 1,088 bytes | 32 bytes | | ML-KEM-1024 | NIST Nivel 5 | 1,568 bytes | 1,568 bytes | 32 bytes |
ML-KEM-768 es el punto dulce que todos están deployando. Seguridad nivel 3 (aproximadamente equivalente a AES-192), tamaños de clave razonables, y es lo que tanto Chrome como Firefox eligieron para su implementación híbrida.
La matemática core se basa en la dureza del problema Module Learning With Errors (MLWE) en lattices. Sin meternos en el rabbit hole de teoría de números — la operación fundamental es multiplicar polinomios en un anillo y agregar ruido estructurado. La seguridad descansa en la suposición de que recuperar el secreto de muestras ruidosas de ring-LWE es difícil tanto para computadoras clásicas como cuánticas.
El Enfoque Híbrido: X25519Kyber768
Nadie está deployando ML-KEM solo. Toda la industria convergió en un enfoque híbrido donde combinás un intercambio de claves clásico con uno post-cuántico. El secreto compartido se deriva de ambos, así que manteners la seguridad incluso si cualquiera de los dos algoritmos resulta tener una debilidad inesperada.
Así se ve el handshake TLS 1.3 con X25519Kyber768Draft00:
Client Server
ClientHello
+ key_share: {
X25519Kyber768Draft00: x25519_pub || mlkem768_encaps_key,
X25519: x25519_pub_fallback
}
+ supported_groups: [X25519Kyber768Draft00, X25519, ...]
+ signature_algorithms: [ecdsa_secp256r1_sha256, ...]
-------->
ServerHello
+ key_share: {
X25519Kyber768Draft00:
x25519_server_pub || mlkem768_ciphertext
}
{EncryptedExtensions}
{Certificate}
{CertificateVerify}
{Finished}
<--------
{Finished}
-------->
[Application Data] <-------> [Application Data]
La computación del secreto compartido combinado:
# Pseudocódigo para derivación de secreto híbrido
def derive_hybrid_secret(x25519_shared, mlkem_shared):
# Concatenar ambos secretos compartidos
combined = x25519_shared + mlkem_shared # 32 + 32 = 64 bytes
# Alimentar al key schedule de TLS 1.3
early_secret = HKDF_Extract(salt=0, ikm=PSK or 0)
handshake_secret = HKDF_Extract(
salt=Derive_Secret(early_secret, "derived", ""),
ikm=combined # <-- Ambos secretos contribuyen aquí
)
return handshake_secret
La propiedad crítica: si ML-KEM es roto por un ataque clásico que no anticipamos, X25519 todavía te protege. Si X25519 es roto por una computadora cuántica, ML-KEM todavía te protege. Necesitás que ambos fallen simultáneamente para que el handshake se comprometa.
Implementación en Chrome
Un análisis de la implementación de BoringSSL en Chromium revela el flujo real:
// De ssl/extensions/ext_key_share.cc (simplificado)
static bool ext_key_share_add_clienthello(
const SSL_HANDSHAKE *hs, CBB *out) {
// Lista de grupos preferidos, probados en orden:
// 1. X25519Kyber768Draft00 (PQ híbrido)
// 2. X25519 (fallback clásico)
for (uint16_t group_id : hs->config->supported_group_list) {
CBB key_exchange;
if (group_id == SSL_GROUP_X25519_KYBER768_DRAFT00) {
// Generar par de claves X25519
uint8_t x25519_public[32], x25519_private[32];
X25519_keypair(x25519_public, x25519_private);
// Generar clave de encapsulación ML-KEM-768
uint8_t mlkem_encaps_key[MLKEM768_PUBLIC_KEY_BYTES];
uint8_t mlkem_decaps_key[MLKEM768_SECRET_KEY_BYTES];
MLKEM768_generate_key(mlkem_encaps_key, mlkem_decaps_key);
// key_share = x25519_pub || mlkem_encaps_key
CBB_add_bytes(&key_exchange, x25519_public, 32);
CBB_add_bytes(&key_exchange, mlkem_encaps_key,
MLKEM768_PUBLIC_KEY_BYTES);
}
}
}
El aumento de tamaño del ClientHello es significativo. Una entrada key_share de X25519 solo son 32 bytes. Con el grupo híbrido, son 32 + 1184 = 1216 bytes. Sumando padding y extensiones, y tu ClientHello infla de ~250 bytes a ~1400+ bytes.
El Problema de los Middleboxes
Aquí es donde la teoría se encuentra con la realidad brutal de internet. Los problemas más comunes encontrados en la práctica incluyen:
Segmentación TCP
Muchos ClientHellos TLS entran en un solo segmento TCP (MSS ~1460 bytes en la mayoría de las redes). El ClientHello post-cuántico agrandado a veces requiere dos segmentos. Algunos middleboxes — firewalls, motores DPI, load balancers — reensamblan registros TLS pero asumen que empiezan y terminan dentro de un solo segmento TCP.
Antes (clásico):
[Segmento TCP 1: header IP + header TCP + ClientHello completo (250 bytes)]
Después (PQ híbrido):
[Segmento TCP 1: header IP + header TCP + ClientHello parte 1 (1400 bytes)]
[Segmento TCP 2: ClientHello parte 2 (bytes restantes)]
La solución a nivel aplicación es TCP_NODELAY + asegurarte de que tu biblioteca TLS envíe el ClientHello completo antes de esperar respuesta. A nivel infraestructura, se necesita auditar cada middlebox en el camino.
QUIC la Tiene Más Fácil
TLS sobre QUIC (usado por HTTP/3) maneja esto más elegantemente porque el propio framing de QUIC ya soporta handshakes criptográficos multi-paquete. El ClientHello agrandado cabe en el paquete inicial de QUIC que puede tener hasta 1200 bytes de payload cripto en un datagrama UDP de 1280 bytes.
Implicaciones de Certificate Transparency
Post-quantum no cambia los certificados todavía, pero cambia cómo considerars sobre CT. El deployment actual usa ML-KEM solo para intercambio de claves — la autenticación sigue usando firmas clásicas ECDSA o RSA. Esto es deliberado: el intercambio de claves es el problema urgente (ataques HNDL), mientras que la autenticación es menos crítica porque no es posible "grabar ahora, forjar después" una firma.
Sin embargo, las firmas post-cuánticas vienen para los certificados. ML-DSA (FIPS 204, basado en CRYSTALS-Dilithium) está estandarizado, y SLH-DSA (FIPS 205, basado en SPHINCS+) es la alternativa stateless. ¿El problema? Las firmas ML-DSA-65 son 3,309 bytes (vs 72 bytes para ECDSA P-256). Una cadena de certificados con tres certificados ML-DSA agrega ~10KB al handshake.
Tamaño cadena de certificados actual (ECDSA):
Firma cert hoja: 72 bytes
Firma cert intermedio: 72 bytes
Firma cert raíz: 72 bytes
Total firmas: ~216 bytes
Cadena de certificados futura (ML-DSA-65):
Firma cert hoja: 3,309 bytes
Firma cert intermedio: 3,309 bytes
Firma cert raíz: 3,309 bytes
Total firmas: ~9,927 bytes
Esto va a hacer el problema de middleboxes 10 veces peor. Empezar a planificar ya.
Guía Práctica de Deployment
Así se configura nginx con intercambio de claves post-cuántico:
# nginx.conf - requiere OpenSSL 3.2+ con oqs-provider
ssl_protocols TLSv1.3;
ssl_ecdh_curve X25519Kyber768Draft00:X25519:secp256r1;
# Importante: listar híbrido primero, fallback clásico segundo
ssl_prefer_server_ciphers off;
# Dejar que el cliente elija - ellos conocen sus propias capacidades
Para aplicaciones Node.js detrás del reverse proxy:
import { createServer } from 'https';
import { readFileSync } from 'fs';
const server = createServer({
key: readFileSync('/etc/ssl/private/server.key'),
cert: readFileSync('/etc/ssl/certs/server.crt'),
// Node.js 22+ con OpenSSL 3.2+
ecdhCurve: 'X25519Kyber768Draft00:X25519',
minVersion: 'TLSv1.3'
});
Verificación:
# Verificar si tu server ofrece PQ híbrido
openssl s_client -connect tudominio.com:443 \
-groups X25519Kyber768Draft00 \
-tls1_3 2>&1 | grep "Server Temp Key"
# Output esperado:
# Server Temp Key: X25519Kyber768Draft00, 1216 bits
Performance: Números Reales
Benchmark del overhead en un setup de clase producción (nginx, 64 cores AMD EPYC, 10Gbps):
| Métrica | Solo X25519 | X25519Kyber768 | Overhead | |---------|------------|----------------|----------| | Latencia handshake (p50) | 1.2ms | 1.4ms | +16% | | Latencia handshake (p99) | 3.1ms | 3.8ms | +22% | | Tamaño ClientHello | 253 bytes | 1,438 bytes | +468% | | Tamaño ServerHello | 105 bytes | 1,193 bytes | +1036% | | CPU por handshake | 0.08ms | 0.12ms | +50% | | Handshakes/seg (1 core) | 12,500 | 8,333 | -33% |
El overhead de latencia es despreciable para la mayoría de las aplicaciones. El overhead de CPU importa a escala pero es manejable. El tema real es ancho de banda — ese es handshakes más grandes se acumulan cuando se está haciendo millones de conexiones nuevas por segundo.
Qué Teners que Hacer Ahora Mismo
- Habilitar PQ híbrido en tu edge si tu reverse proxy lo soporta. Cloudflare y AWS CloudFront ya lo tienen habilitado por defecto.
- Auditar tus middleboxes para tolerancia de tamaño de ClientHello. Mandá un ClientHello de 1500 bytes por todo tu path y verificá que llega intacto.
- Monitorear tus logs de CT para cualquier certificado en tu dominio que use tipos de clave inesperados.
- No esperes por firmas post-cuánticas. El intercambio de claves es la pieza urgente. Comenzar por ahí.
- Probar con navegadores reales. Chrome 124+ y Firefox 128+ soportan
X25519Kyber768Draft00.
La transición post-cuántica no es un problema del futuro. Chrome ya está negociando Kyber híbrido con cada servidor que lo soporta. Si tu servidor no lo ofrece, el tráfico de tus usuarios se está acumulando en el almacenamiento de algún adversario, esperando el día en que una computadora cuántica pueda leerlo. La matemática de cuándo migrar no es difícil. La respuesta es ahora.