diff --git a/core/src/main/java/org/apache/cxf/configuration/jsse/TLSClientParametersConfig.java b/core/src/main/java/org/apache/cxf/configuration/jsse/TLSClientParametersConfig.java index 7eb060074f9..d0fdcd9cde0 100644 --- a/core/src/main/java/org/apache/cxf/configuration/jsse/TLSClientParametersConfig.java +++ b/core/src/main/java/org/apache/cxf/configuration/jsse/TLSClientParametersConfig.java @@ -131,6 +131,9 @@ public static TLSClientParameters createTLSClientParametersFromType(TLSClientPar if (params.isSetCertAlias()) { ret.setCertAlias(params.getCertAlias()); } + if (params.isSetNamedGroups()) { + ret.setNamedGroups(params.getNamedGroups().getNamedGroup()); + } if (iparams != null && iparams.isSetKeyManagersRef() && !usingDefaults) { ret.setKeyManagers(iparams.getKeyManagersRef()); } diff --git a/core/src/main/java/org/apache/cxf/configuration/jsse/TLSParameterBase.java b/core/src/main/java/org/apache/cxf/configuration/jsse/TLSParameterBase.java index ac17533ad2d..920b85cc895 100644 --- a/core/src/main/java/org/apache/cxf/configuration/jsse/TLSParameterBase.java +++ b/core/src/main/java/org/apache/cxf/configuration/jsse/TLSParameterBase.java @@ -50,6 +50,7 @@ public class TLSParameterBase { protected SecureRandom secureRandom; protected String protocol; protected String certAlias; + protected List namedGroups = new ArrayList<>(); /** * Set the JSSE provider. If not set, * it uses system default. @@ -187,4 +188,24 @@ public final void setCertAlias(String ctAlias) { public String getCertAlias() { return certAlias; } + + /** + * Sets the ordered list of TLS named groups (key-exchange groups) to offer + * during the handshake, e.g. {@code "X25519MLKEM768"}, {@code "x25519"}. + * Applied via {@code SSLParameters.setNamedGroups()} which requires JDK 20+; + * silently ignored on older JDKs. + */ + public final void setNamedGroups(List groups) { + namedGroups = groups; + } + + /** + * Returns the configured TLS named groups list. + */ + public List getNamedGroups() { + if (namedGroups == null) { + namedGroups = new ArrayList<>(); + } + return namedGroups; + } } diff --git a/core/src/main/java/org/apache/cxf/configuration/jsse/TLSServerParametersConfig.java b/core/src/main/java/org/apache/cxf/configuration/jsse/TLSServerParametersConfig.java index e63623b2caa..625fbbb42f2 100644 --- a/core/src/main/java/org/apache/cxf/configuration/jsse/TLSServerParametersConfig.java +++ b/core/src/main/java/org/apache/cxf/configuration/jsse/TLSServerParametersConfig.java @@ -95,6 +95,9 @@ public TLSServerParametersConfig(TLSServerParametersType params) if (params.isSetSniHostCheck()) { this.setSniHostCheck(params.isSniHostCheck()); } + if (params.isSetNamedGroups()) { + this.setNamedGroups(params.getNamedGroups().getNamedGroup()); + } if (iparams != null && iparams.isSetKeyManagersRef()) { this.setKeyManagers(iparams.getKeyManagersRef()); } diff --git a/core/src/main/resources/schemas/configuration/security.xsd b/core/src/main/resources/schemas/configuration/security.xsd index fbc28a30e78..0055e78f5a2 100644 --- a/core/src/main/resources/schemas/configuration/security.xsd +++ b/core/src/main/resources/schemas/configuration/security.xsd @@ -383,6 +383,20 @@ + + + + + This structure holds an ordered list of TLS named groups (key-exchange groups) to offer + during the handshake. Examples: "X25519MLKEM768" (PQC hybrid, JDK 27+ / BC JSSE 1.81+), + "x25519", "secp256r1". When set, the list is applied via SSLParameters.setNamedGroups() + (requires JDK 20+; silently ignored on older JDKs). + + + + + + @@ -512,6 +526,15 @@ + + + + This element contains the ordered list of TLS named groups to offer during + the handshake (e.g. "X25519MLKEM768", "x25519", "secp256r1"). + Requires JDK 20+; silently ignored on older JDKs. + + + @@ -651,6 +674,15 @@ + + + + This element contains the ordered list of TLS named groups to offer during + the handshake (e.g. "X25519MLKEM768", "x25519", "secp256r1"). + Requires JDK 20+; silently ignored on older JDKs. + + + diff --git a/parent/pom.xml b/parent/pom.xml index 90a2ad91ae8..59fb62a111a 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -97,7 +97,7 @@ 4.0.0-M1 [3.0, 4.0) 3.1.0 - 1.81 + 1.84 6.3.1 4.1.0 4.8.25 diff --git a/rt/transports/http-jetty/src/main/java/org/apache/cxf/transport/http_jetty/BCJsseServerALPNProcessor.java b/rt/transports/http-jetty/src/main/java/org/apache/cxf/transport/http_jetty/BCJsseServerALPNProcessor.java new file mode 100644 index 00000000000..3fcf1c072d4 --- /dev/null +++ b/rt/transports/http-jetty/src/main/java/org/apache/cxf/transport/http_jetty/BCJsseServerALPNProcessor.java @@ -0,0 +1,79 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.cxf.transport.http_jetty; + +import javax.net.ssl.SSLEngine; + +import org.eclipse.jetty.alpn.server.ALPNServerConnection; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.ssl.ALPNProcessor; +import org.eclipse.jetty.io.ssl.SslConnection; +import org.eclipse.jetty.io.ssl.SslHandshakeListener; + +/** + * Jetty {@link ALPNProcessor.Server} for BouncyCastle JSSE ({@code bctls-jdk18on}). + * + *

Jetty's built-in {@code JDK9ServerALPNProcessor} only matches engines whose + * module is {@code java.base}, so it rejects BC JSSE's {@code ProvSSLEngine} and + * throws {@code IllegalStateException: No suitable ALPNProcessor}. This processor + * bridges the gap: it claims BC JSSE engines and wires up the same + * {@link SSLEngine#setHandshakeApplicationProtocolSelector} callback that the + * built-in processor uses, delegating to {@link ALPNServerConnection#select}. + * + *

A {@link SslHandshakeListener} is also registered so that if the client does not + * advertise any ALPN protocols, {@link ALPNServerConnection#unsupported()} is called + * on handshake success to fall back to the server's default protocol (HTTP/1.1). + * + *

Registered via {@code META-INF/services/org.eclipse.jetty.io.ssl.ALPNProcessor$Server}. + */ +public class BCJsseServerALPNProcessor implements ALPNProcessor.Server { + + @Override + public boolean appliesTo(SSLEngine sslEngine) { + return sslEngine.getClass().getName().startsWith("org.bouncycastle."); + } + + @Override + public void configure(SSLEngine sslEngine, Connection connection) { + ALPNServerConnection alpnConn = (ALPNServerConnection) connection; + + // Called when the client includes an ALPN extension in its ClientHello. + sslEngine.setHandshakeApplicationProtocolSelector((engine, protocols) -> { + try { + alpnConn.select(protocols); + return alpnConn.getProtocol(); + } catch (Throwable t) { + return null; + } + }); + + // When the client sends no ALPN extension the selector above is never invoked, + // leaving getProtocol() null. The SslHandshakeListener fires on handshake + // completion and calls unsupported() to fall back to the default protocol. + SslConnection.SslEndPoint sslEndPoint = (SslConnection.SslEndPoint) alpnConn.getEndPoint(); + sslEndPoint.getSslConnection().addHandshakeListener(new SslHandshakeListener() { + @Override + public void handshakeSucceeded(Event event) { + if (alpnConn.getProtocol() == null) { + alpnConn.unsupported(); + } + } + }); + } +} diff --git a/rt/transports/http-jetty/src/main/java/org/apache/cxf/transport/http_jetty/JettyHTTPServerEngine.java b/rt/transports/http-jetty/src/main/java/org/apache/cxf/transport/http_jetty/JettyHTTPServerEngine.java index 9a32cd63bb6..f17a42dc9ef 100644 --- a/rt/transports/http-jetty/src/main/java/org/apache/cxf/transport/http_jetty/JettyHTTPServerEngine.java +++ b/rt/transports/http-jetty/src/main/java/org/apache/cxf/transport/http_jetty/JettyHTTPServerEngine.java @@ -40,6 +40,8 @@ import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; import jakarta.annotation.PostConstruct; import jakarta.servlet.RequestDispatcher; @@ -731,6 +733,19 @@ protected void doStart() throws Exception { public void checkKeyStore() { //we'll handle this later } + @Override + public SSLParameters customize(SSLParameters sslParams) { + sslParams = super.customize(sslParams); + org.apache.cxf.transport.https.SSLUtils.applyNamedGroups( + sslParams, tlsServerParameters.getNamedGroups()); + return sslParams; + } + @Override + public void customize(SSLEngine sslEngine) { + super.customize(sslEngine); + org.apache.cxf.transport.https.SSLUtils.applyNamedGroupsBC( + sslEngine, tlsServerParameters.getNamedGroups()); + } }; decorateCXFJettySslSocketConnector(sslcf); } @@ -857,7 +872,21 @@ public Connection upgradeConnection(Connector c, EndPoint endPoint, protected SSLContext createSSLContext(SslContextFactory scf) throws Exception { // The full SSL context is provided by SSLContextServerParameters if (tlsServerParameters instanceof SSLContextServerParameters sslContextServerParameters) { - return sslContextServerParameters.getSslContext(); + SSLContext context = sslContextServerParameters.getSslContext(); + // Without setting included cipher suites here, Jetty's default + // SslContextFactory filter drops suite names it does not + // recognise (e.g. BouncyCastle JSSE names), causing + // handshake_failure even if the SSLContext supports them. + String[] supported = + SSLUtils.getServerSupportedCipherSuites(context); + String[] included = SSLUtils.getCiphersuitesToInclude( + tlsServerParameters.getCipherSuites(), + tlsServerParameters.getCipherSuitesFilter(), + context.getServerSocketFactory().getDefaultCipherSuites(), + supported, + LOG); + scf.setIncludeCipherSuites(included); + return context; } String proto = tlsServerParameters.getSecureSocketProtocol() == null diff --git a/rt/transports/http-jetty/src/main/resources/META-INF/services/org.eclipse.jetty.io.ssl.ALPNProcessor$Server b/rt/transports/http-jetty/src/main/resources/META-INF/services/org.eclipse.jetty.io.ssl.ALPNProcessor$Server new file mode 100644 index 00000000000..4bef6cb1d47 --- /dev/null +++ b/rt/transports/http-jetty/src/main/resources/META-INF/services/org.eclipse.jetty.io.ssl.ALPNProcessor$Server @@ -0,0 +1 @@ +org.apache.cxf.transport.http_jetty.BCJsseServerALPNProcessor diff --git a/rt/transports/http/src/main/java/org/apache/cxf/transport/http/HttpClientHTTPConduit.java b/rt/transports/http/src/main/java/org/apache/cxf/transport/http/HttpClientHTTPConduit.java index 5efbef4c1e6..a4840ec06ef 100644 --- a/rt/transports/http/src/main/java/org/apache/cxf/transport/http/HttpClientHTTPConduit.java +++ b/rt/transports/http/src/main/java/org/apache/cxf/transport/http/HttpClientHTTPConduit.java @@ -433,10 +433,14 @@ protected void setupConnection(Message message, Address address, HTTPClientPolic if (clientParameters.getSecureSocketProtocol() != null) { String protocol = clientParameters.getSecureSocketProtocol(); SSLParameters params = new SSLParameters(cipherSuites, new String[] {protocol}); + org.apache.cxf.transport.https.SSLUtils.applyNamedGroups( + params, clientParameters.getNamedGroups()); cb.sslParameters(params); } else { final SSLParameters params = new SSLParameters(cipherSuites, TLSClientParameters.getPreferredClientProtocols()); + org.apache.cxf.transport.https.SSLUtils.applyNamedGroups( + params, clientParameters.getNamedGroups()); cb.sslParameters(params); } } diff --git a/rt/transports/http/src/main/java/org/apache/cxf/transport/https/SSLUtils.java b/rt/transports/http/src/main/java/org/apache/cxf/transport/https/SSLUtils.java index 9ef974cc11d..970f9382710 100644 --- a/rt/transports/http/src/main/java/org/apache/cxf/transport/https/SSLUtils.java +++ b/rt/transports/http/src/main/java/org/apache/cxf/transport/https/SSLUtils.java @@ -18,6 +18,7 @@ */ package org.apache.cxf.transport.https; +import java.lang.reflect.Method; import java.net.Socket; import java.nio.ByteBuffer; import java.security.GeneralSecurityException; @@ -531,4 +532,43 @@ public javax.security.cert.X509Certificate[] getPeerCertificateChain() throws SS } }; + /** + * Applies the given named groups to {@code SSLParameters} via reflection. + * {@code SSLParameters.setNamedGroups()} requires JDK 20+; this method + * silently skips the call on older JDKs. + */ + public static void applyNamedGroups(SSLParameters params, List namedGroups) { + if (namedGroups == null || namedGroups.isEmpty()) { + return; + } + try { + Method m = SSLParameters.class.getMethod("setNamedGroups", String[].class); + m.invoke(params, (Object) namedGroups.toArray(new String[0])); + } catch (NoSuchMethodException e) { + LOG.fine("SSLParameters.setNamedGroups() not available on this JDK; namedGroups ignored"); + } catch (ReflectiveOperationException e) { + LOG.warning("Failed to apply TLS namedGroups: " + e.getMessage()); + } + } + + // Reflection-based BC JSSE path: avoids a compile-time dependency on bctls-jdk18on. + public static void applyNamedGroupsBC(SSLEngine sslEngine, List namedGroups) { + if (namedGroups == null || namedGroups.isEmpty()) { + return; + } + try { + Class bcEngineClass = Class.forName("org.bouncycastle.jsse.BCSSLEngine"); + if (!bcEngineClass.isInstance(sslEngine)) { + return; + } + Class bcParamsClass = Class.forName("org.bouncycastle.jsse.BCSSLParameters"); + Object bcParams = bcEngineClass.getMethod("getParameters").invoke(sslEngine); + bcParamsClass.getMethod("setNamedGroups", String[].class) + .invoke(bcParams, (Object) namedGroups.toArray(new String[0])); + bcEngineClass.getMethod("setParameters", bcParamsClass).invoke(sslEngine, bcParams); + } catch (ReflectiveOperationException e) { + LOG.fine("Could not apply named groups via BC JSSE engine: " + e.getMessage()); + } + } + } diff --git a/systests/transports/pom.xml b/systests/transports/pom.xml index 2326fb1db1f..cafe5d38abf 100644 --- a/systests/transports/pom.xml +++ b/systests/transports/pom.xml @@ -111,6 +111,23 @@ + + + pqc-tls-test + + test + + + + **/pqc/PQCTLSTest.java + + + X25519MLKEM768,x25519,secp256r1,secp384r1,secp521r1 + + + @@ -245,6 +262,18 @@ test keys + + org.bouncycastle + bcprov-jdk18on + ${cxf.bcprov.version} + test + + + org.bouncycastle + bctls-jdk18on + ${cxf.bcprov.version} + test + org.springframework spring-web diff --git a/systests/transports/src/test/java/org/apache/cxf/systest/https/pqc/BCJssePQCTest.java b/systests/transports/src/test/java/org/apache/cxf/systest/https/pqc/BCJssePQCTest.java new file mode 100644 index 00000000000..6be5fe5f83e --- /dev/null +++ b/systests/transports/src/test/java/org/apache/cxf/systest/https/pqc/BCJssePQCTest.java @@ -0,0 +1,166 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.cxf.systest.https.pqc; + +import java.io.InputStream; +import java.net.URL; +import java.security.KeyStore; +import java.security.Security; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManagerFactory; +import javax.xml.namespace.QName; + +import org.apache.cxf.Bus; +import org.apache.cxf.BusFactory; +import org.apache.cxf.bus.spring.SpringBusFactory; +import org.apache.cxf.testutil.common.AbstractBusClientServerTestBase; +import org.apache.cxf.testutil.common.AbstractBusTestServerBase; +import org.apache.hello_world.Greeter; +import org.apache.hello_world.services.SOAPService; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jsse.BCSSLParameters; +import org.bouncycastle.jsse.BCSSLSocket; +import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; + +import org.junit.AfterClass; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Verifies BouncyCastle JSSE ({@code bctls-jdk18on} 1.84+) TLS support on JDK 17-26 + * using standard CXF XML configuration. + * + *

The server is configured via {@code bcjsse-server.xml} using + * {@code jsseProvider="BCJSSE"} on {@code httpj:tlsServerParameters}. + * HTTP/2 is supported via {@link BCJsseServerALPNProcessor}, which bridges + * BC JSSE's {@code ProvSSLEngine} into Jetty's ALPN negotiation framework. + * The client is configured via {@code bcjsse-client.xml}. + * + *

On JDK 27+, {@code PQCTLSTest} provides coverage via SunJSSE (JEP 527). + */ +public class BCJssePQCTest extends AbstractBusClientServerTestBase { + + static final String PORT = allocatePort(BCJsseJettyServer.class); + + // ------------------------------------------------------------------ setup + + @BeforeClass + public static void setup() throws Exception { + // JDK 27+ is covered by PQCTLSTest via SunJSSE (JEP 527). + Assume.assumeTrue( + "BCJssePQCTest targets JDK 17-26; use PQCTLSTest on JDK 27+", + Runtime.version().feature() < 27); + // BC JSSE builds its context-level named-group map when SSLContext is first + // created; X25519MLKEM768 must be present there before the server starts. + System.setProperty("jdk.tls.namedGroups", "X25519MLKEM768"); + // Append so Sun's JKS (which supports private-key entries) stays first. + // BC's JKS is read-only and cannot load private-key entries. + Security.addProvider(new BouncyCastleProvider()); + Security.addProvider(new BouncyCastleJsseProvider()); + assertTrue("Server failed to launch", launchServer(BCJsseJettyServer.class, true)); + } + + @AfterClass + public static void cleanup() throws Exception { + stopAllServers(); + Security.removeProvider(BouncyCastleJsseProvider.PROVIDER_NAME); + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); + System.clearProperty("jdk.tls.namedGroups"); + } + + // ------------------------------------------------------------------ server + + /** + * CXF/Jetty server configured via {@code bcjsse-server.xml}: uses + * {@code jsseProvider="BCJSSE"} on {@code httpj:tlsServerParameters} so that + * CXF builds the SSLContext from BC JSSE rather than SunJSSE. HTTP/2 is + * enabled via {@link BCJsseServerALPNProcessor}. + */ + public static class BCJsseJettyServer extends AbstractBusTestServerBase { + @Override + protected void run() { + URL busFile = BCJsseJettyServer.class.getResource("bcjsse-server.xml"); + Bus busLocal = new SpringBusFactory().createBus(busFile); + BusFactory.setDefaultBus(busLocal); + setBus(busLocal); + } + } + + // ------------------------------------------------------------------ tests + + @Test + public void testJettyMlKemHandshake() throws Exception { + SpringBusFactory bf = new SpringBusFactory(); + URL busFile = BCJssePQCTest.class.getResource("bcjsse-client.xml"); + Bus bus = bf.createBus(busFile.toString()); + BusFactory.setDefaultBus(bus); + BusFactory.setThreadDefaultBus(bus); + + QName serviceName = + new QName("http://apache.org/hello_world/services", "SOAPService"); + URL wsdl = SOAPService.WSDL_LOCATION; + SOAPService service = new SOAPService(wsdl, serviceName); + assertNotNull("Service is null", service); + + Greeter greeter = service.getHttpsPort(); + assertNotNull("Port is null", greeter); + + updateAddressPort(greeter, PORT); + + assertEquals("Hello Kitty", greeter.greetMe("Kitty")); + + assertX25519MlKem768Negotiated(); + + ((java.io.Closeable) greeter).close(); + bus.shutdown(true); + } + + // Indirect proof: client restricted to X25519MLKEM768 only; server must pick from offered groups. + private void assertX25519MlKem768Negotiated() throws Exception { + KeyStore ts = KeyStore.getInstance("JKS"); + try (InputStream is = BCJssePQCTest.class.getClassLoader() + .getResourceAsStream("keys/Truststore.jks")) { + ts.load(is, "password".toCharArray()); + } + TrustManagerFactory tmf = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ts); + + SSLContext ctx = SSLContext.getInstance("TLS", "BCJSSE"); + ctx.init(null, tmf.getTrustManagers(), null); + + try (SSLSocket sock = (SSLSocket) ctx.getSocketFactory() + .createSocket("localhost", Integer.parseInt(PORT))) { + sock.setSoTimeout(5000); + BCSSLSocket bcSock = (BCSSLSocket) sock; + BCSSLParameters p = bcSock.getParameters(); + p.setNamedGroups(new String[] {"X25519MLKEM768"}); + bcSock.setParameters(p); + sock.startHandshake(); + assertEquals("TLSv1.3", sock.getSession().getProtocol()); + } + } +} diff --git a/systests/transports/src/test/java/org/apache/cxf/systest/https/pqc/PQCTLSTest.java b/systests/transports/src/test/java/org/apache/cxf/systest/https/pqc/PQCTLSTest.java new file mode 100644 index 00000000000..3f6b3e94cef --- /dev/null +++ b/systests/transports/src/test/java/org/apache/cxf/systest/https/pqc/PQCTLSTest.java @@ -0,0 +1,148 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.cxf.systest.https.pqc; + +import java.lang.reflect.Method; +import java.net.URL; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.xml.namespace.QName; + +import org.apache.cxf.Bus; +import org.apache.cxf.BusFactory; +import org.apache.cxf.bus.spring.SpringBusFactory; +import org.apache.cxf.testutil.common.AbstractBusClientServerTestBase; +import org.apache.cxf.testutil.common.AbstractBusTestServerBase; +import org.apache.hello_world.Greeter; +import org.apache.hello_world.services.SOAPService; + +import org.junit.AfterClass; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Verifies that CXF can complete a TLS 1.3 handshake using the post-quantum + * hybrid key-encapsulation mechanism X25519MLKEM768 (JEP 527, JDK 27+). + * + *

The test requires SunJSSE to list {@value #MLKEM_GROUP} as a supported TLS + * named group. This is the case only when running under the {@code pqc-tls-test} + * surefire execution, which overrides the parent-pom's classical-only + * {@code jdk.tls.namedGroups} setting and requires JDK 27+. The test is + * automatically skipped on older JDKs or when the named-group override is absent. + * + *

The server is configured via {@code pqc-server.xml} (standard + * {@code httpj:tlsServerParameters}) and the client via {@code pqc-client.xml}. + * Because {@code jdk.tls.namedGroups} lists {@code X25519MLKEM768} first in the + * {@code pqc-tls-test} surefire execution, SunJSSE negotiates it by preference + * without any per-connection named-group override. + */ +public class PQCTLSTest extends AbstractBusClientServerTestBase { + + static final String MLKEM_GROUP = "X25519MLKEM768"; + + static final String PORT = allocatePort(PQCServer.class); + + /** + * True when SunJSSE lists {@value #MLKEM_GROUP} as a supported TLS named + * group in the current JVM configuration — JDK 27+ (JEP 527) with the right + * {@code jdk.tls.namedGroups} setting. + */ + private static final boolean ML_KEM_AVAILABLE = isMlKemTlsSupported(); + + // ------------------------------------------------------------------ server + + public static class PQCServer extends AbstractBusTestServerBase { + @Override + protected void run() { + URL busFile = PQCServer.class.getResource("pqc-server.xml"); + Bus busLocal = new SpringBusFactory().createBus(busFile); + BusFactory.setDefaultBus(busLocal); + setBus(busLocal); + } + } + + // ------------------------------------------------------------------ setup + + @BeforeClass + public static void startServer() throws Exception { + Assume.assumeTrue( + "X25519MLKEM768 TLS not available — requires JDK 27+ with " + + "jdk.tls.namedGroups including " + MLKEM_GROUP, + ML_KEM_AVAILABLE); + assertTrue("Server failed to launch", + launchServer(PQCServer.class, true)); + } + + @AfterClass + public static void cleanup() throws Exception { + stopAllServers(); + } + + // ------------------------------------------------------------------ tests + + @Test + public void testMlKemHandshakeSucceeds() throws Exception { + SpringBusFactory bf = new SpringBusFactory(); + URL busFile = PQCTLSTest.class.getResource("pqc-client.xml"); + Bus bus = bf.createBus(busFile.toString()); + BusFactory.setDefaultBus(bus); + BusFactory.setThreadDefaultBus(bus); + + QName serviceName = + new QName("http://apache.org/hello_world/services", "SOAPService"); + URL wsdl = SOAPService.WSDL_LOCATION; + SOAPService service = new SOAPService(wsdl, serviceName); + assertNotNull("Service is null", service); + + Greeter port = service.getHttpsPort(); + assertNotNull("Port is null", port); + + updateAddressPort(port, PORT); + + assertEquals("Hello Kitty", port.greetMe("Kitty")); + + ((java.io.Closeable) port).close(); + bus.shutdown(true); + } + + // ------------------------------------------------------------------ helpers + + /** + * Returns true only when SunJSSE lists {@value #MLKEM_GROUP} in its supported + * TLS named groups — JDK 27+ (JEP 527) with the right + * {@code jdk.tls.namedGroups} configuration. + */ + private static boolean isMlKemTlsSupported() { + try { + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(null, null, null); + Method gm = SSLParameters.class.getMethod("getNamedGroups"); + String[] groups = (String[]) gm.invoke(ctx.getSupportedSSLParameters()); + return groups != null && java.util.Arrays.asList(groups).contains(MLKEM_GROUP); + } catch (Exception e) { + return false; + } + } +} diff --git a/systests/transports/src/test/resources/logging.properties b/systests/transports/src/test/resources/logging.properties index 4889366a3ab..d7cb5074122 100644 --- a/systests/transports/src/test/resources/logging.properties +++ b/systests/transports/src/test/resources/logging.properties @@ -22,7 +22,7 @@ # Default Logging Configuration File # # You can use a different file by specifying a filename -# with the java.util.logging.config.file system property. +# with the java.util.logging.config.file system property. # For example java -Djava.util.logging.config.file=myfile ############################################################ @@ -30,7 +30,7 @@ # Global properties ############################################################ -# "handlers" specifies a comma separated list of log Handler +# "handlers" specifies a comma separated list of log Handler # classes. These handlers will be installed during VM startup. # Note that these classes must be on the system classpath. # By default we only configure a ConsoleHandler, which will only diff --git a/systests/transports/src/test/resources/org/apache/cxf/systest/https/pqc/bcjsse-client.xml b/systests/transports/src/test/resources/org/apache/cxf/systest/https/pqc/bcjsse-client.xml new file mode 100644 index 00000000000..e452fe527bf --- /dev/null +++ b/systests/transports/src/test/resources/org/apache/cxf/systest/https/pqc/bcjsse-client.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + X25519MLKEM768 + + + + + diff --git a/systests/transports/src/test/resources/org/apache/cxf/systest/https/pqc/bcjsse-server.xml b/systests/transports/src/test/resources/org/apache/cxf/systest/https/pqc/bcjsse-server.xml new file mode 100644 index 00000000000..9a7a877579f --- /dev/null +++ b/systests/transports/src/test/resources/org/apache/cxf/systest/https/pqc/bcjsse-server.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + X25519MLKEM768 + + + + + + + + diff --git a/systests/transports/src/test/resources/org/apache/cxf/systest/https/pqc/pqc-client.xml b/systests/transports/src/test/resources/org/apache/cxf/systest/https/pqc/pqc-client.xml new file mode 100644 index 00000000000..d18ba890af7 --- /dev/null +++ b/systests/transports/src/test/resources/org/apache/cxf/systest/https/pqc/pqc-client.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + diff --git a/systests/transports/src/test/resources/org/apache/cxf/systest/https/pqc/pqc-server.xml b/systests/transports/src/test/resources/org/apache/cxf/systest/https/pqc/pqc-server.xml new file mode 100644 index 00000000000..b9c900b3f22 --- /dev/null +++ b/systests/transports/src/test/resources/org/apache/cxf/systest/https/pqc/pqc-server.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +