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<>();
