This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/netty-server in repository https://gitbox.apache.org/repos/asf/cxf.git
commit c3c9f2de27280fc978af9de69d0defa4a5668109 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Mon Aug 31 10:42:56 2026 +0100 Wire through the TLS server parameter for Netty --- .../server/NettyHttpServletPipelineFactory.java | 10 ++ .../NettyHttpServletPipelineFactoryTest.java | 114 +++++++++++++++++++++ .../org/apache/cxf/transport/https/SSLUtils.java | 65 +++++++++++- .../apache/cxf/transport/https/SSLUtilsTest.java | 58 +++++++++++ 4 files changed, 246 insertions(+), 1 deletion(-) diff --git a/rt/transports/http-netty/netty-server/src/main/java/org/apache/cxf/transport/http/netty/server/NettyHttpServletPipelineFactory.java b/rt/transports/http-netty/netty-server/src/main/java/org/apache/cxf/transport/http/netty/server/NettyHttpServletPipelineFactory.java index 8b7f84d5c65..4134c3f44f7 100644 --- a/rt/transports/http-netty/netty-server/src/main/java/org/apache/cxf/transport/http/netty/server/NettyHttpServletPipelineFactory.java +++ b/rt/transports/http-netty/netty-server/src/main/java/org/apache/cxf/transport/http/netty/server/NettyHttpServletPipelineFactory.java @@ -233,6 +233,7 @@ public class NettyHttpServletPipelineFactory extends ChannelInitializer<Channel> private SslContext configureServerHttp2SSLOnDemand() throws Exception { if (tlsServerParameters != null) { final SSLContextInitParameters initParams = SSLUtils.getSSLContextInitParameters(tlsServerParameters); + final SSLContext sslContext = SSLUtils.getSSLContext(tlsServerParameters); // Use only JDK provider for now, leaving OpenSsl as an option final SslProvider provider = SslProvider.JDK; @@ -264,6 +265,15 @@ public class NettyHttpServletPipelineFactory extends ChannelInitializer<Channel> ApplicationProtocolNames.HTTP_2, ApplicationProtocolNames.HTTP_1_1 )); + + if (!tlsServerParameters.getIncludeProtocols().isEmpty() + || !tlsServerParameters.getExcludeProtocols().isEmpty()) { + builder.protocols(SSLUtils.getProtocolsToInclude( + tlsServerParameters.getIncludeProtocols(), + tlsServerParameters.getExcludeProtocols(), + sslContext.getDefaultSSLParameters().getProtocols(), + sslContext.getSupportedSSLParameters().getProtocols())); + } final TrustManager[] trustManagers = initParams.getTrustManagers(); if (trustManagers != null && trustManagers.length > 0) { diff --git a/rt/transports/http-netty/netty-server/src/test/java/org/apache/cxf/transport/http/netty/server/NettyHttpServletPipelineFactoryTest.java b/rt/transports/http-netty/netty-server/src/test/java/org/apache/cxf/transport/http/netty/server/NettyHttpServletPipelineFactoryTest.java new file mode 100644 index 00000000000..47bac891f07 --- /dev/null +++ b/rt/transports/http-netty/netty-server/src/test/java/org/apache/cxf/transport/http/netty/server/NettyHttpServletPipelineFactoryTest.java @@ -0,0 +1,114 @@ +/** + * 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.netty.server; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.KeyStore; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; + +import org.apache.cxf.configuration.jsse.TLSServerParameters; + +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.handler.ssl.SslContext; +import io.netty.util.concurrent.ImmediateEventExecutor; + +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class NettyHttpServletPipelineFactoryTest { + @Test + public void testHttp2UsesIncludedProtocols() throws Exception { + TLSServerParameters parameters = serverParameters(); + parameters.setIncludeProtocols(Collections.singletonList("TLSv1.2")); + + SslContext context = createHttp2Context(parameters); + + assertArrayEquals(new String[] {"TLSv1.2"}, context.newEngine( + UnpooledByteBufAllocator.DEFAULT).getEnabledProtocols()); + } + + @Test + public void testHttp2UsesExcludedProtocols() throws Exception { + TLSServerParameters parameters = serverParameters(); + parameters.setExcludeProtocols(Collections.singletonList("TLSv1.2")); + + SslContext context = createHttp2Context(parameters); + + assertFalse(Arrays.asList(context.newEngine( + UnpooledByteBufAllocator.DEFAULT).getEnabledProtocols()).contains("TLSv1.2")); + } + + @Test + public void testHttp2PreservesUnconstrainedDefaults() throws Exception { + TLSServerParameters parameters = serverParameters(); + + SslContext context = createHttp2Context(parameters); + + assertArrayEquals(SSLContext.getDefault().getDefaultSSLParameters().getProtocols(), + context.newEngine(UnpooledByteBufAllocator.DEFAULT).getEnabledProtocols()); + } + + @Test + public void testHttp2RejectsUnsatisfiableProtocols() throws Exception { + TLSServerParameters parameters = serverParameters(); + parameters.setIncludeProtocols(Collections.singletonList("NoSuchProtocol")); + + try { + createHttp2Context(parameters); + fail("Expected protocol configuration to be rejected"); + } catch (InvocationTargetException ex) { + assertTrue(ex.getCause() instanceof java.security.GeneralSecurityException); + } + } + + private static TLSServerParameters serverParameters() throws Exception { + TLSServerParameters parameters = new TLSServerParameters(); + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(NettyHttpServletPipelineFactoryTest.class.getResourceAsStream("/keys/clientstore.jks"), + "cspass".toCharArray()); + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance( + KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, "cspass".toCharArray()); + KeyManager[] keyManagers = keyManagerFactory.getKeyManagers(); + parameters.setKeyManagers(keyManagers); + return parameters; + } + + private static SslContext createHttp2Context(TLSServerParameters parameters) throws Exception { + NettyHttpServletPipelineFactory factory = new NettyHttpServletPipelineFactory( + parameters, false, 1024, new HashMap<>(), new NettyHttpServerEngine(), + ImmediateEventExecutor.INSTANCE, true); + Method method = NettyHttpServletPipelineFactory.class + .getDeclaredMethod("configureServerHttp2SSLOnDemand"); + method.setAccessible(true); + return (SslContext) method.invoke(factory); + } +} 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 f1085557b8a..cee024b09a6 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 @@ -26,6 +26,7 @@ import java.security.Principal; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.logging.Logger; @@ -68,6 +69,38 @@ public final class SSLUtils { //Helper class } + public static String[] getProtocolsToInclude(List<String> includeProtocols, + List<String> excludeProtocols, + String[] defaultProtocols, + String[] supportedProtocols) + throws GeneralSecurityException { + boolean hasInclude = includeProtocols != null && !includeProtocols.isEmpty(); + boolean hasExclude = excludeProtocols != null && !excludeProtocols.isEmpty(); + if (!hasInclude && !hasExclude) { + return defaultProtocols; + } + + List<String> protocols = new ArrayList<>(); + if (hasInclude) { + for (String supported : supportedProtocols) { + if (includeProtocols.contains(supported)) { + protocols.add(supported); + } + } + } else { + protocols.addAll(Arrays.asList(defaultProtocols)); + } + if (hasExclude) { + protocols.removeAll(excludeProtocols); + } + if (protocols.isEmpty()) { + throw new GeneralSecurityException( + "No TLS protocol remains enabled after applying the configured" + + " includeProtocols/excludeProtocols constraints"); + } + return protocols.toArray(new String[0]); + } + public static HostnameVerifier getHostnameVerifier(TLSClientParameters tlsClientParameters) { HostnameVerifier verifier; @@ -173,7 +206,37 @@ public final class SSLUtils { SSLEngine serverEngine = sslContext.createSSLEngine(); serverEngine.setUseClientMode(false); - serverEngine.setNeedClientAuth(parameters.getClientAuthentication().isRequired()); + + List<String> includeProtocols = parameters.getIncludeProtocols(); + List<String> excludeProtocols = parameters.getExcludeProtocols(); + if (!includeProtocols.isEmpty() || !excludeProtocols.isEmpty()) { + serverEngine.setEnabledProtocols( + getProtocolsToInclude( + includeProtocols, + excludeProtocols, + serverEngine.getEnabledProtocols(), + serverEngine.getSupportedProtocols())); + } + + String[] cipherSuites = + org.apache.cxf.configuration.jsse.SSLUtils.getCiphersuitesToInclude( + parameters.getCipherSuites(), + parameters.getCipherSuitesFilter(), + serverEngine.getEnabledCipherSuites(), + serverEngine.getSupportedCipherSuites(), + LOG); + serverEngine.setEnabledCipherSuites(cipherSuites); + + org.apache.cxf.configuration.security.ClientAuthentication clientAuth = + parameters.getClientAuthentication(); + if (clientAuth != null) { + if (clientAuth.isSetWant()) { + serverEngine.setWantClientAuth(clientAuth.isWant()); + } + if (clientAuth.isSetRequired()) { + serverEngine.setNeedClientAuth(clientAuth.isRequired()); + } + } return serverEngine; } diff --git a/rt/transports/http/src/test/java/org/apache/cxf/transport/https/SSLUtilsTest.java b/rt/transports/http/src/test/java/org/apache/cxf/transport/https/SSLUtilsTest.java index 4c56afd0f50..b9bff35aec1 100644 --- a/rt/transports/http/src/test/java/org/apache/cxf/transport/https/SSLUtilsTest.java +++ b/rt/transports/http/src/test/java/org/apache/cxf/transport/https/SSLUtilsTest.java @@ -19,10 +19,13 @@ package org.apache.cxf.transport.https; +import java.security.GeneralSecurityException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.Arrays; import java.util.Collections; +import java.util.List; import javax.net.ssl.ExtendedSSLSession; import javax.net.ssl.HostnameVerifier; @@ -30,6 +33,8 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.X509TrustManager; +import org.apache.cxf.configuration.jsse.TLSServerParameters; +import org.apache.cxf.configuration.security.ClientAuthentication; import org.apache.cxf.transport.https.SSLUtils.SSLEngineWrapper; import org.apache.cxf.transport.https.SSLUtils.X509TrustManagerWrapper; @@ -51,7 +56,42 @@ import static org.mockito.Mockito.when; public class SSLUtilsTest { + private SSLEngine engine; + + @Test + public void testProtocolConstraints() throws Exception { + String[] protocols = SSLUtils.getProtocolsToInclude( + Arrays.asList("TLSv1.3", "TLSv1"), + Collections.singletonList("TLSv1.3"), + new String[] {"TLSv1.3", "TLSv1.2"}, + new String[] {"TLSv1", "TLSv1.2", "TLSv1.3"}); + + assertThat(protocols, is(new String[] {"TLSv1"})); + } + + @Test(expected = GeneralSecurityException.class) + public void testProtocolConstraintsRejectEmptyResult() throws Exception { + SSLUtils.getProtocolsToInclude( + Collections.singletonList("TLSv1.3"), + Collections.emptyList(), + new String[] {"TLSv1.2"}, + new String[] {"TLSv1.2"}); + } + + @Test + public void testProtocolConstraintsUseConfiguredContext() throws Exception { + SSLContext context = SSLContext.getInstance("TLSv1.2"); + context.init(null, null, null); + + String[] protocols = SSLUtils.getProtocolsToInclude( + Collections.emptyList(), + Collections.emptyList(), + context.getDefaultSSLParameters().getProtocols(), + context.getSupportedSSLParameters().getProtocols()); + + assertThat(protocols, is(new String[] {"TLSv1.2"})); + } @Before public void setUp() throws NoSuchAlgorithmException { @@ -125,4 +165,22 @@ public class SSLUtilsTest { wrapper.checkServerTrusted(new X509Certificate[0], "RSA", mockEngine); verify(passingVerifier).verify(eq("service.example.com"), any()); } + + @Test + public void testServerEngineHonorsConfiguredConstraints() throws Exception { + TLSServerParameters parameters = new TLSServerParameters(); + parameters.setExcludeProtocols(Arrays.asList("SSLv3", "TLSv1", "TLSv1.1")); + ClientAuthentication clientAuth = new ClientAuthentication(); + clientAuth.setWant(Boolean.TRUE); + parameters.setClientAuthentication(clientAuth); + + SSLEngine serverEngine = SSLUtils.createServerSSLEngine(parameters); + + assertThat(serverEngine.getUseClientMode(), is(false)); + assertThat(serverEngine.getWantClientAuth(), is(true)); + List<String> enabled = Arrays.asList(serverEngine.getEnabledProtocols()); + assertThat(enabled.contains("SSLv3"), is(false)); + assertThat(enabled.contains("TLSv1"), is(false)); + assertThat(enabled.contains("TLSv1.1"), is(false)); + } }
