PQC Status
Guide

Post-quantum TLS in Java: where the JDK stands and what to do now

The JDK ships ML-KEM primitives, but hybrid TLS group support depends on the release you run: how to check it, what to change, and how to prove it works.

· 9 min

Java gives you post-quantum building blocks before it gives you a post-quantum TLS handshake. For a server-side Java application behind a reverse proxy, the fastest route to hybrid ML-KEM (Module-Lattice-Based Key Encapsulation Mechanism, standardized as NIST FIPS 203) key exchange is to upgrade the proxy, not the JVM. When Java is the TLS client, hybrid key exchange depends on the JDK release and on the JSSE provider in use, so the only reliable answer comes from the release notes of the build you actually deploy, confirmed by a handshake test.

First decide which side of the handshake you own

A TLS key exchange is negotiated by the two stacks that perform the handshake, not by application code. Three cases cover almost every Java deployment, and they have different answers.

Order the work using harvest now, decrypt later (HNDL): an attacker who records traffic today and decrypts it years later is defeated by the key exchange, not by the certificate. That is why ML-KEM key establishment comes first and signature migration can follow. The IANA TLS registry lists the hybrid groups in use, including X25519MLKEM768 (codepoint 0x11EC), SecP256r1MLKEM768 and SecP384r1MLKEM1024 (IANA TLS parameters), and RFC 10024 specifies post-quantum traditional hybrid key agreement for TLS 1.3 (RFC 10024).

Where the JDK stands today

Two layers must be kept apart: the cryptographic primitives exposed through the Java Cryptography Architecture (JCA), and hybrid group support inside JSSE.

Primitives: available in current releases

JEP 452 added a Key Encapsulation Mechanism (KEM) API, javax.crypto.KEM, in JDK 21. JDK 24 added implementations of ML-KEM (JEP 496) and of ML-DSA (Module-Lattice-Based Digital Signature Algorithm, NIST FIPS 204) in JEP 497, reachable through the usual KeyPairGenerator, KEM and Signature APIs with algorithm names for each parameter set. Because JDK features carry forward, later releases include them. This is what you need for your own protocols, key wrapping, token formats and test harnesses. It is not, by itself, TLS.

Hybrid TLS groups: read the release notes of your JDK

Offering a hybrid group in a TLS 1.3 handshake is separate work inside JSSE: the group has to be registered, key shares generated and parsed, and the shared secret derived from both components. Whether the JDK you run does this, under which group names, and whether it is enabled by default, varies by release and by vendor build, since distributions backport selectively. Do not infer TLS support from the presence of ML-KEM in the JCA, and do not rely on an article written about a different build. Check the release notes of your exact JDK version, then verify empirically with the handshake test below.

LayerWhat to verifyWhere to look
JCA primitivesML-KEM and ML-DSA key pair generation, encapsulation, signature verificationJDK release notes, standard algorithm names documentation
JSSE hybrid groupsWhether the hybrid group appears in ClientHello or ServerHelloJDK release notes, plus a real handshake with debug logging
Native TLS behind a connectorVersion of the native library used by the connector (ML-KEM groups need OpenSSL 3.5 or later in OpenSSL builds)Dependency tree, native library version at runtime
Proxy or ingressGroup list and ordering, classical fallback still presentProxy configuration and its own release notes

Your options, in order of effort

Terminate inbound TLS at a proxy

This is the pragmatic default for server-side Java. The proxy needs a TLS library that implements the ML-KEM groups: in OpenSSL builds that means OpenSSL 3.5 or later. Configure the hybrid group first and keep classical groups after it, so clients without hybrid support still connect, and validate in staging before production. The step by step directives for the common servers are in our guide to enabling ML-KEM on nginx, Apache, HAProxy and Caddy. One practical caution: a hybrid key share is substantially larger than an X25519 key share, which can push the ClientHello across packet boundaries. Old middleboxes and badly behaved network appliances sometimes mishandle that, so test from the networks your clients actually use.

Upgrade the JDK where the JVM terminates TLS

If a Java listener faces clients directly, or if Java is the client in a connection you care about, the clean fix is to run a JDK release whose JSSE offers the hybrid group, then confirm it on the wire. JSSE exposes the documented system property jdk.tls.namedGroups to restrict and order the groups it offers. The names it accepts depend on the release, so take them from your release notes rather than copying an example, and always leave a classical group such as X25519 in the list. Do not pin a single post-quantum group in production.

Add a provider library, or route outbound traffic through a gateway

A third-party JCA or JSSE provider can supply algorithms and, in some cases, a TLS stack independent of the JDK. Weigh it carefully: installing a provider changes security behaviour for the whole JVM, it interacts with FIPS mode and with certificate path validation, and it becomes a dependency you must patch. Keep the change scoped to the SSLContext of the specific client when the library allows it, rather than reordering global providers. For outbound calls from legacy JVMs that cannot be upgraded, an egress proxy that performs the handshake is often simpler and easier to audit than modifying every application. Note too that connectors using a native TLS library (for example Netty with netty-tcnative) follow that library version, not the JDK.

Inventory Java crypto usage before changing anything

Every official timeline starts with discovery, and auditors ask for the inventory before they ask for the migration. These read-only commands give you a first pass; adapt the paths to your build layout.

# TLS and crypto entry points in application code
grep -rnE "SSLContext|SSLSocketFactory|HttpsURLConnection|TrustManager|KeyManagerFactory" --include="*.java" src/

# algorithm names and keystore formats hard coded in source or configuration
grep -rnE "RSA|ECDSA|SHA1|MD5|PKCS12|JKS" --include="*.java" --include="*.properties" --include="*.yml" .

# which TLS stack each service really uses
mvn -q dependency:tree | grep -Ei "netty|tcnative|conscrypt|bouncycastle|okhttp|httpclient"
./gradlew dependencies --configuration runtimeClasspath | grep -Ei "netty|tcnative|okhttp|httpclient"

# keys and certificates in a keystore, with their algorithms
keytool -list -v -keystore keystore.p12 -storetype PKCS12 | grep -E "Alias name|Signature algorithm name|Subject Public Key Algorithm"

Record more than TLS. Java applications also sign JSON Web Tokens and SAML assertions, verify signed JARs, encrypt database columns, and pin trust anchors. Note the quantum-vulnerable public key algorithms (RSA, ECDSA, EdDSA, ECDH, finite-field Diffie-Hellman and DSA are all affected by Shor's algorithm, whatever the key size), where each one is used, who owns it, and whether the algorithm is configurable without a code change. A machine-readable cryptography bill of materials is one practical way to keep that list current: see what a CBOM is and how to produce one. Keep an eye on the JDK security policy files as well, since jdk.tls.disabledAlgorithms and jdk.certpath.disabledAlgorithms in java.security already govern what your JVMs will accept.

Test what the handshake actually negotiates

Claims about support are worth nothing without a negotiated group. Run three checks, in this order.

# 1. external view of the listener (OpenSSL 3.5 or later on the client side)
openssl s_client -connect api.example.com:443 -groups X25519MLKEM768 </dev/null 2>&1 | grep -i "Negotiated TLS1.3 group"

# 2. the classical fallback must still work
openssl s_client -connect api.example.com:443 -groups X25519 </dev/null 2>&1 | grep -i "Negotiated TLS1.3 group"

# 3. what the Java client offers, and what the peer selects
java -Djavax.net.debug=ssl:handshake -jar client.jar 2>&1 | grep -iE "supported_groups|named group|key_share"

Step 3 is the decisive one for Java as a client: the debug output shows the groups in the ClientHello and the group chosen for the key exchange. If the hybrid group never appears, the JDK build does not offer it, whatever the documentation suggested. Keep the output as evidence, with the JDK version string and the date. Our free scanner covers step 1 from the outside, including TLS versions, cipher suites and the certificate chain; what a result does and does not mean is described in our methodology page.

How this lands in audits

The inventory you just built is the artefact most frameworks ask for, and the following is information rather than legal advice. PCI DSS v4.0.1 requirement 12.3.3, mandatory since 31 March 2025, asks for a documented inventory of cipher suites and protocols in use, reviewed at least every 12 months, with a strategy for anticipated changes in cryptographic vulnerabilities: see our page on PCI DSS requirement 12.3.3. Under DORA, Commission Delegated Regulation (EU) 2024/1774 Art. 6 requires an encryption and cryptographic controls policy with provisions for updating or changing cryptographic technology as cryptanalysis develops, and Art. 7 requires key management and a register of certificates for ICT assets supporting critical or important functions (EUR-Lex). Interpretation, not text: in a Java estate, the practical proof of Art. 6 is that your services can change groups and algorithms through configuration and a JDK or proxy upgrade, not through a rewrite. For algorithm status, NIST published FIPS 203, 204 and 205 as final standards on 13 August 2024 (FIPS 203), while NIST IR 8547, which proposes deprecating 112-bit security public key algorithms after 2030 and disallowing quantum-vulnerable public key algorithms after 2035, remains an initial public draft (NIST IR 8547 ipd).

The next concrete step

Pick one externally reachable Java service and scan your domain with PQC Status to see which group it negotiates today. If the answer is classical only and a proxy terminates TLS, upgrade that proxy to a build with OpenSSL 3.5 or later and put the hybrid group ahead of X25519 in staging. If the JVM terminates TLS, or if Java is the client in a connection that carries long-lived secrets, record the JDK version in your inventory, read its release notes for hybrid key exchange, and capture a handshake log as evidence either way. Re-scan after each change, and keep the classical fallback until your clients no longer need it.

Frequently asked questions

Does the JDK support hybrid post-quantum TLS out of the box?

The JDK ships ML-KEM and ML-DSA as cryptographic primitives: JEP 452 added the Key Encapsulation Mechanism API in JDK 21, and JDK 24 added ML-KEM (JEP 496) and ML-DSA (JEP 497) implementations. Hybrid key exchange inside the TLS stack (JSSE) is separate work, and its availability depends on the release and on the vendor build you deploy. Check the release notes of your exact JDK version, then confirm on the wire with a real handshake rather than assuming.

Do I need to upgrade Java if my application sits behind a reverse proxy?

Not for inbound traffic. The proxy performs the handshake, so the group negotiated with browsers and API clients comes from the proxy and its TLS library, which needs OpenSSL 3.5 or later for the ML-KEM groups in OpenSSL builds. You still need to look at the JVM for outbound connections, where Java is the TLS client, and for any listener that terminates TLS inside the JVM.

How do I check which key exchange group my Java client offers?

Start the application with the javax.net.debug system property set to log the handshake, then look at the supported_groups extension in the ClientHello and at the group selected for the key exchange. If no hybrid group appears, that build does not offer one. Save the output together with the JDK version string as evidence for your cryptographic inventory.

Should I force only a post-quantum group in a Java application?

No. Keep a classical group such as X25519 after the hybrid group so peers without hybrid support can still connect, and test any change in staging first. JSSE exposes the documented jdk.tls.namedGroups property to restrict and order the groups offered, but the accepted names depend on the JDK release, so take them from your release notes rather than from an example.