ARTEMIS-4245 Expose web SNI settings

This commit is contained in:
Domenico Francesco Bruscino 2023-04-19 19:02:38 +02:00 committed by Robbie Gemmell
parent 589157ca5f
commit d2a4837b69
7 changed files with 208 additions and 15 deletions

View File

@ -69,6 +69,12 @@ public class BindingDTO {
@XmlAttribute
private String trustStorePassword;
@XmlAttribute
private Boolean sniHostCheck;
@XmlAttribute
private Boolean sniRequired;
public String getKeyStorePassword() throws Exception {
return getPassword(this.keyStorePassword);
}
@ -181,6 +187,22 @@ public class BindingDTO {
apps.add(app);
}
public Boolean getSniHostCheck() {
return sniHostCheck;
}
public void setSniHostCheck(Boolean sniHostCheck) {
this.sniHostCheck = sniHostCheck;
}
public Boolean getSniRequired() {
return sniRequired;
}
public void setSniRequired(Boolean sniRequired) {
this.sniRequired = sniRequired;
}
public BindingDTO() {
apps = new ArrayList<>();
}

View File

@ -47,6 +47,8 @@ public class WebServerDTOTest {
Assert.assertNull(defaultBinding.getExcludedCipherSuites());
Assert.assertNull(defaultBinding.getKeyStorePassword());
Assert.assertNull(defaultBinding.getTrustStorePassword());
Assert.assertNull(defaultBinding.getSniHostCheck());
Assert.assertNull(defaultBinding.getSniRequired());
}
@Test

View File

@ -137,5 +137,38 @@
<artifactId>jakarta.json-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-security-resources</id>
<phase>validate</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.testOutputDirectory}</outputDirectory>
<resources>
<resource>
<directory>../tests/security-resources</directory>
<includes>
<include>server-keystore.p12</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -64,6 +64,10 @@ public class WebServerComponent implements ExternalComponent, WebServerComponent
// this should match the value of <display-name> in the console war's WEB-INF/web.xml
public static final String WEB_CONSOLE_DISPLAY_NAME = System.getProperty("org.apache.activemq.artemis.webConsoleDisplayName", "hawtio");
public static final boolean DEFAULT_SNI_HOST_CHECK_VALUE = true;
public static final boolean DEFAULT_SNI_REQUIRED_VALUE = false;
private Server server;
private HandlerList handlers;
private WebServerDTO webServerConfig;
@ -245,7 +249,10 @@ public class WebServerComponent implements ExternalComponent, WebServerComponent
SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslFactory, "HTTP/1.1");
httpConfiguration.addCustomizer(new SecureRequestCustomizer());
SecureRequestCustomizer secureRequestCustomizer = new SecureRequestCustomizer();
secureRequestCustomizer.setSniHostCheck(binding.getSniHostCheck() != null ? binding.getSniHostCheck() : DEFAULT_SNI_HOST_CHECK_VALUE);
secureRequestCustomizer.setSniRequired(binding.getSniRequired() != null ? binding.getSniRequired() : DEFAULT_SNI_REQUIRED_VALUE);
httpConfiguration.addCustomizer(secureRequestCustomizer);
httpConfiguration.setSendServerVersion(false);
HttpConnectionFactory httpFactory = new HttpConnectionFactory(httpConfiguration);

View File

@ -16,8 +16,11 @@
*/
package org.apache.activemq.cli.test;
import javax.net.ssl.SNIHostName;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
@ -72,6 +75,19 @@ import org.apache.activemq.artemis.dto.BindingDTO;
import org.apache.activemq.artemis.dto.BrokerDTO;
import org.apache.activemq.artemis.dto.WebServerDTO;
import org.apache.activemq.artemis.utils.ThreadLeakCheckRule;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.conn.routing.HttpRoutePlanner;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.DefaultRoutePlanner;
import org.apache.http.protocol.HttpContext;
import org.apache.http.ssl.SSLContextBuilder;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.webapp.WebInfConfiguration;
@ -88,6 +104,10 @@ public class WebServerComponentTest extends Assert {
static final String URL = System.getProperty("url", "http://localhost:8161/WebServerComponentTest.txt");
static final String SECURE_URL = System.getProperty("url", "https://localhost:8448/WebServerComponentTest.txt");
static final String KEY_STORE_PATH = WebServerComponentTest.class.getClassLoader().getResource("server-keystore.p12").getFile();
static final String KEY_STORE_PASSWORD = "securepass";
private List<ActiveMQComponent> testedComponents;
@Before
@ -215,12 +235,17 @@ public class WebServerComponentTest extends Assert {
Assert.assertFalse(webServerComponent.isStarted());
}
@Test
public void simpleSecureServer() throws Exception {
private WebServerComponent startSimpleSecureServer(Boolean sniHostCheck, Boolean sniRequired) throws Exception {
BindingDTO bindingDTO = new BindingDTO();
bindingDTO.uri = "https://localhost:0";
bindingDTO.keyStorePath = "./src/test/resources/server.keystore";
bindingDTO.setKeyStorePassword("password");
bindingDTO.setUri("https://localhost:0");
bindingDTO.setKeyStorePath(KEY_STORE_PATH);
bindingDTO.setKeyStorePassword(KEY_STORE_PASSWORD);
if (sniHostCheck != null) {
bindingDTO.setSniHostCheck(sniHostCheck);
}
if (sniRequired != null) {
bindingDTO.setSniRequired(sniRequired);
}
if (System.getProperty("java.vendor").contains("IBM")) {
//By default on IBM Java 8 JVM, org.eclipse.jetty.util.ssl.SslContextFactory doesn't include TLSv1.2
// while it excludes all TLSv1 and TLSv1.1 cipher suites.
@ -240,14 +265,21 @@ public class WebServerComponentTest extends Assert {
webServerComponent.configure(webServerDTO, "./src/test/resources/", "./src/test/resources/");
testedComponents.add(webServerComponent);
webServerComponent.start();
return webServerComponent;
}
@Test
public void simpleSecureServer() throws Exception {
WebServerComponent webServerComponent = startSimpleSecureServer(null, null);
final int port = webServerComponent.getPort();
// Make the connection attempt.
SSLContext context = new SSLSupport()
.setKeystorePath(bindingDTO.keyStorePath)
.setKeystorePassword(bindingDTO.getKeyStorePassword())
.setTruststorePath(bindingDTO.keyStorePath)
.setTruststorePassword(bindingDTO.getKeyStorePassword())
.setKeystorePath(KEY_STORE_PATH)
.setKeystorePassword(KEY_STORE_PASSWORD)
.setTruststorePath(KEY_STORE_PATH)
.setTruststorePassword(KEY_STORE_PASSWORD)
.createContext();
SSLEngine engine = context.createSSLEngine();
@ -283,15 +315,107 @@ public class WebServerComponentTest extends Assert {
Assert.assertFalse(webServerComponent.isStarted());
}
@Test
public void testSimpleSecureServerWithSniHostCheckEnabled() throws Exception {
testSimpleSecureServerWithSniHostCheck(true);
}
@Test
public void testSimpleSecureServerWithSniHostCheckDisabled() throws Exception {
testSimpleSecureServerWithSniHostCheck(false);
}
@Test
public void testSimpleSecureServerWithSniHostCheckDefault() throws Exception {
testSimpleSecureServerWithSniHostCheck(null);
}
private void testSimpleSecureServerWithSniHostCheck(Boolean enabled) throws Exception {
WebServerComponent webServerComponent = startSimpleSecureServer(enabled, null);
try {
int port = webServerComponent.getPort(0);
Assert.assertEquals(200, testSimpleSecureServer("localhost", port, "localhost", null));
Assert.assertEquals(200, testSimpleSecureServer("localhost", port, "127.0.0.1", null));
Assert.assertEquals(enabled == null || enabled ? 400 : 200, testSimpleSecureServer("localhost", port, "artemis", null));
} finally {
webServerComponent.stop(true);
}
}
@Test
public void testSimpleSecureServerWithSniRequiredEnabled() throws Exception {
testSimpleSecureServerWithSniRequired(true);
}
@Test
public void testSimpleSecureServerWithSniRequiredDisabled() throws Exception {
testSimpleSecureServerWithSniRequired(false);
}
@Test
public void testSimpleSecureServerWithSniRequiredDefault() throws Exception {
testSimpleSecureServerWithSniRequired(null);
}
private void testSimpleSecureServerWithSniRequired(Boolean enabled) throws Exception {
WebServerComponent webServerComponent = startSimpleSecureServer(null, enabled);
try {
int port = webServerComponent.getPort(0);
Assert.assertEquals(200, testSimpleSecureServer("localhost", port, null, "localhost"));
Assert.assertEquals(200, testSimpleSecureServer("localhost", port, null, "127.0.0.1"));
Assert.assertEquals(enabled != null && enabled ? 400 : 200, testSimpleSecureServer("localhost", port, null, null));
Assert.assertEquals(enabled != null && enabled ? 400 : 200, testSimpleSecureServer("localhost", port, null, "artemis"));
} finally {
webServerComponent.stop(true);
}
}
private int testSimpleSecureServer(String webServerHostname, int webServerPort, String requestHostname, String sniHostname) throws Exception {
HttpGet request = new HttpGet("https://" + (requestHostname != null ? requestHostname : webServerHostname) + ":" + webServerPort + "/WebServerComponentTest.txt");
HttpHost webServerHost = HttpHost.create("https://" + webServerHostname + ":" + webServerPort);
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, (certificate, authType) -> true).build();
SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier()) {
@Override
protected void prepareSocket(SSLSocket socket) throws IOException {
super.prepareSocket(socket);
if (sniHostname != null) {
SSLParameters sslParameters = socket.getSSLParameters();
sslParameters.setServerNames(Collections.singletonList(new SNIHostName(sniHostname)));
socket.setSSLParameters(sslParameters);
}
}
};
HttpRoutePlanner httpRoutePlanner = new DefaultRoutePlanner(null) {
@Override
public HttpRoute determineRoute(HttpHost host,
org.apache.http.HttpRequest request,
HttpContext context) throws HttpException {
HttpRoute baseHttpRoute = super.determineRoute(host, request, context);
return new HttpRoute(webServerHost, baseHttpRoute.getLocalAddress(), baseHttpRoute.isSecure());
}
};
try (CloseableHttpClient client = HttpClientBuilder.create().setRoutePlanner(httpRoutePlanner).
setSSLSocketFactory(sslConnectionSocketFactory).setSSLHostnameVerifier(new NoopHostnameVerifier()).build()) {
try (CloseableHttpResponse response = client.execute(request)) {
return response.getStatusLine().getStatusCode();
}
}
}
@Test
public void simpleSecureServerWithClientAuth() throws Exception {
BindingDTO bindingDTO = new BindingDTO();
bindingDTO.uri = "https://localhost:0";
bindingDTO.keyStorePath = "./src/test/resources/server.keystore";
bindingDTO.setKeyStorePassword("password");
bindingDTO.clientAuth = true;
bindingDTO.trustStorePath = "./src/test/resources/server.keystore";
bindingDTO.setTrustStorePassword("password");
bindingDTO.setKeyStorePath(KEY_STORE_PATH);
bindingDTO.setKeyStorePassword(KEY_STORE_PASSWORD);
bindingDTO.setClientAuth(true);
bindingDTO.setTrustStorePath(KEY_STORE_PATH);
bindingDTO.setTrustStorePassword(KEY_STORE_PASSWORD);
if (System.getProperty("java.vendor").contains("IBM")) {
//By default on IBM Java 8 JVM, org.eclipse.jetty.util.ssl.SslContextFactory doesn't include TLSv1.2
// while it excludes all TLSv1 and TLSv1.1 cipher suites.

View File

@ -20,6 +20,7 @@ import java.util.Properties;
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration;
import org.apache.activemq.artemis.api.core.JsonUtil;
import org.apache.activemq.artemis.component.WebServerComponent;
import org.apache.activemq.artemis.core.config.Configuration;
import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl;
import org.apache.activemq.artemis.dto.AppDTO;
@ -97,6 +98,8 @@ public class WebServerDTOConfigTest {
properties.put(ActiveMQDefaultConfiguration.getDefaultSystemWebPropertyPrefix() + "bindings." + bindingName + ".excludedCipherSuites", "test-excludedCipherSuites,3");
properties.put(ActiveMQDefaultConfiguration.getDefaultSystemWebPropertyPrefix() + "bindings." + bindingName + ".keyStorePassword", "test-keyStorePassword");
properties.put(ActiveMQDefaultConfiguration.getDefaultSystemWebPropertyPrefix() + "bindings." + bindingName + ".trustStorePassword", "test-trustStorePassword");
properties.put(ActiveMQDefaultConfiguration.getDefaultSystemWebPropertyPrefix() + "bindings." + bindingName + ".sniHostCheck", !WebServerComponent.DEFAULT_SNI_HOST_CHECK_VALUE);
properties.put(ActiveMQDefaultConfiguration.getDefaultSystemWebPropertyPrefix() + "bindings." + bindingName + ".sniRequired", !WebServerComponent.DEFAULT_SNI_REQUIRED_VALUE);
properties.put(ActiveMQDefaultConfiguration.getDefaultSystemWebPropertyPrefix() + "bindings." + bindingName + "." + INVALID_ATTRIBUTE_NAME, "true");
Configuration configuration = new ConfigurationImpl();
String systemWebPropertyPrefix = ActiveMQDefaultConfiguration.getDefaultSystemWebPropertyPrefix();
@ -114,6 +117,8 @@ public class WebServerDTOConfigTest {
Assert.assertEquals("test-excludedCipherSuites,3", String.join(",", testBinding.getExcludedCipherSuites()));
Assert.assertEquals("test-keyStorePassword", testBinding.getKeyStorePassword());
Assert.assertEquals("test-trustStorePassword", testBinding.getTrustStorePassword());
Assert.assertEquals(!WebServerComponent.DEFAULT_SNI_HOST_CHECK_VALUE, testBinding.getSniRequired());
Assert.assertEquals(!WebServerComponent.DEFAULT_SNI_REQUIRED_VALUE, testBinding.getSniHostCheck());
testStatus(configuration.getStatus(), "system-" + systemWebPropertyPrefix, "bindings." + bindingName + ".");
}