Here's the uncomfortable part about quantum computers and TLS: you don't have to wait for them to be attacked by them. An attacker can record your encrypted traffic today, sit on it for years, and decrypt it the day a large enough quantum computer exists. It's called harvest-now-decrypt-later, and it's the reason post-quantum cryptography is a today problem, not a 2035 problem.
Java 27 does something about it without asking you to lift a finger. JEP 527 adds a hybrid post-quantum key exchange to TLS 1.3 and turns it on by default. Every TLS connection your HttpClient, RestClient, or JDBC driver makes now offers a quantum-safe key share alongside the classical one.
I pulled the exact details from the OpenJDK inside.java write-ups and the JDK 27 early-access builds. This post covers what actually changes in the handshake, how to confirm your client is really using the hybrid group, how to control the named groups with jdk.tls.namedGroups, how to enable it on the server side, and what I'd test before trusting it in production. If you've worked with the new PEM API in JDK 27, this is the other big security feature landing in the same release.
What is post-quantum TLS, and why does Java 27 add it now?
Post-quantum TLS is a key exchange that stays secure even against an attacker with a quantum computer, and Java 27 adds it because the threat is about recorded traffic, not future traffic. The classical key exchanges TLS has used for years, like X25519 and the NIST curves, fall to Shor's algorithm on a sufficiently large quantum machine.
The fix isn't to throw out the classical algorithms. It's to combine them. A hybrid key exchange runs a classical algorithm and a post-quantum algorithm together and mixes both shared secrets into the session keys. To break the session, an attacker has to break both. So if ML-KEM turns out to have a flaw, you still have X25519 protecting you, and if a quantum computer breaks X25519, you still have ML-KEM. You lose nothing and gain a hedge.
ML-KEM is the Module-Lattice Key Encapsulation Mechanism, standardized by NIST as FIPS 203. Java already shipped the raw ML-KEM primitive in JDK 24 through JEP 496. JEP 527 is the part that actually wires it into TLS 1.3, so you get the protection on real connections instead of having to build your own protocol.
What does JEP 527 actually change in the TLS handshake?
JEP 527 adds three hybrid named groups to the SunJSSE provider and enables one of them by default. The named groups are:
| Named group | Combination |
|---|---|
X25519MLKEM768 | X25519 ECDHE + ML-KEM-768 |
SecP256r1MLKEM768 | secp256r1 ECDHE + ML-KEM-768 |
SecP384r1MLKEM1024 | secp384r1 ECDHE + ML-KEM-1024 |
By default, a Java 27 TLS client offers two key shares in its ClientHello: one for X25519MLKEM768 and one for classical x25519. That dual key share is a thoughtful detail. If the server speaks the hybrid group, it picks X25519MLKEM768 and you get post-quantum protection. If the server is old and only knows x25519, it picks that one from the same ClientHello, so the handshake completes in the normal number of round trips with no HelloRetryRequest penalty.
The trade-off is size. An X25519MLKEM768 key share is roughly 1.2KB, because the ML-KEM-768 encapsulation key is 1184 bytes, versus 32 bytes for X25519 on its own. So your ClientHello grows by about a kilobyte. That matters for the rollout testing later, but it costs you nothing in code.
How do you confirm your Java 27 client is using post-quantum TLS?
Turn on JSSE handshake debugging and read the named group in the handshake output. Since the feature is on by default, there's nothing to enable first, you just need to prove it's happening.
java -Djavax.net.debug=ssl,handshake -jar your-app.jarIn the ClientHello you'll see the supported groups and the key_share extension listing X25519MLKEM768 ahead of x25519. In the ServerHello, the selected group tells you what was actually negotiated. If you see X25519MLKEM768 there, the session is post-quantum. If you see x25519, the peer didn't support the hybrid group and you fell back to classical.
A small standalone check is the fastest way to see it against a real endpoint:
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
public class TlsProbe {
public static void main(String[] args) throws Exception {
SSLSocketFactory factory =
(SSLSocketFactory) SSLSocketFactory.getDefault();
try (SSLSocket socket =
(SSLSocket) factory.createSocket("example.org", 443)) {
socket.startHandshake();
System.out.println("Protocol: " + socket.getSession().getProtocol());
// Run with -Djavax.net.debug=ssl,handshake to see the
// negotiated named group in the handshake log.
}
}
}The standard JSSE session API doesn't expose the negotiated named group as a getter, so the handshake log is the source of truth. I keep -Djavax.net.debug=ssl,handshake handy for exactly this kind of check.
How do you control the named groups with jdk.tls.namedGroups?
Set the jdk.tls.namedGroups system property to an ordered, comma-separated list, and SunJSSE offers them in that order. This is how you prefer a different hybrid scheme, pin to post-quantum only for testing, or disable the hybrid groups if you hit an interop problem.
# Prefer the secp256r1 hybrid, then the X25519 hybrid, then classical fallbacks
java -Djdk.tls.namedGroups="SecP256r1MLKEM768,X25519MLKEM768,secp256r1,x25519" \
-jar your-app.jarYou can do the same per connection with SSLParameters, which is cleaner when only one client needs special handling:
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
SSLParameters params = tlsSock.getSSLParameters();
params.setNamedGroups(new String[] {
"SecP256r1MLKEM768", "X25519MLKEM768", "secp256r1", "x25519"
});
tlsSock.setSSLParameters(params);Two practical recipes. To force post-quantum only, so a test fails loudly if the peer can't do it, set the list to just X25519MLKEM768. To disable post-quantum entirely as a temporary fallback for a broken middlebox, set it to classical groups like x25519,secp256r1 and the hybrid shares disappear from your ClientHello.
How do you enable post-quantum key exchange on the server side?
Run your TLS server on JDK 27 and the hybrid groups are available automatically, because the server selects a group from what the client offers. There's no separate switch. The server side of the handshake just needs to know X25519MLKEM768, which SunJSSE does on JDK 27.
The catch is ordering. A server picks the first mutually supported group based on its own preference list, so if you've pinned jdk.tls.namedGroups on the server to a classical-only list from some older hardening guide, it will never choose the hybrid group even when the client offers it. Check your server's existing TLS configuration for a hardcoded named-group list and add the hybrid groups to the front. For a Spring Boot service behind its own TLS, that's the JVM flag on the server process, not anything in your application code.
This is also where you decide your security posture across services. If you run both client and server, putting X25519MLKEM768 first on both guarantees post-quantum protection on internal hops, which fits the least-privilege model I wrote about in my zero trust microservices guide.
What should you test before you roll it out?
Test interop and ClientHello size first, because those are where a hybrid key exchange bites you, not the cryptography itself. The math is solid. The failure mode is some box in the middle that doesn't like a bigger handshake.
Here's my checklist before flipping anything to production:
- Interop with older peers. Point your Java 27 client at the actual endpoints it talks to and confirm the handshake still completes. With the default dual key share it should fall back to
x25519cleanly, but verify it instead of assuming. - Middleboxes and the larger ClientHello. The extra ~1.2KB can trip up old load balancers, deep-packet-inspection firewalls, or anything that makes assumptions about handshake size. Test through your real network path, not just localhost.
- Handshake cost. Measure connection setup time under load. ML-KEM operations are fast, but the larger key share means more bytes on the wire per handshake.
- A loud failure path. Run one test environment pinned to
X25519MLKEM768only, so if a dependency silently can't do post-quantum, your tests catch it instead of quietly downgrading in production.
The reason I'd turn this on deliberately rather than just inheriting the default is that "secure by default" only helps if it actually negotiates. A misconfigured server preference list or a grumpy middlebox can leave you on classical key exchange while you think you're protected. Run the handshake debug once per critical connection, confirm you see X25519MLKEM768, and then you know the harvest-now-decrypt-later window is closed for that traffic. That five-minute check is the whole point of the feature being real instead of theoretical.
For the full specification and testing guidance, see the OpenJDK write-ups on post-quantum hybrid key exchange in JDK 27 and the JDK 27 Quality Outreach heads-up, plus the NIST FIPS 203 ML-KEM standard that defines the post-quantum primitive.
Keep Reading
- How to Read and Write PEM Files in Java (JDK 27 PEM API). The other major security API shipping in JDK 27.
- How to Block SSRF in Spring Boot 4.1 with InetAddressFilter. Another outbound-connection security control worth turning on.
- Java 26 HTTP/3 in the HttpClient. The HttpClient that benefits from this TLS upgrade automatically.
