From 9a4c1ce6848fa465cfb33fa59ecb0fc30bfab69e Mon Sep 17 00:00:00 2001 From: Matt Pavlovich Date: Mon, 14 Sep 2026 14:32:22 -0500 Subject: [PATCH 1/2] Add JDK 27 support --- .github/workflows/ci-nightly.yml | 4 ++-- .github/workflows/ci-quick.yml | 2 +- .github/workflows/ci-weekly.yml | 2 +- .../org/apache/activemq/security/broker1.ks | Bin 2102 -> 2139 bytes .../org/apache/activemq/security/broker1.ts | Bin 1589 -> 1672 bytes .../org/apache/activemq/security/broker2.ks | Bin 2100 -> 2140 bytes .../org/apache/activemq/security/broker2.ts | Bin 1589 -> 1672 bytes .../org/apache/activemq/security/client.ks | Bin 2097 -> 2140 bytes .../org/apache/activemq/security/client.ts | Bin 1592 -> 1674 bytes pom.xml | 4 ++-- 10 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-nightly.yml b/.github/workflows/ci-nightly.yml index 08a538d0882..befb6166609 100644 --- a/.github/workflows/ci-nightly.yml +++ b/.github/workflows/ci-nightly.yml @@ -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 }} @@ -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 }} diff --git a/.github/workflows/ci-quick.yml b/.github/workflows/ci-quick.yml index 7554eaf413b..23092bec49e 100644 --- a/.github/workflows/ci-quick.yml +++ b/.github/workflows/ci-quick.yml @@ -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 }} diff --git a/.github/workflows/ci-weekly.yml b/.github/workflows/ci-weekly.yml index e5cf9e9e85f..e5ae7d24952 100644 --- a/.github/workflows/ci-weekly.yml +++ b/.github/workflows/ci-weekly.yml @@ -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 }} diff --git a/activemq-unit-tests/src/test/resources/org/apache/activemq/security/broker1.ks b/activemq-unit-tests/src/test/resources/org/apache/activemq/security/broker1.ks index e23f1a95afc3616b18e875e5f93354ece47ab428..6cc5c390fa0729874fa520904659f6ba4028ce97 100644 GIT binary patch delta 2097 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 WM_NoaI%T_NG}GUHx%-m*BJ6Lpx~k;> diff --git a/activemq-unit-tests/src/test/resources/org/apache/activemq/security/broker1.ts b/activemq-unit-tests/src/test/resources/org/apache/activemq/security/broker1.ts index b3394237f0d31f9c1a3c0526409278561aed4d69..d95b5733f93a2395d84302d114ef965b5b92135e 100644 GIT binary patch literal 1672 zcmezO_TO6u1_mZLW=qb=OwB7{U|?LZFfmh{fi*(U)WDK~fmy_$iCMs)iSfq*W+p}^ zCQgRlo!j63Z8<7qz{|#|)#lOmotKf3o0Y-9)Q}%2#2m`PCCrvskeHm2YA9&H4-()O z=15E~$t+9FErh9I7KU1Apdij`WM*J#Xkut-WNK&-CBbiGU}|DuXk-Z$AlKj~MkQn? zGO{u-H!<=v0KLM+)WpchaFT!78odpT;zbi`3imA+- zmFBH`S(?7=;x3_uvwhv4U#kD?Nv z?Wo4=`b_hr;=haTXQxb(Xq|QY+Z*e;^eyWYdCl1PI1Kln6xc6%$GiX70yW<5f(;2p zXW05Yx7aFpZ8@yJqwt}Vdx^Zdzv)#@#-5f;7Td`W!P$mAsVF}?wa5sPXEKf;@{A}j z&j&Z zM_KnoQ3pO_F6;G051K;dK3?2gKXL2iE86on-YlLLwrzgF>%z;D`z9!_SjfQ?e)WuO z@nr7X8NZYlI+lIZ6kNYbq;jkN)S`r2b^LuVFVCoR<=cBRbLE^!?^4;#8|Ke?z1RIL zU!bsyLdND3r>hp-6L7bgm|Uf6lrdqB*`epUf!>mLLzc6xlv{D6i0SeH`JcO<ZRs124R0)Zc=7af7c*_Mst_ZG&nm~CPS@F+ z?j0Wb>8|g?wcqUeeu>=aJ(;+1?Zy+AR2`;qA7_fXdtsOTr4GwW2bL~y>J$6+VcLtm zORay}s~Z|*7Cwkz?hg8KQSB1r%DtD~U5d7gE$rvJXq0GDFz+wpttVf*Rd^<@q-<6lSECCFL&ith(Cu_UG}b;Rm^D SB{IH*g(O?Oj!6t+jPs4_r)5 zjEoFj21i9%Re8> zG^e(%F224+WF7O>$u6#c8&q2}FRR`S-TdHmd*xkyt7+Y}rT71)NpK37rI$VyGhJo9 z?Xbh+f_GPSuc+vp<-Ps*QqAWwrM*&<4)3&QJ8h*=*ku>3x_MIFP1&@BWd4gfFRM4d z_F;0GsXU|UA8%JpO+QO$i%sY)mY+}epITJ^U*ap{!dEZ<3ZHrGG(qV@?_#q*GnVx4 zUvy>Fua3lPuNPR(+-zCN_I2W}+)l$?GqYsu-4~tP;FVx_C#i`o;Ci>vf?ul&&i>`! z@Ps2@HeToCiWbFkdnRT^21eu%0!AS)gcuoE_4*?>>i6tiq!;*kvAe9aP>)S8+s8B0 zVt1%d-85_Ixp%E4ZCrQ1pMQ1zVx99=4*LaXz4;YxvlwQjJN|NJ&e1S%TvSnc_V&dO z+p^UDv-nPYV(ve2!pYi&-{!yOch*_n=veee%h&jT#O+(jPqqhzY>qB?UVQdooa86T z36@uGak%%Kn4xG=X>hUQ%QVJjyC*K;UDGQcP3gK38MVNBY)mjg4Bc`$wL=|0;cR zjiT#nXXmC)cr`a7YX zhq=<1SlEppE2~UmF59{5>vkKD4HrW7IySbx-`xAmF)Q@istLt?(_c;LoELfU?Ww3+ z5!JhTgqG*t?LRGc)7Ce%Ye%7Ws6Wx=e0)!jy*8UyN-txMS zw*H-Vl}UEW?C&`9Lgq&2PkX0iZ7O1^extnlx96_5C6_oO9Hx1&UH_?8H|Ng|yI5vb zhlzLAl(U|yn66dPeM-pOars@TlbVla&&X8RX};%_@Ku(D4_pJ3%QsEcPmKQbJLeGF z659~g!ds;-l>*8JxBfNcPCg-_+T&~Ief`0%#nI)rL-(I*KHIqFti%0J3zw|T|99xY z1;!+&x&QT8r+k?H`S=%JF~Pi@fhv*LzsasT++zPnx#@VztM8%r{qLRow~GawQHMMcYQcSgP!3Q1zX2wMA}S)CYeyp7&o_+LjcovD@is v>TqvwfD&8q*{T+|x|>I@dn<8;dv9rrIq=-V^;Lyb3-4n#quQt8Dej#BT)=r- diff --git a/activemq-unit-tests/src/test/resources/org/apache/activemq/security/broker2.ks b/activemq-unit-tests/src/test/resources/org/apache/activemq/security/broker2.ks index a5f0ecfb6e20762610f458447760d911895f5d84..582a80156dc415bb1ae618a39909cfa7242ee315 100644 GIT binary patch delta 2098 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 diff --git a/activemq-unit-tests/src/test/resources/org/apache/activemq/security/client.ks b/activemq-unit-tests/src/test/resources/org/apache/activemq/security/client.ks index e0474af1d456382ab44e13b98ad059c90d1e446e..657fab9b05e199c0cbf0b8dc36f282286b923fed 100644 GIT binary patch delta 2100 zcmb8sX*kr20>|SYc$(k)3L*Nmnl{T4x)yt0Q zY~)xax7%^=y|yT=>3NusqujzlT&W(R>>GtY7_#AFHrUz`Cb>)>1DkA!=itrx<>0^W z&%6Vo3QzXCO*?M#*;l`>C@%e`cTD%#HT}|;rN$+DUKNUhr&bAn^=EwSG=JMQza%t| z(s_{>cE>;m|64yZG@PYqw60|*fkz~GM`Ncld|ruO;hE0oIXs5`%JNrx4PwI<%rjIO z8|CMdeZ5xBG!(+!JDBkfa_A0xua6Lx7p8o#V^%=%`o>`F&H4G(SDJzk?Ynf_q#r5Ggm?Wk{eb`{ z3bV5nlKhM!9bR?w`fZNw&Ik=pqBP5KCC}rZe9}we-RLmshU@s2X&=5J$!ytJMw5l% z4a0_vjfOvJXpfbqMdUjQ%!(lwKw9|wjkE&N#HWMI=}{b1)!L~)X7p;P)wz*uZ@cf} zDG5|LZ_H2c@H7(*M>dOlN8D^ydkAEypk6GU4q8qL@!9RDDN%?NO&-lP(Z$=l%a1bX zS>x#?yOKX=Jyca%j_y1kaghJ;wgYN)#w!Uq)U)Neyjo#_Ddl)v*Vm--F`rsfPV;l{ zmo}lwU)8g`IWQt{HQxRftv)NdtjNnP&M_oYpLgY95Mcby?kgJHi&~yi=6xhzKQ63^_NOqtTlXLMKbo2Cw2d}j(cx( zUW&$@qp#UcX1$~c!&Fq(u%K&oC$%BQ!NP)xRc&7vANMr-oj;b@n&ix4 z9(5@K(RS^3b2>UkBtx`Fd*^D^%-=2djpnTkw$#;chSb|e2-Hd65cH!#CNbKsPZIV!*U95SO1Dc4-$Xw7WVH-AR9dY&l)#Cp#Aj5oe@=c?je_utzb zN+4cil+5cvNMJVKl< zu7Q*wckkQ(-9Uv9uD2-eRz86QDGX9YouEM^5j9lQhy;64eiecmi9l4*_z(QQ&-8J) z9N5WURHjrJiSd6#!`bBtp+$S*r3Nc!EGdketof=Pj}Y-@gFm7hR6`zz4b7$|v~WH@ z3p-+pcPG0owQQLm0=3>fH!R)?`alHukZ0-|P*HbR?TF^a3vopd%o$^u^-W)kqh$6C zz}@O3-E(*&ZRj5D{nLuUtg&iG!LbKOu=KN<_-e`7*msiM3!i(-c~=!eC%*cU>6E!d zXNqdKC7A14hpYV%Waa6*)Vt`DVXF%f>rc|Eyk4D&W;m0|XSUG+RsE77eQypvuSdxE zOP11g-T02%$ksU$`T!QgZm&VwgM3@(jT4G5Kyy*AxOgk&l(segy{< zfB?^>2{?kpKM^GOU?MQIliYFVE0zw48wb>ASC_4c<(h?e|I-l~1pp9b$Z2RiJ=N*j z_SqQcSmB<_%C>i5h$w>s70%Al>*rG+T0|6j6b&>8K4V_dUl;_xoBS5VpNbs{flWzS zY_>UjfT-JogOtT|-Bhh1SKcVr^zTqd39DRf(&%-WdHA#9!llaV<0Ds^%x16}IeeRw z3S(_{M(XKDbEnfEx5+U<%GnzMhxuQrZfT;iR*bQo)eCn$s>`L3>IgngVsj^|S$N#| z;m15#nya^3AIZT&#pDKoV%T)=}E3QUVWg-_Ury5yX=i_5*9 NNA|@GsMd$4{|jL&y`=yE delta 2057 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 - [24,) - You must use Java 24+ to build (we leverage MRJAR). + [27,) + You must use Java 27+ to build a release; the multi-release jars carry Java 21, 24 and 27 classes. From b2663822592bf3cc5e82ec999f80c00b47ae6334 Mon Sep 17 00:00:00 2001 From: Matt Pavlovich Date: Mon, 14 Sep 2026 14:50:06 -0500 Subject: [PATCH 2/2] feat: Post-quantum TLS via JDK 27 --- activemq-amqp/pom.xml | 28 ++++ .../transport/amqp/SslTestSupport.java | 126 ++++++++++++++ .../amqp/PostQuantumKeyExchangeTest.java | 131 +++++++++++++++ .../org/apache/activemq/broker/Connector.java | 15 ++ .../activemq/broker/TransportConnector.java | 44 +++++ .../activemq/broker/jmx/ConnectorView.java | 15 ++ .../broker/jmx/ConnectorViewMBean.java | 9 + .../nio/AutoInitNioSSLTransport.java | 7 +- activemq-client/pom.xml | 27 +++ .../transport/nio/NIOSSLTransport.java | 61 ++++++- .../transport/tcp/SslParameterSupport.java | 85 ++++++++++ .../activemq/transport/tcp/SslTransport.java | 53 +++++- .../transport/tcp/TcpTransportServer.java | 26 +++ .../transport/tcp/SslParameterSupport.java | 90 ++++++++++ .../org/apache/activemq/package-info.java | 31 ++++ .../transport/tcp/SslParameterSupport.java | 88 ++++++++++ activemq-http/pom.xml | 28 ++++ .../SecureSocketConnectorFactory.java | 53 +++++- .../PostQuantumKeyExchangeJettyTest.java | 136 ++++++++++++++++ activemq-unit-tests/pom.xml | 28 ++++ .../tcp/SslNamedGroupOptionsTest.java | 154 ++++++++++++++++++ .../tcp/SslParameterSupportTest.java | 98 +++++++++++ assembly/src/release/conf/activemq.xml | 7 + 23 files changed, 1324 insertions(+), 16 deletions(-) create mode 100644 activemq-amqp/src/test/java/org/apache/activemq/transport/amqp/SslTestSupport.java create mode 100644 activemq-amqp/src/test/java27/org/apache/activemq/transport/amqp/PostQuantumKeyExchangeTest.java create mode 100644 activemq-client/src/main/java/org/apache/activemq/transport/tcp/SslParameterSupport.java create mode 100644 activemq-client/src/main/java21/org/apache/activemq/transport/tcp/SslParameterSupport.java create mode 100644 activemq-client/src/main/java27/org/apache/activemq/package-info.java create mode 100644 activemq-client/src/main/java27/org/apache/activemq/transport/tcp/SslParameterSupport.java create mode 100644 activemq-http/src/test/java27/org/apache/activemq/transport/https/PostQuantumKeyExchangeJettyTest.java create mode 100644 activemq-unit-tests/src/test/java/org/apache/activemq/transport/tcp/SslNamedGroupOptionsTest.java create mode 100644 activemq-unit-tests/src/test/java/org/apache/activemq/transport/tcp/SslParameterSupportTest.java diff --git a/activemq-amqp/pom.xml b/activemq-amqp/pom.xml index 41a89c912d8..4d653753a71 100644 --- a/activemq-amqp/pom.xml +++ b/activemq-amqp/pom.xml @@ -392,5 +392,33 @@ + + jdk27-plus + + [27,) + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-jdk27-test-source + generate-test-sources + + add-test-source + + + + src/test/java27 + + + + + + + + diff --git a/activemq-amqp/src/test/java/org/apache/activemq/transport/amqp/SslTestSupport.java b/activemq-amqp/src/test/java/org/apache/activemq/transport/amqp/SslTestSupport.java new file mode 100644 index 00000000000..70e47889857 --- /dev/null +++ b/activemq-amqp/src/test/java/org/apache/activemq/transport/amqp/SslTestSupport.java @@ -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]; + } + } +} diff --git a/activemq-amqp/src/test/java27/org/apache/activemq/transport/amqp/PostQuantumKeyExchangeTest.java b/activemq-amqp/src/test/java27/org/apache/activemq/transport/amqp/PostQuantumKeyExchangeTest.java new file mode 100644 index 00000000000..655d35a4fb1 --- /dev/null +++ b/activemq-amqp/src/test/java27/org/apache/activemq/transport/amqp/PostQuantumKeyExchangeTest.java @@ -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 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; + } +} diff --git a/activemq-broker/src/main/java/org/apache/activemq/broker/Connector.java b/activemq-broker/src/main/java/org/apache/activemq/broker/Connector.java index bf6fd7bec76..f5415c74d62 100644 --- a/activemq-broker/src/main/java/org/apache/activemq/broker/Connector.java +++ b/activemq-broker/src/main/java/org/apache/activemq/broker/Connector.java @@ -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(); } diff --git a/activemq-broker/src/main/java/org/apache/activemq/broker/TransportConnector.java b/activemq-broker/src/main/java/org/apache/activemq/broker/TransportConnector.java index 938165195c2..02a81880d46 100644 --- a/activemq-broker/src/main/java/org/apache/activemq/broker/TransportConnector.java +++ b/activemq-broker/src/main/java/org/apache/activemq/broker/TransportConnector.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.util.Arrays; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; @@ -43,9 +44,12 @@ import org.apache.activemq.transport.TransportServer; import org.apache.activemq.transport.discovery.DiscoveryAgent; import org.apache.activemq.transport.discovery.DiscoveryAgentFactory; +import org.apache.activemq.transport.tcp.SslParameterSupport; import org.apache.activemq.util.ExceptionUtils; import org.apache.activemq.util.ServiceStopper; import org.apache.activemq.util.ServiceSupport; +import org.apache.activemq.util.StringArrayConverter; +import org.apache.activemq.util.URISupport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -283,6 +287,46 @@ private void onAcceptError(Exception error, String remoteHost) { started.set(true); LOG.info("Connector {} started", getName()); + logNamedGroups(); + } + + /** shows what key exchange a connector was told to request, so refusals of classical peers are explainable */ + private void logNamedGroups() { + boolean requirePostQuantum = isRequirePostQuantumKeyExchange(); + String[] effective = SslParameterSupport.effectiveNamedGroups(getNamedGroups(), requirePostQuantum); + if (effective != null) { + LOG.info("Connector {} TLS named groups {}{}", getName(), Arrays.toString(effective), + requirePostQuantum ? " (post-quantum key exchange required)" : ""); + } + } + + @Override + public String[] getNamedGroups() { + return StringArrayConverter.convertToStringArray(transportOption("namedGroups")); + } + + @Override + public String[] getSignatureSchemes() { + return StringArrayConverter.convertToStringArray(transportOption("signatureSchemes")); + } + + @Override + public boolean isRequirePostQuantumKeyExchange() { + return Boolean.parseBoolean(transportOption("requirePostQuantumKeyExchange")); + } + + /** a transport.* option as written on the connector URI, or null when absent */ + private String transportOption(String name) { + URI uri = getUri(); + if (uri == null) { + return null; + } + try { + return URISupport.parseParameters(uri).get("transport." + name); + } catch (URISyntaxException e) { + LOG.debug("Could not read transport options of connector {}", getName(), e); + return null; + } } public String getPublishableConnectString() throws Exception { diff --git a/activemq-broker/src/main/java/org/apache/activemq/broker/jmx/ConnectorView.java b/activemq-broker/src/main/java/org/apache/activemq/broker/jmx/ConnectorView.java index acd00377d04..c79cdfbe6db 100644 --- a/activemq-broker/src/main/java/org/apache/activemq/broker/jmx/ConnectorView.java +++ b/activemq-broker/src/main/java/org/apache/activemq/broker/jmx/ConnectorView.java @@ -234,4 +234,19 @@ public int getConnectionCount() { public String getName() { return this.connector.getName(); } + + @Override + public String[] getNamedGroups() { + return this.connector.getNamedGroups(); + } + + @Override + public String[] getSignatureSchemes() { + return this.connector.getSignatureSchemes(); + } + + @Override + public boolean isRequirePostQuantumKeyExchange() { + return this.connector.isRequirePostQuantumKeyExchange(); + } } diff --git a/activemq-broker/src/main/java/org/apache/activemq/broker/jmx/ConnectorViewMBean.java b/activemq-broker/src/main/java/org/apache/activemq/broker/jmx/ConnectorViewMBean.java index 4d040225619..0cb3df6fc8d 100644 --- a/activemq-broker/src/main/java/org/apache/activemq/broker/jmx/ConnectorViewMBean.java +++ b/activemq-broker/src/main/java/org/apache/activemq/broker/jmx/ConnectorViewMBean.java @@ -148,4 +148,13 @@ public interface ConnectorViewMBean extends Service { @MBeanInfo("Connector name") String getName(); + + @MBeanInfo("TLS named groups offered, in preference order; unset means the JDK default") + String[] getNamedGroups(); + + @MBeanInfo("TLS signature schemes offered, in preference order; unset means the JDK default") + String[] getSignatureSchemes(); + + @MBeanInfo("Only the post-quantum hybrid key exchange groups are offered") + boolean isRequirePostQuantumKeyExchange(); } diff --git a/activemq-broker/src/main/java/org/apache/activemq/transport/nio/AutoInitNioSSLTransport.java b/activemq-broker/src/main/java/org/apache/activemq/transport/nio/AutoInitNioSSLTransport.java index 06aa4677715..fdd41747994 100644 --- a/activemq-broker/src/main/java/org/apache/activemq/transport/nio/AutoInitNioSSLTransport.java +++ b/activemq-broker/src/main/java/org/apache/activemq/transport/nio/AutoInitNioSSLTransport.java @@ -30,7 +30,6 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; -import javax.net.ssl.SSLParameters; import org.apache.activemq.thread.TaskRunnerFactory; import org.apache.activemq.util.IOExceptionSupport; @@ -90,11 +89,7 @@ protected void initializeStreams() throws IOException { sslEngine = sslContext.createSSLEngine(); } - if (verifyHostName) { - SSLParameters sslParams = new SSLParameters(); - sslParams.setEndpointIdentificationAlgorithm("HTTPS"); - sslEngine.setSSLParameters(sslParams); - } + configureSslParameters(sslEngine); sslEngine.setUseClientMode(false); if (enabledCipherSuites != null) { diff --git a/activemq-client/pom.xml b/activemq-client/pom.xml index 29cb8f4a455..c359f95ffec 100644 --- a/activemq-client/pom.xml +++ b/activemq-client/pom.xml @@ -428,5 +428,32 @@ + + jdk27-plus + + [27,) + + + + + maven-compiler-plugin + + + java27-compile + compile + + compile + + + 27 + ${project.basedir}/src/main/java27 + true + + + + + + + diff --git a/activemq-client/src/main/java/org/apache/activemq/transport/nio/NIOSSLTransport.java b/activemq-client/src/main/java/org/apache/activemq/transport/nio/NIOSSLTransport.java index 384f3f2af06..e3a31145463 100644 --- a/activemq-client/src/main/java/org/apache/activemq/transport/nio/NIOSSLTransport.java +++ b/activemq-client/src/main/java/org/apache/activemq/transport/nio/NIOSSLTransport.java @@ -41,6 +41,7 @@ import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; +import org.apache.activemq.transport.tcp.SslParameterSupport; import org.apache.activemq.MaxFrameSizeExceededException; import org.apache.activemq.command.ConnectionInfo; import org.apache.activemq.openwire.OpenWireFormat; @@ -60,6 +61,9 @@ public class NIOSSLTransport extends NIOTransport { protected String[] enabledCipherSuites; protected String[] enabledProtocols; protected boolean verifyHostName = false; + protected String[] namedGroups; + protected String[] signatureSchemes; + protected boolean requirePostQuantumKeyExchange; protected SSLContext sslContext; protected SSLEngine sslEngine; @@ -123,11 +127,7 @@ protected void initializeStreams() throws IOException { sslEngine = sslContext.createSSLEngine(); } - if (verifyHostName) { - SSLParameters sslParams = new SSLParameters(); - sslParams.setEndpointIdentificationAlgorithm("HTTPS"); - sslEngine.setSSLParameters(sslParams); - } + configureSslParameters(sslEngine); sslEngine.setUseClientMode(false); if (enabledCipherSuites != null) { @@ -640,4 +640,55 @@ public boolean isVerifyHostName() { public void setVerifyHostName(boolean verifyHostName) { this.verifyHostName = verifyHostName; } + + public String[] getNamedGroups() { + return namedGroups; + } + + /** + * TLS named groups (key exchange algorithms) to offer, in preference order, + * for example {@code X25519MLKEM768,x25519}. Needs Java 21 or later. + */ + public void setNamedGroups(String[] namedGroups) { + this.namedGroups = namedGroups; + } + + public String[] getSignatureSchemes() { + return signatureSchemes; + } + + /** TLS signature schemes to offer, in preference order. Needs Java 21 or later. */ + public void setSignatureSchemes(String[] signatureSchemes) { + this.signatureSchemes = signatureSchemes; + } + + public boolean isRequirePostQuantumKeyExchange() { + return requirePostQuantumKeyExchange; + } + + /** + * Offer only the post-quantum hybrid key exchange groups of TLS 1.3, so a + * peer without them is refused. Needs Java 27 or later. + */ + public void setRequirePostQuantumKeyExchange(boolean requirePostQuantumKeyExchange) { + this.requirePostQuantumKeyExchange = requirePostQuantumKeyExchange; + } + + /** + * Hostname verification, named groups and signature schemes in one pass over the + * engine's own parameters. Called before the client mode and client authentication + * flags are set, since setSSLParameters copies those flags too. + */ + protected void configureSslParameters(SSLEngine engine) { + SSLParameters sslParams = engine.getSSLParameters(); + boolean changed = false; + if (verifyHostName) { + sslParams.setEndpointIdentificationAlgorithm("HTTPS"); + changed = true; + } + changed |= SslParameterSupport.apply(sslParams, namedGroups, signatureSchemes, requirePostQuantumKeyExchange); + if (changed) { + engine.setSSLParameters(sslParams); + } + } } diff --git a/activemq-client/src/main/java/org/apache/activemq/transport/tcp/SslParameterSupport.java b/activemq-client/src/main/java/org/apache/activemq/transport/tcp/SslParameterSupport.java new file mode 100644 index 00000000000..9639308244d --- /dev/null +++ b/activemq-client/src/main/java/org/apache/activemq/transport/tcp/SslParameterSupport.java @@ -0,0 +1,85 @@ +/** + * 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.tcp; + +import javax.net.ssl.SSLParameters; + +/** + * Applies the TLS key exchange and signature settings of the ssl transports + * ({@code namedGroups}, {@code signatureSchemes}, + * {@code requirePostQuantumKeyExchange}) to an {@link SSLParameters}. + * + *

This instance of the class is for JDK [17, 21). The JDK gained + * {@code SSLParameters.setNamedGroups} in 20 and the post-quantum hybrid groups + * in 27, so here every request fails fast with a message naming the JDK needed; + * the multi-release variants under {@code META-INF/versions/21} and + * {@code META-INF/versions/27} do the work. + */ +public final class SslParameterSupport { + + /** the hybrid key exchange groups of JEP 527, in preference order */ + public static final String[] POST_QUANTUM_NAMED_GROUPS = {"X25519MLKEM768", "SecP256r1MLKEM768", "SecP384r1MLKEM1024"}; + + private SslParameterSupport() { + } + + /** whether this JDK can restrict key exchange to the post-quantum hybrid groups */ + public static boolean isPostQuantumKeyExchangeAvailable() { + return false; + } + + /** whether this JDK can apply named groups and signature schemes at all */ + public static boolean isNamedGroupsAvailable() { + return false; + } + + /** + * Validates the combination and sets the requested groups and schemes on the parameters. + * + * @return true when the parameters were changed + * @throws IllegalArgumentException when requirePostQuantumKeyExchange and namedGroups are both set + * @throws IllegalStateException when this JDK cannot honour a request + */ + public static boolean apply(SSLParameters parameters, String[] namedGroups, String[] signatureSchemes, boolean requirePostQuantumKeyExchange) { + checkCombination(namedGroups, requirePostQuantumKeyExchange); + if (requirePostQuantumKeyExchange) { + throw new IllegalStateException("requirePostQuantumKeyExchange requires Java 27 or later"); + } + if (isSet(namedGroups) || isSet(signatureSchemes)) { + throw new IllegalStateException("namedGroups and signatureSchemes require Java 21 or later"); + } + return false; + } + + /** the groups a connection will request: the explicit list, the post-quantum list, or null for the JDK default */ + public static String[] effectiveNamedGroups(String[] namedGroups, boolean requirePostQuantumKeyExchange) { + if (requirePostQuantumKeyExchange) { + return POST_QUANTUM_NAMED_GROUPS.clone(); + } + return isSet(namedGroups) ? namedGroups.clone() : null; + } + + static void checkCombination(String[] namedGroups, boolean requirePostQuantumKeyExchange) { + if (requirePostQuantumKeyExchange && isSet(namedGroups)) { + throw new IllegalArgumentException("requirePostQuantumKeyExchange and namedGroups cannot both be set"); + } + } + + static boolean isSet(String[] values) { + return values != null && values.length > 0; + } +} diff --git a/activemq-client/src/main/java/org/apache/activemq/transport/tcp/SslTransport.java b/activemq-client/src/main/java/org/apache/activemq/transport/tcp/SslTransport.java index 770c3aa1a0f..4df02c027d1 100644 --- a/activemq-client/src/main/java/org/apache/activemq/transport/tcp/SslTransport.java +++ b/activemq-client/src/main/java/org/apache/activemq/transport/tcp/SslTransport.java @@ -52,6 +52,10 @@ public class SslTransport extends TcpTransport { */ private Boolean verifyHostName = null; + private String[] namedGroups; + private String[] signatureSchemes; + private boolean requirePostQuantumKeyExchange; + /** * Connect to a remote node such as a Broker. * @@ -120,18 +124,26 @@ protected void initialiseSocket(Socket sock) throws SocketException, IllegalArgu // Lets try to configure the SSL SNI field. Handy in case your using // a single proxy to route to different messaging apps. - final SSLParameters sslParams = new SSLParameters(); + // Start from the socket's own parameters: setSSLParameters copies the client + // authentication flags too, so a fresh SSLParameters would clear what the + // server socket configured on an accepted connection (AMQ-8445). + final SSLSocket sslSocket = (SSLSocket) this.socket; + final SSLParameters sslParams = sslSocket.getSSLParameters(); + boolean changed = false; if (remoteLocation != null) { sslParams.setServerNames(Collections.singletonList(new SNIHostName(remoteLocation.getHost()))); + changed = true; } if (verifyHostName) { sslParams.setEndpointIdentificationAlgorithm("HTTPS"); + changed = true; } - if (remoteLocation != null || verifyHostName) { - // AMQ-8445 only set SSLParameters if it has been populated before - ((SSLSocket) this.socket).setSSLParameters(sslParams); + changed |= SslParameterSupport.apply(sslParams, namedGroups, signatureSchemes, requirePostQuantumKeyExchange); + + if (changed) { + sslSocket.setSSLParameters(sslParams); } super.initialiseSocket(sock); @@ -176,6 +188,39 @@ public void setVerifyHostName(Boolean verifyHostName) { this.verifyHostName = verifyHostName; } + public String[] getNamedGroups() { + return namedGroups; + } + + /** + * TLS named groups (key exchange algorithms) to offer, in preference order, + * for example {@code X25519MLKEM768,x25519}. Needs Java 21 or later. + */ + public void setNamedGroups(String[] namedGroups) { + this.namedGroups = namedGroups; + } + + public String[] getSignatureSchemes() { + return signatureSchemes; + } + + /** TLS signature schemes to offer, in preference order. Needs Java 21 or later. */ + public void setSignatureSchemes(String[] signatureSchemes) { + this.signatureSchemes = signatureSchemes; + } + + public boolean isRequirePostQuantumKeyExchange() { + return requirePostQuantumKeyExchange; + } + + /** + * Offer only the post-quantum hybrid key exchange groups of TLS 1.3, so a + * peer without them is refused. Needs Java 27 or later. + */ + public void setRequirePostQuantumKeyExchange(boolean requirePostQuantumKeyExchange) { + this.requirePostQuantumKeyExchange = requirePostQuantumKeyExchange; + } + /** * @return peer certificate chain associated with the ssl socket */ diff --git a/activemq-client/src/main/java/org/apache/activemq/transport/tcp/TcpTransportServer.java b/activemq-client/src/main/java/org/apache/activemq/transport/tcp/TcpTransportServer.java index 32cc6261c25..03c830124e8 100644 --- a/activemq-client/src/main/java/org/apache/activemq/transport/tcp/TcpTransportServer.java +++ b/activemq-client/src/main/java/org/apache/activemq/transport/tcp/TcpTransportServer.java @@ -56,6 +56,7 @@ import org.apache.activemq.util.IOExceptionSupport; import org.apache.activemq.util.InetAddressUtil; import org.apache.activemq.util.IntrospectionSupport; +import org.apache.activemq.util.StringArrayConverter; import org.apache.activemq.util.ServiceListener; import org.apache.activemq.util.ServiceStopper; import org.apache.activemq.util.ServiceSupport; @@ -197,6 +198,11 @@ private void configureServerSocket(ServerSocket socket) throws SocketException { } } + configureNamedGroups(((SSLServerSocket) socket).getSSLParameters(), (SSLServerSocket) socket); + } else if (isSslServer()) { + // nio+ssl listens on a plain socket and builds an engine per connection; + // still refuse a setting this JDK cannot honour before the connector starts + configureNamedGroups(new SSLParameters(), null); } //AMQ-6599 - don't strip out set properties on the socket as we need to set them @@ -205,6 +211,26 @@ private void configureServerSocket(ServerSocket socket) throws SocketException { } } + /** + * Key exchange and signature settings are applied to the server socket here, so + * they hold for the handshake of every accepted connection, including the blocking + * auto transports that handshake during protocol detection. The options stay in + * the map so the accepted transports apply them again (AMQ-6599). A setting this + * JDK cannot honour stops the connector from starting, like a bad cipher suite. + */ + private void configureNamedGroups(SSLParameters sslParams, SSLServerSocket socket) throws SocketException { + String[] namedGroups = StringArrayConverter.convertToStringArray(transportOptions.get("namedGroups")); + String[] signatureSchemes = StringArrayConverter.convertToStringArray(transportOptions.get("signatureSchemes")); + boolean requirePostQuantum = Boolean.parseBoolean(String.valueOf(transportOptions.get("requirePostQuantumKeyExchange"))); + try { + if (SslParameterSupport.apply(sslParams, namedGroups, signatureSchemes, requirePostQuantum) && socket != null) { + socket.setSSLParameters(sslParams); + } + } catch (IllegalStateException | IllegalArgumentException e) { + throw new SocketException("Invalid transport options: " + e.getMessage()); + } + } + /** * @return Returns the wireFormatFactory. */ diff --git a/activemq-client/src/main/java21/org/apache/activemq/transport/tcp/SslParameterSupport.java b/activemq-client/src/main/java21/org/apache/activemq/transport/tcp/SslParameterSupport.java new file mode 100644 index 00000000000..433b9c422de --- /dev/null +++ b/activemq-client/src/main/java21/org/apache/activemq/transport/tcp/SslParameterSupport.java @@ -0,0 +1,90 @@ +/** + * 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.tcp; + +import javax.net.ssl.SSLParameters; + +/** + * Applies the TLS key exchange and signature settings of the ssl transports + * ({@code namedGroups}, {@code signatureSchemes}, + * {@code requirePostQuantumKeyExchange}) to an {@link SSLParameters}. + * + *

This instance of the class is for JDK [21, 27): named groups and signature + * schemes are applied, but the post-quantum hybrid groups of JEP 527 do not + * exist before JDK 27, so requiring them fails fast rather than failing every + * handshake later with an unhelpful alert. + */ +public final class SslParameterSupport { + + /** the hybrid key exchange groups of JEP 527, in preference order */ + public static final String[] POST_QUANTUM_NAMED_GROUPS = {"X25519MLKEM768", "SecP256r1MLKEM768", "SecP384r1MLKEM1024"}; + + private SslParameterSupport() { + } + + /** whether this JDK can restrict key exchange to the post-quantum hybrid groups */ + public static boolean isPostQuantumKeyExchangeAvailable() { + return false; + } + + /** whether this JDK can apply named groups and signature schemes at all */ + public static boolean isNamedGroupsAvailable() { + return true; + } + + /** + * Validates the combination and sets the requested groups and schemes on the parameters. + * + * @return true when the parameters were changed + * @throws IllegalArgumentException when requirePostQuantumKeyExchange and namedGroups are both set + * @throws IllegalStateException when this JDK cannot honour a request + */ + public static boolean apply(SSLParameters parameters, String[] namedGroups, String[] signatureSchemes, boolean requirePostQuantumKeyExchange) { + checkCombination(namedGroups, requirePostQuantumKeyExchange); + if (requirePostQuantumKeyExchange) { + throw new IllegalStateException("requirePostQuantumKeyExchange requires Java 27 or later"); + } + boolean changed = false; + if (isSet(namedGroups)) { + parameters.setNamedGroups(namedGroups.clone()); + changed = true; + } + if (isSet(signatureSchemes)) { + parameters.setSignatureSchemes(signatureSchemes.clone()); + changed = true; + } + return changed; + } + + /** the groups a connection will request: the explicit list, the post-quantum list, or null for the JDK default */ + public static String[] effectiveNamedGroups(String[] namedGroups, boolean requirePostQuantumKeyExchange) { + if (requirePostQuantumKeyExchange) { + return POST_QUANTUM_NAMED_GROUPS.clone(); + } + return isSet(namedGroups) ? namedGroups.clone() : null; + } + + static void checkCombination(String[] namedGroups, boolean requirePostQuantumKeyExchange) { + if (requirePostQuantumKeyExchange && isSet(namedGroups)) { + throw new IllegalArgumentException("requirePostQuantumKeyExchange and namedGroups cannot both be set"); + } + } + + static boolean isSet(String[] values) { + return values != null && values.length > 0; + } +} diff --git a/activemq-client/src/main/java27/org/apache/activemq/package-info.java b/activemq-client/src/main/java27/org/apache/activemq/package-info.java new file mode 100644 index 00000000000..769a66299ad --- /dev/null +++ b/activemq-client/src/main/java27/org/apache/activemq/package-info.java @@ -0,0 +1,31 @@ +/** + * 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. + */ + +/** + * Java 27 variants of client classes, packaged under {@code META-INF/versions/27} + * of the multi-release jar. The {@code jdk27-plus} profile in the module pom + * compiles this root with {@code --release 27} only when the build runs on + * JDK 27 or newer. + * + *

A class placed here with the same name as one in {@code src/main/java} + * replaces it at run time on JDK 27 and later, as {@code SubjectShim} does in + * activemq-broker; a class that exists only here must be reached by name, as + * {@code TaskRunnerFactory} does for the Java 21 virtual thread classes. + * The first intended occupants are the TLS 1.3 hybrid key exchange settings + * that JEP 527 introduced in JDK 27. + */ +package org.apache.activemq; diff --git a/activemq-client/src/main/java27/org/apache/activemq/transport/tcp/SslParameterSupport.java b/activemq-client/src/main/java27/org/apache/activemq/transport/tcp/SslParameterSupport.java new file mode 100644 index 00000000000..7b2f599fa19 --- /dev/null +++ b/activemq-client/src/main/java27/org/apache/activemq/transport/tcp/SslParameterSupport.java @@ -0,0 +1,88 @@ +/** + * 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.tcp; + +import javax.net.ssl.SSLParameters; + +/** + * Applies the TLS key exchange and signature settings of the ssl transports + * ({@code namedGroups}, {@code signatureSchemes}, + * {@code requirePostQuantumKeyExchange}) to an {@link SSLParameters}. + * + *

This instance of the class is for JDK 27+, where TLS 1.3 offers the hybrid + * post-quantum key exchange groups of JEP 527. {@code X25519MLKEM768} is already + * the JDK's first preference; {@code requirePostQuantumKeyExchange} narrows the + * offer to the three hybrid groups so a peer that cannot do post-quantum key + * exchange is refused with "No common named group" instead of falling back. + */ +public final class SslParameterSupport { + + /** the hybrid key exchange groups of JEP 527, in preference order */ + public static final String[] POST_QUANTUM_NAMED_GROUPS = {"X25519MLKEM768", "SecP256r1MLKEM768", "SecP384r1MLKEM1024"}; + + private SslParameterSupport() { + } + + /** whether this JDK can restrict key exchange to the post-quantum hybrid groups */ + public static boolean isPostQuantumKeyExchangeAvailable() { + return true; + } + + /** whether this JDK can apply named groups and signature schemes at all */ + public static boolean isNamedGroupsAvailable() { + return true; + } + + /** + * Validates the combination and sets the requested groups and schemes on the parameters. + * + * @return true when the parameters were changed + * @throws IllegalArgumentException when requirePostQuantumKeyExchange and namedGroups are both set + */ + public static boolean apply(SSLParameters parameters, String[] namedGroups, String[] signatureSchemes, boolean requirePostQuantumKeyExchange) { + checkCombination(namedGroups, requirePostQuantumKeyExchange); + boolean changed = false; + String[] groups = effectiveNamedGroups(namedGroups, requirePostQuantumKeyExchange); + if (groups != null) { + parameters.setNamedGroups(groups); + changed = true; + } + if (isSet(signatureSchemes)) { + parameters.setSignatureSchemes(signatureSchemes.clone()); + changed = true; + } + return changed; + } + + /** the groups a connection will request: the explicit list, the post-quantum list, or null for the JDK default */ + public static String[] effectiveNamedGroups(String[] namedGroups, boolean requirePostQuantumKeyExchange) { + if (requirePostQuantumKeyExchange) { + return POST_QUANTUM_NAMED_GROUPS.clone(); + } + return isSet(namedGroups) ? namedGroups.clone() : null; + } + + static void checkCombination(String[] namedGroups, boolean requirePostQuantumKeyExchange) { + if (requirePostQuantumKeyExchange && isSet(namedGroups)) { + throw new IllegalArgumentException("requirePostQuantumKeyExchange and namedGroups cannot both be set"); + } + } + + static boolean isSet(String[] values) { + return values != null && values.length > 0; + } +} diff --git a/activemq-http/pom.xml b/activemq-http/pom.xml index 40ff58ca4ad..d29b6290782 100644 --- a/activemq-http/pom.xml +++ b/activemq-http/pom.xml @@ -276,5 +276,33 @@ + + jdk27-plus + + [27,) + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-jdk27-test-source + generate-test-sources + + add-test-source + + + + src/test/java27 + + + + + + + + diff --git a/activemq-http/src/main/java/org/apache/activemq/transport/SecureSocketConnectorFactory.java b/activemq-http/src/main/java/org/apache/activemq/transport/SecureSocketConnectorFactory.java index 0b7f0b4094a..a889f73384c 100644 --- a/activemq-http/src/main/java/org/apache/activemq/transport/SecureSocketConnectorFactory.java +++ b/activemq-http/src/main/java/org/apache/activemq/transport/SecureSocketConnectorFactory.java @@ -17,9 +17,11 @@ package org.apache.activemq.transport; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; import org.apache.activemq.broker.SslContext; import org.apache.activemq.transport.http.BlockingQueueTransport; +import org.apache.activemq.transport.tcp.SslParameterSupport; import org.apache.activemq.util.IntrospectionSupport; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.HttpConfiguration; @@ -48,6 +50,9 @@ public class SecureSocketConnectorFactory extends SocketConnectorFactory { private String keyCertificateAlgorithm; private String protocol; private String auth; + private String[] namedGroups; + private String[] signatureSchemes; + private boolean requirePostQuantumKeyExchange; private SslContext context; private SslContextFactory.Server contextFactory; @@ -73,9 +78,12 @@ public Connector createConnector(Server server) throws Exception { // Get a reference to the current ssl context factory... + // fail at connector start, not on the first handshake, when this JDK cannot honour the request + SslParameterSupport.apply(new SSLParameters(), namedGroups, signatureSchemes, requirePostQuantumKeyExchange); + SslContextFactory.Server factory; if (contextFactory == null) { - factory = new SslContextFactory.Server(); + factory = new NamedGroupsSslContextFactory(); if (context != null) { // Should not be using this method since it does not use all of the values // from the passed SslContext instance..... @@ -319,4 +327,47 @@ public String getTrustStorePassword() { public void setTrustStorePassword(String trustStorePassword) { this.trustStorePassword = trustStorePassword; } + + public String[] getNamedGroups() { + return namedGroups; + } + + /** + * TLS named groups (key exchange algorithms) to offer, in preference order, + * for example {@code X25519MLKEM768,x25519}. Needs Java 21 or later. + */ + public void setNamedGroups(String[] namedGroups) { + this.namedGroups = namedGroups; + } + + public String[] getSignatureSchemes() { + return signatureSchemes; + } + + /** TLS signature schemes to offer, in preference order. Needs Java 21 or later. */ + public void setSignatureSchemes(String[] signatureSchemes) { + this.signatureSchemes = signatureSchemes; + } + + public boolean isRequirePostQuantumKeyExchange() { + return requirePostQuantumKeyExchange; + } + + /** + * Offer only the post-quantum hybrid key exchange groups of TLS 1.3, so a + * peer without them is refused. Needs Java 27 or later. + */ + public void setRequirePostQuantumKeyExchange(boolean requirePostQuantumKeyExchange) { + this.requirePostQuantumKeyExchange = requirePostQuantumKeyExchange; + } + + /** Jetty applies these parameters to every engine it creates for the connector. */ + private final class NamedGroupsSslContextFactory extends SslContextFactory.Server { + @Override + public SSLParameters customize(SSLParameters sslParams) { + SSLParameters customized = super.customize(sslParams); + SslParameterSupport.apply(customized, namedGroups, signatureSchemes, requirePostQuantumKeyExchange); + return customized; + } + } } diff --git a/activemq-http/src/test/java27/org/apache/activemq/transport/https/PostQuantumKeyExchangeJettyTest.java b/activemq-http/src/test/java27/org/apache/activemq/transport/https/PostQuantumKeyExchangeJettyTest.java new file mode 100644 index 00000000000..11acd0b8a6e --- /dev/null +++ b/activemq-http/src/test/java27/org/apache/activemq/transport/https/PostQuantumKeyExchangeJettyTest.java @@ -0,0 +1,136 @@ +/** + * 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.https; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.io.IOException; +import java.net.URI; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collection; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.apache.activemq.broker.BrokerService; +import org.apache.activemq.broker.TransportConnector; +import org.apache.activemq.spring.SpringSslContext; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/** + * The Jetty backed https and wss connectors honour requirePostQuantumKeyExchange + * through the SslContextFactory customize hook. Lives in src/test/java27, so it + * runs on JDK 27 and later only. + */ +@RunWith(Parameterized.class) +public class PostQuantumKeyExchangeJettyTest { + + private static final String[] CLASSICAL_ONLY = {"x25519", "secp256r1"}; + private static final String[] HYBRID_ONLY = {"X25519MLKEM768"}; + + @Parameterized.Parameters(name = "{0}") + public static Collection transports() { + return Arrays.asList(new Object[][] {{"https"}, {"wss"}}); + } + + @Parameterized.Parameter + public String transport; + + private BrokerService broker; + + @Before + public void setUp() throws Exception { + broker = new BrokerService(); + broker.setPersistent(false); + broker.setUseJmx(false); + broker.setAdvisorySupport(false); + SpringSslContext context = new SpringSslContext(); + context.setKeyStore("src/test/resources/server.keystore"); + context.setKeyStoreKeyPassword("password"); + context.setTrustStore("src/test/resources/client.keystore"); + context.setTrustStorePassword("password"); + context.afterPropertiesSet(); + broker.setSslContext(context); + } + + @After + public void tearDown() throws Exception { + broker.stop(); + broker.waitUntilStopped(); + } + + @Test(timeout = 60000) + public void requiredPostQuantumKeyExchangeRefusesClassicalOnlyClients() throws Exception { + TransportConnector connector = broker.addConnector(transport + "://127.0.0.1:0?transport.requirePostQuantumKeyExchange=true"); + broker.start(); + broker.waitUntilStarted(); + + 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 defaultConnectorStillAcceptsClassicalClients() throws Exception { + TransportConnector connector = broker.addConnector(transport + "://127.0.0.1:0"); + broker.start(); + broker.waitUntilStarted(); + + assertEquals("TLSv1.3", handshake(connector, CLASSICAL_ONLY)); + } + + private 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.getPublishableConnectURI(); + try (SSLSocket socket = (SSLSocket) context.getSocketFactory().createSocket(uri.getHost(), uri.getPort())) { + socket.setSoTimeout(10000); + if (clientNamedGroups != null) { + SSLParameters parameters = socket.getSSLParameters(); + // SSLParameters.setNamedGroups exists since Java 20; this module compiles for 17 + SSLParameters.class.getMethod("setNamedGroups", String[].class).invoke(parameters, (Object) clientNamedGroups); + socket.setSSLParameters(parameters); + } + socket.startHandshake(); + return socket.getSession().getProtocol(); + } + } + + 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]; + } + } +} diff --git a/activemq-unit-tests/pom.xml b/activemq-unit-tests/pom.xml index cb66259eda6..9b60559bc1c 100644 --- a/activemq-unit-tests/pom.xml +++ b/activemq-unit-tests/pom.xml @@ -1254,5 +1254,33 @@ + + jdk27-plus + + [27,) + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-jdk27-test-source + generate-test-sources + + add-test-source + + + + src/test/java27 + + + + + + + + diff --git a/activemq-unit-tests/src/test/java/org/apache/activemq/transport/tcp/SslNamedGroupOptionsTest.java b/activemq-unit-tests/src/test/java/org/apache/activemq/transport/tcp/SslNamedGroupOptionsTest.java new file mode 100644 index 00000000000..30f5b8bdb9f --- /dev/null +++ b/activemq-unit-tests/src/test/java/org/apache/activemq/transport/tcp/SslNamedGroupOptionsTest.java @@ -0,0 +1,154 @@ +/** + * 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.tcp; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.lang.management.ManagementFactory; +import java.util.Arrays; +import java.util.Collection; + +import javax.management.JMX; +import javax.management.ObjectName; + +import org.apache.activemq.broker.BrokerService; +import org.apache.activemq.broker.TransportConnector; +import org.apache.activemq.broker.jmx.BrokerMBeanSupport; +import org.apache.activemq.broker.jmx.ConnectorViewMBean; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/** + * A connector with a key exchange setting the running JDK cannot honour must + * refuse to start with a message naming the JDK needed, on the blocking and the + * NIO ssl transports alike; on a JDK that can, it starts. + */ +@RunWith(Parameterized.class) +public class SslNamedGroupOptionsTest { + + private static final int JAVA = Runtime.version().feature(); + + @Parameterized.Parameters(name = "{0}") + public static Collection transports() { + return Arrays.asList(new Object[][] {{"ssl"}, {"nio+ssl"}}); + } + + @Parameterized.Parameter + public String transport; + + private BrokerService broker; + + @Before + public void setUp() { + System.setProperty("javax.net.ssl.keyStore", SslTransportBrokerTest.SERVER_KEYSTORE); + System.setProperty("javax.net.ssl.keyStorePassword", SslTransportBrokerTest.PASSWORD); + System.setProperty("javax.net.ssl.keyStoreType", SslTransportBrokerTest.KEYSTORE_TYPE); + System.setProperty("javax.net.ssl.trustStore", SslTransportBrokerTest.TRUST_KEYSTORE); + System.setProperty("javax.net.ssl.trustStorePassword", SslTransportBrokerTest.PASSWORD); + System.setProperty("javax.net.ssl.trustStoreType", SslTransportBrokerTest.KEYSTORE_TYPE); + broker = new BrokerService(); + broker.setPersistent(false); + broker.setUseJmx(false); + } + + @After + public void tearDown() throws Exception { + broker.stop(); + broker.waitUntilStopped(); + } + + @Test + public void requirePostQuantumKeyExchangeNeedsJava27() throws Exception { + String uri = transport + "://localhost:0?transport.requirePostQuantumKeyExchange=true"; + if (JAVA >= 27) { + assertNotNull(broker.addConnector(uri)); + broker.start(); + assertTrue(broker.isStarted()); + } else { + assertRefused(uri, "Java 27"); + } + } + + @Test + public void namedGroupsNeedJava21() throws Exception { + String uri = transport + "://localhost:0?transport.namedGroups=x25519,secp256r1"; + if (JAVA >= 21) { + assertNotNull(broker.addConnector(uri)); + broker.start(); + assertTrue(broker.isStarted()); + } else { + assertRefused(uri, "Java 21"); + } + } + + @Test + public void jmxShowsTheConfiguredValues() throws Exception { + broker.setUseJmx(true); + broker.getManagementContext().setCreateConnector(false); + TransportConnector plain = broker.addConnector(transport + "://localhost:0"); + TransportConnector configured = JAVA >= 21 + ? broker.addConnector(transport + "://localhost:0?transport.namedGroups=x25519,secp256r1&transport.signatureSchemes=ed25519,rsa_pss_rsae_sha256") + : null; + TransportConnector postQuantum = JAVA >= 27 + ? broker.addConnector(transport + "://localhost:0?transport.requirePostQuantumKeyExchange=true") + : null; + broker.start(); + + ConnectorViewMBean plainView = view(plain); + assertNull(plainView.getNamedGroups()); + assertNull(plainView.getSignatureSchemes()); + assertFalse(plainView.isRequirePostQuantumKeyExchange()); + if (configured != null) { + ConnectorViewMBean view = view(configured); + assertArrayEquals(new String[] {"x25519", "secp256r1"}, view.getNamedGroups()); + assertArrayEquals(new String[] {"ed25519", "rsa_pss_rsae_sha256"}, view.getSignatureSchemes()); + assertFalse(view.isRequirePostQuantumKeyExchange()); + } + if (postQuantum != null) { + ConnectorViewMBean view = view(postQuantum); + assertTrue(view.isRequirePostQuantumKeyExchange()); + assertNull("the switch, not an explicit list", view.getNamedGroups()); + } + } + + private ConnectorViewMBean view(TransportConnector connector) throws Exception { + ObjectName name = BrokerMBeanSupport.createConnectorName(broker.getBrokerObjectName().toString(), "clientConnectors", connector.getName()); + return JMX.newMBeanProxy(ManagementFactory.getPlatformMBeanServer(), name, ConnectorViewMBean.class); + } + + @Test + public void requireAndNamedGroupsTogetherAreRefused() { + assertRefused(transport + "://localhost:0?transport.requirePostQuantumKeyExchange=true&transport.namedGroups=x25519", "cannot both be set"); + } + + private void assertRefused(String uri, String expectedText) { + Exception thrown = assertThrows(Exception.class, () -> broker.addConnector(uri)); + String messages = ""; + for (Throwable t = thrown; t != null; t = t.getCause()) { + messages += t.getMessage() + " | "; + } + assertTrue(messages, messages.contains(expectedText)); + } +} diff --git a/activemq-unit-tests/src/test/java/org/apache/activemq/transport/tcp/SslParameterSupportTest.java b/activemq-unit-tests/src/test/java/org/apache/activemq/transport/tcp/SslParameterSupportTest.java new file mode 100644 index 00000000000..60658625df5 --- /dev/null +++ b/activemq-unit-tests/src/test/java/org/apache/activemq/transport/tcp/SslParameterSupportTest.java @@ -0,0 +1,98 @@ +/** + * 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.tcp; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import javax.net.ssl.SSLParameters; + +import org.junit.Test; + +/** + * Runs against the activemq-client jar, so the multi-release variant matching + * the running JDK is the one under test: 17 to 20 refuse everything, 21 to 26 + * apply groups and schemes, 27 and later also honour the post-quantum switch. + * The getters are read reflectively because this module compiles for Java 17. + */ +public class SslParameterSupportTest { + + private static final int JAVA = Runtime.version().feature(); + + @Test + public void availabilityFollowsTheRunningJdk() { + assertEquals(JAVA >= 21, SslParameterSupport.isNamedGroupsAvailable()); + assertEquals(JAVA >= 27, SslParameterSupport.isPostQuantumKeyExchangeAvailable()); + } + + @Test + public void nothingRequestedChangesNothing() { + var parameters = new SSLParameters(); + assertFalse(SslParameterSupport.apply(parameters, null, null, false)); + assertFalse(SslParameterSupport.apply(parameters, new String[0], new String[0], false)); + } + + @Test + public void requireAndNamedGroupsTogetherAreRejectedOnEveryJdk() { + var thrown = assertThrows(IllegalArgumentException.class, + () -> SslParameterSupport.apply(new SSLParameters(), new String[] {"x25519"}, null, true)); + assertTrue(thrown.getMessage(), thrown.getMessage().contains("cannot both be set")); + } + + @Test + public void namedGroupsAndSignatureSchemes() throws Exception { + var parameters = new SSLParameters(); + String[] groups = {"x25519", "secp384r1"}; + String[] schemes = {"ed25519", "rsa_pss_rsae_sha256"}; + if (JAVA >= 21) { + assertTrue(SslParameterSupport.apply(parameters, groups, schemes, false)); + assertArrayEquals(groups, read(parameters, "getNamedGroups")); + assertArrayEquals(schemes, read(parameters, "getSignatureSchemes")); + } else { + var thrown = assertThrows(IllegalStateException.class, () -> SslParameterSupport.apply(parameters, groups, schemes, false)); + assertTrue(thrown.getMessage(), thrown.getMessage().contains("Java 21")); + } + } + + @Test + public void requirePostQuantumKeyExchange() throws Exception { + var parameters = new SSLParameters(); + if (JAVA >= 27) { + assertTrue(SslParameterSupport.apply(parameters, null, null, true)); + assertArrayEquals(SslParameterSupport.POST_QUANTUM_NAMED_GROUPS, read(parameters, "getNamedGroups")); + } else { + var thrown = assertThrows(IllegalStateException.class, () -> SslParameterSupport.apply(parameters, null, null, true)); + assertTrue(thrown.getMessage(), thrown.getMessage().contains("Java 27")); + } + } + + @Test + public void effectiveNamedGroupsDescribeWhatWillBeRequested() { + assertNull(SslParameterSupport.effectiveNamedGroups(null, false)); + assertArrayEquals(new String[] {"x25519"}, SslParameterSupport.effectiveNamedGroups(new String[] {"x25519"}, false)); + assertArrayEquals(SslParameterSupport.POST_QUANTUM_NAMED_GROUPS, SslParameterSupport.effectiveNamedGroups(null, true)); + assertEquals("X25519MLKEM768", SslParameterSupport.POST_QUANTUM_NAMED_GROUPS[0]); + } + + private static String[] read(SSLParameters parameters, String getter) throws Exception { + return (String[]) SSLParameters.class.getMethod(getter).invoke(parameters); + } +} diff --git a/assembly/src/release/conf/activemq.xml b/assembly/src/release/conf/activemq.xml index 551bdb4a153..4d3a28d8cd5 100644 --- a/assembly/src/release/conf/activemq.xml +++ b/assembly/src/release/conf/activemq.xml @@ -180,7 +180,14 @@ Additional transports are disabled by default to reduce the exposed attack surface. Uncomment only the protocols you actually need, and prefer the secured variants (openwire+ssl, amqp+ssl, stomp+ssl, mqtt+nio+ssl, wss) in production deployments. + + On Java 27 and later TLS 1.3 negotiates the post-quantum hybrid key exchange + X25519MLKEM768 by default whenever the client supports it. To refuse clients that + cannot, add transport.requirePostQuantumKeyExchange=true to an ssl connector, or + choose the groups yourself with transport.namedGroups=X25519MLKEM768,SecP256r1MLKEM768 + (Java 21 or later for namedGroups). Both settings need TLS 1.3 on the connection. --> +