Post-Quantum Cryptography in Delphi

· Components
Post-Quantum Cryptography in Delphi

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:

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:

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.

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.