Un ordenador cuántico suficientemente grande rompería RSA, X25519/X448 y ECDSA/ECDH en todas las curvas de la página Criptografía de Clave Pública: ninguna de esas matemáticas resiste el algoritmo de Shor. No necesita existir hoy para ser un problema hoy, un adversario puede grabar tráfico cifrado ahora y descifrarlo cuando exista esa máquina, una estrategia habitualmente llamada "capturar ahora, descifrar después". Cualquier cosa que deba permanecer confidencial durante años, no días, ya debería estar migrando hacia un intercambio de claves poscuántico.
El NIST estandarizó tres esquemas, todos implementados aquí, y todos construidos únicamente sobre SHAKE (vea Hash y Autenticación de Mensajes), de modo que su seguridad descansa solo en que el hash sea sólido: ninguna suposición de retículos ni de teoría de números que romper.
| Esquema | Propósito | Estándar | Unidad |
| ML-KEM | Encapsulado de claves, reemplaza a Diffie-Hellman | FIPS 203 | sgcCrypto_MLKEM |
| ML-DSA | Firmas, basado en retículos, la opción predeterminada | FIPS 204 | sgcCrypto_MLDSA |
| SLH-DSA | Firmas, basado solo en hash, alternativa conservadora | FIPS 205 | sgcCrypto_SLHDSA |
ML-KEM y la construcción híbrida (recomendado)
ML-KEM reemplaza un intercambio Diffie-Hellman: el emisor encapsula un secreto compartido con la clave pública del destinatario, el destinatario lo desencapsula con la clave privada, y ambos terminan con los mismos 32 bytes. Viene en tres conjuntos de parámetros, cuanto mayor, mayor margen de seguridad y mayores claves.
procedure sgcMLKEM_GenerateKeyPair(aParams: TsgcMLKEMParams; out aPublicKey, aPrivateKey: TBytes);
procedure sgcMLKEM_Encapsulate(aParams: TsgcMLKEMParams; const aPublicKey: TBytes; out aCiphertext, aSharedSecret: TBytes);
function sgcMLKEM_Decapsulate(aParams: TsgcMLKEMParams; const aPrivateKey, aCiphertext: TBytes): TBytes;
var
oPub, oPriv, oCiphertext, oSecretA, oSecretB: TBytes;
begin
sgcMLKEM_GenerateKeyPair(mlkem768, oPub, oPriv);
sgcMLKEM_Encapsulate(mlkem768, oPub, oCiphertext, oSecretA); { sender }
oSecretB := sgcMLKEM_Decapsulate(mlkem768, oPriv, oCiphertext); { recipient }
{ oSecretA = oSecretB }
end;
La decapsulación de FIPS 203 nunca falla.
Un texto cifrado corrupto o malicioso no genera un error, en su lugar devuelve silenciosamente un secreto distinto, derivado de un valor de rechazo implícito incorporado en la clave privada. Devolver un error en su lugar entregaría a un atacante un oráculo de descifrado con el que sondear la clave privada; el estándar evita eso por diseño. Lo que sea que proteja el secreto derivado tiene que detectar la discrepancia por sí mismo, por ejemplo una etiqueta AEAD que falla al verificarse más adelante.
Lo que realmente conviene desplegar hoy es la construcción híbrida: X25519 combinado con ML-KEM-768, de modo que el secreto compartido depende de ambos. Sigue siendo seguro si ML-KEM llega a romperse alguna vez por un avance criptoanalítico, y sigue siendo seguro si un ordenador cuántico rompe X25519; un atacante necesita que fallen ambos a la vez.
procedure sgcHybrid_GenerateKeyPair(out aPublicKey, aPrivateKey: TBytes);
procedure sgcHybrid_Encapsulate(const aPublicKey: TBytes; out aCiphertext, aSharedSecret: TBytes);
function sgcHybrid_Decapsulate(const aPrivateKey, aCiphertext: TBytes; out aSharedSecret: TBytes): Boolean;
X-Wing y los grupos híbridos de TLS 1.3
sgcCrypto_MLKEM_Hybrid contiene tres familias híbridas. Las funciones sgcHybrid_ anteriores son el formato original de sgcCrypto y se mantienen por compatibilidad. X-Wing es el KEM híbrido estandarizado de uso general, X25519 combinado con ML-KEM-768, y es el que conviene usar en código nuevo fuera de TLS. La tercera familia construye los key share híbridos de TLS 1.3 y nada más.
X-Wing (draft-connolly-cfrg-xwing-kem-10) almacena un único secreto: la clave privada es una semilla de 32 bytes, y cada operación la expande al material ML-KEM-768 y X25519 que necesita. La clave pública ocupa 1216 bytes, el texto cifrado 1120 bytes y el secreto compartido 32 bytes. El encapsulado ejecuta la comprobación de la clave de encapsulado de FIPS 203 y genera un error cuando la clave pública no la supera.
procedure sgcXWing_GenerateKeyPair(out aPublicKey, aPrivateKey: TBytes);
procedure sgcXWing_Encapsulate(const aPublicKey: TBytes; out aCiphertext, aSharedSecret: TBytes);
function sgcXWing_Decapsulate(const aPrivateKey, aCiphertext: TBytes; out aSharedSecret: TBytes): Boolean;
var
oPub, oPriv, oCiphertext, oSecretA, oSecretB: TBytes;
begin
sgcXWing_GenerateKeyPair(oPub, oPriv);
sgcXWing_Encapsulate(oPub, oCiphertext, oSecretA); { sender }
if sgcXWing_Decapsulate(oPriv, oCiphertext, oSecretB) then
{ oSecretA = oSecretB };
end;
Los key share híbridos de TLS 1.3 de RFC 10024 son X25519MLKEM768, SecP256r1MLKEM768 y SecP384r1MLKEM1024, con los puntos de código IANA 0x11EC, 0x11EB y 0x11ED. No son KEM que pueda usar por su cuenta. El secreto compartido es una simple concatenación de las dos mitades, algo que solo resulta seguro porque TLS lo introduce en su esquema de derivación de claves junto con la transcripción del protocolo de enlace, así que úselos para construir un key share de TLS y nada más. El motor TLS nativo ofrece X25519MLKEM768 en primer lugar de forma predeterminada, de modo que un cliente o un servidor que lo utilice ya negocia un intercambio de claves poscuántico sin ningún código adicional.
Los grupos híbridos solo existen en TLS 1.3. El motor nativo también habla TLS 1.2, y una conexión que recae en él porque el interlocutor no admite TLS 1.3 usa un grupo clásico, X25519, secp256r1 o secp384r1, sin protección poscuántica. Establezca TLSOptions.Version en un cliente o SSLOptions.Version en un servidor en tls1_3, y liste solo grupos híbridos en NativeTLS_Options.Groups, cuando el intercambio de claves deba ser siempre poscuántico.
La demo Post-Quantum TLS, Demos\18.Cryptography\08.PostQuantum_TLS, tiene un selector de versión TLS con las opciones TLS 1.3 y TLS 1.2, solo TLS 1.3 y solo TLS 1.2, y un selector de conjunto de cifrado, y su línea de estado muestra la versión, el grupo y el conjunto de cifrado negociados. La demo de servidor puede firmar con un certificado ML-DSA-44, que solo sirve a clientes TLS 1.3, porque TLS 1.2 no tiene ningún esquema de firma ML-DSA.
type
TsgcTLSHybridGroup = (thgX25519MLKEM768, thgSecP256r1MLKEM768, thgSecP384r1MLKEM1024);
function sgcTLSHybrid_GroupID(aGroup: TsgcTLSHybridGroup): Word;
function sgcTLSHybrid_GroupName(aGroup: TsgcTLSHybridGroup): string;
function sgcTLSHybrid_Available(aGroup: TsgcTLSHybridGroup): Boolean;
procedure sgcTLSHybrid_ClientKeyShare(aGroup: TsgcTLSHybridGroup; out aKeyShare, aPrivateState: TBytes);
function sgcTLSHybrid_ServerKeyShare(aGroup: TsgcTLSHybridGroup; const aClientKeyShare: TBytes;
out aKeyShare, aSharedSecret: TBytes): Boolean;
function sgcTLSHybrid_ClientSharedSecret(aGroup: TsgcTLSHybridGroup;
const aPrivateState, aServerKeyShare: TBytes; out aSharedSecret: TBytes): Boolean;
El cliente mantiene aPrivateState en secreto, lo pasa sin cambios a sgcTLSHybrid_ClientSharedSecret y lo borra después; su estructura interna no forma parte de la API. Las dos funciones Boolean devuelven False, con todas las salidas vacías, ante cualquier entrada inválida del par: un key share de longitud incorrecta, una clave de encapsulado ML-KEM que no supera la comprobación de FIPS 203, un punto que no está en la curva, o un resultado X25519 compuesto solo por ceros. RFC 10024 exige abortar la conexión con una alerta illegal_parameter en cada uno de esos casos.
Todo lo que se apoya en X25519, es decir las funciones sgcHybrid_, X-Wing y el grupo X25519MLKEM768, queda fuera de una compilación con SGC_CRYPTO_FIPS, vea Modo FIPS. Los dos grupos SecP se mantienen, porque P-256, P-384 y ML-KEM son todos algoritmos aprobados. Llame a sgcTLSHybrid_Available antes de ofrecer o aceptar un grupo, en lugar de dar por supuesto que está presente.
ML-DSA (recomendado para firmas)
Firmar extrae un vector de enmascaramiento e intenta construir una firma, reiniciando cuando el intento filtraría información sobre la clave; el número esperado de intentos es pequeño pero no fijo, de modo que firmar tarda una cantidad de tiempo variable. Eso es inherente al esquema y no es un canal lateral sobre la clave. Las firmas son grandes: 2420 bytes para ML-DSA-44 frente a 64 de Ed25519, y claves públicas de 1312 bytes frente a 32, así que calcule esa diferencia antes de adoptarlo en un protocolo sensible al tamaño.
procedure sgcMLDSA_GenerateKeyPair(aParams: TsgcMLDSAParams; out aPublicKey, aPrivateKey: TBytes);
function sgcMLDSA_Sign(aParams: TsgcMLDSAParams; const aPrivateKey, aMessage, aContext: TBytes): TBytes;
function sgcMLDSA_Verify(aParams: TsgcMLDSAParams; const aPublicKey, aMessage, aSignature, aContext: TBytes): Boolean;
var
oPub, oPriv, oSignature, oEmpty: TBytes;
begin
sgcMLDSA_GenerateKeyPair(mldsa44, oPub, oPriv);
oSignature := sgcMLDSA_Sign(mldsa44, oPriv, oMessage, oEmpty);
if sgcMLDSA_Verify(mldsa44, oPub, oMessage, oSignature, oEmpty) then
{ valid };
end;
SLH-DSA (alternativa conservadora)
SLH-DSA descansa únicamente en la función hash: ningún problema de retículos que romper, nunca, en ningún momento futuro. Esa garantía es cara. Una firma va desde 7856 bytes (la más pequeña, SLH-DSA-SHAKE-128s) hasta 49856 bytes (la más grande, SLH-DSA-SHAKE-256f), y firmar con un conjunto de parámetros s (pequeño) recorre cientos de miles de operaciones hash y puede tardar decenas de segundos; los conjuntos f (rápido) firman en torno a uno a siete segundos con aproximadamente el doble de tamaño de firma. La verificación es rápida para cualquier conjunto de parámetros, lo que se adapta mejor a un caso de uso de firmar pocas veces y verificar a menudo, como firmware o firma de código, que a un protocolo interactivo.
procedure sgcSLHDSA_GenerateKeyPair(aParams: TsgcSLHDSAParams; out aPublicKey, aPrivateKey: TBytes);
function sgcSLHDSA_Sign(aParams: TsgcSLHDSAParams; const aPrivateKey, aMessage, aContext: TBytes;
aDeterministic: Boolean = True): TBytes;
function sgcSLHDSA_Verify(aParams: TsgcSLHDSAParams; const aPublicKey, aMessage, aSignature, aContext: TBytes): Boolean;
Formatos de clave: SubjectPublicKeyInfo, PKCS#8 y PEM
Las cadenas de bytes en bruto que consumen los algoritmos FIPS no son un formato de almacenamiento, y ninguna otra implementación las leerá. Para escribir una clave en disco, incluirla en un certificado o entregarla a otra biblioteca, expórtela en los contenedores estándar: un SubjectPublicKeyInfo X.509 para una clave pública, un OneAsymmetricKey PKCS#8 para una clave privada, y la armadura PEM (RFC 7468) alrededor de cualquiera de los dos, con las etiquetas PUBLIC KEY y PRIVATE KEY. Los tres esquemas exponen las mismas ocho funciones, cada una siguiendo el RFC que define su codificación.
| Esquema | RFC del contenedor | Funciones DER | Funciones PEM |
| ML-DSA | RFC 9881 | sgcMLDSA_ExportSubjectPublicKeyInfo, sgcMLDSA_ImportSubjectPublicKeyInfo, sgcMLDSA_ExportPrivateKeyInfo, sgcMLDSA_ImportPrivateKeyInfo | sgcMLDSA_ExportPublicKeyPEM, sgcMLDSA_ImportPublicKeyPEM, sgcMLDSA_ExportPrivateKeyPEM, sgcMLDSA_ImportPrivateKeyPEM |
| ML-KEM | RFC 9935 | sgcMLKEM_ExportSubjectPublicKeyInfo, sgcMLKEM_ImportSubjectPublicKeyInfo, sgcMLKEM_ExportPrivateKeyInfo, sgcMLKEM_ImportPrivateKeyInfo | sgcMLKEM_ExportPublicKeyPEM, sgcMLKEM_ImportPublicKeyPEM, sgcMLKEM_ExportPrivateKeyPEM, sgcMLKEM_ImportPrivateKeyPEM |
| SLH-DSA | RFC 9909 | sgcSLHDSA_ExportSubjectPublicKeyInfo, sgcSLHDSA_ImportSubjectPublicKeyInfo, sgcSLHDSA_ExportPrivateKeyInfo, sgcSLHDSA_ImportPrivateKeyInfo | sgcSLHDSA_ExportPublicKeyPEM, sgcSLHDSA_ImportPublicKeyPEM, sgcSLHDSA_ExportPrivateKeyPEM, sgcSLHDSA_ImportPrivateKeyPEM |
El identificador de algoritmo lleva el identificador de objeto del NIST con los parámetros ausentes, de modo que el conjunto de parámetros viaja con la clave y la importación lo devuelve. sgcMLDSA_OID, sgcMLKEM_OID y sgcSLHDSA_OID dan la cadena con puntos, y las funciones ParamsFromOID correspondientes hacen la conversión inversa.
Tres formas de clave privada para ML-DSA y ML-KEM.
Las funciones de exportación toman un parámetro aFormat de tipo TsgcPQCPrivateKeyFormat, declarado en sgcCrypto_DER. pqkfSeed, el valor predeterminado, escribe solo la semilla, 32 bytes para ML-DSA y los 64 bytes d || z para ML-KEM; esta es la forma que recomiendan ambos RFC y es con diferencia la más pequeña. pqkfExpanded escribe la clave privada FIPS expandida. pqkfBoth escribe las dos y genera un error cuando la clave expandida no es la expansión de la semilla, que es lo que los RFC llaman una clave malformada.
Una semilla no se puede recuperar después a partir de una clave expandida, así que pídala en el momento de generar el par con sgcMLDSA_GenerateKeyPairAndSeed o sgcMLKEM_GenerateKeyPairAndSeed cuando tenga intención de almacenar la forma recomendada. SLH-DSA tiene una única forma de clave privada, SK.seed || SK.prf || PK.seed || PK.root, así que su exportación no toma ningún parámetro de formato.
function sgcMLDSA_ExportPrivateKeyPEM(aParams: TsgcMLDSAParams; const aSeed, aPrivateKey: TBytes;
aFormat: TsgcPQCPrivateKeyFormat = pqkfSeed): string;
function sgcMLDSA_ImportPrivateKeyPEM(const aPEM: string; out aParams: TsgcMLDSAParams;
out aSeed, aPrivateKey, aPublicKey: TBytes): Boolean;
var
vParams: TsgcMLDSAParams;
oSeed, oPub, oPriv: TBytes;
vPublicPEM, vPrivatePEM: string;
begin
sgcMLDSA_GenerateKeyPairAndSeed(mldsa65, oSeed, oPub, oPriv);
vPublicPEM := sgcMLDSA_ExportPublicKeyPEM(mldsa65, oPub);
vPrivatePEM := sgcMLDSA_ExportPrivateKeyPEM(mldsa65, oSeed, oPriv, pqkfSeed);
{ reading it back: the parameter set comes from the key itself }
if sgcMLDSA_ImportPrivateKeyPEM(vPrivatePEM, vParams, oSeed, oPriv, oPub) then
{ vParams = mldsa65, and oPriv and oPub are the expansion of oSeed };
end;
La importación valida, no confía.
Todas las funciones de importación devuelven False en lugar de generar un error, y devuelven False ante DER malformado, ante datos sobrantes al final, ante un identificador de objeto desconocido, ante parámetros presentes cuando deben estar ausentes, y ante una clave cuya longitud no coincide con el conjunto de parámetros. Más allá de la estructura, una clave pública ML-KEM debe superar la comprobación de módulo de FIPS 203, una clave privada ML-KEM expandida debe superar la comprobación de hash y la prueba de consistencia por pares, y una clave privada ML-DSA expandida se reconstruye hasta su clave pública, de modo que los valores t0 y tr almacenados deben coincidir ambos con ella. Una clave pública incrustada que no se corresponde con la clave privada se rechaza. Cuando una clave privada contiene una semilla, las tres salidas vuelven rellenas, así que quien la llama nunca tiene que expandirla por separado.
sgcSLHDSA_ImportPrivateKeyInfo y sgcSLHDSA_ImportPrivateKeyPEM toman un parámetro adicional aVerify, True de forma predeterminada, que recalcula PK.root a partir de la semilla privada y rechaza una clave cuya raíz almacenada difiera. Esa reconstrucción cuesta lo mismo que generar una clave, lo que son segundos para los conjuntos de parámetros s, así que pase False solo para claves que su propia aplicación haya generado o en las que ya confíe. El identificador de objeto, las longitudes y la estructura se siguen comprobando cuando es False.
Estos son los contenedores que lee el resto de la biblioteca. Una clave privada ML-DSA en PKCS#8 es con la que firma el cliente JWT, un SubjectPublicKeyInfo es con lo que valida el servidor JWT, y ambos son lo que Certificados y PKI incluye en un certificado o en una solicitud de firma de certificado.
Elegir un conjunto de parámetros
Cada función de tamaño (sgcMLKEM_PublicKeySize, sgcMLDSA_SignatureSize, sgcSLHDSA_SignatureSize y sus equivalentes PrivateKeySize) toma el mismo enum de parámetros que la propia operación, de modo que un protocolo puede dimensionar sus búferes antes de realizar ninguna operación criptográfica:
| Conjunto de parámetros | Clave pública | Clave privada | Texto cifrado / Firma |
| ML-KEM-512 | 800 | 1632 | 768 |
| ML-KEM-768 | 1184 | 2400 | 1088 |
| ML-KEM-1024 | 1568 | 3168 | 1568 |
| ML-DSA-44 | 1312 | 2560 | 2420 |
| ML-DSA-65 | 1952 | 4032 | 3309 |
| ML-DSA-87 | 2592 | 4896 | 4627 |
| SLH-DSA-SHAKE-128s | 32 | 64 | 7856 |
| SLH-DSA-SHAKE-128f | 32 | 64 | 17088 |
| SLH-DSA-SHAKE-192s | 48 | 96 | 16224 |
| SLH-DSA-SHAKE-192f | 48 | 96 | 35664 |
| SLH-DSA-SHAKE-256s | 64 | 128 | 29792 |
| SLH-DSA-SHAKE-256f | 64 | 128 | 49856 |
Todos los tamaños están en bytes. El secreto compartido de ML-KEM siempre tiene 32 bytes con independencia del conjunto de parámetros (sgcMLKEM_SharedSecretSize).