Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-24.04, ubuntu-22.04, macos-26, macos-15, windows-2025, windows-2022 ]
java-version: [ 17, 21, 25 ]
java-version: [ 17, 21, 25, 27-ea ]

runs-on: ${{ matrix.os }}

Expand Down Expand Up @@ -75,7 +75,7 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-24.04, ubuntu-22.04, macos-26, macos-15, windows-2025, windows-2022 ]
java-version: [ 17, 21, 25 ]
java-version: [ 17, 21, 25, 27-ea ]

runs-on: ${{ matrix.os }}

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-quick.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
strategy:
matrix:
os: [ ubuntu-24.04, macos-26, windows-2025 ]
java-version: [ 17, 21, 25 ]
java-version: [ 17, 21, 25, 27-ea ]

runs-on: ${{ matrix.os }}

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-weekly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-24.04, macos-26, windows-2025 ]
java-version: [ 17, 21, 25 ]
java-version: [ 17, 21, 25, 27-ea ]

runs-on: ${{ matrix.os }}

Expand Down
28 changes: 28 additions & 0 deletions activemq-amqp/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -392,5 +392,33 @@
</build>
</profile>

<profile>
<id>jdk27-plus</id>
<activation>
<jdk>[27,)</jdk>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-jdk27-test-source</id>
<phase>generate-test-sources</phase>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>src/test/java27</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* 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.activemq.transport.amqp;

import static org.junit.Assert.assertNotNull;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URI;
import java.security.KeyStore;
import java.security.cert.X509Certificate;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.broker.DefaultSslContext;
import org.apache.activemq.broker.SslContext;
import org.apache.activemq.broker.TransportConnector;

/**
* Raw TLS helpers shared by the ssl connector tests: a broker side
* {@link SslContext} from a test keystore, a connector built by hand so it binds
* with that context, and a client handshake whose named groups can be chosen.
*/
public final class SslTestSupport {

public static final char[] PASSWORD = "password".toCharArray();
public static final String KEYSTORE = "keystore";

private SslTestSupport() {
}

public static SslContext sslContext(String keystoreName) throws Exception {
KeyStore keyStore = loadKeyStore(keystoreName);
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, PASSWORD);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
return new DefaultSslContext(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
}

/**
* {@code BrokerService.addConnector(URI)} binds at once with the broker level
* context, so the connector is built by hand and bound at broker start.
*/
public static TransportConnector addConnector(BrokerService broker, String uri, SslContext sslContext) throws Exception {
TransportConnector connector = new TransportConnector();
connector.setUri(new URI(uri));
connector.setSslContext(sslContext);
return broker.addConnector(connector);
}

/**
* Completes a TLS handshake against the connector, trusting whatever it
* presents, and returns the negotiated protocol.
*
* @param clientNamedGroups the key exchange groups the client offers, or null for the JDK default
* @throws IOException when the server refuses the handshake
*/
public static String handshake(TransportConnector connector, String[] clientNamedGroups) throws Exception {
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, new TrustManager[] {new TrustAll()}, null);
URI uri = connector.getConnectUri();
try (SSLSocket socket = (SSLSocket) context.getSocketFactory().createSocket(uri.getHost(), uri.getPort())) {
socket.setSoTimeout(10000);
if (clientNamedGroups != null) {
SSLParameters parameters = socket.getSSLParameters();
setNamedGroups(parameters, clientNamedGroups);
socket.setSSLParameters(parameters);
}
socket.startHandshake();
return socket.getSession().getProtocol();
}
}

/** SSLParameters.setNamedGroups exists since Java 20; this module compiles for 17 */
public static void setNamedGroups(SSLParameters parameters, String[] namedGroups) throws Exception {
SSLParameters.class.getMethod("setNamedGroups", String[].class).invoke(parameters, (Object) namedGroups);
}

public static KeyStore loadKeyStore(String keystoreName) throws Exception {
var url = SslTestSupport.class.getClassLoader().getResource(keystoreName);
assertNotNull("test keystore not on classpath: " + keystoreName, url);
KeyStore keyStore = KeyStore.getInstance("jks");
try (var in = new FileInputStream(new File(url.toURI()))) {
keyStore.load(in, PASSWORD);
}
return keyStore;
}

private static final class TrustAll implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
}

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
}

@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* 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.activemq.transport.amqp;

import static org.apache.activemq.transport.amqp.SslTestSupport.KEYSTORE;
import static org.apache.activemq.transport.amqp.SslTestSupport.addConnector;
import static org.apache.activemq.transport.amqp.SslTestSupport.handshake;
import static org.apache.activemq.transport.amqp.SslTestSupport.sslContext;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;

import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.broker.TransportConnector;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

/**
* Lives in src/test/java27, so it only runs on JDK 27 and later, where TLS 1.3
* offers the hybrid post-quantum key exchange groups. Every SSL capable
* transport is bound with the new options and probed with raw handshakes whose
* client side offers chosen groups.
*/
@Category(ParallelTest.class)
@RunWith(Parameterized.class)
public class PostQuantumKeyExchangeTest {

private static final String[] CLASSICAL_ONLY = {"x25519", "secp256r1"};
private static final String[] HYBRID_ONLY = {"X25519MLKEM768"};

@Parameterized.Parameters(name = "{0}")
public static Collection<Object[]> transports() {
return Arrays.asList(new Object[][] {
{"ssl"},
{"nio+ssl"},
{"auto+ssl"},
{"auto+nio+ssl"},
{"amqp+ssl"},
{"amqp+nio+ssl"},
{"mqtt+ssl"},
{"mqtt+nio+ssl"},
{"stomp+ssl"},
{"stomp+nio+ssl"},
});
}

@Parameterized.Parameter
public String transport;

private BrokerService broker;

@Before
public void setUp() {
broker = new BrokerService();
broker.setPersistent(false);
broker.setUseJmx(false);
broker.setAdvisorySupport(false);
}

@After
public void tearDown() throws Exception {
broker.stop();
broker.waitUntilStopped();
}

@Test(timeout = 60000)
public void requiredPostQuantumKeyExchangeRefusesClassicalOnlyClients() throws Exception {
TransportConnector connector = start("?transport.requirePostQuantumKeyExchange=true");

assertEquals("TLSv1.3", handshake(connector, null));
assertEquals("TLSv1.3", handshake(connector, HYBRID_ONLY));
assertThrows(IOException.class, () -> handshake(connector, CLASSICAL_ONLY));
}

@Test(timeout = 60000)
public void namedGroupsSelectTheHybridGroup() throws Exception {
TransportConnector connector = start("?transport.namedGroups=SecP256r1MLKEM768");

assertEquals("TLSv1.3", handshake(connector, new String[] {"SecP256r1MLKEM768"}));
assertThrows(IOException.class, () -> handshake(connector, HYBRID_ONLY));
assertThrows(IOException.class, () -> handshake(connector, CLASSICAL_ONLY));
}

@Test(timeout = 60000)
public void defaultConnectorStillAcceptsClassicalClients() throws Exception {
TransportConnector connector = start("");

assertEquals("TLSv1.3", handshake(connector, CLASSICAL_ONLY));
assertEquals("TLSv1.3", handshake(connector, HYBRID_ONLY));
}

@Test(timeout = 60000)
public void requireAndNamedGroupsTogetherStopTheConnector() throws Exception {
addConnector(broker, transport + "://localhost:0?transport.requirePostQuantumKeyExchange=true&transport.namedGroups=x25519", sslContext(KEYSTORE));
Exception thrown = assertThrows(Exception.class, () -> broker.start());
String messages = "";
for (Throwable t = thrown; t != null; t = t.getCause()) {
messages += t.getMessage() + " | ";
}
assertTrue(messages, messages.contains("cannot both be set"));
}

private TransportConnector start(String options) throws Exception {
TransportConnector connector = addConnector(broker, transport + "://localhost:0" + options, sslContext(KEYSTORE));
broker.start();
broker.waitUntilStarted();
return connector;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,19 @@ public interface Connector extends Service {
* @return connector name
*/
public String getName();

/**
* @return the TLS named groups this connector offers, in preference order, or null for the JDK default
*/
String[] getNamedGroups();

/**
* @return the TLS signature schemes this connector offers, in preference order, or null for the JDK default
*/
String[] getSignatureSchemes();

/**
* @return true when the connector offers only the post-quantum hybrid key exchange groups
*/
boolean isRequirePostQuantumKeyExchange();
}
Loading
Loading