This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 376cde800b17 CAMEL-26545: camel-core - the post-quantum named groups
no longer race on a shared SSLContextParameters, and the NIST hybrids count too
(#29092)
376cde800b17 is described below
commit 376cde800b17dfca247b147617cfc60c9fa4c044
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Nov 12 17:07:40 2026 +0100
CAMEL-26545: camel-core - the post-quantum named groups no longer race on a
shared SSLContextParameters, and the NIST hybrids count too (#29092)
The post-quantum named group auto-configuration of SSLContextParameters no
longer races on a shared instance: the window in which the computed ordering is
published on the instance is held under a lock, and the instance is left
untouched afterwards. Any of the hybrids X25519MLKEM768, SecP256r1MLKEM768 and
SecP384r1MLKEM1024 now counts, an order that already leads with a hybrid is
kept, and a DEBUG line says when no hybrid group is available. The 4.24 upgrade
guide and the JSSE utility [...]
Co-Authored-By: Claude Opus 5 <[email protected]>
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Andrea Cosentino <[email protected]>
---
.../camel/support/jsse/SSLContextParameters.java | 158 +++++++++++++++----
.../support/jsse/SSLContextParametersTest.java | 173 ++++++++++++++++++---
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 20 +++
.../ROOT/pages/camel-configuration-utilities.adoc | 48 ++++--
4 files changed, 336 insertions(+), 63 deletions(-)
diff --git
a/core/camel-api/src/main/java/org/apache/camel/support/jsse/SSLContextParameters.java
b/core/camel-api/src/main/java/org/apache/camel/support/jsse/SSLContextParameters.java
index 093d50a82dc1..76f3283e514f 100644
---
a/core/camel-api/src/main/java/org/apache/camel/support/jsse/SSLContextParameters.java
+++
b/core/camel-api/src/main/java/org/apache/camel/support/jsse/SSLContextParameters.java
@@ -23,6 +23,8 @@ import java.security.Security;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
@@ -45,19 +47,45 @@ public class SSLContextParameters extends
BaseSSLContextParameters {
protected static final String DEFAULT_SECURE_SOCKET_PROTOCOL = "TLSv1.3";
/**
- * The post-quantum hybrid named group to detect for auto-configuration.
+ * The post-quantum hybrid named groups to detect for auto-configuration.
Any one of them is enough to switch the
+ * auto-configuration on: a JVM or a crypto policy restricted to the NIST
curves offers the SecP* hybrids without
+ * the X25519 one. The preferred ordering is derived from this list, so a
hybrid added here takes part in both the
+ * detection and the ordering.
*/
- private static final String PQC_NAMED_GROUP = "X25519MLKEM768";
+ private static final List<String> PQC_NAMED_GROUPS
+ = List.of("X25519MLKEM768", "SecP256r1MLKEM768",
"SecP384r1MLKEM1024");
/**
- * The preferred named group ordering when PQC groups are available. This
ordering ensures post-quantum key exchange
- * is preferred while maintaining classical fallbacks for compatibility.
+ * The classical groups kept after the hybrids, as fallbacks for a peer
that cannot negotiate a hybrid.
*/
- private static final List<String> PQC_PREFERRED_NAMED_GROUPS
- = List.of("X25519MLKEM768", "x25519", "secp256r1", "secp384r1");
+ private static final List<String> CLASSICAL_FALLBACK_NAMED_GROUPS =
List.of("x25519", "secp256r1", "secp384r1");
+
+ /**
+ * The preferred named group ordering when PQC groups are available: the
hybrids, then the classical fallbacks, so
+ * that the post-quantum key exchange is preferred while the classical one
stays available for compatibility.
+ */
+ private static final List<String> PQC_PREFERRED_NAMED_GROUPS =
preferredNamedGroups();
private static final Logger LOG =
LoggerFactory.getLogger(SSLContextParameters.class);
+ private static List<String> preferredNamedGroups() {
+ List<String> groups = new ArrayList<>(PQC_NAMED_GROUPS);
+ groups.addAll(CLASSICAL_FALLBACK_NAMED_GROUPS);
+ return List.copyOf(groups);
+ }
+
+ /**
+ * Logged once per JVM, so that a deployment without any post-quantum
group says so instead of staying silent.
+ */
+ private static volatile boolean pqcNamedGroupsUnavailableLogged;
+
+ /**
+ * Guards the window in which the auto-configured post-quantum ordering is
published on this instance. A single
+ * {@code SSLContextParameters} is shared by every component of a {@link
CamelContext}, so concurrent
+ * {@link #createSSLContext(CamelContext)} calls would otherwise race on
it.
+ */
+ private final Lock lock = new ReentrantLock();
+
/**
* The optional key manager configuration for creating the {@link
KeyManager}s used in constructing an
* {@link SSLContext}.
@@ -323,13 +351,19 @@ public class SSLContextParameters extends
BaseSSLContextParameters {
context.init(keyManagers, trustManagers, secureRandom);
- // Auto-configure PQC named groups when available and no user
configuration is present
+ // The auto-configuration publishes the computed ordering on this
instance for as long as it takes to build the
+ // configurers, which read it, and takes it back afterwards. That
window has to be exclusive: this instance is
+ // shared by every component of the context, so a concurrent
createSSLContext() would otherwise skip the
+ // auto-configuration because it saw the ordering of the other thread,
or build its configurers after that
+ // thread has reset it - leaving the post-quantum preference applied
or not depending on timing.
+ lock.lock();
boolean autoConfiguredPqc = false;
- if (this.getNamedGroups() == null && this.getNamedGroupsFilter() ==
null) {
- autoConfiguredPqc = applyPqcNamedGroupDefaults(context);
- }
-
try {
+ // Auto-configure PQC named groups when available and no user
configuration is present
+ if (this.getNamedGroups() == null && this.getNamedGroupsFilter()
== null) {
+ autoConfiguredPqc = applyPqcNamedGroupDefaults(context);
+ }
+
this.configureSSLContext(context);
// Decorate the context.
@@ -344,6 +378,7 @@ public class SSLContextParameters extends
BaseSSLContextParameters {
if (autoConfiguredPqc) {
this.setNamedGroups(null);
}
+ lock.unlock();
}
return context;
@@ -352,45 +387,112 @@ public class SSLContextParameters extends
BaseSSLContextParameters {
/**
* Auto-configures post-quantum named groups when the JVM supports them
and the user hasn't explicitly configured
* named groups or named groups filters.
- * <p/>
- * When {@code X25519MLKEM768} is available (typically JDK 25+), this
method sets the preferred named group ordering
- * to prioritize post-quantum key exchange, protecting against
harvest-now-decrypt-later attacks.
+ * <p>
+ * When a hybrid ML-KEM group is available (JDK 27 with JEP 527, or a
build that backports it) and the JVM does not
+ * already lead with one, this method sets the preferred named group
ordering to prioritize post-quantum key
+ * exchange, protecting against harvest-now-decrypt-later attacks.
*
* @param context the initialized SSLContext to probe for available named
groups
* @return {@code true} if PQC named groups were auto-configured,
{@code false} otherwise
*/
private boolean applyPqcNamedGroupDefaults(SSLContext context) {
SSLEngine probeEngine = context.createSSLEngine();
- String[] availableGroups =
getNamedGroupsFromParams(probeEngine.getSSLParameters());
-
- if (availableGroups == null) {
+ String[] available =
getNamedGroupsFromParams(probeEngine.getSSLParameters());
+ List<String> ordered = resolvePqcNamedGroups(available);
+
+ if (ordered == null) {
+ if (hasPqcNamedGroup(available)) {
+ LOG.debug("The JVM already leads with the post-quantum named
group {}; its named group order is left"
+ + " alone",
+ available[0]);
+ } else {
+ logPqcNamedGroupsUnavailable();
+ }
return false;
}
- List<String> available = Arrays.asList(availableGroups);
- if (!available.contains(PQC_NAMED_GROUP)) {
- return false;
+ NamedGroupsParameters ngp = new NamedGroupsParameters();
+ ngp.setNamedGroup(ordered);
+ this.setNamedGroups(ngp);
+
+ LOG.info("Auto-configured PQC named groups: {}", ordered);
+ return true;
+ }
+
+ /**
+ * Orders the named groups available on this JVM so that the post-quantum
hybrids come first, followed by the
+ * classical fallbacks and then everything else, preserving the spelling
the JVM uses.
+ * <p>
+ * An order that already leads with a hybrid is left alone: it is either
the default of the JVM, which this
+ * reordering would only confirm, or one an operator chose through {@code
jdk.tls.namedGroups}, such as a CNSA 2.0
+ * setup that puts the P-384 hybrid first, which it must not rewrite.
+ *
+ * @param availableGroups the named groups the JVM offers, in its order
+ * @return the preferred ordering, or {@code null} when
the JVM offers no post-quantum group or
+ * already leads with one - in both cases the
ordering is left alone rather than rewritten
+ */
+ static @Nullable List<String> resolvePqcNamedGroups(String @Nullable []
availableGroups) {
+ if (availableGroups == null || availableGroups.length == 0 ||
isPqcNamedGroup(availableGroups[0])) {
+ return null;
}
- // Build preferred ordering: PQC preferred groups first (if
available), then remaining
+ // Match case-insensitively, since the spelling of a group is up to
the provider, but keep the name the
+ // provider gave: that is the one it accepts back on SSLParameters.
List<String> ordered = new ArrayList<>();
+ boolean postQuantum = false;
for (String preferred : PQC_PREFERRED_NAMED_GROUPS) {
- if (available.contains(preferred)) {
- ordered.add(preferred);
+ for (String available : availableGroups) {
+ if (preferred.equalsIgnoreCase(available)) {
+ ordered.add(available);
+ postQuantum |= PQC_NAMED_GROUPS.contains(preferred);
+ break;
+ }
}
}
+
+ if (!postQuantum) {
+ return null;
+ }
+
for (String group : availableGroups) {
if (!ordered.contains(group)) {
ordered.add(group);
}
}
- NamedGroupsParameters ngp = new NamedGroupsParameters();
- ngp.setNamedGroup(ordered);
- this.setNamedGroups(ngp);
+ return ordered;
+ }
- LOG.info("Auto-configured PQC named groups: {}", ordered);
- return true;
+ /**
+ * Whether the JVM offers any of the post-quantum hybrid groups, in
whatever position.
+ */
+ static boolean hasPqcNamedGroup(String @Nullable [] availableGroups) {
+ if (availableGroups != null) {
+ for (String group : availableGroups) {
+ if (isPqcNamedGroup(group)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static boolean isPqcNamedGroup(String group) {
+ for (String pqc : PQC_NAMED_GROUPS) {
+ if (pqc.equalsIgnoreCase(group)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static void logPqcNamedGroupsUnavailable() {
+ if (!pqcNamedGroupsUnavailableLogged) {
+ pqcNamedGroupsUnavailableLogged = true;
+ LOG.debug("This JVM offers no post-quantum TLS named group ({}),
so the TLS key exchange stays classical;"
+ + " a JDK that offers a hybrid ML-KEM group, such as JDK
27 (JEP 527), enables it.",
+ String.join(", ", PQC_NAMED_GROUPS));
+ }
}
@Override
diff --git
a/core/camel-core/src/test/java/org/apache/camel/support/jsse/SSLContextParametersTest.java
b/core/camel-core/src/test/java/org/apache/camel/support/jsse/SSLContextParametersTest.java
index 653ec03e5255..20c129f83ae2 100644
---
a/core/camel-core/src/test/java/org/apache/camel/support/jsse/SSLContextParametersTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/support/jsse/SSLContextParametersTest.java
@@ -21,6 +21,12 @@ import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import javax.net.ssl.SSLContext;
@@ -40,6 +46,11 @@ import static org.junit.jupiter.api.Assertions.*;
@Isolated("This test is regularly flaky")
public class SSLContextParametersTest extends AbstractJsseParametersTest {
+ /**
+ * The hybrid ML-KEM named groups, in the order the auto-configuration
prefers them.
+ */
+ private static final List<String> PQC_GROUPS = List.of("X25519MLKEM768",
"SecP256r1MLKEM768", "SecP384r1MLKEM1024");
+
@Test
public void testFilter() {
SSLContextParameters parameters = new SSLContextParameters();
@@ -752,7 +763,7 @@ public class SSLContextParametersTest extends
AbstractJsseParametersTest {
String[] controlNamedGroups =
controlEngine.getSSLParameters().getNamedGroups();
// default - no named groups configured
- // When PQC groups are available (JDK 25+), auto-configuration
reorders named groups
+ // When a hybrid group is enabled (JDK 27+), the auto-configuration
keeps or puts it first
SSLContextParameters scp = new SSLContextParameters();
SSLContext context = scp.createSSLContext(null);
@@ -760,11 +771,12 @@ public class SSLContextParametersTest extends
AbstractJsseParametersTest {
SSLSocket socket = (SSLSocket)
context.getSocketFactory().createSocket();
SSLServerSocket serverSocket = (SSLServerSocket)
context.getServerSocketFactory().createServerSocket();
- if (Arrays.asList(controlNamedGroups).contains("X25519MLKEM768")) {
- // PQC auto-configuration reorders groups with X25519MLKEM768 first
- assertEquals("X25519MLKEM768",
engine.getSSLParameters().getNamedGroups()[0]);
- assertEquals("X25519MLKEM768",
socket.getSSLParameters().getNamedGroups()[0]);
- assertEquals("X25519MLKEM768",
serverSocket.getSSLParameters().getNamedGroups()[0]);
+ String expectedFirstGroup = expectedFirstPqcGroup(controlNamedGroups);
+ if (expectedFirstGroup != null) {
+ // PQC auto-configuration reorders groups with the hybrid ML-KEM
group first
+ assertEquals(expectedFirstGroup,
engine.getSSLParameters().getNamedGroups()[0]);
+ assertEquals(expectedFirstGroup,
socket.getSSLParameters().getNamedGroups()[0]);
+ assertEquals(expectedFirstGroup,
serverSocket.getSSLParameters().getNamedGroups()[0]);
} else {
// No PQC available, should keep JVM defaults
assertArrayEquals(controlNamedGroups,
engine.getSSLParameters().getNamedGroups());
@@ -822,7 +834,7 @@ public class SSLContextParametersTest extends
AbstractJsseParametersTest {
SSLEngine controlEngine = controlContext.createSSLEngine();
String[] controlNamedGroups =
controlEngine.getSSLParameters().getNamedGroups();
- // default - no filter, keeps defaults (or PQC-reordered defaults on
JDK 25+)
+ // default - no filter, keeps defaults (or hybrid-first defaults on
JDK 27+)
SSLContextParameters scp = new SSLContextParameters();
SSLContext context = scp.createSSLContext(null);
@@ -830,10 +842,11 @@ public class SSLContextParametersTest extends
AbstractJsseParametersTest {
SSLSocket socket = (SSLSocket)
context.getSocketFactory().createSocket();
SSLServerSocket serverSocket = (SSLServerSocket)
context.getServerSocketFactory().createServerSocket();
- if (Arrays.asList(controlNamedGroups).contains("X25519MLKEM768")) {
- assertEquals("X25519MLKEM768",
engine.getSSLParameters().getNamedGroups()[0]);
- assertEquals("X25519MLKEM768",
socket.getSSLParameters().getNamedGroups()[0]);
- assertEquals("X25519MLKEM768",
serverSocket.getSSLParameters().getNamedGroups()[0]);
+ String expectedFirstGroup = expectedFirstPqcGroup(controlNamedGroups);
+ if (expectedFirstGroup != null) {
+ assertEquals(expectedFirstGroup,
engine.getSSLParameters().getNamedGroups()[0]);
+ assertEquals(expectedFirstGroup,
socket.getSSLParameters().getNamedGroups()[0]);
+ assertEquals(expectedFirstGroup,
serverSocket.getSSLParameters().getNamedGroups()[0]);
} else {
assertArrayEquals(controlNamedGroups,
engine.getSSLParameters().getNamedGroups());
assertArrayEquals(controlNamedGroups,
socket.getSSLParameters().getNamedGroups());
@@ -1122,8 +1135,9 @@ public class SSLContextParametersTest extends
AbstractJsseParametersTest {
// Named groups may be reordered by PQC auto-configuration, but same
groups should be present
String[] controlGroups =
controlEngine.getSSLParameters().getNamedGroups();
String[] engineGroups = engine.getSSLParameters().getNamedGroups();
- if (controlGroups != null &&
Arrays.asList(controlGroups).contains("X25519MLKEM768")) {
- assertEquals("X25519MLKEM768", engineGroups[0]);
+ String expectedFirstGroup = controlGroups == null ? null :
expectedFirstPqcGroup(controlGroups);
+ if (expectedFirstGroup != null) {
+ assertEquals(expectedFirstGroup, engineGroups[0]);
assertEquals(controlGroups.length, engineGroups.length);
} else {
assertArrayEquals(controlGroups, engineGroups);
@@ -1223,17 +1237,16 @@ public class SSLContextParametersTest extends
AbstractJsseParametersTest {
controlContext.init(null, null, null);
SSLEngine controlEngine = controlContext.createSSLEngine();
String[] controlNamedGroups =
controlEngine.getSSLParameters().getNamedGroups();
- boolean pqcAvailable = controlNamedGroups != null
- &&
Arrays.asList(controlNamedGroups).contains("X25519MLKEM768");
+ String expectedFirstGroup = controlNamedGroups == null ? null :
expectedFirstPqcGroup(controlNamedGroups);
SSLContextParameters scp = new SSLContextParameters();
SSLContext context = scp.createSSLContext(null);
SSLEngine engine = context.createSSLEngine();
String[] resultGroups = engine.getSSLParameters().getNamedGroups();
- if (pqcAvailable) {
- // X25519MLKEM768 should be first
- assertEquals("X25519MLKEM768", resultGroups[0]);
+ if (expectedFirstGroup != null) {
+ // the hybrid ML-KEM group should be first
+ assertEquals(expectedFirstGroup, resultGroups[0]);
// All original groups should still be present
List<String> resultList = Arrays.asList(resultGroups);
for (String group : controlNamedGroups) {
@@ -1299,6 +1312,107 @@ public class SSLContextParametersTest extends
AbstractJsseParametersTest {
return enabled.toArray(new String[0]);
}
+ @Test
+ public void testResolvePqcNamedGroupsPrefersTheHybridGroups() {
+ String[] available = { "x25519", "secp256r1", "X25519MLKEM768",
"secp384r1", "ffdhe2048" };
+
+ List<String> ordered =
SSLContextParameters.resolvePqcNamedGroups(available);
+
+ assertEquals(List.of("X25519MLKEM768", "x25519", "secp256r1",
"secp384r1", "ffdhe2048"), ordered);
+ }
+
+ @Test
+ public void testResolvePqcNamedGroupsWithoutTheX25519Hybrid() {
+ // a JVM or a crypto policy restricted to the NIST curves still gets a
post-quantum preference
+ String[] available = { "secp256r1", "SecP384r1MLKEM1024", "secp384r1",
"SecP256r1MLKEM768" };
+
+ List<String> ordered =
SSLContextParameters.resolvePqcNamedGroups(available);
+
+ assertEquals(List.of("SecP256r1MLKEM768", "SecP384r1MLKEM1024",
"secp256r1", "secp384r1"), ordered);
+ }
+
+ @Test
+ public void
testResolvePqcNamedGroupsLeavesAnOrderThatAlreadyLeadsWithAHybridAlone() {
+ // an operator who put a hybrid first through jdk.tls.namedGroups,
here a CNSA 2.0 style setup, keeps that
+ // order: the P-384 hybrid must not be demoted behind the P-256 one
+ assertNull(SSLContextParameters.resolvePqcNamedGroups(
+ new String[] { "SecP384r1MLKEM1024", "SecP256r1MLKEM768",
"secp384r1" }));
+ // and the default order of a JDK that leads with the X25519 hybrid is
left as it is
+ assertNull(SSLContextParameters.resolvePqcNamedGroups(
+ new String[] { "X25519MLKEM768", "x25519", "secp256r1",
"secp384r1" }));
+ }
+
+ @Test
+ public void testHasPqcNamedGroup() {
+ // tells a JVM that leads with a hybrid, and is left alone, from one
that offers none, so the log is right
+ assertTrue(SSLContextParameters.hasPqcNamedGroup(new String[] {
"X25519MLKEM768", "x25519" }));
+ assertTrue(SSLContextParameters.hasPqcNamedGroup(new String[] {
"secp256r1", "secp256r1mlkem768" }));
+ assertFalse(SSLContextParameters.hasPqcNamedGroup(new String[] {
"x25519", "secp256r1" }));
+ assertFalse(SSLContextParameters.hasPqcNamedGroup(new String[0]));
+ assertFalse(SSLContextParameters.hasPqcNamedGroup(null));
+ }
+
+ @Test
+ public void testResolvePqcNamedGroupsKeepsTheSpellingOfTheProvider() {
+ String[] available = { "x25519", "x25519mlkem768" };
+
+ List<String> ordered =
SSLContextParameters.resolvePqcNamedGroups(available);
+
+ assertEquals(List.of("x25519mlkem768", "x25519"), ordered);
+ }
+
+ @Test
+ public void testResolvePqcNamedGroupsWithoutAnyHybridGroup() {
+ // nothing post-quantum on offer: the JVM ordering is left alone
rather than rewritten
+ assertNull(SSLContextParameters.resolvePqcNamedGroups(new String[] {
"x25519", "secp256r1", "secp384r1" }));
+ assertNull(SSLContextParameters.resolvePqcNamedGroups(new String[0]));
+ assertNull(SSLContextParameters.resolvePqcNamedGroups(null));
+ }
+
+ @Test
+ public void testPqcAutoConfigurationIsThreadSafe() throws Exception {
+ SSLContext controlContext = SSLContext.getInstance("TLSv1.3");
+ controlContext.init(null, null, null);
+ String[] controlNamedGroups =
controlContext.createSSLEngine().getSSLParameters().getNamedGroups();
+ String expectedFirstGroup = controlNamedGroups == null ? null :
expectedFirstPqcGroup(controlNamedGroups);
+
+ // one instance shared by many components, as a global camel.ssl
configuration is
+ final SSLContextParameters scp = new SSLContextParameters();
+ final int threads = 8;
+ final CountDownLatch start = new CountDownLatch(1);
+ final List<Callable<SSLContext>> tasks = new LinkedList<>();
+ for (int i = 0; i < threads; i++) {
+ tasks.add(() -> {
+ start.await();
+ return scp.createSSLContext(null);
+ });
+ }
+
+ ExecutorService executor = Executors.newFixedThreadPool(threads);
+ try {
+ List<Future<SSLContext>> futures = new LinkedList<>();
+ for (Callable<SSLContext> task : tasks) {
+ futures.add(executor.submit(task));
+ }
+ start.countDown();
+
+ for (Future<SSLContext> future : futures) {
+ SSLEngine engine = future.get(30,
TimeUnit.SECONDS).createSSLEngine();
+ if (expectedFirstGroup != null) {
+ assertEquals(expectedFirstGroup,
engine.getSSLParameters().getNamedGroups()[0],
+ "every concurrently created context must prefer
the post-quantum group");
+ } else {
+ assertArrayEquals(controlNamedGroups,
engine.getSSLParameters().getNamedGroups());
+ }
+ }
+ } finally {
+ executor.shutdownNow();
+ }
+
+ // the auto-configured ordering must not be left behind on the shared
instance
+ assertNull(scp.getNamedGroups());
+ }
+
protected void assertStartsWith(String[] values, String prefix) {
assertNotNull(values, "The values should not be null");
for (String value : values) {
@@ -1313,4 +1427,27 @@ public class SSLContextParametersTest extends
AbstractJsseParametersTest {
}
}
+ /**
+ * The hybrid ML-KEM group the auto-configuration is expected to put
first, or {@code null} when this JVM offers
+ * none of them and the ordering must be left untouched. A JVM that
already leads with a hybrid keeps its order, so
+ * that hybrid is the expected first group even when the
auto-configuration would have preferred another.
+ */
+ private static String expectedFirstPqcGroup(String[] availableGroups) {
+ if (availableGroups.length > 0) {
+ for (String pqc : PQC_GROUPS) {
+ if (pqc.equalsIgnoreCase(availableGroups[0])) {
+ return availableGroups[0];
+ }
+ }
+ }
+ for (String pqc : PQC_GROUPS) {
+ for (String available : availableGroups) {
+ if (pqc.equalsIgnoreCase(available)) {
+ return available;
+ }
+ }
+ }
+ return null;
+ }
+
}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index c2068729eeb0..03577d5cf348 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -421,6 +421,26 @@ The protected
`RedeliveryErrorHandler.defensiveCopyExchangeIfNeeded(Exchange)` m
A custom error handler that overrides it to change what is redelivered must
change the message before the redelivery
instead, for example with `onRedelivery`.
+=== camel-core - the post-quantum TLS named groups: the NIST hybrids count,
and an order that already leads with a hybrid is kept
+
+`SSLContextParameters` prefers a hybrid post-quantum key exchange on a JVM
that offers one, when no named groups and no
+named groups filter are configured (see the
+xref:camel-configuration-utilities.adoc#_auto_configuration_zero_config_pqc[auto-configuration
of the JSSE utility]).
+Three things changed:
+
+* Only `X25519MLKEM768` was recognized. `SecP256r1MLKEM768` and
`SecP384r1MLKEM1024` now count too, so a JVM or a crypto
+ policy restricted to the NIST curves, which enables only those two, gets the
post-quantum preference it did not get
+ before.
+* An order that already leads with a hybrid group is kept as it is. It was
rewritten to `X25519MLKEM768, x25519,
+ secp256r1, secp384r1, ...` before. An operator who put another hybrid first
through `jdk.tls.namedGroups`, for example
+ a CNSA 2.0 setup with `SecP384r1MLKEM1024` first, keeps that order, and the
default order of JDK 27 (JEP 527), which
+ leads with `X25519MLKEM768`, is no longer rewritten.
+* The `INFO` line `Auto-configured PQC named groups: [...]` is therefore
logged only when Camel reorders the groups. On a
+ JVM whose order already leads with a hybrid, and on one that enables no
hybrid at all, a `DEBUG` line of
+ `org.apache.camel.support.jsse.SSLContextParameters` says why nothing was
done. Anything that relied on the `INFO` line
+ as the confirmation that a post-quantum key exchange is preferred should
read the named groups of a created
+ `SSLEngine` or `SSLSocket` instead: `getSSLParameters().getNamedGroups()`.
+
=== camel-core - the documented default of skipBindingOnErrorCode is true
The `skipBindingOnErrorCode` option of the Rest DSL (on `restConfiguration`,
`rest`, its verbs and `restBinding`) is now
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-configuration-utilities.adoc
b/docs/user-manual/modules/ROOT/pages/camel-configuration-utilities.adoc
index 51284fe306cd..79f5d1150282 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-configuration-utilities.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-configuration-utilities.adoc
@@ -448,11 +448,12 @@ becomes available.
=== JDK Requirements
PQC TLS support requires a JDK that implements post-quantum key exchange
algorithms.
-JDK 25+ is expected to include support for `X25519MLKEM768`, a hybrid key
exchange
-that combines the classical X25519 algorithm with the ML-KEM-768 post-quantum
algorithm
-(NIST FIPS 203).
+JDK 27 includes them (JEP 527): `X25519MLKEM768`, a hybrid key exchange that
combines the classical
+X25519 algorithm with the ML-KEM-768 post-quantum algorithm (NIST FIPS 203)
and is enabled by default,
+and the NIST curve hybrids `SecP256r1MLKEM768` and `SecP384r1MLKEM1024`, which
are enabled through the
+`jdk.tls.namedGroups` system property. Some vendor builds of earlier JDKs may
backport `X25519MLKEM768`.
-To verify whether your JDK supports PQC named groups, you can inspect the
available groups:
+To verify whether your JDK enables PQC named groups, you can inspect the
enabled groups:
[source,java]
----
@@ -460,34 +461,47 @@ SSLContext ctx = SSLContext.getDefault();
SSLEngine engine = ctx.createSSLEngine();
String[] groups = engine.getSSLParameters().getNamedGroups();
System.out.println(Arrays.toString(groups));
-// Look for X25519MLKEM768 in the output
+// Look for X25519MLKEM768, SecP256r1MLKEM768 or SecP384r1MLKEM1024 in the
output
----
=== Auto-Configuration (Zero-Config PQC)
-When running on a JDK that supports `X25519MLKEM768`, Camel *automatically*
configures
-PQC-preferred named groups on all `SSLContextParameters` instances — no
configuration is required.
+When running on a JDK that enables a hybrid ML-KEM group (`X25519MLKEM768`,
`SecP256r1MLKEM768` or
+`SecP384r1MLKEM1024`), Camel *automatically* prefers it on all
`SSLContextParameters` instances, without
+any configuration.
The auto-configuration activates only when:
* The user has *not* explicitly set `namedGroups` or `namedGroupsFilter`
-* The JVM's TLS provider reports `X25519MLKEM768` as an available named group
+* The JVM's TLS provider reports at least one of the hybrid groups among its
enabled named groups
+* The enabled named groups do not already lead with a hybrid group
-When activated, Camel sets the following named group ordering:
+When activated, Camel sets the following named group ordering, keeping the
groups the JVM offers and the
+spelling it uses:
[source,text]
----
-X25519MLKEM768, x25519, secp256r1, secp384r1, [remaining JVM defaults...]
+X25519MLKEM768, SecP256r1MLKEM768, SecP384r1MLKEM1024, x25519, secp256r1,
secp384r1, [remaining JVM defaults...]
----
This ensures post-quantum key exchange is preferred while maintaining backward
compatibility
-with servers that do not yet support PQC. A log message confirms activation:
+with servers that do not yet support PQC. A log message confirms that the
groups were reordered. It lists the groups
+that JVM enables, hybrids first, so its content varies by JVM:
[source,text]
----
INFO o.a.c.s.j.SSLContextParameters - Auto-configured PQC named groups:
[X25519MLKEM768, x25519, secp256r1, secp384r1, ...]
----
+A JVM restricted to the NIST curves logs `[SecP256r1MLKEM768,
SecP384r1MLKEM1024, secp256r1, secp384r1]`, for example.
+
+An order that already leads with a hybrid group is kept as it is, and that
message is not logged: this is the
+case for the default order of JDK 27, which starts with `X25519MLKEM768`, and
for an order chosen through the
+`jdk.tls.namedGroups` system property, for example a CNSA 2.0 setup that puts
`SecP384r1MLKEM1024` first.
+A `DEBUG` message of `org.apache.camel.support.jsse.SSLContextParameters` says
so, as it does on a JDK that
+enables no hybrid group at all. To verify which key exchange is preferred,
read the named groups of a created
+`SSLEngine` or `SSLSocket` as shown above.
+
=== Explicit Configuration via application.properties
You can explicitly control PQC TLS settings in `application.properties`:
@@ -557,7 +571,7 @@ Or use a named groups filter:
[source,properties]
----
-camel.ssl.namedGroupsExclude = X25519MLKEM768
+camel.ssl.namedGroupsExclude =
X25519MLKEM768,SecP256r1MLKEM768,SecP384r1MLKEM1024
----
=== TLS-Layer PQC vs. Application-Layer PQC
@@ -581,15 +595,15 @@ Camel provides PQC protection at two distinct layers:
| Data at rest and message payloads
| *Algorithms*
-| X25519MLKEM768 (hybrid KEM)
+| X25519MLKEM768, SecP256r1MLKEM768, SecP384r1MLKEM1024 (hybrid KEM)
| ML-KEM, ML-DSA, SLH-DSA, LMS, XMSS, Falcon, and more
| *JDK requirement*
-| JDK 25+ (for built-in PQC named groups)
+| JDK 27+ (for built-in PQC named groups)
| JDK 21+ with Bouncy Castle provider
| *User action*
-| Zero-config on JDK 25+ (auto-configured)
+| Zero-config on JDK 27+ (auto-configured)
| Requires explicit component configuration
|===
@@ -605,8 +619,8 @@ and xref:components:dataformats:pqc-dataformat.adoc[PQC
Data Format].
For users currently running classical TLS, migration to PQC TLS is
straightforward:
-1. **Upgrade your JDK** to JDK 25+ (or a JDK that supports `X25519MLKEM768`)
-2. **Upgrade Camel** to 4.19 or later
+1. **Upgrade your JDK** to JDK 27+ (or a JDK build that backports
`X25519MLKEM768`)
+2. **Upgrade Camel** to 4.19 or later (4.24 for the NIST hybrids)
3. **No configuration changes needed** — PQC named groups are auto-configured
4. **Verify** that your TLS peers (servers, brokers, load balancers) support
PQC key exchange or
at least accept the classical fallback groups (`x25519`, `secp256r1`)