Some time ago, I wrote a post about post-quantum cryptography in .NET, where I introduced the concept of post-quantum cryptography and discussed the early BouncyCastle.NET implementation of Kyber and Dilithium. Today I would like to revisit this post, as both of these algorithms have been officially standardized as ML-KEM and ML-DSA.
Background and standardization efforts π
In 2016 NIST (National Institute of Standards and Technology) announced a post-quantum cryptography competition, with the aim to provide a standardized crypto scheme resistant to quantum threats.
In 2022 the four winners were announced:
- π CRYSTALS-Kyber - asymmetric encryption, lattice-based
- βοΈ CRYSTALS-Dilithium - signature, lattice-based
- βοΈ FALCON - signature, lattice-based
- βοΈ SPHINCS+ - signature, hash-based
In August of 2024, the first three PQC (post-quantum cryptography) standards were officially formalized:
- π ML-KEM (Kyber) β Module-Lattice-Based Key-Encapsulation Mechanism Standard β FIPS-203
- βοΈ ML-DSA (Dilithium) β Module-Lattice-Based Digital Signature Standard β FIPS-204
- βοΈ SLH-DSA (SPHINCS+) β Stateless Hash-Based Digital Signature Standard β FIPS-205
This means that software developers across the world are now officially encouraged to start using these algorithms in their applications, as they prepare for the quantum era.
ML-KEM is also being standardized for TLS 1.3 as three hybrid key agreements: X25519MLKEM768, SecP256r1MLKEM768, and SecP384r1MLKEM1024, which combine a post-quantum KEM with an elliptic curve Diffie-Hellman(ECDHE). X25519MLKEM768 is supported by default in Chrome, Edge, Firefox, Brave and Opera already.
Let’s walk through the details of working with ML-KEM and ML-DSA using BouncyCastle 2.5.0. This is the version in which the old Kyber/Dilithium implementations were replaced by the new standards.
ML-KEM: Key Encapsulation π
The ML-KEM process begins with key generation. Of course unlike traditional asymmetric cryptography, ML-KEM’s parameters are specifically chosen to resist quantum attacks:
// First, Alice generates her key pair
var random = new SecureRandom();
var keyGenParameters = new MLKemKeyGenerationParameters(random, MLKemParameters.ml_kem_768);
var kyberKeyPairGenerator = new MLKemKeyPairGenerator();
kyberKeyPairGenerator.Init(keyGenParameters);
var aliceKeyPair = kyberKeyPairGenerator.GenerateKeyPair();
We are using the ML-KEM-768 parameter set, which is the recommended choice for most applications, and provides a good balance between security and performance. The effective strength of the handshake is comparable to AES-192, though the negotiated secret is 32 bytes, so suitable for AES-256.
One interesting aspect of ML-KEM is its larger key sizes compared to traditional algorithms. This reflects the fundamental trade-offs required for quantum resistance.
Public Key (bytes) | Ciphertext (bytes) | Shared Secret (bytes) |
---|---|---|
ML-KEM-512 | 800 | 768 |
ML-KEM-768 | 1184 | 1088 |
ML-KEM-1024 | 1568 | 1568 |
ECDH (X25519) | 32 | β |
RSA 2048* | ~256 | 256 |
To achieve the same 32 bytes of shared secret, ML-KEM-768 requires 1184 bytes of public key and 1088 bytes of ciphertext. That’s over 2kB of data during a single handshake.
The public key is shared with Bob while Alice retains the private key. The next phase involves Bob’s encapsulation of the shared secret:
// Bob uses Alice's public key to encapsulate a shared secret
var encapsulator = new MLKemEncapsulator(MLKemParameters.ml_kem_768);
encapsulator.Init(MLKemPublicKeyParameters.FromEncoding(MLKemParameters.ml_kem_768, pubEncoded));
var cipherText = new byte[encapsulator.EncapsulationLength];
var bobSecret = new byte[encapsulator.SecretLength];
encapsulator.Encapsulate(cipherText, 0, cipherText.Length, bobSecret, 0, bobSecret.Length);
Here, ML-KEM’s quantum resistance is evident β the lattice-based mathematical structures ensure the encapsulation remains secure against both classical and quantum attacks.
Finally, Alice decapsulates the shared secret:
// Alice decapsulates the shared secret using her private key
var decapsulator = new MLKemDecapsulator(MLKemParameters.ml_kem_768);
decapsulator.Init(alicePrivate);
var aliceSecret = new byte[decapsulator.SecretLength];
decapsulator.Decapsulate(cipherText, 0, cipherText.Length, aliceSecret, 0, aliceSecret.Length);
At this point Alice and Bob share a secret key that can be used for symmetric encryption (ideally AES-256). The ML-KEM handshake is complete, and the shared secret is secure against quantum adversaries.
ML-DSA: Digital Signatures π
The ML-DSA implementation showcases how post-quantum signatures differ from traditional approaches. Here is the sample implementation. We start with a message that Alice wants to sign:
// Alice prepares her message
var raw = "Hello, I'm Alice and you can verify that!";
var data = Hex.Encode(Encoding.ASCII.GetBytes(raw));
For ML-DSA, key generation employs parameters that are specifically optimized for digital signatures. The process uses a secure random number generator to ensure that the generated keys are unpredictable and resistant to cryptographic attacks. The parameter set ml-dsa-65 (the new designation for what was previously known as Dilithium3) is chosen for its balanced trade-off between signature size, verification speed, and security level.
// Generate Alice's signing keypair
var random = new SecureRandom();
var keyGenParameters = new MLDsaKeyGenerationParameters(random, MLDsaParameters.ml_dsa_65);
var mldsaKeyPairGenerator = new MLDsaKeyPairGenerator();
mldsaKeyPairGenerator.Init(keyGenParameters);
var keyPair = mldsaKeyPairGenerator.GenerateKeyPair();
Once the keys are generated, Alice proceeds to sign her prepared message. Here, the signer is initialized in signing mode (indicated by the true parameter) using Alice’s private key. The message data is then fed into the signer, which processes the input through the ML-DSA algorithm, ultimately producing a signature.
// Alice signs the message
var alice = new MLDsaSigner(MLDsaParameters.ml_dsa_65, deterministic: true);
alice.Init(true, privateKey);
alice.BlockUpdate(data, 0, data.Length);
var signature = alice.GenerateSignature();
Finally, verification shows how others can validate the signature:
// Bob verifies Alice's signature
var bob = new MLDsaSigner(MLDsaParameters.ml_dsa_65, deterministic: true);
bob.Init(false, publicKey);
bob.BlockUpdate(data, 0, data.Length);
var verified = bob.VerifySignature(signature);
The underlying mathematical structure ensures that only someone with access to the corresponding private key could have generated the valid signature.
Next steps π
Although BouncyCastle.NET delivers robust implementations today, native .NET support is on the horizon. In fact, thereβs an open issue earmarked for .NET 10. Additionally, one advantage of BouncyCastle.NET is that its algorithms are implemented in managed code, making it easy to compile for more niche .NET targets like WebAssembly.
Post-quantum cryptography’s standardization marks a crucial milestone in our online security. The journey toward quantum-resistant cryptography is just beginning. As the standarization progresses, we can expect more libraries and tools to support these new algorithms. The future of cryptography is quantum-resistant, and the time to prepare is… now!
The full source code for this post is available on GitHub.