Post-Quantum Cryptography in Java: What the NIST Standards Mean for Your TLS, JWT, and Key Management Code
In August 2024, NIST finalized the world’s first post-quantum cryptography standards. JDK 24 and 25 have already begun shipping the new algorithms. If you run Java services that handle authentication, encrypted transport, or long-lived secrets, this transition affects you — and the window to act is narrower than most teams realize.
For most of software history, breaking RSA-2048 or ECDH key exchange required either a room-sized classical computer running for billions of years, or simply being fictional. Quantum computers have changed that calculus — not today, but on a timeline that is now concretely measurable. Shor’s algorithm, running on a sufficiently large fault-tolerant quantum computer, reduces RSA and elliptic curve key exchange from computationally infeasible to a matter of hours. The question is no longer whether this is theoretically possible — it is a question of when.
That “when” is what drove NIST to spend eight years evaluating 69 candidate algorithms and then, in August 2024, publish FIPS 203, FIPS 204, and FIPS 205 as finalized standards. For Java developers, the practical impact falls across three critical areas: TLS handshakes, JWT signing and verification, and key generation and storage. This article walks through each one, explains what the new algorithms are and why they exist, and shows exactly what you need to change in your code.
What this article covers
- Why quantum computers break RSA and ECDH — and why symmetric AES-256 is mostly fine
- The three finalized NIST PQC standards and what each one replaces in your Java stack
- JDK 24 and 25 support status: what is in the JCA provider today and what requires BouncyCastle
- Concrete code changes for TLS configuration, JWT signing, and
KeyStore/KeyGeneratorusage - The “harvest now, decrypt later” threat model and why urgency is higher than most teams assume
- A migration checklist and algorithm recommendation table for 2026
1. Why Quantum Computers Break the Algorithms You Are Currently Using
The security of RSA, Diffie-Hellman, and elliptic curve cryptography all rest on mathematical problems that are hard for classical computers — specifically, integer factorisation and the discrete logarithm problem. Shor’s algorithm, published in 1994, solves both problems in polynomial time on a quantum computer. That means a quantum computer with enough logical qubits could, in principle, factor a 2048-bit RSA modulus or solve an elliptic curve discrete log in hours rather than geological time.
Importantly, symmetric encryption is not equally vulnerable. Grover’s algorithm offers a quantum speedup for searching unsorted data — effectively halving the security of symmetric keys. AES-128 drops to approximately 64 bits of quantum security; AES-256 drops to 128 bits, which remains strong by any practical standard. So the asymmetric algorithms you use for key exchange and digital signatures are the critical exposure surface — not your bulk data encryption.
Nation-state adversaries and well-funded groups are believed to be recording encrypted TLS traffic today with the explicit intention of decrypting it once a cryptographically relevant quantum computer exists. If any data you transmit today must remain confidential for more than 5–10 years — healthcare records, financial contracts, classified communications — your current RSA/ECDH TLS sessions are already at risk. This threat model is sometimes called HNDL (Harvest Now, Decrypt Later) and it is the primary driver of the urgency behind the NIST timeline.
The following timeline places the NIST process in context so you can calibrate your own migration urgency:
2016
NIST opens the Post-Quantum Cryptography Standardization process. 69 algorithms submitted from researchers worldwide.
2022
NIST announces four finalists: CRYSTALS-Kyber (KEM), CRYSTALS-Dilithium, FALCON, and SPHINCS+ (signatures).
August 2024
FIPS 203 (ML-KEM / Kyber), FIPS 204 (ML-DSA / Dilithium), and FIPS 205 (SLH-DSA / SPHINCS+) finalized. These are the first post-quantum cryptographic standards in history.
2025 — JDK 24 / 25
JEP 496 (Quantum-Resistant Module-Lattice-Based Key Encapsulation) and JEP 497 (Quantum-Resistant Module-Lattice-Based Digital Signatures) integrated into the JCA provider in JDK 24 and 25 as preview / standard features.
2030 (estimated)
NIST plans to begin deprecating classical algorithms for federal use. Most compliance frameworks (FedRAMP, CMMC, PCI-DSS) expected to follow with mandatory PQC timelines.
2035 (estimated)
Cryptographically relevant quantum computers (CRQCs) possible according to multiple national intelligence assessments, including the US NSA and UK NCSC. Classical asymmetric algorithms considered at risk.
Security bit levels: classical vs post-quantum algorithms under quantum attack

2. The Three NIST Standards: What Each One Does and What It Replaces
Before diving into Java code, it is worth being precise about what each standard actually is — because the naming is inconsistent across documentation and the algorithms serve distinct purposes.
| FIPS Standard | Algorithm name | Origin name | Type | Replaces | JDK 25 support |
|---|---|---|---|---|---|
| FIPS 203 | ML-KEM | CRYSTALS-Kyber | Key Encapsulation Mechanism (KEM) | RSA-OAEP, ECDH key exchange in TLS | JEP 496 — standard |
| FIPS 204 | ML-DSA | CRYSTALS-Dilithium | Digital signature | RSA-PSS, ECDSA in JWT, TLS cert signing | JEP 497 — standard |
| FIPS 205 | SLH-DSA | SPHINCS+ | Stateless hash-based signature | RSA-PSS, ECDSA (conservative alternative) | BouncyCastle 1.80+ |
| FIPS 206 (draft) | FN-DSA | FALCON | Digital signature (compact) | ECDSA where small signature size matters | BouncyCastle 1.80+ |
The two algorithms with first-class JDK support — ML-KEM and ML-DSA — are both based on the hardness of the Module Learning With Errors (MLWE) problem, which is believed to resist attacks from both classical and quantum computers. ML-KEM handles key encapsulation (the process of establishing a shared secret over an untrusted channel, used in TLS), while ML-DSA handles digital signatures (used in JWT signing, TLS certificate verification, and code signing).
SLH-DSA (SPHINCS+) is the conservative choice if you distrust lattice-based cryptography — it is based entirely on hash functions, which have a longer and better-understood security history. Its downside is signature size: an SLH-DSA signature is roughly 8–50 KB depending on the parameter set, versus 2–4 KB for ML-DSA. That matters for JWT tokens in HTTP headers or for TLS certificate chains. Still, for signing artifacts, firmware, or code — where size is less critical — SLH-DSA is an excellent choice.
3. JDK 24 and 25 Support: What Is in the Box
JEP 496 landed in JDK 24 as a preview feature and was finalized in JDK 25, adding ML-KEM to the JCA provider under the algorithm name "ML-KEM" with parameter sets ML-KEM-512, ML-KEM-768, and ML-KEM-1024. JEP 497 follows the same track, adding ML-DSA with parameter sets ML-DSA-44, ML-DSA-65, and ML-DSA-87.
Critically, however, TLS 1.3 integration is not yet complete in the standard JDK TLS provider (JSSE). The SunJSSE provider does not yet support ML-KEM as a named group for TLS key exchange by default. You can negotiate it experimentally via the jdk.tls.namedGroups system property, but for production-grade hybrid TLS (a pattern we cover below), BouncyCastle’s JSSE provider is the more reliable path for now.
PQC feature availability by Java version

For teams on JDK 17 or 21 (the current LTS releases as of writing), the practical path to PQC today runs through BouncyCastle 1.80+, which implements FIPS 203, 204, and 205 and integrates cleanly with the standard JCA KeyPairGenerator, Signature, and KeyGenerator APIs. The same code runs on JDK 25 with the native provider once you remove the BouncyCastle provider registration — the API surface is identical.
4. TLS: Hybrid Key Exchange and What to Configure
The recommended migration strategy for TLS is not to rip out ECDH and replace it wholesale with ML-KEM overnight. Instead, the cryptographic community — including the IETF TLS hybrid design draft — recommends a hybrid key exchange approach: combine a classical algorithm (ECDH P-256 or X25519) with a post-quantum KEM (ML-KEM-768) such that the resulting shared secret is secure unless both algorithms are simultaneously broken. This gives you quantum resistance against HNDL attacks while maintaining backward compatibility with classical infrastructure.
In practice, hybrid TLS in Java today requires BouncyCastle’s JSSE provider. Below is a working configuration using Spring Boot 3.x with the BC-JSSE provider, enabling the X25519MLKEM768 hybrid named group — the same group now supported in Chrome and Firefox:
<!-- pom.xml — add BouncyCastle TLS and FIPS providers -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.80</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bctls-jdk18on</artifactId>
<version>1.80</version>
</dependency>
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.net.ssl.*;
import java.security.Security;
public class PQCTlsConfig {
public static SSLContext buildHybridTlsContext() throws Exception {
// Register BC providers at the front of the provider list
Security.insertProviderAt(new BouncyCastleProvider(), 1);
Security.insertProviderAt(new BouncyCastleJsseProvider(), 2);
SSLContext ctx = SSLContext.getInstance("TLSv1.3", "BCJSSE");
// KeyManager and TrustManager loaded from your existing KeyStore
KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX", "BCJSSE");
TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX", "BCJSSE");
// kmf.init(keyStore, password); tmf.init(trustStore);
ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
// Enable the hybrid X25519 + ML-KEM-768 named group for key exchange
SSLParameters params = ctx.getDefaultSSLParameters();
params.setNamedGroups(new String[]{
"X25519MLKEM768", // hybrid PQC (recommended for transition)
"x25519", // classical fallback
"secp256r1" // classical fallback
});
ctx.getDefaultSSLParameters().setNamedGroups(params.getNamedGroups());
return ctx;
}
}
TLS has two distinct cryptographic operations: key exchange (establishing the session secret — this is where ML-KEM applies) and certificate signature verification (authenticating the server’s identity — this is where ML-DSA would apply). Today’s recommendation is to upgrade key exchange to hybrid PQC immediately, but continue using classical ECDSA or RSA for certificate signatures until browser trust stores and intermediate CA infrastructure catches up to ML-DSA certificates.
5. JWT Signing: ML-DSA Keys and What Changes in Your Token Pipeline
JSON Web Tokens signed with RS256 (RSA + SHA-256) or ES256 (ECDSA + P-256) are quantum-vulnerable — Shor’s algorithm can recover the private key from any captured signature. The post-quantum replacement is ML-DSA, which produces a different key type, a different signature format, and larger tokens.
The first thing to understand is that no major JWT library — Nimbus JOSE+JWT, jjwt, Auth0 java-jwt — natively supports ML-DSA algorithm identifiers yet, because the IANA “JSON Web Algorithms” registry has not yet assigned official algorithm names for FIPS 204. IETF drafts are in progress (draft-ietf-cose-dilithium), and the expected registered name is "ML-DSA-65". In the interim, you can use custom algorithm identifiers with Nimbus JOSE+JWT’s extensible algorithm framework:
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jwt.*;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
import java.security.*;
import java.util.Date;
public class PQCJwtExample {
static {
Security.addProvider(new BouncyCastleProvider());
Security.addProvider(new BouncyCastlePQCProvider());
}
// Generate an ML-DSA-65 key pair using BouncyCastle (JDK 17/21 compatible)
public static KeyPair generateMlDsaKeyPair() throws Exception {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("ML-DSA", "BCPQC");
// ML-DSA-65 offers 128-bit quantum security — the recommended general-purpose parameter set
kpg.initialize(new org.bouncycastle.pqc.jcajce.spec.MLDSAParameterSpec(
org.bouncycastle.pqc.jcajce.spec.MLDSAParameterSpec.ml_dsa_65
));
return kpg.generateKeyPair();
}
// Sign a JWT with ML-DSA-65 using a custom JWS algorithm identifier
// Note: "ML-DSA-65" as alg header is a placeholder until IANA registration completes
public static String signToken(KeyPair keyPair, String subject) throws Exception {
JWSSigner signer = new Ed25519Signer((java.security.interfaces.EdECPrivateKey) null) {
// Custom signer delegating to ML-DSA via JCA Signature API
@Override
public Base64URL sign(JWSHeader header, byte[] signingInput) throws JOSEException {
try {
Signature sig = Signature.getInstance("ML-DSA", "BCPQC");
sig.initSign(keyPair.getPrivate());
sig.update(signingInput);
return Base64URL.encode(sig.sign());
} catch (Exception e) { throw new JOSEException("ML-DSA signing failed", e); }
}
@Override
public Set<JWSAlgorithm> supportedJWSAlgorithms() {
return Collections.singleton(new JWSAlgorithm("ML-DSA-65"));
}
};
JWSHeader header = new JWSHeader.Builder(new JWSAlgorithm("ML-DSA-65"))
.type(JOSEObjectType.JWT).build();
JWTClaimsSet claims = new JWTClaimsSet.Builder()
.subject(subject)
.issuer("https://auth.example.com")
.expirationTime(new Date(System.currentTimeMillis() + 3_600_000))
.build();
SignedJWT jwt = new SignedJWT(header, claims);
jwt.sign(signer);
return jwt.serialize();
}
}
An ML-DSA-65 signature is approximately 3,293 bytes, compared to 64 bytes for ES256 (ECDSA P-256). This has a direct impact on HTTP Authorization header sizes, cookie storage limits, and any system that stores or indexes JWT tokens. Plan for roughly a 4–5× increase in token payload size when using ML-DSA. If that is unacceptable, FN-DSA (FALCON) produces ~1,280-byte signatures — but at the cost of a more complex key generation procedure and no current IANA registration.
Signature and public key sizes: classical vs post-quantum algorithms (bytes)

6. Key Management: KeyStore, KeyGenerator, and Storage Implications
Key management is where the practical friction of PQC migration is highest. The standard Java KeyStore formats — JKS, PKCS12, and JCE — support any key type that implements the java.security.Key interface, so ML-KEM and ML-DSA keys can be stored in an existing PKCS12 keystore without format changes. However, hardware security modules (HSMs), cloud KMS services, and PKCS#11 providers are a different story.
As of April 2026, most HSMs do not yet support ML-KEM or ML-DSA natively. AWS KMS, Google Cloud KMS, and Azure Key Vault have all announced PQC roadmaps but have not shipped GA support. The practical consequence is that for applications relying on an HSM-backed keystore, you will need to either generate and store PQC keys in a software keystore (with appropriate wrapping or protection strategy) or wait for HSM vendor support.
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
import org.bouncycastle.pqc.jcajce.spec.MLKEMParameterSpec;
import java.security.*;
import java.security.KeyStore.*;
import java.io.*;
public class PQCKeyManagement {
static {
Security.addProvider(new BouncyCastleProvider());
Security.addProvider(new BouncyCastlePQCProvider());
}
// Generate ML-KEM-768 key pair — recommended for general-purpose key encapsulation
public static KeyPair generateMlKemKeyPair() throws Exception {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("ML-KEM", "BCPQC");
kpg.initialize(MLKEMParameterSpec.ml_kem_768);
return kpg.generateKeyPair();
}
// Store PQC keys in a standard PKCS12 keystore — no format change required
public static void storePqcKeyPair(KeyPair keyPair, char[] password,
String alias, String outputPath) throws Exception {
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(null, password); // create new keystore
// Self-signed certificate wrapper is needed for PKCS12 private key entries
// In production, get this from your CA with an ML-DSA subject key
java.security.cert.Certificate[] chain = generateSelfSignedCert(keyPair);
ks.setKeyEntry(alias, keyPair.getPrivate(), password, chain);
try (FileOutputStream fos = new FileOutputStream(outputPath)) {
ks.store(fos, password);
}
System.out.println("Stored ML-KEM-768 key pair to: " + outputPath);
}
// Retrieve and use the stored ML-KEM key pair
public static KeyPair loadPqcKeyPair(String keystorePath,
char[] password, String alias) throws Exception {
KeyStore ks = KeyStore.getInstance("PKCS12");
try (FileInputStream fis = new FileInputStream(keystorePath)) {
ks.load(fis, password);
}
PrivateKey privateKey = (PrivateKey) ks.getKey(alias, password);
PublicKey publicKey = ks.getCertificate(alias).getPublicKey();
return new KeyPair(publicKey, privateKey);
}
private static java.security.cert.Certificate[] generateSelfSignedCert(KeyPair kp)
throws Exception {
// Use BouncyCastle X509 certificate generator for ML-DSA/ML-KEM subject keys
// Omitted for brevity — see BouncyCastle X509v3CertificateBuilder docs
return new java.security.cert.Certificate[]{};
}
}
7. What Breaks in Your Existing Code and How to Fix It
Beyond the specific code changes above, there are several existing patterns that break or require adjustment when you introduce PQC algorithms. Each one is straightforward to address once you know to look for it.
| Existing pattern | Problem | Fix |
|---|---|---|
Hardcoded algorithm strings like "RSA", "EC" | Will not resolve to PQC algorithms | Replace with "ML-KEM" / "ML-DSA" and add BC provider |
| JWT libraries with algorithm whitelist | Most libraries reject unknown algorithm identifiers | Use extensible custom algorithm framework (Nimbus) or wait for library updates |
| TLS cipher suite allow-lists | Static lists will block hybrid KEM named groups | Add X25519MLKEM768 to named group list; keep classical fallbacks |
| Key size validation (e.g. “minimum 2048-bit RSA”) | ML-KEM/ML-DSA key sizes are not directly comparable to RSA key sizes | Validate by algorithm name and parameter set, not raw key size in bits |
| HTTP header size limits (load balancers, API gateways) | ML-DSA JWT tokens are 4–5× larger than ECDSA tokens | Increase header buffer limits; consider short-lived opaque token references for high-throughput APIs |
| HSM-backed private key operations | Most HSMs do not yet support PQC algorithms natively | Use software keystore for PQC keys now; migrate to HSM when vendor support lands |
| Certificate pinning with classical ECDSA pins | Pinned certificates will be invalid once you rotate to ML-DSA certs | Plan pin rotation as part of certificate migration; pin by SPKI hash, not algorithm |
Serialised PublicKey objects in databases | PQC public keys use different DER/SubjectPublicKeyInfo OIDs | Re-encode keys in standard SPKI format; update OID-based lookups |
8. Your Migration Checklist for 2026
PQC migration does not need to happen in a single sprint. The recommended approach is a phased transition that prioritises the highest-risk attack surface first — TLS key exchange, which is the vector for HNDL attacks — and defers less urgent changes like certificate authority migration to later phases.
Phase 1 — immediate (now)
1. Audit all services using RSA or ECDH for TLS key exchange. Add hybrid named groups (X25519MLKEM768) via BouncyCastle JSSE.
2. Upgrade BouncyCastle to 1.80+ in all projects.
3. Upgrade to JDK 21 LTS minimum; evaluate JDK 25 for services where you need native ML-KEM / ML-DSA support without BouncyCastle.
4. Ensure AES-256 (not AES-128) is used for all symmetric encryption at rest and in transit.
Phase 2 — near term (6–18 months)
5. Identify all JWT issuers and verifiers. Plan algorithm agility: store algorithm identifier alongside keys so you can rotate without code changes.
6. Increase HTTP header and response size limits to accommodate larger PQC signatures.
7. Engage your HSM vendor to confirm PQC roadmap timing.
8. Review certificate pinning implementations to ensure they are algorithm-agnostic.
Phase 3 — medium term (18–36 months)
9. Rotate JWT signing keys to ML-DSA-65 once IANA registration and library support is GA.
10. Request or issue ML-DSA TLS certificates from your CA once the CA/B Forum and browser trust stores support them.
11. Migrate HSM-backed key operations to PQC once your HSM vendor ships support.
12. Review all compliance requirements (FedRAMP, PCI-DSS, ISO 27001) for PQC migration mandates as standards bodies update their frameworks.
9. What We’ve Learned
The shift to post-quantum cryptography is not a distant theoretical concern — it is a concrete engineering project with a specific starting point, specific algorithms, and specific Java APIs you can use today. NIST’s finalization of FIPS 203, 204, and 205 in August 2024 gave the industry the stable target it needed. JDK 24 and 25 have made ML-KEM and ML-DSA first-class JCA citizens. BouncyCastle 1.80+ bridges the gap for teams on JDK 17 and 21.
The most urgent change is enabling hybrid TLS key exchange — because the HNDL threat means traffic recorded today can be decrypted in a future with quantum hardware. JWT signing and key management follow on a less urgent but still concrete timeline. The key lesson across all three areas is the same: build algorithm agility in now, before you are forced to make breaking changes under a compliance deadline. The algorithms change; the JCA API pattern stays the same.

