Someone can record your TLS traffic today and wait. A cryptographically relevant quantum computer (CRQC) does not have to exist yet. RSA and elliptic-curve Diffie–Hellman fall to Shor’s algorithm once one does, so a capture made in 2026 becomes plaintext on the day the machine arrives.

That attack has a name: store now, decrypt later (the JEP’s “harvest now, decrypt later”). Classical ECDHE alone does not beat it. You need a key exchange that still holds if the elliptic curve does not.

Java 27 / JEP 527 is the shipping vehicle: Post-Quantum Hybrid Key Exchange for TLS 1.3, final, GA 15 September 2026. SunJSSE concatenates a traditional ephemeral ECDHE share with ML-KEM (the NIST FIPS 203 KEM derived from Kyber). The session is safe if either algorithm holds. Applications on javax.net.ssl pick this up by default. You do not implement a KEM.

SSLParameters params = tlsSock.getSSLParameters();
params.setNamedGroups(new String[] {
        "X25519MLKEM768", "x25519"
});
tlsSock.setSSLParameters(params);

That snippet is the override. Most code never writes it.

The problem it solves

TLS 1.3’s confidentiality rests on the handshake secret. Today that secret is usually ECDHE — x25519 or secp256r1. Both are discrete-log problems. A CRQC solves them.

The urgency is the recording, not the calendar. Traffic you send this week can be stored cheaply. Certificates expire; ciphertext on disk does not. Anything that must stay secret past the arrival of a CRQC — medical records, long-lived API tokens, backups — is already in scope.

A hybrid key exchange runs two algorithms in parallel and mixes their shared secrets. Break the curve and ML-KEM still binds the session. Break ML-KEM (new, less battle-tested) and ECDHE still binds it. That is the entire security claim, and it is why the IETF TLS working group standardized hybrid groups instead of flipping a switch to “PQ only.”

JEP 527 wires those groups into the JDK’s TLS 1.3 stack. The KEM API landed in Java 21 (JEP 452); ML-KEM itself landed in Java 24 (JEP 496). This JEP is the handshake, not another crypto primitive.

When it shipped

ReleaseStatusSpec
Java 21KEM APIJEP 452
Java 24ML-KEM (FIPS 203) in JCAJEP 496
Java 26HttpClient honors named groups on SSLParametersJDK-8367112
Java 27Hybrid TLS 1.3 key exchange, finalJEP 527

No preview flag. No incubator module. The names go into the Java Security Standard Algorithm Names spec, matching IANA. Java 27 GA is 15 September 2026.

Note: JEP 496 is explicit that ML-KEM is not Kyber and the two are not interoperable. TLS speaks ML-KEM. Do not configure a Kyber draft group and expect the JDK to negotiate it.

Mental model

TLS calls key-exchange schemes named groups. The client lists the groups it supports, in preference order, in ClientHello. For the groups it most prefers it also sends key shares up front, so a matching server can complete the handshake in one round trip.

Hybrid groups are named groups like any other. JSSE does not grow a new TLS version or a new SSLContext algorithm. You already know the knobs: jdk.tls.namedGroups and SSLParameters.setNamedGroups.

PieceRole
ECDHEClassical ephemeral agreement (x25519, secp256r1, secp384r1)
ML-KEMNIST KEM; encapsulation in the key share, decapsulation on the peer
Hybrid named groupOne IANA name whose share is the concatenation of both
Preference listOrder in ClientHello; first mutual group wins

The JEP’s non-goals matter as much as the feature. Hybrid key exchange is for javax.net.ssl and TLS 1.3 only. It is not for TLS 1.2. It is not a public “pure ML-KEM” named group — the JEP may add those later; Java 27 does not. And it is not an invitation to call KEM.getInstance("ML-KEM") from your servlet filter. The handshake owns that.

The three hybrid groups

SunJSSE adds exactly three schemes. Names are case-sensitive IANA identifiers — copy them, do not camelCase them into something adjacent.

Named groupClassical halfPQ halfDefault?
X25519MLKEM768X25519 ECDHEML-KEM-768Yes — first
SecP256r1MLKEM768secp256r1 ECDHEML-KEM-768No
SecP384r1MLKEM1024secp384r1 ECDHEML-KEM-1024No

X25519MLKEM768 is first because it is the fastest hybrid and the one browsers and libraries already enable. The two NIST-curve hybrids exist for deployments that must stay on P-256 or P-384 (FIPS-shaped environments, hardware that only does those curves). You turn them on; they do not sneak into the default list.

A hybrid share is large. For X25519MLKEM768 the client’s key-exchange value is 1,216 bytes (1,184 of ML-KEM-768 encapsulation key plus 32 of X25519). Classical x25519 is 32 bytes. That extra kilobyte is the cost of the hedge, and it is why some middleboxes misbehave — more on that in Gotchas.

What a JDK 27 client actually offers

JEP 527’s default named-group list, in order:

X25519MLKEM768, x25519, secp256r1, secp384r1, secp521r1,
x448, ffdhe2048, ffdhe3072, ffdhe4096

Based on that ordering, a TLS 1.3 client sends two key shares: X25519MLKEM768 and x25519. A peer that knows the hybrid group takes it. A peer that does not still has a classical X25519 share and completes the handshake without an extra round trip. You keep talking; you just are not PQ-safe with that peer.

No code change is required unless the application already selects named groups. A pin is an allow-list. If yours was written in 2022 and lists x25519,secp256r1, Java 27 will honor that list and never offer a hybrid.

Print the engine’s view of the defaults:

import javax.net.ssl.SSLContext;
import java.util.Arrays;

public class NamedGroups {
    public static void main(String[] args) throws Exception {
        var params = SSLContext.getDefault()
                .createSSLEngine()
                .getSSLParameters();
        System.out.println(Arrays.toString(params.getNamedGroups()));
    }
}
java NamedGroups.java

On an unmodified JDK 27 you should see X25519MLKEM768 at the front of that array. If you see null, you are looking at a pre-populated SSLParameters that never went through a connection object — start from SSLEngine or SSLSocket as above, not new SSLParameters().

Prefer, pin, or expand

Two supported ways to change the list. Both override the default; neither merges with it.

Process-wide: jdk.tls.namedGroups is a comma-separated list in preference order. JSSE documents it as a quoted list of standard names:

java -Djdk.tls.namedGroups="X25519MLKEM768,x25519,secp256r1" NamedGroups.java

Use this when operators, not code, own the policy: a container flag, a launch script, a support runbook. Every SSLSocket, SSLEngine, and HTTPS HttpClient in the process sees it.

Per connection: SSLParameters.setNamedGroups. This is the JEP’s own example — two hybrids and two classical groups, hybrids first:

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;

public class PinHybrids {
    static SSLSocket configure(SSLSocket tlsSock) {
        SSLParameters params = tlsSock.getSSLParameters();
        params.setNamedGroups(new String[] {
                "SecP256r1MLKEM768", "X25519MLKEM768",
                "secp256r1", "x25519"
        });
        tlsSock.setSSLParameters(params);
        return tlsSock;
    }

    public static void main(String[] args) throws Exception {
        SSLSocket tlsSock = (SSLSocket) SSLContext.getDefault()
                .getSocketFactory()
                .createSocket();
        configure(tlsSock);
        System.out.println(String.join(", ",
                tlsSock.getSSLParameters().getNamedGroups()));
    }
}
SecP256r1MLKEM768, X25519MLKEM768, secp256r1, x25519

Copy parameters from the socket, then set named groups. Starting from new SSLParameters() and stuffing only group names can drop cipher suites and protocols you meant to keep.

Hybrid-only is legal and sharp:

params.setNamedGroups(new String[] { "X25519MLKEM768" });

The handshake then fails against a peer that cannot do that group. Use it in a lab that must prove PQ negotiation, not on a client that talks to the open web.

TLS 1.3 is a separate switch. Hybrid groups are ignored on a 1.2 connection. If the threat model is store-now-decrypt-later, also stop offering TLS 1.2:

params.setProtocols(new String[] { "TLSv1.3" });

Servers use the same list. A JSSE server with the Java 27 defaults already accepts X25519MLKEM768. If you pin groups on the server, put a hybrid on that pin or clients that prefer PQ will fall through to whatever classical group you left — or fail, if they pinned hybrids only.

HttpClient sits on JSSE

java.net.http.HttpClient does not grow a “post-quantum” enum. HTTPS is TLS via JSSE, so the default named groups apply to HttpClient.newHttpClient() the same way they apply to SSLSocket. The HTTP/3 opt-in is a different transport question; this post is the key exchange underneath TLS 1.3.

Until Java 26, HttpClient quietly ignored named groups and signature schemes you put on SSLParameters. Java 26 fixed that (JDK-8367112). On Java 27 a per-client pin actually reaches the handshake:

import javax.net.ssl.SSLParameters;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class HybridGet {
    public static void main(String[] args) throws Exception {
        SSLParameters ssl = new SSLParameters();
        ssl.setNamedGroups(new String[] { "X25519MLKEM768", "x25519" });
        ssl.setProtocols(new String[] { "TLSv1.3" });

        HttpClient client = HttpClient.newBuilder()
                .sslParameters(ssl)
                .build();

        HttpResponse<Void> response = client.send(
                HttpRequest.newBuilder(URI.create("https://example.com/"))
                        .GET()
                        .build(),
                HttpResponse.BodyHandlers.discarding());
        System.out.println(response.statusCode());
    }
}

Keep that HttpClient as a field. The TLS story here is the parameters, not a new send API.

If you only need the Java 27 defaults, do not set sslParameters at all. An empty SSLParameters you constructed yourself is not “leave the defaults alone” — it is a template. Prefer the system property when the policy is process-wide, and prefer mutating getSSLParameters() when you already hold an SSLSocket.

Confirm the handshake

SSLSession will tell you TLSv1.3 and a cipher suite. It will not print the named group in an obvious getter. Handshake logging will.

java -Djavax.net.debug=ssl:handshake HybridGet.java

In the ClientHello dump, look for X25519MLKEM768 in the supported-groups list and in key_share. A successful PQ negotiation names that group again when the server selects it. A classical fallback still shows x25519 in key_share — that is the second share doing its job, not a JDK bug.

Filter if the log is too loud:

java -Djavax.net.debug=ssl:handshake HybridGet.java 2>&1 \
        | grep -E 'X25519MLKEM768|NamedGroup|supported_groups|key_share'

A 200 from example.com with only x25519 selected means the peer has not enabled the hybrid group. Try an origin you know speaks PQ TLS, or talk to your own JSSE server running the same JDK. The client’s offer and the peer’s choice are different facts.

Gotchas

A named-groups pin opts you out. Any explicit list that omits the hybrids never offers them. Search startup scripts and Kubernetes JAVA_TOOL_OPTIONS for jdk.tls.namedGroups before you declare the fleet PQ-ready.

TLS 1.2 is still classical. Downgrade, a mis-set protocol list, or a peer that only speaks 1.2 means ECDHE without ML-KEM. Hybrid is a TLS 1.3 named group, not a property of “HTTPS.”

The ClientHello got bigger. About 1.2 KB extra for X25519MLKEM768. Older load balancers, DPI boxes, and “TLS inspection” appliances have historically reset large ClientHellos. If handshakes fail only after the Java 27 upgrade and only through a particular proxy, capture the first flight before you blame JSSE.

Fallback is silent. Default clients send a classical x25519 share on purpose. A session that negotiates x25519 looks identical to your application code. Logging the handshake (or requiring the hybrid group in a canary) is how you know.

Case and spelling are IANA, not JavaStyle. X25519MLKEM768 is not x25519mlkem768. SecP256r1MLKEM768 is not secp256r1MLKEM768. Unknown names are ignored by the provider; a typo can leave you on classical groups with no exception at setNamedGroups.

Third-party JSSE providers are on their own. JEP 527 is the SunJSSE implementation. A custom SSLContext provider that has not added these groups will not negotiate them, default list or not.

Do not implement the KEM. KeyPairGenerator.getInstance("ML-KEM") is the JCA primitive for application-level encapsulation. TLS hybrid key exchange is not that API with extra steps. Mixing the two — encapsulating a session key yourself, then wrapping it in TLS — is how you get a protocol nobody else will speak.

Cheat sheet

Java 27 / JEP 527: final, no preview flag, SunJSSE TLS 1.3
GA 15 September 2026

Hybrid = ECDHE + ML-KEM (FIPS 203). Safe if either algorithm holds.
Not Kyber-the-draft; not a from-scratch KEM; not TLS 1.2; not other APIs.

IANA named groups
  X25519MLKEM768       default, first, two ClientHello shares with x25519
  SecP256r1MLKEM768    opt-in (P-256 + ML-KEM-768)
  SecP384r1MLKEM1024   opt-in (P-384 + ML-KEM-1024)

Default list
  X25519MLKEM768, x25519, secp256r1, secp384r1, secp521r1,
  x448, ffdhe2048, ffdhe3072, ffdhe4096

Process-wide
  -Djdk.tls.namedGroups="X25519MLKEM768,x25519,secp256r1"

Per connection
  params = socket.getSSLParameters();
  params.setNamedGroups(new String[] { "X25519MLKEM768", "x25519" });
  params.setProtocols(new String[] { "TLSv1.3" });
  socket.setSSLParameters(params);

HttpClient (Java 26+ honors named groups on SSLParameters)
  HttpClient.newBuilder().sslParameters(ssl).build()
  HTTPS uses JSSE defaults if you set nothing

See it
  java -Djavax.net.debug=ssl:handshake ...
  SSLEngine.getSSLParameters().getNamedGroups()

Do:

  • Upgrade to Java 27 and leave the defaults alone unless you already pin groups.
  • Add X25519MLKEM768 to any existing jdk.tls.namedGroups allow-list.
  • Restrict to TLSv1.3 when store-now-decrypt-later is in the threat model.
  • Offer a classical share next to the hybrid on clients that must still reach old peers.
  • Confirm with handshake logs, not with response.statusCode().

Don’t:

  • Treat a 2022 named-groups pin as “we already configured TLS.”
  • Ship hybrid-only named groups to the public internet unless you own both ends.
  • Expect TLS 1.2, a third-party JSSE provider, or a typo’d group name to protect you.
  • Call KEM / KeyPairGenerator “ML-KEM” from application code to “do PQ TLS.”
  • Assume HttpClient on Java 25 applied your setNamedGroups — that starts in 26.

Wrap-up

JEP 527 is a handshake change, not a new library. Java 27 puts X25519MLKEM768 at the front of the TLS 1.3 named-group list, sends a classical x25519 share beside it, and lets javax.net.ssl callers inherit the hedge. The session stays confidential if either half of the hybrid holds — which is the point of combining ML-KEM with ECDHE instead of betting the fleet on a young algorithm.

The work on your side is inventory. Find every jdk.tls.namedGroups and every setNamedGroups call. If the list already exists, add a hybrid. If it does not, the JDK already did. Then look at a handshake log once, because fallback to x25519 is silent and a 200 OK does not mean the quantum-resistant half ran.