BCJssePQCTest.java
/**
* 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.
*
* <p>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}.
*
* <p>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());
}
}
}