A JWT is a small thing with a long reach. It is the proof of identity between your services, and the algorithm that signs it tends to be the slowest part of a system to change, because every issuer and every verifier has to move at once. That makes it worth knowing that the option exists before you need it.
sgcWebSockets 2026.10 adds the post-quantum JWT algorithms of RFC 9964 to the JWT client and server: ML-DSA-44, ML-DSA-65 and ML-DSA-87. They sit beside HS, RS and ES as three more values of the same property, and everything else about your token stays where it was.
Issuing a Token
Pick the algorithm in the header, give the component a private key, and sign:
oJWT := TsgcHTTP_JWT_Client.Create(nil);
oJWT.JWTOptions.Header.alg := jwtMLDSA65;
oJWT.JWTOptions.Algorithms.MLDSA.PrivateKey.Text := vPrivatePEM;
oJWT.JWTOptions.Payload.iss := 'my-service';
oJWT.JWTOptions.Payload.sub := 'user-1';
vToken := oJWT.Sign;
The private key is an ordinary PKCS#8 PEM. All three forms RFC 9881 defines are read, so a key stored as the 32 byte seed, as the expanded key, or as both, all work.
Validating One
The server side is the same shape, with the public key and the family switched on:
oServer := TsgcHTTP_JWT_Server.Create(nil);
oServer.JWTOptions.Algorithms.MLDSA.Enabled := True;
oServer.JWTOptions.Algorithms.MLDSA.PublicKey.Text := vPublicPEM;
if oServer.Validate(vToken, vHeader, vPayload, vError) then
ShowMessage(vPayload);
Each family is gated on its own, so a deployment that only ever issues ML-DSA tokens can turn the others off and a token arriving with alg set to something else is refused rather than checked.
The Parameter Set Has to Match
There is one rule in RFC 9964 worth stating out loud, because it is the mistake that would otherwise go unnoticed: the key has to belong to the parameter set the alg names. A key that does not match is refused. Signing with the wrong one raises, and validating with the wrong one returns false rather than raising, so an attacker cannot use the difference to learn anything.
Keys as JSON Web Keys
RFC 9964 registers a key type for these algorithms, AKP, and the library reads and writes it, which is what you want when keys are published at a JWKS endpoint rather than dropped in a file:
uses
sgcHTTP_JWT_MLDSA;
var
vJWK, vPublicPEM, vPrivatePEM: string;
begin
// {"kty":"AKP","alg":"ML-DSA-65","pub":"...","priv":"..."}
vJWK := sgcMLDSA_ExportPrivateJWK(mldsa65, oSeed);
// and back again, as the PEM the components read
sgcMLDSA_ImportJWKAsPEM(vJWK, vPublicPEM, vPrivatePEM);
end;
The private member is the 32 byte seed, which is what the RFC defines, and the reader is strict about it: the members must be canonical base64url of exactly the right length, alg is required, and when a private key is present it must agree with the public one in the same JWK. A JWK that fails any of those is rejected instead of half loaded.
Interoperability, Demonstrated
A signature format is only useful if somebody else produces the same bytes. Two checks ship as tests rather than as claims:
- The published examples of RFC 9964 are reproduced byte for byte. The signer is the deterministic variant, so the same key and the same input always give the same signature, and the appendix of the RFC becomes something you can assert on.
- Tokens produced by an independent implementation, in this case the Python library dilithium-py, validate here, and the key containers written by hand from RFC 9881 round trip unchanged.
While We Were In There: a Binary HMAC Secret
An unrelated limitation went away in the same pass. The HS algorithms took their key as text, and a real HMAC key is random bytes, most of which are not valid text at all. There is now a second property that takes the key as base64url, the same encoding the k member of an oct JWK uses:
oJWT.JWTOptions.Algorithms.HS.SecretBase64URL := 'AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ...';
When it is set it takes precedence over the text secret, on the client and on the server alike.
Claims Written by Somebody Else
One more fix worth knowing if you accept tokens from other issuers. A JSON string can be written as raw UTF-8 or with every non-ASCII character escaped, and different libraries choose differently. Both forms now resolve to the same text, so a name with an accent in it reads correctly whichever issuer produced it, and an alg header written with escapes is recognised as the algorithm it spells.
Upgrading
The three algorithms are additions at the end of the existing list, so a stored ordinal keeps its meaning and nothing you already do changes. Signing with ML-DSA needs the sgcCrypto pack, and a build without it refuses those tokens with a clear message instead of failing obscurely.
Read Next
- Post-Quantum Cryptography in Delphi
- Delphi TLS 1.3 Without OpenSSL DLLs
- JWT Delphi Client and JWT Delphi Server, the basics
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.
