Hi Sean, Thank you for the detailed response, and for filing JDK-8388519 — the two APIs together (query supported groups before setting, read the negotiated group after the handshake) close exactly the operational gap I was trying to describe. 
Hi Sean,

Thank you for the detailed response, and for filing JDK-8388519 — the
two APIs together (query supported groups before setting, read the
negotiated group after the handshake) close exactly the operational
gap I was trying to describe. The JDK-8301626 JFR event pointer is
appreciated as well.

> Were you setting (requesting) *only* a pure ML-KEM group? Or did
> you also set X25519? If the latter, this is correct behavior per
> the TLS 1.3 RFC. The server chose one of the groups the client
> supports.

Both variants, and I should have been clearer about the wire
behaviour in the first one. With setNamedGroups({"MLKEM768","x25519"})
the server never actually chooses between the two: MLKEM768 is
removed on the client side while the ClientHello is being built, and
the supported_groups extension on the wire contains only [x25519]
(the 27-ea debug trace shows "Ignore inactive or disabled named
group: MLKEM768" followed by "named groups": [x25519]). I fully agree
that a server picking x25519 out of an offered {MLKEM768, x25519}
would be correct RFC 8446 negotiation — my concern was that the offer
itself is silently narrowed before it leaves the client, which the
application cannot observe through any public API on current
releases.

> You probably already know this, but you can set only PQC or hybrid
> named groups if you don't want classical groups to be used.

I tested that guard too, and it has a sharp edge worth noting: with
setNamedGroups({"MLKEM768"}) alone on a JDK that does not support it,
the call succeeds and nothing fails locally. The client still sends a
ClientHello — with no supported_groups or key_share extension at all
— and the failure surfaces only as the peer's fatal alert, whose type
depends on the peer (a Bouncy Castle 1.84 server produced
handshake_failure; in an earlier test a stock JSSE server produced
missing_extension). So the connection does fail closed, but remotely
and in a way that is hard to attribute to the dropped group. The
supported-groups query API in JDK-8388519 gives applications the
right tool here: check first, then set.

On that API, one data point that may be useful for the RFE: on
26.0.1 and 27-ea, SSLContext.getSupportedSSLParameters()
.getNamedGroups() already returns the provider's supported list (my
reproducer uses it as its baseline). But as far as I can tell the
getSupportedSSLParameters() spec only guarantees that the ciphersuite
and protocol arrays are populated, so an application cannot rely on
it across providers — a first-class API in the style of
getSupportedCipherSuites would give that query an actual contract.

> I'm not sure what you mean by security-policy parameter.

Only that choosing among the classical groups was largely an
interoperability and performance preference, whereas whether a
hybrid/PQ group is actually offered now determines a connection's
harvest-now-decrypt-later exposure. But I think your point stands:
with both new APIs an application can validate its configuration and
verify the result, and the "ignore unknown names" contract becomes
entirely workable. I'm happy to consider my questions 1 and 2
addressed by JDK-8388519.

Understood regarding external code. If any of the measurement data
would be useful as the RFE moves forward (the results matrix across
26.0.1 GA and 27-ea, verbatim traces), I'm glad to post it inline on
the list. I'll follow the RFE with interest — thanks again for the
quick and constructive engagement.

Regards,
Arpan Sharma

On Mon, Jul 20, 2026 at 4:51 PM Sean Mullan <[email protected]> wrote:
>
> Hi,
>
> Thanks for taking the time to review the APIs and send us your feedback.
>
> On 7/17/26 4:02 PM, Arpan Sharma wrote:
>
> Hello, I've been measuring post-quantum TLS key exchange on the JVM and ran into a behaviour I'd like to raise as a specification question rather than an implementation bug, since the current behaviour conforms to its documentation. I've also
>
> Hello,
>
> I've been measuring post-quantum TLS key exchange on the JVM and ran into a
> behaviour I'd like to raise as a specification question rather than an
> implementation bug, since the current behaviour conforms to its documentation.
> I've also filed it via bugreport.java.com (reference 38bc5adc-fe85-492c-a579-
> ce0444a8e58e).
>
> THE BEHAVIOUR
> An application that calls SSLParameters.setNamedGroups() with a named group the
> provider does not support has that group silently dropped. The TLS handshake
> proceeds on a classical group, no exception surfaces, nothing is logged at
> default levels, and no public API reveals which group was actually negotiated.
> The only signal is a DEBUG line under -Djavax.net.debug.
>
> FYI, this doesn't completely address your concern, but In JDK 28, we added a JFR event that captures the negotiated named group [1] and is also logged at the DEBUG level.
>
> This is documented -- the setNamedGroups() spec (added by JDK-8291950 in JDK 20)
> states that providers "should ignore unknown named group scheme names" -- so I'm
> raising the contract, not the implementation.
>
> WHY IT MATTERS NOW
> Under CNSA 2.0 and NIST IR 8547 timelines, the natural Java migration step is
> exactly this call: setNamedGroups() with a post-quantum group first in
> preference
> order. When that group is unsupported on the running JDK, the
> application deploys
> believing it has post-quantum key exchange while every connection silently falls
> back to classical ECDH -- the precise harvest-now-decrypt-later exposure the
> migration was meant to close. Because no public API exposes the negotiated named
> group, this is undetectable at runtime by the application, by health checks, or
> by compliance tooling. (Verified: after a handshake that negotiated x25519,
> SSLParameters.getNamedGroups() still returns the requested "MLKEM768,x25519" --
> it reports what was set, not what was used.)
>
> You probably already know this, but you can set only PQC or hybrid named groups if you don't want classical groups to be used.
>
> JEP 527 DOES NOT RESOLVE IT
> I tested JDK 27-ea+30-2302 alongside 26.0.1+8-34. JDK 27 correctly negotiates
> X25519MLKEM768 -- JEP 527 works as intended. But the silent-drop behaviour
> persists for every group outside its scope. Requesting the pure-ML-KEM group
> MLKEM768 on 27-ea: it is recognised as a defined-but-disabled group and still
> silently discarded, the handshake completing on x25519 with no error:
>
> Were you setting (requesting) *only* a pure ML-KEM group? Or did you also set X25519? If the latter, this is correct behavior per the TLS 1.3 RFC. The server chose one of the groups the client supports. It does not have to fail if the client's most preferred group is not selected. It does not have to fail if it doesn't support one of the groups.
>
>   JDK 26.0.1: SupportedGroupsExtension.java:181
>               "Ignore unspecified named group: X25519MLKEM768"
>   JDK 27-ea:  SupportedGroupsExtension.java:200
>               "Ignore inactive or disabled named group: MLKEM768"
>
> A control case with a nonexistent group name ("NoSuchGroup12345") is dropped
> identically on both JDKs, so the behaviour is generic rather than PQC-specific.
>
> AN INCONSISTENCY WORTH NOTING
> The setNamedGroups() spec already throws IllegalArgumentException when a named
> group entry is a duplicate, null, or blank -- but only "ignores" one that is
> unknown/unsupported. And a comparable security-policy setter fails loudly:
>   sslSocket.setEnabledProtocols(new String[]{"TLSv9.9"})
>     -> IllegalArgumentException: Unsupported protocol: TLSv9.9  (both JDKs)
> So the API already distinguishes invalid input (throw) from valid input, and a
> sibling setter rejects unsupported values outright; only an
> unsupported-but-well-
> formed named group is silently accepted and then dropped.
>
> I think it would be useful to add an API to get a provider's supported groups, ala SSLSocket.getSupportedCipherSuites. This would allow an application to check what is supported and set the ones it wants to use before starting a handshake. Throwing an exception on an invalid protocol is clearly different than an invalid or unsupported group. Different providers may support different groups, or even custom ones. Allowing an application to be flexible and not tied to one provider is a plus.
>
> QUESTIONS FOR THE LIST
> 1. Is the "providers should ignore unknown named group scheme names" contract
>    still considered correct, now that named groups are a security-policy
>    parameter (post-JEP-527) rather than a performance hint?
>
> I'm not sure what you mean by security-policy parameter. As mentioned before, if you want to guarantee pure ML-KEM is used, you should just specify that and nothing else.
>
> 2. Would a CSR revising it be welcome -- either throwing
> IllegalArgumentException
>    for an unsupported group (consistent with setEnabledProtocols, and with the
>    existing duplicate/null/blank handling), or at minimum emitting a platform-
>    logger warning when a requested group is dropped?
>
> No, see above. I would be ok with adding an API to get the supported groups though (see above).
>
> 3. Is there interest in exposing the negotiated named group through a public API
>    (e.g. on ExtendedSSLSession), so applications can verify their key-exchange
>    posture? Today there is no way to do so short of parsing debug output or
>    reflecting into sun.security.ssl internals.
>
> Yes, I think it would be useful to add an API to SSLSession or ExtendedSSLSession that returns the group that was negotiated.
>
> I have filed an RFE to add these two APIs [2].
>
> REPRODUCER AND DATA
> A single-file, dependency-free reproducer runs on a stock JDK in one command
> (no server, no network); the full probe harness, pinned environment with build
> strings, verbatim javax.net.debug traces, and the complete results matrix across
> JDK 26 GA and 27-ea are here:
>   https://github.com/Arpan0995/jdk-tls-namedgroup-silent-drop
>
> Sorry, but I cannot review code that is outside of openjdk.
>
> --Sean
>
> [1] https://bugs.openjdk.org/browse/JDK-8301626
>
> [2] https://bugs.openjdk.org/browse/JDK-8388519

Reply via email to