Dentro de la suite PQC de NIST: ML-KEM (FIPS 203), ML-DSA (FIPS 204), SLH-DSA (FIPS 205)
Cuatro años después de nombrar a los finalistas de ronda 3, los estándares están firmados. Aquí está qué hace cada uno, qué parámetro elegir en cada nivel de seguridad, y la llamada mínima en Python y TypeScript.
TL;DR ¶
NIST publicó tres estándares finales en 2024-2025:
- FIPS 203 — ML-KEM (Module-Lattice Key Encapsulation, antes Kyber)
- FIPS 204 — ML-DSA (Module-Lattice Digital Signature, antes Dilithium)
- FIPS 205 — SLH-DSA (Stateless Hash-based Digital Signature, antes SPHINCS+)
Un cuarto (FIPS 206 — FN-DSA, antes Falcon) está en borrador, esperado para 2026. Cualquier librería criptográfica que uses debe ya exponer los nombres FIPS y sus parámetros. Si la tuya no, está desactualizada.
1. ML-KEM — FIPS 203 ¶
Qué hace: encapsula un secreto compartido de 32 bytes bajo la clave pública del destinatario. Reemplazo directo para ECDH o RSA-KEM. Se usa en TLS híbrido KEX, JWE key wrap, derivación de claves de grupo MLS.
Reducción de seguridad: dureza de Module-LWE (Learning With Errors sobre retículos modulares). Mejor ataque clásico conocido: ~ 2^l/2 para nivel de parámetro l, sin speedup cuántico más allá de Grover.
Parámetros:
1 parámetro cat. sec clave pub ciphertext secreto compartido 2 ───────────────────────────────────────────────────────────────────── 3 ML-KEM-512 NIST L1 800 B 768 B 32 B 4 ML-KEM-768 NIST L3 1184 B 1088 B 32 B 5 ML-KEM-1024 NIST L5 1568 B 1568 B 32 B
Elige ML-KEM-768 por defecto. L3 es la base recomendada; L5 solo si estás cifrando archivos de estado-nación.
Llamada mínima posible (Python) ¶
1 # pip install pqcrypto 2 from pqcrypto.kem import ml_kem_768 3 4 # Destinatario genera un keypair y publica pub. 5 pub, priv = ml_kem_768.generate_keypair() 6 # Emisor encapsula contra pub. 7 ciphertext, ss_emisor = ml_kem_768.encrypt(pub) 8 # Destinatario desencapsula el ciphertext. 9 ss_destinatario = ml_kem_768.decrypt(priv, ciphertext) 10 assert ss_emisor == ss_destinatario # 32 bytes de secreto compartido
Llamada mínima posible (TypeScript) ¶
1 import { ml_kem768 } from '@noble/post-quantum/ml-kem'; 2 3 const kp = ml_kem768.keygen(); // {publicKey, secretKey} 4 const { cipherText, sharedSecret: sndSS } = ml_kem768.encapsulate(kp.publicKey); 5 const rcvSS = ml_kem768.decapsulate(cipherText, kp.secretKey); 6 // sndSS === rcvSS (32 bytes)
2. ML-DSA — FIPS 204 ¶
Qué hace: firma un mensaje arbitrario bajo una clave privada, verifica bajo una clave pública. Reemplazo directo para Ed25519, ECDSA, RSA-PSS. Se usa en JWT, firma de código, OCSP, CMS.
Reducción de seguridad: Module-LWE más Module-SIS (Short Integer Solutions). Asunción de dureza prácticamente igual a ML-KEM.
Parámetros:
1 parámetro cat. sec clave pub priv firma 2 ────────────────────────────────────────────────────── 3 ML-DSA-44 NIST L2 1312 B 2560 B 2420 B 4 ML-DSA-65 NIST L3 1952 B 4032 B 3293 B 5 ML-DSA-87 NIST L5 2592 B 4896 B 4595 B
Elige ML-DSA-65 para builds nuevos. L3 es la base recomendada.
Llamada mínima posible (Python) ¶
1 from pqcrypto.sign import ml_dsa_65 2 3 pub, priv = ml_dsa_65.generate_keypair() 4 msg = b"el mensaje a firmar" 5 sig = ml_dsa_65.sign(priv, msg) # 3293 bytes 6 ok = ml_dsa_65.verify(pub, msg, sig) # lanza excepción si falla
Llamada mínima posible (TypeScript) ¶
1 import { ml_dsa65 } from '@noble/post-quantum/ml-dsa'; 2 3 const kp = ml_dsa65.keygen(); 4 const msg = new TextEncoder().encode('el mensaje a firmar'); 5 const sig = ml_dsa65.sign(msg, kp.secretKey); 6 const ok = ml_dsa65.verify(sig, msg, kp.publicKey); // boolean
3. SLH-DSA — FIPS 205 ¶
Qué hace: firma recorriendo un árbol Merkle profundo de firmas de-un-solo-uso. Seguridad solo de la función hash. La primitiva de firma más conservadora de la suite.
Reducción de seguridad: resistencia a colisiones y preimagen de SHA-2 o SHAKE. Sin asunción teórica de números, sin asunción de retículos.
Parámetros (cada uno viene en s para "firma pequeña" y f para
"firma rápida"):
1 parámetro cat. sec clave pub firma verify 2 ────────────────────────────────────────────────────────────── 3 SLH-DSA-128s L1 32 B 7856 B rápido 4 SLH-DSA-128f L1 32 B 17088 B rápido 5 SLH-DSA-192s L3 48 B 16224 B rápido 6 SLH-DSA-192f L3 48 B 35664 B rápido 7 SLH-DSA-256s L5 64 B 29792 B rápido 8 SLH-DSA-256f L5 64 B 49856 B rápido
Variantes s: firma más pequeña, firma más lenta (minutos por clave).
Variantes f: firma más rápida (segundos), firma mayor.
Elige SLH-DSA-128s para raíces de firma de código. La verificación es solo-hash y rápida en cualquier plataforma — incluyendo PHP puro sin extensiones.
Llamada mínima posible (Python) ¶
1 from pqcrypto.sign import sphincs_sha2_128s_simple as slh 2 3 pub, priv = slh.generate_keypair() 4 msg = b"hash de imagen de firmware" 5 sig = slh.sign(priv, msg) 6 ok = slh.verify(pub, msg, sig)
4. FN-DSA (Falcon) — FIPS 206 (borrador) ¶
Qué hace: firma de retículos con las firmas más pequeñas de la suite.
Estado: borrador a mediados de 2026. Final esperado para 2026/2027.
Por qué esperar: Falcon depende de aritmética de punto flotante en la implementación de referencia. Los riesgos de canal lateral y de portado son reales. NIST aconseja contra despliegue hasta que FIPS 206 se finalice y existan implementaciones validadas de tiempo constante.
Cuándo elegirlo: cargas con limitación de ancho de banda donde el tamaño de firma importa más que la simplicidad de implementación (actualizaciones de firmware IoT, algunos flujos internos de HSM).
5. El árbol de decisión ¶
1 ¿Necesitas encapsular una clave (TLS, JWE, MLS, envelope KMS)? 2 └─ ML-KEM-768 3 4 ¿Necesitas firmar un token por mensaje (JWT, OCSP, firma de requests)? 5 ├─ ¿Servicio de larga duración? → ML-DSA-65 híbrido con Ed25519 6 └─ ¿Perf por petición? → ML-DSA-65 solo (ya es rápido) 7 8 ¿Necesitas firmar un artefacto de release (one-shot, verificado años)? 9 └─ SLH-DSA-128s (la elección "bóveda de 100 años") 10 11 ¿Firma con restricción de ancho de banda? 12 └─ Espera a FIPS 206 (Falcon) 13 14 ¿Verificación PHP pura en hosting compartido? 15 └─ SLH-DSA-128s — la verificación es solo-hash
6. Híbrido sigue siendo la jugada correcta ¶
Guía de NIST en sí: en esta ventana de transición, combina un algoritmo clásico con uno PQ para que una ruptura en cualquiera de los dos te deje seguro. El patrón estándar:
1 # Secreto compartido TLS híbrido = HKDF(X25519_ss || ML_KEM_ss, salt, info) 2 import os 3 from cryptography.hazmat.primitives import hashes 4 from cryptography.hazmat.primitives.kdf.hkdf import HKDF 5 from pqcrypto.kem import ml_kem_768 6 7 # Corre ambos KEMs en paralelo. 8 pq_pub, pq_priv = ml_kem_768.generate_keypair() 9 pq_ct, pq_ss = ml_kem_768.encrypt(pq_pub) 10 x_ss = os.urandom(32) # en realidad: secreto compartido X25519 11 12 shared = HKDF( 13 algorithm=hashes.SHA384(), length=32, 14 salt=b'qbit404-hybrid', info=b'tls-1.3-key-derivation' 15 ).derive(x_ss + pq_ss)
Esta es la misma construcción ratificada en draft-ietf-tls-hybrid-design.
Una ruptura de ML-KEM (improbable pero no-cero dada la juventud del campo)
no filtra el secreto compartido porque el atacante todavía tiene que
romper X25519 para la misma sesión — lo que requiere Shor, justo lo
que la migración PQ está evitando.
7. Checklist de despliegue ¶
1 [ ] CBOM (cryptography BOM) — enumera cada clave. 2 [ ] TLS de borde: KEM híbrido anunciado. 3 [ ] Firma de código: rotación de raíz SLH-DSA planificada. 4 [ ] Emisores JWT: ML-DSA híbrido emitiendo. 5 [ ] SDK pinning: alias ML-KEM / ML-DSA, NO alias Kyber / Dilithium. 6 [ ] Validación CAVP: requerida para cargas FedRAMP / seguridad nacional.
Matriz de librerías (mediados de 2026):
| Stack | KEM | Firma | Firma hash | ¿Alias FIPS? |
|---|---|---|---|---|
| OpenSSL ≥ 3.4 | ML-KEM | ML-DSA | SLH-DSA | sí |
| BoringSSL | ML-KEM (X25519MLKEM768) | (planificado) | (planificado) | sí |
| Noble (JS/TS) | ml_kem768 | ml_dsa65 | slh_dsa_sha2_128s | sí |
| pqcrypto (Python) | ml_kem_768 | ml_dsa_65 | sphincs_sha2_128s | parcial |
| liboqs (C / FFI) | matriz completa | matriz completa | matriz completa | sí |
8. Referencias ¶
- NIST FIPS 203 (ML-KEM), FIPS 204 (ML-DSA), FIPS 205 (SLH-DSA), FIPS 206 (FN-DSA, borrador)
- NIST SP 800-227 — implementando ML-KEM en TLS 1.3 (2025)
- Playbook de migración PQC conjunto BSI / ANSSI (2024)
- Hybrid Key Exchange in TLS 1.3, draft-ietf-tls-hybrid-design
La suite PQC ya no "viene". Está publicada, congelada en parámetros, y soportada en OpenSSL y Noble. El trabajo restante es operacional: inventario, rotación, validación. Las matemáticas están hechas; el trabajo es tuyo.
OPS RELACIONADAS
Q-day: cómo los analistas fijan la ventana entre 2029 y 2034
Los pronósticos convergen en una ventana de cinco años. Comparamos seis estimaciones publicadas lado a lado, mostramos qué asume cada una, y enviamos una calculadora del teorema de Mosca para que derives tu propio plazo de migración.
Grover muerde a AES: por qué 128 ha muerto y 256 es tu nueva base
El algoritmo de Grover da un speedup cuadrático sobre la búsqueda de clave por fuerza bruta. AES-128 baja a ~64 bits efectivos. AES-256 se queda en ~128 bits. El arreglo es una línea de config — pero hay que encontrar cada sitio donde vive estado de cifrado.
Cae RSA-2048: el estimado de 20M de qubits ruidosos, redibujado
Gidney y Ekerå dijeron ~20 millones de qubits ruidosos y ~8 horas. Tres años después de mejoras en corrección de errores, el número se mueve — pero no la conclusión. Dónde aterrizan los estimados de 2026.