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`)

Reply via email to