Java 24 Quantum-Resistant Crypto: ML-KEM and ML-DSA

Introduction The security of today's internet - HTTPS, SSH, signed software updates - rests mostly on two kinds of public-key cryptography: RSA and elliptic curve cryptography. Both rely on mathematical problems, factoring large numbers and computing discrete logarithms, which are infeasible for today's computers. A large enough quantum computer, however, could solve them using Shor's algorithm. No such computer exists yet, but encrypted data can be recorded now and decrypted later - a threat called "harvest now, decrypt later" - so the move to new algorithms has already begun. In August 2024, the US National Institute of Standards and Technology (NIST) published the first post-quantum standards, and Java 24Click to read tutorial: Overview of New Features in Java 24 implemented two of them: ML-KEM (JEP 496) for establishing shared keys, and ML-DSA (JEP 497) for digital signatures. This tutorial explains them, and shows how to use both through the standard Java APIs with runnable examples. The two algorithms
  • ML-KEM (Module-Lattice-Based Key-Encapsulation Mechanism, standardized as FIPS 203, formerly known as Kyber) lets two parties agree on a shared secret key over an insecure channel - the job Diffie-Hellman key exchange does today. It is a key encapsulation mechanism, used through the KEM API added in Java 21Click to read: KEM API added in Java 21.
  • ML-DSA (Module-Lattice-Based Digital Signature Algorithm, FIPS 204, formerly Dilithium) signs and verifies data - the job of RSA, ECDSA and EdDSA signatures - through the standard Signature API.
Both are based on hard problems over mathematical structures called lattices, for which no efficient quantum algorithm is known. Each comes in three parameter sets with increasing security: ML-KEM-512, -768 and -1024, and ML-DSA-44, -65 and -87.
Sharing a key with ML-KEM
Establishing a shared secret key with ML-KEM
//MlKemDemo.java
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.spec.NamedParameterSpec;
import java.util.Arrays;
import javax.crypto.KEM;
import javax.crypto.SecretKey;

public class MlKemDemo {
  public static void main(String[] args) throws Exception {
    // The receiver creates an ML-KEM-768 key pair and publishes the public key
    KeyPairGenerator kpg = KeyPairGenerator.getInstance("ML-KEM");
    kpg.initialize(NamedParameterSpec.ML_KEM_768);
    KeyPair receiver = kpg.generateKeyPair();

    // The sender encapsulates a fresh secret key using the receiver's public key
    KEM kem = KEM.getInstance("ML-KEM");
    KEM.Encapsulated encapsulated = kem.newEncapsulator(receiver.getPublic()).encapsulate();
    SecretKey senderKey = encapsulated.key();
    byte[] ciphertext = encapsulated.encapsulation();                // sent to the receiver

    // The receiver recovers the same key with its private key
    SecretKey receiverKey = kem.newDecapsulator(receiver.getPrivate()).decapsulate(ciphertext);

    System.out.println("Public key size  : " + receiver.getPublic().getEncoded().length + " bytes (encoded)");
    System.out.println("Encapsulation    : " + ciphertext.length + " bytes");
    System.out.println("Shared key       : " + senderKey.getEncoded().length * 8 + " bits, algorithm " + senderKey.getAlgorithm());
    System.out.println("Keys are equal   : " + Arrays.equals(senderKey.getEncoded(), receiverKey.getEncoded()));
  }
}
 OUTPUT of the above code
Public key size  : 1206 bytes (encoded)
Encapsulation    : 1088 bytes
Shared key       : 256 bits, algorithm Generic
Keys are equal   : true
The flow is exactly the same as with the classic DHKEM - only the algorithm name changes, which is what the KEM API was designed for. The shared 256-bit key is then used with a symmetric cipher such as AES-GCM, which is already considered safe against quantum computers at that key size. Note the sizes: ML-KEM keys and ciphertexts are larger than elliptic curve ones, which have only 32 bytes. Signing and verifying with ML-DSA
Signing and verifying data with ML-DSA
//MlDsaDemo.java
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;

public class MlDsaDemo {
  static byte[] sign(String algorithm, KeyPair keys, byte[] data) throws Exception {
    Signature signer = Signature.getInstance(algorithm);
    signer.initSign(keys.getPrivate());
    signer.update(data);
    return signer.sign();
  }

  static boolean verify(String algorithm, KeyPair keys, byte[] data, byte[] signature) throws Exception {
    Signature verifier = Signature.getInstance(algorithm);
    verifier.initVerify(keys.getPublic());
    verifier.update(data);
    return verifier.verify(signature);
  }

  public static void main(String[] args) throws Exception {
    byte[] release = "release-1.4.2.jar sha256=ab12...".getBytes(StandardCharsets.UTF_8);

    KeyPair mlDsa = KeyPairGenerator.getInstance("ML-DSA").generateKeyPair();     // default: ML-DSA-65
    byte[] signature = sign("ML-DSA", mlDsa, release);
    System.out.println("ML-DSA signature : " + signature.length + " bytes, valid = " + verify("ML-DSA", mlDsa, release, signature));

    byte[] tampered = "release-1.4.3.jar sha256=ab12...".getBytes(StandardCharsets.UTF_8);
    System.out.println("Tampered data    : valid = " + verify("ML-DSA", mlDsa, tampered, signature));

    KeyPair ed = KeyPairGenerator.getInstance("Ed25519").generateKeyPair();        // a classic signature, for comparison
    System.out.println("Ed25519 signature: " + sign("Ed25519", ed, release).length + " bytes");
  }
}
 OUTPUT of the above code
ML-DSA signature : 3309 bytes, valid = true
Tampered data    : valid = false
Ed25519 signature: 64 bytes
Again the standard API is unchanged. The trade-off shows in the sizes: an ML-DSA-65 signature is about 3.3 KB, against 64 bytes for Ed25519, and its public key is about 2 KB. That is acceptable for most uses, but matters for protocols which send many signatures, such as TLS certificate chains.
Using post-quantum cryptography in practice
  • Hybrid schemes: during the transition, protocols usually combine a classic and a post-quantum algorithm, so that the result is secure as long as either one is unbroken. TLS 1.3 implementations, including browsers, already use a hybrid of X25519 and ML-KEM-768 for key exchange.
  • Crypto-agility: because Java's security APIs select algorithms by name, code written against KEM, Signature and KeyPairGenerator can switch algorithms through configuration - which is worth designing for now.
  • Keys and certificates: keytool can generate ML-DSA and ML-KEM key pairs, so post-quantum keys can be stored in key stores like any other.
  • Key derivation: to turn a shared secret into several keys, use the Key Derivation Function APIClick to read tutorial: Java 25 Key Derivation Function (KDF) API Tutorial, final in Java 25, which completes the toolkit for hybrid schemes such as HPKE.
Summary In this tutorial we saw why quantum computers threaten RSA and elliptic curve cryptography, and how Java 24 added the NIST post-quantum standards ML-KEM and ML-DSA. We established a shared secret key with ML-KEM through the KEM API, signed and verified data with ML-DSA through the Signature API, and compared their sizes with classic algorithms. Lastly, we looked at hybrid schemes and crypto-agility for using post-quantum cryptography in practice. Next, read the overview of new features in Java 24Click to read tutorial: Overview of New Features in Java 24.