There is a question worth asking about the data your application moves today: how long does it stay sensitive? A medical record, a contract, a payroll file, a set of credentials. If the answer is more than a few years, then the interesting threat is not an attacker who breaks your encryption now. It is an attacker who records the traffic now, stores it, and decrypts it later when the tools have caught up. The habit has a name, harvest now, decrypt later, and it does not require anyone to build a quantum computer today. It only requires them to believe someone will.
That is why the standards moved first. NIST published the algorithms in 2024, browsers and CDNs turned on hybrid key exchange during 2025, and the deadlines for retiring RSA and elliptic curve key agreement are now written into public guidance. sgcWebSockets 2026.10 brings the whole set to Delphi, written in Object Pascal inside the sgcCrypto pack, with no external library to install or deploy.
Three Algorithms, One Pack
The three NIST standards do different jobs, and you will normally use the first two:
- ML-KEM (FIPS 203) agrees a shared secret. It replaces the job Diffie-Hellman does today.
- ML-DSA (FIPS 204) signs. It replaces RSA and ECDSA signatures.
- SLH-DSA (FIPS 205) also signs, built only on hash functions, for the case where you want the most conservative assumption available and can pay for the signature size.
Agreeing a Key With ML-KEM
A key encapsulation mechanism is simpler than Diffie-Hellman to use. The sender takes the recipient's public key and produces two things: a fresh shared secret, and a ciphertext that delivers it. The recipient turns the ciphertext back into the same secret.
uses
sgcCrypto_MLKEM;
var
oPublicKey, oPrivateKey, oCiphertext, oSecret, oSame: TBytes;
begin
sgcMLKEM_GenerateKeyPair(mlkem768, oPublicKey, oPrivateKey);
// the sender, who only ever sees the public key
sgcMLKEM_Encapsulate(mlkem768, oPublicKey, oCiphertext, oSecret);
// the recipient, who holds the private key, gets the same 32 bytes
oSame := sgcMLKEM_Decapsulate(mlkem768, oPrivateKey, oCiphertext);
end;
The parameter sets are mlkem512, mlkem768 and mlkem1024. The input checks FIPS 203 requires run before anything else, so a malformed public key raises instead of quietly producing a secret, and a ciphertext that does not decrypt yields a pseudorandom secret rather than an error, which is the implicit rejection the standard asks for.
X-Wing, When You Do Not Want to Choose
Adopting a new algorithm means trusting it. Hybrid constructions remove that decision: they combine a classical algorithm with a post-quantum one so the result is safe as long as either half holds. X-Wing pairs X25519 with ML-KEM-768 and exposes a single key pair.
uses
sgcCrypto_MLKEM_Hybrid;
var
oPublicKey, oPrivateKey, oCiphertext, oSecret: TBytes;
begin
sgcXWing_GenerateKeyPair(oPublicKey, oPrivateKey);
sgcXWing_Encapsulate(oPublicKey, oCiphertext, oSecret);
oSecret := sgcXWing_Decapsulate(oPrivateKey, oCiphertext);
end;
Signing With ML-DSA
Signing takes the parameter set, the private key, the message and a context string, which is normally empty:
uses
sgcCrypto_MLDSA;
var
oSeed, oPublicKey, oPrivateKey, oSignature: TBytes;
begin
sgcMLDSA_GenerateKeyPairAndSeed(mldsa65, oSeed, oPublicKey, oPrivateKey);
oSignature := sgcMLDSA_Sign(mldsa65, oPrivateKey, aMessage, nil);
if sgcMLDSA_Verify(mldsa65, oPublicKey, aMessage, oSignature, nil) then
ShowMessage('signature is valid');
end;
Note the seed. An ML-DSA private key can be stored as the 32 bytes it was generated from, or as the expanded key, or as both, and the library reads and writes all three. The seed is the form you want in a configuration file: it is small, and the expanded key is derived from it deterministically.
Keys That Travel
An algorithm nobody can exchange keys with is not much use, so the encodings follow the published profiles: SubjectPublicKeyInfo and PKCS#8, in DER or PEM, per RFC 9935 for ML-KEM, RFC 9881 for ML-DSA and RFC 9909 for SLH-DSA.
var
vPublicPEM, vPrivatePEM: string;
begin
vPublicPEM := sgcMLDSA_ExportPublicKeyPEM(mldsa65, oPublicKey);
vPrivatePEM := sgcMLDSA_ExportPrivateKeyPEM(mldsa65, oSeed, nil);
end;
Those are ordinary BEGIN PUBLIC KEY and BEGIN PRIVATE KEY blocks that other implementations read. A private key is checked when it is imported: the public half is derived again and compared, so a key that does not belong to its certificate is refused there and then rather than producing signatures nobody can verify.
Certificates and a Post-Quantum CA
Certificates and certificate requests can carry post-quantum keys, and a certificate authority can hold one too, so a whole chain can be post-quantum:
var
vKey: TsgcX509SignKey;
vOptions: TsgcX509Options;
oCertDER: TBytes;
begin
vKey := sgcX509_MLDSAKey(mldsa65, oPrivateKey, oPublicKey);
vOptions.Subject.CommonName := 'My Post-Quantum CA';
vOptions.KeyUsage := [kuKeyCertSign, kuCRLSign];
oCertDER := sgcX509_CreateSelfSignedEx(vKey, vOptions);
end;
The key usage rules of the profiles are enforced before anything is signed, so a post-quantum key asked to do key encipherment raises rather than producing a certificate that violates its own profile. Chain validation now also enforces the path length and the name constraints the issuing certificates declare.
Where It Is Already Wired In
Two places in the library use all of this for you, with no cryptography to write:
- TLS. The native TLS 1.3 engine negotiates the hybrid key exchange groups of RFC 10024,
X25519MLKEM768,SecP256r1MLKEM768andSecP384r1MLKEM1024, and the default list already starts with a hybrid one. See Delphi TLS 1.3 Without OpenSSL DLLs. - JWT. Tokens can be signed and verified with ML-DSA under RFC 9964. See Sign a JWT With ML-DSA in Delphi.
How We Know It Is Right
An implementation of a standard is a claim until something checks it. All three algorithms are validated against the NIST known answer vectors, and those vectors ship with the library as a runnable test set rather than as a sentence in a datasheet. The ML-DSA signer is the deterministic variant, so the same key and the same message always give the same signature, which is what makes the published examples in the RFCs reproducible byte for byte.
Which One Should You Use
For key agreement, use a hybrid: X-Wing on its own, or the hybrid TLS groups if the traffic is TLS. You give up almost nothing and you are covered whichever assumption turns out to be wrong. For signatures, ML-DSA-65 is the sensible default, with ML-DSA-44 when size matters and ML-DSA-87 when it does not. Reach for SLH-DSA when the signature will be verified many years from now by something you cannot update, such as firmware, and the signature size is affordable.
Upgrading
All of it lives in the sgcCrypto pack and adds no dependency: no OpenSSL, no DLL, nothing to install on the machine that runs your application. Existing code is untouched until you call it.
Read Next
- Delphi TLS 1.3 Without OpenSSL DLLs
- Sign a JWT With ML-DSA in Delphi
- sgcWebSockets 2026.10, everything else in this release
Watch It
There is a short video of this on the eSeGeCe channel.
Questions, feedback or migration help? Get in touch — you will get a reply from the people who wrote the code.
