A sufficiently large quantum computer would break RSA, X25519/X448 and ECDSA/ECDH on every curve on the Public Key Cryptography page: none of that math resists Shor's algorithm. It does not need to exist today to be a problem today, an adversary can record encrypted traffic now and decrypt it once such a machine exists, a strategy usually called "harvest now, decrypt later". Anything that must stay confidential for years, not days, should already be moving to post-quantum key exchange.
NIST standardized three schemes, all implemented here, and all built on nothing but SHAKE (see Hashing & Message Authentication), so their security rests only on the hash being sound: no lattice or number theory assumption to break.
| Scheme | Purpose | Standard | Unit |
| ML-KEM | Key encapsulation, replaces Diffie-Hellman | FIPS 203 | sgcCrypto_MLKEM |
| ML-DSA | Signatures, lattice based, the default choice | FIPS 204 | sgcCrypto_MLDSA |
| SLH-DSA | Signatures, hash based only, conservative fallback | FIPS 205 | sgcCrypto_SLHDSA |
ML-KEM and the hybrid construction (recommended)
ML-KEM replaces a Diffie-Hellman exchange: the sender encapsulates a shared secret to the recipient's public key, the recipient decapsulates it with the private key, and both end up with the same 32 bytes. It comes in three parameter sets, larger meaning a bigger security margin and bigger keys.
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;
FIPS 203 decapsulation never fails.
A corrupt or malicious ciphertext does not raise an error, it silently returns a different secret, derived from an implicit rejection value baked into the private key. Returning an error instead would hand an attacker a decryption oracle to probe the private key with; the standard avoids that by design. Whatever the derived secret protects has to detect the mismatch on its own, for example an AEAD tag failing to verify downstream.
What to actually deploy today is the hybrid construction: X25519 combined with ML-KEM-768, so the shared secret depends on both. It stays safe if ML-KEM is ever broken by a cryptanalytic advance, and it stays safe if a quantum computer breaks X25519; an attacker needs both to fail at once.
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;
ML-DSA (recommended for signatures)
Signing draws a masking vector and tries to build a signature, restarting when the attempt would leak information about the key; the expected number of attempts is small but not fixed, so signing takes a variable amount of time. That is inherent to the scheme and is not a side channel on the key. Signatures are large: 2420 bytes for ML-DSA-44 against 64 for Ed25519, and public keys 1312 bytes against 32, so budget for the difference before adopting it in a size sensitive protocol.
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 (conservative fallback)
SLH-DSA rests on nothing but the hash function: no lattice problem to break, ever, at any point in the future. That guarantee is expensive. A signature runs from 7856 bytes (the smallest, SLH-DSA-SHAKE-128s) to 49856 bytes (the largest, SLH-DSA-SHAKE-256f), and signing an s (small) parameter set walks hundreds of thousands of hash operations and can take tens of seconds; the f (fast) sets sign in around one to seven seconds at roughly twice the signature size. Verification is quick for every parameter set, which suits a sign-rarely, verify-often use case such as firmware or code signing better than an interactive protocol.
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;
Choosing a parameter set
Every size call (sgcMLKEM_PublicKeySize, sgcMLDSA_SignatureSize, sgcSLHDSA_SignatureSize and their PrivateKeySize counterparts) takes the same parameter enum as the operation itself, so a protocol can size its buffers before doing any cryptography:
| Parameter set | Public key | Private key | Ciphertext / Signature |
| 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 |
All sizes are in bytes. The ML-KEM shared secret is always 32 bytes regardless of parameter set (sgcMLKEM_SharedSecretSize).