Let's get started with a Microservice Architecture with Spring Cloud:
Quantum-Resistant ML-KEM and ML-DSA in Java
Last updated: July 1, 2026
1. Overview
ML-KEM (Module-Lattice-Based Key Encapsulation Mechanism) is a post-quantum cryptographic key exchange algorithm that allows two parties to securely establish a shared secret key even against attacks from future quantum computers. ML-DSA (Module-Lattice-Based Digital Signature Algorithm), on the other hand, is a post-quantum digital signature algorithm that enables secure authentication and message integrity protection, resistant to attacks by quantum computers.
ML-KEM and ML-DSA weren’t part of the standard JDK (Java Development Kit) before Java 24. We had to use external libraries like Bouncy Castle to perform cryptographic operations.
Both algorithms have been part of the native Java API since Java 24. The ML-KEM and ML-DSA implementations within the native Java API were proposed by JEP 496 and JEP 497, respectively.
In this tutorial, we’ll discuss the usage of quantum-resistant ML-KEM and ML-DSA in Java.
2. Quantum-Resistant ML-KEM
A key encapsulation mechanism allows two parties, i.e., a sender and a receiver, to securely exchange a shared secret key over an insecure network. The mechanism consists of the following steps:
- The receiver creates a public/private key pair
- The sender creates an encrypted shared secret key using the public key
- The receiver decrypts the encrypted shared secret key using the private key
Consequently, both parties obtain the same secret key. This shared key can be used with symmetric-key algorithms such as AES (Advanced Encryption Standard) and Blowfish.
ML-KEM is based on the M-LWE (Module Learning with Errors) problem, which is computationally hard. The hardness of the M-LWE problem further depends on the hardness of certain computational problems in module lattices. Therefore, ML-KEM is resistant to both classical and quantum attacks.
2.1. Generating a Key Pair
Let’s start by generating an ML-KEM key pair at the receiver side:
KeyPairGenerator kpg = KeyPairGenerator.getInstance("ML-KEM");
kpg.initialize(NamedParameterSpec.ML_KEM_768);
KeyPair receiverKeyPair = kpg.generateKeyPair();
PrivateKey privateKey = receiverKeyPair.getPrivate();
PublicKey publicKey = receiverKeyPair.getPublic();
Firstly, we get a KeyPairGenerator object for the ML-KEM algorithm. Then, we choose ML_KEM_768 to set the security strength of the keys we’ll generate. JEP 496 adds support for ML-KEM-512, ML-KEM-768, and ML-KEM-1024. Indeed, ML_KEM_768 is the default option, i.e., if we don’t initialize the KeyPairGenerator object with the initialize() method, the security level will still be ML_KEM_768.
It’s also possible to pass the security strength directly to getInstance(). We don’t have to call the initialize() method in this case, either:
KeyPairGenerator kpg = KeyPairGenerator.getInstance("ML-KEM-768");
KeyPair receiverKeyPair = kpg.generateKeyPair();
receiverKeyPair, of type KeyPair, holds a public and private key. We can access these keys using the getPublic() and getPrivate() member methods, respectively. It’s safe for the receiver to share the public key with the sender, but the private key must be kept secret.
2.2. Generating and Encapsulating a Secret Shared Key
Once the sender receives the receiver’s public key, it generates and encapsulates a shared secret key that will be used by both the sender and the receiver:
KEM senderKem = KEM.getInstance("ML-KEM");
KEM.Encapsulator encapsulator = senderKem.newEncapsulator(publicKey);
KEM.Encapsulated encapsulated = encapsulator.encapsulate();
SecretKey senderSharedSecret = encapsulated.key();
byte[] ciphertext = encapsulated.encapsulation();
senderSharedSecret is the actual secret key used in communication. But the sender doesn’t send it directly to the receiver. It sends it encrypted by encapsulating it with the receiver’s public key. ciphertext corresponds to the encrypted version of senderSharedSecret.
2.3. Decapsulating the Secret Shared Key
After the receiver receives the encrypted secret key, it decapsulates the secret key using the private key:
KEM receiverKem = KEM.getInstance("ML-KEM");
KEM.Decapsulator decapsulator = receiverKem.newDecapsulator(privateKey);
SecretKey receiverSharedSecret = decapsulator.decapsulate(ciphertext);
The actual secret key, receiverSharedSecret, is extracted by decapsulating the received ciphertext. Consequently, both sides obtain the same shared secret key for symmetric encryption and decryption. We can check that both parties use the same key as follows:
boolean match = Arrays.equals(senderSharedSecret.getEncoded(), receiverSharedSecret.getEncoded());
The comparison returns true. Therefore, we achieve secure communication over insecure networks. Besides, the method is resistant to quantum attacks, unlike RSA or Diffie-Hellman.
3. Quantum-Resistant ML-DSA
A digital signature algorithm is a cryptographic method that verifies the authenticity and integrity of digital messages or documents. The algorithm consists of the following steps:
- The signer (sender) creates a public/private key pair
- The signer creates a signature using the message and the private key
- The verifier (receiver) verifies the signature using the received message and the public key
Like ML-KEM, ML-DSA is based on the M-LWE problem and on another computationally hard problem, M-SIS (Module Short Integer Solution). Therefore, ML-DSA is resistant to both classical and quantum attacks.
3.1. Generating a Key Pair
Let’s start by generating an ML-DSA key pair on the signer side:
KeyPairGenerator kpg = KeyPairGenerator.getInstance("ML-DSA");
kpg.initialize(NamedParameterSpec.ML_DSA_65);
KeyPair kp = kpg.generateKeyPair();
PrivateKey privateKey = kp.getPrivate();
PublicKey publicKey = kp.getPublic();
Firstly, we get a KeyPairGenerator object for the ML-DSA algorithm. Then, we choose ML_DSA_65 to set the security strength of the keys we’ll generate. Indeed, ML_DSA_65 is the default option. JEP 497 adds support for ML-DSA-44, ML-DSA-65, and ML-DSA-87.
Like ML-KEM, it’s possible to pass the security strength directly to getInstance().
3.2. Signing
Then, we, the signer, sign the message to be sent using the private key:
Signature signature = Signature.getInstance("ML-DSA");
String message = "This is a test message signed";
byte[] messageBytes = message.getBytes();
signature.initSign(privateKey);
signature.update(messageBytes);
byte[] sigBytes = signature.sign();
Firstly, we get a Signature object that implements the ML-DSA algorithm. Then, we initialize this object for signing using the initSign() method. We pass the private key to this method. Then, we feed the message to the Signature object using the update() method. Finally, we create the digital signature using the Signature object’s sign() method.
3.3. Verification
Then, having received the message and the signature, we, the verifier, verify the signature using the public key:
signature.initVerify(publicKey);
signature.update(messageBytes);
boolean isValid = signature.verify(sigBytes);
This time, we initialize the Signature object for verification using the initVerify() method. Then we feed the message received from the signer to the Signature object using update(). Finally, we verify the signature using the Signature object’s verify() method. It returns true. Successful validation means that the signature was created using the corresponding private key.
4. Conclusion
In this article, we discussed the usage of quantum-resistant ML-KEM and ML-DSA in Java. Firstly, we learned that these algorithms weren’t available in the standard JDK before Java 24.
Then, we saw the usage of the algorithms in Java. We simulated the secret key exchange between a receiver and a sender using ML-KEM. Similarly, we simulated message signing and signature verification between two parties using ML-DSA. As we saw in the examples, Java 24 provided ML-KEM implementations of the KeyPairGenerator and KEM classes, and ML-DSA implementations of the KeyPairGenerator and Signature classes.
As usual, the complete source code for the examples is available over on GitHub.

















