This is an automated email from the ASF dual-hosted git repository.

acosentino pushed a commit to branch 23188
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 451dd6116043d2f47c23300b7527accdd678343a
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Mar 12 14:22:55 2026 +0100

    CAMEL-23188 - camel-core: Auto-configure PQC TLS named groups when JVM 
supports them
    
    When the JVM provides X25519MLKEM768 (typically JDK 25+), 
SSLContextParameters
    now auto-configures named groups with the post-quantum hybrid group as the 
first
    preference (X25519MLKEM768, x25519, secp256r1, secp384r1), protecting 
against
    harvest-now-decrypt-later attacks. User-configured namedGroups or 
namedGroupsFilter
    override the auto-detected defaults. On JDKs without PQC support, behavior 
is
    unchanged.
    
    Signed-off-by: Andrea Cosentino <[email protected]>
---
 .../camel/support/jsse/SSLContextParameters.java   |  68 +++++++++++++
 .../support/jsse/SSLContextParametersTest.java     | 110 +++++++++++++++++++--
 2 files changed, 169 insertions(+), 9 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 bfea52034dbd..e44b5f07011d 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
@@ -20,6 +20,7 @@ import java.io.IOException;
 import java.security.GeneralSecurityException;
 import java.security.SecureRandom;
 import java.security.Security;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
@@ -42,6 +43,18 @@ 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.
+     */
+    private static final String PQC_NAMED_GROUP = "X25519MLKEM768";
+
+    /**
+     * 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.
+     */
+    private static final List<String> PQC_PREFERRED_NAMED_GROUPS
+            = List.of("X25519MLKEM768", "x25519", "secp256r1", "secp384r1");
+
     private static final Logger LOG = 
LoggerFactory.getLogger(SSLContextParameters.class);
 
     /**
@@ -300,6 +313,12 @@ public class SSLContextParameters extends 
BaseSSLContextParameters {
 
         context.init(keyManagers, trustManagers, secureRandom);
 
+        // Auto-configure PQC named groups when available and no user 
configuration is present
+        boolean autoConfiguredPqc = false;
+        if (this.getNamedGroups() == null && this.getNamedGroupsFilter() == 
null) {
+            autoConfiguredPqc = applyPqcNamedGroupDefaults(context);
+        }
+
         this.configureSSLContext(context);
 
         // Decorate the context.
@@ -310,9 +329,58 @@ public class SSLContextParameters extends 
BaseSSLContextParameters {
                         this.getSSLSocketFactoryConfigurers(context),
                         this.getSSLServerSocketFactoryConfigurers(context)));
 
+        // Reset auto-configured PQC named groups so they don't persist on 
this instance
+        if (autoConfiguredPqc) {
+            this.setNamedGroups(null);
+        }
+
         return context;
     }
 
+    /**
+     * 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.
+     *
+     * @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 = 
probeEngine.getSSLParameters().getNamedGroups();
+
+        if (availableGroups == null) {
+            return false;
+        }
+
+        List<String> available = Arrays.asList(availableGroups);
+        if (!available.contains(PQC_NAMED_GROUP)) {
+            return false;
+        }
+
+        // Build preferred ordering: PQC preferred groups first (if 
available), then remaining
+        List<String> ordered = new ArrayList<>();
+        for (String preferred : PQC_PREFERRED_NAMED_GROUPS) {
+            if (available.contains(preferred)) {
+                ordered.add(preferred);
+            }
+        }
+        for (String group : availableGroups) {
+            if (!ordered.contains(group)) {
+                ordered.add(group);
+            }
+        }
+
+        NamedGroupsParameters ngp = new NamedGroupsParameters();
+        ngp.setNamedGroup(ordered);
+        this.setNamedGroups(ngp);
+
+        LOG.info("Auto-configured PQC named groups: {}", ordered);
+        return true;
+    }
+
     @Override
     protected void configureSSLContext(SSLContext context) throws 
GeneralSecurityException {
         LOG.trace("Configuring client and server side SSLContext parameters on 
SSLContext [{}]...", context);
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 903e69c5d6a5..558e4dcb853b 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
@@ -748,7 +748,8 @@ public class SSLContextParametersTest extends 
AbstractJsseParametersTest {
         SSLEngine controlEngine = controlContext.createSSLEngine();
         String[] controlNamedGroups = 
controlEngine.getSSLParameters().getNamedGroups();
 
-        // default - no named groups configured, should keep defaults
+        // default - no named groups configured
+        // When PQC groups are available (JDK 25+), auto-configuration 
reorders named groups
         SSLContextParameters scp = new SSLContextParameters();
         SSLContext context = scp.createSSLContext(null);
 
@@ -756,9 +757,17 @@ public class SSLContextParametersTest extends 
AbstractJsseParametersTest {
         SSLSocket socket = (SSLSocket) 
context.getSocketFactory().createSocket();
         SSLServerSocket serverSocket = (SSLServerSocket) 
context.getServerSocketFactory().createServerSocket();
 
-        assertArrayEquals(controlNamedGroups, 
engine.getSSLParameters().getNamedGroups());
-        assertArrayEquals(controlNamedGroups, 
socket.getSSLParameters().getNamedGroups());
-        assertArrayEquals(controlNamedGroups, 
serverSocket.getSSLParameters().getNamedGroups());
+        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]);
+        } else {
+            // No PQC available, should keep JVM defaults
+            assertArrayEquals(controlNamedGroups, 
engine.getSSLParameters().getNamedGroups());
+            assertArrayEquals(controlNamedGroups, 
socket.getSSLParameters().getNamedGroups());
+            assertArrayEquals(controlNamedGroups, 
serverSocket.getSSLParameters().getNamedGroups());
+        }
 
         // empty ngp - sets empty list
         NamedGroupsParameters ngp = new NamedGroupsParameters();
@@ -810,7 +819,7 @@ public class SSLContextParametersTest extends 
AbstractJsseParametersTest {
         SSLEngine controlEngine = controlContext.createSSLEngine();
         String[] controlNamedGroups = 
controlEngine.getSSLParameters().getNamedGroups();
 
-        // default - no filter, keeps defaults
+        // default - no filter, keeps defaults (or PQC-reordered defaults on 
JDK 25+)
         SSLContextParameters scp = new SSLContextParameters();
         SSLContext context = scp.createSSLContext(null);
 
@@ -818,9 +827,15 @@ public class SSLContextParametersTest extends 
AbstractJsseParametersTest {
         SSLSocket socket = (SSLSocket) 
context.getSocketFactory().createSocket();
         SSLServerSocket serverSocket = (SSLServerSocket) 
context.getServerSocketFactory().createServerSocket();
 
-        assertArrayEquals(controlNamedGroups, 
engine.getSSLParameters().getNamedGroups());
-        assertArrayEquals(controlNamedGroups, 
socket.getSSLParameters().getNamedGroups());
-        assertArrayEquals(controlNamedGroups, 
serverSocket.getSSLParameters().getNamedGroups());
+        if (Arrays.asList(controlNamedGroups).contains("X25519MLKEM768")) {
+            assertEquals("X25519MLKEM768", 
engine.getSSLParameters().getNamedGroups()[0]);
+            assertEquals("X25519MLKEM768", 
socket.getSSLParameters().getNamedGroups()[0]);
+            assertEquals("X25519MLKEM768", 
serverSocket.getSSLParameters().getNamedGroups()[0]);
+        } else {
+            assertArrayEquals(controlNamedGroups, 
engine.getSSLParameters().getNamedGroups());
+            assertArrayEquals(controlNamedGroups, 
socket.getSSLParameters().getNamedGroups());
+            assertArrayEquals(controlNamedGroups, 
serverSocket.getSSLParameters().getNamedGroups());
+        }
 
         // empty filter - no includes means no groups match
         FilterParameters filter = new FilterParameters();
@@ -1040,7 +1055,15 @@ public class SSLContextParametersTest extends 
AbstractJsseParametersTest {
 
         assertArrayEquals(controlEngine.getEnabledCipherSuites(), 
engine.getEnabledCipherSuites());
         assertArrayEquals(controlEngine.getEnabledProtocols(), 
engine.getEnabledProtocols());
-        assertArrayEquals(controlEngine.getSSLParameters().getNamedGroups(), 
engine.getSSLParameters().getNamedGroups());
+        // 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]);
+            assertEquals(controlGroups.length, engineGroups.length);
+        } else {
+            assertArrayEquals(controlGroups, engineGroups);
+        }
         assertEquals(1, 
engine.getSSLParameters().getSignatureSchemes().length);
         assertEquals("rsa_pss_rsae_sha256", 
engine.getSSLParameters().getSignatureSchemes()[0]);
     }
@@ -1130,6 +1153,75 @@ public class SSLContextParametersTest extends 
AbstractJsseParametersTest {
         assertEquals(defaultContext.getProvider().getName(), 
context.getProvider().getName());
     }
 
+    @Test
+    public void testPqcNamedGroupsAutoConfigured() throws Exception {
+        SSLContext controlContext = SSLContext.getInstance("TLSv1.3");
+        controlContext.init(null, null, null);
+        SSLEngine controlEngine = controlContext.createSSLEngine();
+        String[] controlNamedGroups = 
controlEngine.getSSLParameters().getNamedGroups();
+        boolean pqcAvailable = controlNamedGroups != null
+                && 
Arrays.asList(controlNamedGroups).contains("X25519MLKEM768");
+
+        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]);
+            // All original groups should still be present
+            List<String> resultList = Arrays.asList(resultGroups);
+            for (String group : controlNamedGroups) {
+                assertTrue(resultList.contains(group), "Expected group " + 
group + " to be present");
+            }
+        } else {
+            // No PQC, defaults should be unchanged
+            assertArrayEquals(controlNamedGroups, resultGroups);
+        }
+    }
+
+    @Test
+    public void testPqcNamedGroupsUserConfigOverrides() throws Exception {
+        // User-configured named groups should NOT be overridden by PQC 
auto-config
+        SSLContextParameters scp = new SSLContextParameters();
+        NamedGroupsParameters ngp = new NamedGroupsParameters();
+        ngp.setNamedGroup(Collections.singletonList("secp256r1"));
+        scp.setNamedGroups(ngp);
+
+        SSLContext context = scp.createSSLContext(null);
+        SSLEngine engine = context.createSSLEngine();
+
+        assertEquals(1, engine.getSSLParameters().getNamedGroups().length);
+        assertEquals("secp256r1", 
engine.getSSLParameters().getNamedGroups()[0]);
+    }
+
+    @Test
+    public void testPqcNamedGroupsFilterOverrides() throws Exception {
+        // User-configured named groups filter should NOT be overridden by PQC 
auto-config
+        SSLContextParameters scp = new SSLContextParameters();
+        FilterParameters filter = new FilterParameters();
+        filter.getInclude().add("secp.*");
+        scp.setNamedGroupsFilter(filter);
+
+        SSLContext context = scp.createSSLContext(null);
+        SSLEngine engine = context.createSSLEngine();
+
+        for (String group : engine.getSSLParameters().getNamedGroups()) {
+            assertTrue(group.startsWith("secp"), "Expected group starting with 
'secp' but got: " + group);
+        }
+    }
+
+    @Test
+    public void testPqcAutoConfigDoesNotPersist() throws Exception {
+        // PQC auto-config should not permanently mutate the 
SSLContextParameters instance
+        SSLContextParameters scp = new SSLContextParameters();
+        scp.createSSLContext(null);
+
+        // After createSSLContext, namedGroups should be null (reset)
+        assertNull(scp.getNamedGroups());
+    }
+
     protected String[] getDefaultCipherSuiteIncludes(String[] 
availableCipherSuites) {
         List<String> enabled = new LinkedList<>();
 

Reply via email to