From cfbe06f3bc4173fc19324de1d5b811067e2d1aa8 Mon Sep 17 00:00:00 2001 From: jbertram Date: Thu, 11 Aug 2016 14:32:56 -0500 Subject: [PATCH] ARTEMIS-656 support host verification for SSL cert --- .../remoting/impl/netty/NettyConnector.java | 20 ++++- .../impl/netty/TransportConstants.java | 6 ++ .../remoting/impl/netty/NettyAcceptor.java | 23 ++++- docs/user-manual/en/configuring-transports.md | 12 +++ .../ssl/CoreClientOverOneWaySSLTest.java | 76 +++++++++++++++- .../ssl/CoreClientOverTwoWaySSLTest.java | 81 +++++++++++++++++- .../verified-client-side-keystore.jceks | Bin 0 -> 2203 bytes .../verified-client-side-keystore.jks | Bin 0 -> 2226 bytes .../verified-client-side-truststore.jceks | Bin 0 -> 868 bytes .../verified-client-side-truststore.jks | Bin 0 -> 935 bytes .../verified-server-side-keystore.jceks | Bin 0 -> 1248 bytes .../verified-server-side-keystore.jks | Bin 0 -> 2225 bytes .../verified-server-side-truststore.jceks | Bin 0 -> 935 bytes .../verified-server-side-truststore.jks | Bin 0 -> 935 bytes 14 files changed, 212 insertions(+), 6 deletions(-) create mode 100644 tests/unit-tests/src/test/resources/verified-client-side-keystore.jceks create mode 100644 tests/unit-tests/src/test/resources/verified-client-side-keystore.jks create mode 100644 tests/unit-tests/src/test/resources/verified-client-side-truststore.jceks create mode 100644 tests/unit-tests/src/test/resources/verified-client-side-truststore.jks create mode 100644 tests/unit-tests/src/test/resources/verified-server-side-keystore.jceks create mode 100644 tests/unit-tests/src/test/resources/verified-server-side-keystore.jks create mode 100644 tests/unit-tests/src/test/resources/verified-server-side-truststore.jceks create mode 100644 tests/unit-tests/src/test/resources/verified-server-side-truststore.jks diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/NettyConnector.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/NettyConnector.java index 62c405b48b..49c936bfd3 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/NettyConnector.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/NettyConnector.java @@ -18,6 +18,7 @@ package org.apache.activemq.artemis.core.remoting.impl.netty; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; import java.io.IOException; import java.net.ConnectException; import java.net.Inet6Address; @@ -197,6 +198,8 @@ public class NettyConnector extends AbstractConnector { private String enabledProtocols; + private boolean verifyHost; + private boolean tcpNoDelay; private int tcpSendBufferSize; @@ -306,6 +309,8 @@ public class NettyConnector extends AbstractConnector { enabledCipherSuites = ConfigurationHelper.getStringProperty(TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME, TransportConstants.DEFAULT_ENABLED_CIPHER_SUITES, configuration); enabledProtocols = ConfigurationHelper.getStringProperty(TransportConstants.ENABLED_PROTOCOLS_PROP_NAME, TransportConstants.DEFAULT_ENABLED_PROTOCOLS, configuration); + + verifyHost = ConfigurationHelper.getBooleanProperty(TransportConstants.VERIFY_HOST_PROP_NAME, TransportConstants.DEFAULT_VERIFY_HOST, configuration); } else { keyStoreProvider = TransportConstants.DEFAULT_KEYSTORE_PROVIDER; @@ -316,6 +321,7 @@ public class NettyConnector extends AbstractConnector { trustStorePassword = TransportConstants.DEFAULT_TRUSTSTORE_PASSWORD; enabledCipherSuites = TransportConstants.DEFAULT_ENABLED_CIPHER_SUITES; enabledProtocols = TransportConstants.DEFAULT_ENABLED_PROTOCOLS; + verifyHost = TransportConstants.DEFAULT_VERIFY_HOST; } tcpNoDelay = ConfigurationHelper.getBooleanProperty(TransportConstants.TCP_NODELAY_PROPNAME, TransportConstants.DEFAULT_TCP_NODELAY, configuration); @@ -462,7 +468,13 @@ public class NettyConnector extends AbstractConnector { public void initChannel(Channel channel) throws Exception { final ChannelPipeline pipeline = channel.pipeline(); if (sslEnabled && !useServlet) { - SSLEngine engine = context.createSSLEngine(); + SSLEngine engine; + if (verifyHost) { + engine = context.createSSLEngine(host, port); + } + else { + engine = context.createSSLEngine(); + } engine.setUseClientMode(true); @@ -496,6 +508,12 @@ public class NettyConnector extends AbstractConnector { engine.setEnabledProtocols(originalProtocols); } + if (verifyHost) { + SSLParameters sslParameters = engine.getSSLParameters(); + sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); + engine.setSSLParameters(sslParameters); + } + SslHandler handler = new SslHandler(engine); pipeline.addLast(handler); diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/TransportConstants.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/TransportConstants.java index 53dc204d83..40aed3d8e5 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/TransportConstants.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/TransportConstants.java @@ -87,6 +87,8 @@ public class TransportConstants { public static final String NEED_CLIENT_AUTH_PROP_NAME = "needClientAuth"; + public static final String VERIFY_HOST_PROP_NAME = "verifyHost"; + public static final String BACKLOG_PROP_NAME = "backlog"; public static final String NETTY_VERSION; @@ -155,6 +157,8 @@ public class TransportConstants { public static final boolean DEFAULT_NEED_CLIENT_AUTH = false; + public static final boolean DEFAULT_VERIFY_HOST = false; + public static final boolean DEFAULT_TCP_NODELAY = true; public static final int DEFAULT_TCP_SENDBUFFER_SIZE = 32768; @@ -226,6 +230,7 @@ public class TransportConstants { allowableAcceptorKeys.add(TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME); allowableAcceptorKeys.add(TransportConstants.ENABLED_PROTOCOLS_PROP_NAME); allowableAcceptorKeys.add(TransportConstants.NEED_CLIENT_AUTH_PROP_NAME); + allowableAcceptorKeys.add(TransportConstants.VERIFY_HOST_PROP_NAME); allowableAcceptorKeys.add(TransportConstants.TCP_NODELAY_PROPNAME); allowableAcceptorKeys.add(TransportConstants.TCP_SENDBUFFER_SIZE_PROPNAME); allowableAcceptorKeys.add(TransportConstants.TCP_RECEIVEBUFFER_SIZE_PROPNAME); @@ -270,6 +275,7 @@ public class TransportConstants { allowableConnectorKeys.add(TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME); allowableConnectorKeys.add(TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME); allowableConnectorKeys.add(TransportConstants.ENABLED_PROTOCOLS_PROP_NAME); + allowableConnectorKeys.add(TransportConstants.VERIFY_HOST_PROP_NAME); allowableConnectorKeys.add(TransportConstants.TCP_NODELAY_PROPNAME); allowableConnectorKeys.add(TransportConstants.TCP_SENDBUFFER_SIZE_PROPNAME); allowableConnectorKeys.add(TransportConstants.TCP_RECEIVEBUFFER_SIZE_PROPNAME); diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/NettyAcceptor.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/NettyAcceptor.java index f5742c5915..ce506cb492 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/NettyAcceptor.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/remoting/impl/netty/NettyAcceptor.java @@ -19,12 +19,15 @@ package org.apache.activemq.artemis.core.remoting.impl.netty; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLParameters; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.security.AccessController; import java.security.PrivilegedAction; +import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -138,6 +141,8 @@ public class NettyAcceptor extends AbstractAcceptor { private final boolean needClientAuth; + private final boolean verifyHost; + private final boolean tcpNoDelay; private final int backlog; @@ -224,6 +229,8 @@ public class NettyAcceptor extends AbstractAcceptor { enabledProtocols = ConfigurationHelper.getStringProperty(TransportConstants.ENABLED_PROTOCOLS_PROP_NAME, TransportConstants.DEFAULT_ENABLED_PROTOCOLS, configuration); needClientAuth = ConfigurationHelper.getBooleanProperty(TransportConstants.NEED_CLIENT_AUTH_PROP_NAME, TransportConstants.DEFAULT_NEED_CLIENT_AUTH, configuration); + + verifyHost = ConfigurationHelper.getBooleanProperty(TransportConstants.VERIFY_HOST_PROP_NAME, TransportConstants.DEFAULT_VERIFY_HOST, configuration); } else { keyStoreProvider = TransportConstants.DEFAULT_KEYSTORE_PROVIDER; @@ -235,6 +242,7 @@ public class NettyAcceptor extends AbstractAcceptor { enabledCipherSuites = TransportConstants.DEFAULT_ENABLED_CIPHER_SUITES; enabledProtocols = TransportConstants.DEFAULT_ENABLED_PROTOCOLS; needClientAuth = TransportConstants.DEFAULT_NEED_CLIENT_AUTH; + verifyHost = TransportConstants.DEFAULT_VERIFY_HOST; } tcpNoDelay = ConfigurationHelper.getBooleanProperty(TransportConstants.TCP_NODELAY_PROPNAME, TransportConstants.DEFAULT_TCP_NODELAY, configuration); @@ -391,7 +399,13 @@ public class NettyAcceptor extends AbstractAcceptor { ise.initCause(e); throw ise; } - SSLEngine engine = context.createSSLEngine(); + SSLEngine engine; + if (verifyHost) { + engine = context.createSSLEngine(host, port); + } + else { + engine = context.createSSLEngine(); + } engine.setUseClientMode(false); @@ -439,6 +453,13 @@ public class NettyAcceptor extends AbstractAcceptor { } engine.setEnabledProtocols(set.toArray(new String[set.size()])); + + if (verifyHost) { + SSLParameters sslParameters = engine.getSSLParameters(); + sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); + engine.setSSLParameters(sslParameters); + } + return new SslHandler(engine); } diff --git a/docs/user-manual/en/configuring-transports.md b/docs/user-manual/en/configuring-transports.md index 872fe2f514..a44da351aa 100644 --- a/docs/user-manual/en/configuring-transports.md +++ b/docs/user-manual/en/configuring-transports.md @@ -395,6 +395,18 @@ following additional properties: connecting to this acceptor that 2-way SSL is required. Valid values are `true` or `false`. Default is `false`. +- `verifyHost` + + When used on an `acceptor` the `CN` of the connecting client's SSL certificate + will be compared to its hostname to verify they match. This is useful + only for 2-way SSL. + + When used on a `connector` the `CN` of the server's SSL certificate will be + compared to its hostname to verify they match. This is useful for both 1-way + and 2-way SSL. + + Valid values are `true` or `false`. Default is `false`. + ## Configuring Netty HTTP Netty HTTP tunnels packets over the HTTP protocol. It can be useful in diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/ssl/CoreClientOverOneWaySSLTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/ssl/CoreClientOverOneWaySSLTest.java index 50ac9980ca..de46bcd871 100644 --- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/ssl/CoreClientOverOneWaySSLTest.java +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/ssl/CoreClientOverOneWaySSLTest.java @@ -55,7 +55,10 @@ public class CoreClientOverOneWaySSLTest extends ActiveMQTestBase { @Parameterized.Parameters(name = "storeType={0}") public static Collection getParameters() { - return Arrays.asList(new Object[][]{{"JCEKS"}, {"JKS"}}); + return Arrays.asList(new Object[][]{ + {"JCEKS"}, + {"JKS"} + }); } public CoreClientOverOneWaySSLTest(String storeType) { @@ -78,6 +81,10 @@ public class CoreClientOverOneWaySSLTest extends ActiveMQTestBase { * keytool -export -keystore other-server-side-keystore.jks -file activemq-jks.cer -storepass secureexample * keytool -import -keystore other-client-side-truststore.jks -file activemq-jks.cer -storepass secureexample -keypass secureexample -noprompt * + * keytool -genkey -keystore verified-server-side-keystore.jks -storepass secureexample -keypass secureexample -dname "CN=localhost, OU=Artemis, O=ActiveMQ, L=AMQ, S=AMQ, C=AMQ" -keyalg RSA + * keytool -export -keystore verified-server-side-keystore.jks -file activemq-jks.cer -storepass secureexample + * keytool -import -keystore verified-client-side-truststore.jks -file activemq-jks.cer -storepass secureexample -keypass secureexample -noprompt + * * Commands to create the JCEKS artifacts: * keytool -genkey -keystore server-side-keystore.jceks -storetype JCEKS -storepass secureexample -keypass secureexample -dname "CN=ActiveMQ Artemis Server, OU=Artemis, O=ActiveMQ, L=AMQ, S=AMQ, C=AMQ" * keytool -export -keystore server-side-keystore.jceks -file activemq-jceks.cer -storetype jceks -storepass secureexample @@ -86,6 +93,10 @@ public class CoreClientOverOneWaySSLTest extends ActiveMQTestBase { * keytool -genkey -keystore other-server-side-keystore.jceks -storetype JCEKS -storepass secureexample -keypass secureexample -dname "CN=Other ActiveMQ Artemis Server, OU=Artemis, O=ActiveMQ, L=AMQ, S=AMQ, C=AMQ" * keytool -export -keystore other-server-side-keystore.jceks -file activemq-jceks.cer -storetype jceks -storepass secureexample * keytool -import -keystore other-client-side-truststore.jceks -storetype JCEKS -file activemq-jceks.cer -storepass secureexample -keypass secureexample -noprompt + * + * keytool -genkey -keystore verified-server-side-keystore.jceks -storetype JCEKS -storepass secureexample -keypass secureexample -dname "CN=localhost, OU=Artemis, O=ActiveMQ, L=AMQ, S=AMQ, C=AMQ" + * keytool -export -keystore verified-server-side-keystore.jceks -file activemq-jceks.cer -storetype jceks -storepass secureexample + * keytool -import -keystore verified-client-side-truststore.jceks -storetype JCEKS -file activemq-jceks.cer -storepass secureexample -keypass secureexample -noprompt */ private String storeType; private String SERVER_SIDE_KEYSTORE; @@ -123,6 +134,56 @@ public class CoreClientOverOneWaySSLTest extends ActiveMQTestBase { Assert.assertEquals(text, m.getBodyBuffer().readString()); } + @Test + public void testOneWaySSLVerifyHost() throws Exception { + createCustomSslServer(null, null, true); + String text = RandomUtil.randomString(); + + tc.getParams().put(TransportConstants.SSL_ENABLED_PROP_NAME, true); + tc.getParams().put(TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME, storeType); + tc.getParams().put(TransportConstants.TRUSTSTORE_PATH_PROP_NAME, "verified-" + CLIENT_SIDE_TRUSTSTORE); + tc.getParams().put(TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME, PASSWORD); + tc.getParams().put(TransportConstants.VERIFY_HOST_PROP_NAME, true); + + ServerLocator locator = addServerLocator(ActiveMQClient.createServerLocatorWithoutHA(tc)); + ClientSessionFactory sf = addSessionFactory(createSessionFactory(locator)); + ClientSession session = addClientSession(sf.createSession(false, true, true)); + session.createQueue(CoreClientOverOneWaySSLTest.QUEUE, CoreClientOverOneWaySSLTest.QUEUE, false); + ClientProducer producer = addClientProducer(session.createProducer(CoreClientOverOneWaySSLTest.QUEUE)); + + ClientMessage message = createTextMessage(session, text); + producer.send(message); + + ClientConsumer consumer = addClientConsumer(session.createConsumer(CoreClientOverOneWaySSLTest.QUEUE)); + session.start(); + + Message m = consumer.receive(1000); + Assert.assertNotNull(m); + Assert.assertEquals(text, m.getBodyBuffer().readString()); + } + + @Test + public void testOneWaySSLVerifyHostNegative() throws Exception { + createCustomSslServer(null, null, false); + String text = RandomUtil.randomString(); + + tc.getParams().put(TransportConstants.SSL_ENABLED_PROP_NAME, true); + tc.getParams().put(TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME, storeType); + tc.getParams().put(TransportConstants.TRUSTSTORE_PATH_PROP_NAME, CLIENT_SIDE_TRUSTSTORE); + tc.getParams().put(TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME, PASSWORD); + tc.getParams().put(TransportConstants.VERIFY_HOST_PROP_NAME, true); + + ServerLocator locator = addServerLocator(ActiveMQClient.createServerLocatorWithoutHA(tc)); + + try { + ClientSessionFactory sf = addSessionFactory(createSessionFactory(locator)); + fail("Creating a session here should fail due to a certificate with a CN that doesn't match the host name."); + } + catch (Exception e) { + // ignore + } + } + @Test public void testOneWaySSLReloaded() throws Exception { createCustomSslServer(); @@ -589,11 +650,22 @@ public class CoreClientOverOneWaySSLTest extends ActiveMQTestBase { } private void createCustomSslServer(String cipherSuites, String protocols) throws Exception { + createCustomSslServer(cipherSuites, protocols, false); + } + + private void createCustomSslServer(String cipherSuites, String protocols, boolean useVerifiedKeystore) throws Exception { Map params = new HashMap<>(); params.put(TransportConstants.SSL_ENABLED_PROP_NAME, true); params.put(TransportConstants.KEYSTORE_PROVIDER_PROP_NAME, storeType); - params.put(TransportConstants.KEYSTORE_PATH_PROP_NAME, SERVER_SIDE_KEYSTORE); + + if (useVerifiedKeystore) { + params.put(TransportConstants.KEYSTORE_PATH_PROP_NAME, "verified-" + SERVER_SIDE_KEYSTORE); + } + else { + params.put(TransportConstants.KEYSTORE_PATH_PROP_NAME, SERVER_SIDE_KEYSTORE); + } params.put(TransportConstants.KEYSTORE_PASSWORD_PROP_NAME, PASSWORD); + params.put(TransportConstants.HOST_PROP_NAME, "localhost"); if (cipherSuites != null) { params.put(TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME, cipherSuites); diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/ssl/CoreClientOverTwoWaySSLTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/ssl/CoreClientOverTwoWaySSLTest.java index 0d553adce9..403ebc33fa 100644 --- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/ssl/CoreClientOverTwoWaySSLTest.java +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/ssl/CoreClientOverTwoWaySSLTest.java @@ -36,6 +36,7 @@ import org.apache.activemq.artemis.api.core.client.ClientSession; import org.apache.activemq.artemis.api.core.client.ClientSessionFactory; import org.apache.activemq.artemis.api.core.client.ActiveMQClient; import org.apache.activemq.artemis.api.core.client.ServerLocator; +import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptor; import org.apache.activemq.artemis.tests.util.ActiveMQTestBase; import org.apache.activemq.artemis.utils.RandomUtil; import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl; @@ -56,7 +57,9 @@ public class CoreClientOverTwoWaySSLTest extends ActiveMQTestBase { @Parameterized.Parameters(name = "storeType={0}") public static Collection getParameters() { - return Arrays.asList(new Object[][]{{"JCEKS"}, {"JKS"}}); + return Arrays.asList(new Object[][]{ + {"JCEKS"}, + {"JKS"}}); } public CoreClientOverTwoWaySSLTest(String storeType) { @@ -77,10 +80,18 @@ public class CoreClientOverTwoWaySSLTest extends ActiveMQTestBase { * keytool -export -keystore client-side-keystore.jks -file activemq-jks.cer -storepass secureexample * keytool -import -keystore server-side-truststore.jks -file activemq-jks.cer -storepass secureexample -keypass secureexample -noprompt * + * keytool -genkey -keystore verified-client-side-keystore.jks -storepass secureexample -keypass secureexample -dname "CN=localhost, OU=Artemis, O=ActiveMQ, L=AMQ, S=AMQ, C=AMQ" -keyalg RSA + * keytool -export -keystore verified-client-side-keystore.jks -file activemq-jks.cer -storepass secureexample + * keytool -import -keystore verified-server-side-truststore.jks -file activemq-jks.cer -storepass secureexample -keypass secureexample -noprompt + * * Commands to create the JCEKS artifacts: * keytool -genkey -keystore client-side-keystore.jceks -storetype JCEKS -storepass secureexample -keypass secureexample -dname "CN=ActiveMQ Artemis Client, OU=Artemis, O=ActiveMQ, L=AMQ, S=AMQ, C=AMQ" -keyalg RSA * keytool -export -keystore client-side-keystore.jceks -file activemq-jceks.cer -storetype jceks -storepass secureexample * keytool -import -keystore server-side-truststore.jceks -storetype JCEKS -file activemq-jceks.cer -storepass secureexample -keypass secureexample -noprompt + * + * keytool -genkey -keystore verified-client-side-keystore.jceks -storetype JCEKS -storepass secureexample -keypass secureexample -dname "CN=localhost, OU=Artemis, O=ActiveMQ, L=AMQ, S=AMQ, C=AMQ" -keyalg RSA + * keytool -export -keystore verified-client-side-keystore.jceks -file activemq-jceks.cer -storetype jceks -storepass secureexample + * keytool -import -keystore verified-server-side-truststore.jceks -storetype JCEKS -file activemq-jceks.cer -storepass secureexample -keypass secureexample -noprompt */ private String storeType; @@ -148,6 +159,72 @@ public class CoreClientOverTwoWaySSLTest extends ActiveMQTestBase { Assert.assertEquals(text, m.getBodyBuffer().readString()); } + @Test + public void testTwoWaySSLVerifyClientHost() throws Exception { + NettyAcceptor acceptor = (NettyAcceptor) server.getRemotingService().getAcceptor("nettySSL"); + acceptor.getConfiguration().put(TransportConstants.VERIFY_HOST_PROP_NAME, true); + acceptor.getConfiguration().put(TransportConstants.TRUSTSTORE_PATH_PROP_NAME, "verified-" + SERVER_SIDE_TRUSTSTORE); + server.getRemotingService().stop(false); + server.getRemotingService().start(); + server.getRemotingService().startAcceptors(); + + String text = RandomUtil.randomString(); + + tc.getParams().put(TransportConstants.SSL_ENABLED_PROP_NAME, true); + tc.getParams().put(TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME, storeType); + tc.getParams().put(TransportConstants.KEYSTORE_PROVIDER_PROP_NAME, storeType); + tc.getParams().put(TransportConstants.TRUSTSTORE_PATH_PROP_NAME, CLIENT_SIDE_TRUSTSTORE); + tc.getParams().put(TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME, PASSWORD); + tc.getParams().put(TransportConstants.KEYSTORE_PATH_PROP_NAME, "verified-" + CLIENT_SIDE_KEYSTORE); + tc.getParams().put(TransportConstants.KEYSTORE_PASSWORD_PROP_NAME, PASSWORD); + + server.getRemotingService().addIncomingInterceptor(new MyInterceptor()); + + ServerLocator locator = addServerLocator(ActiveMQClient.createServerLocatorWithoutHA(tc)); + ClientSessionFactory sf = createSessionFactory(locator); + ClientSession session = sf.createSession(false, true, true); + session.createQueue(CoreClientOverTwoWaySSLTest.QUEUE, CoreClientOverTwoWaySSLTest.QUEUE, false); + ClientProducer producer = session.createProducer(CoreClientOverTwoWaySSLTest.QUEUE); + + ClientMessage message = createTextMessage(session, text); + producer.send(message); + + ClientConsumer consumer = session.createConsumer(CoreClientOverTwoWaySSLTest.QUEUE); + session.start(); + + Message m = consumer.receive(1000); + Assert.assertNotNull(m); + Assert.assertEquals(text, m.getBodyBuffer().readString()); + } + + @Test + public void testTwoWaySSLVerifyClientHostNegative() throws Exception { + NettyAcceptor acceptor = (NettyAcceptor) server.getRemotingService().getAcceptor("nettySSL"); + acceptor.getConfiguration().put(TransportConstants.VERIFY_HOST_PROP_NAME, true); + server.getRemotingService().stop(false); + server.getRemotingService().start(); + server.getRemotingService().startAcceptors(); + + tc.getParams().put(TransportConstants.SSL_ENABLED_PROP_NAME, true); + tc.getParams().put(TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME, storeType); + tc.getParams().put(TransportConstants.KEYSTORE_PROVIDER_PROP_NAME, storeType); + tc.getParams().put(TransportConstants.TRUSTSTORE_PATH_PROP_NAME, CLIENT_SIDE_TRUSTSTORE); + tc.getParams().put(TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME, PASSWORD); + tc.getParams().put(TransportConstants.KEYSTORE_PATH_PROP_NAME, CLIENT_SIDE_KEYSTORE); + tc.getParams().put(TransportConstants.KEYSTORE_PASSWORD_PROP_NAME, PASSWORD); + + server.getRemotingService().addIncomingInterceptor(new MyInterceptor()); + + ServerLocator locator = addServerLocator(ActiveMQClient.createServerLocatorWithoutHA(tc)); + try { + ClientSessionFactory sf = createSessionFactory(locator); + fail("Creating a session here should fail due to a certificate with a CN that doesn't match the host name."); + } + catch (Exception e) { + // ignore + } + } + @Test public void testTwoWaySSLWithoutClientKeyStore() throws Exception { tc.getParams().put(TransportConstants.SSL_ENABLED_PROP_NAME, true); @@ -183,7 +260,7 @@ public class CoreClientOverTwoWaySSLTest extends ActiveMQTestBase { params.put(TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME, storeType); params.put(TransportConstants.KEYSTORE_PROVIDER_PROP_NAME, storeType); params.put(TransportConstants.NEED_CLIENT_AUTH_PROP_NAME, true); - ConfigurationImpl config = createBasicConfig().addAcceptorConfiguration(new TransportConfiguration(NETTY_ACCEPTOR_FACTORY, params)); + ConfigurationImpl config = createBasicConfig().addAcceptorConfiguration(new TransportConfiguration(NETTY_ACCEPTOR_FACTORY, params, "nettySSL")); server = createServer(false, config); server.start(); waitForServerToStart(server); diff --git a/tests/unit-tests/src/test/resources/verified-client-side-keystore.jceks b/tests/unit-tests/src/test/resources/verified-client-side-keystore.jceks new file mode 100644 index 0000000000000000000000000000000000000000..250832ba8babfe5df8333e11d53ff6123c883134 GIT binary patch literal 2203 zcmchX`8yPh7RP5F`!Xqa2c*h;cRk)5H% z5~U(r2_>>-URyHROW9rB_ul7u?>}&VI6r*8&-aIOzUMrrx3{+!004jv3i$7W21fBWbVdzA5rd}fr{CQ^Wi?!fZyZ; zm0ceDA{^GvE{v8>p!}gD=h9)3LtQuA8aQ+s;2FZQ$x?78!s>%^&F~wYV5G>};C!3Z z>G}NYL+@IrxjvYts_p*22%9~n~#Rw(m^IuAMxhU4XPMZ^s81G1NZqPwe zOVO!K;=G z8Xm=*_OVEBXkoXg6W5Rritbg&4I(!N$$MjDsi>5a^r9t(Gma&5gnNrf1X($~;04U< z-w6$~kU~WAW`Xz2SJLvoj5$PWKzGdqk4s;-j&T&uHeI{~na&gnu&N9#M;Z99n59qR z`!Sao+LjcVsKZk8kMUXyI!I2O5?Z?nI|iHviF&pAdNhS86t=8f^X&HqRw z8}y3IUwh7Zf!`N1VR8+;%9+f>>LWh9_KYp_WY9`|V78WYa?rc9vB~A)-PcPgFN>AN zB+%Lc-zMG}UEgT>T;p-(Sc;I=rdK`X2kUU!HP2^wJqgc^m0j<1 zqAcnDgk2=Hingeo>=(|pwG{eb^b^VpnXk(kZ&c8g%T%Vhs3$QVe10C)?8Apey1=a= zhFu*kS?^6AsT;Zxa?!?GK}75Ik5kYIr-T{*6>Z0p?19HcRRlOV&GeHMv}(ePqny-gAi@5žmOM7lT*QO6^df*ue zwA99X!M^Vm$sHvHGbm%h;x0*=qf>kOtG)h_3GHvnr@-k;p_>NU4E|JU?}F9l zvO>`E7k=!A{%yd0S(mk(00zbK_jqw8DAIOsQuf1Jf_+AOW9mYO zb92VrjQL}ek;ZP8{FP0o(EN1*uG7}nVw(!IoK+Hw#!R^K%jY*jiFL~R1)dmhHT&x9 zyWWWuSIKhg6}=2cX@4_7^eVnt6@B{CY&KD!R5Vv5dH7@6H_5K;9+_8pf8=Lsc8_T# zBpLvF58&NcM9Iuv{HV-c&h_x> zehD`!%znnUEMXnzE{*;90#E4~VNC!4FcljQ_QS@59PWWZKoAH*+fKcM6^8L^U;X>l zpTa<(AQXW0#R*{rU|ruu>ZmTPZRM# zmA?)TgaW|vKv4iT9w-2c2Lb^PZVgh3r@C8VBTc#qvKLeP$@}eTjWizt34QQLz!J+v zx8O%rw4v5mZGBFRC$}}AC#5gRe{t_E;{$~$VOG~H?zXb+3D;NwjBb6Yot|@JZ1JUC z^GN5)RufzYra)mu)s^#@xYKKxr^D7wPkge%Y=d|tMY5M7Y&LpAvELP0`P(}^v&ka; z*r8sVmG^qWX_{+{HS+t(u)TY~ml#CP2H&>WZ;z(|!iWuKiga3V%elSq+- zT_K)rT&U-JCE74FSk!U1w3gA{`ABMF_~EFUW^x1OJe!8vx*6Hhz$82jVO*m^61N4z z(N{MY%}lMh7ZWFWpFm(B0C-;+tAtfJSfGLkL=uAR^ZIBbn$=Z}%L`+pP4B{f?{kCx zHN=ApItUR6NN8eE!v@>auq^LF0}L7?bamgfb0^k55@IzX^UV%ZryNGAWU@Hw8Cs_p zo_Q3*J(oj|Cojq~TQD&S>jPaU{yBW?a#DR&fkz>HEl?dD_++K>MmfpA#9dv3oYkJU zG`rb+%~K_dFEp?vN2F5)KDYX&PQbA|`C+G?=GKvgahIf9-C38*#jK?jH=tI6b$W$r z(iX=vYo(5scN~4l_5Ru7Yw4O0{=4ETn%f;1JEc*Y*`im7P@fzvdA$j&<$cdLs@Sid zX9|6mO&b?bC!LsFCmpI+2g&$KtHKE(%{ihACzp3p<#I?3HG6Pmq=lkz7<`_d7rW~C drN+ND_@Th&al7oZ!!D*bZB}ywCocle{0p54z2E=< literal 0 HcmV?d00001 diff --git a/tests/unit-tests/src/test/resources/verified-client-side-keystore.jks b/tests/unit-tests/src/test/resources/verified-client-side-keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..88e7e4031d83f3dd7c75077e4ca714001415db6a GIT binary patch literal 2226 zcmchY`9IVP7sqE~%wUY=TEb*0vcz}ngs~etGg*^e2s5@&_8~&D6j`#ABFmM?5)mOZ zmdmwAt|&rs-ND4fqvt-)>vf-h;Q8VF@cz8c59fT&d7X3g_2?@I1cICd_}{@4c-`Oo z`U%;^weMU6fta8G1$GRen2|EfU>H~$!U+a5fnXHa2FYx6Q!o0Go$Mu9H`w?c-@I#C z$B4{yt8({#J_MXLX5AcNtXL6|A|N^kwfE2*ayIYRa?YwmWO{N;7O5`pj2FzVWg2A} z@d*)w9FfybZFH^|Kk|C^8Nq?UtD2e$2j>Pv-br_N{5ASnDYDCm59dZE67s`!wb}Qv z+Q|e*4aOSbllhc`4q|=AF*W$ts|~MQ>ScsGkmy_d!*M*RMtbCm3Ma#L8JBRLFCU{W z!;=Dbra#({`IL(WnzZR-WPOJI^VG$9y2I;xLh#O+p}o1$uFI?K9(X@|al@Y7z3GcN zo;qT_2X^vR`l;>Cw_SAZ>KVw5>93xPVXQ4sny2T}bXqh5&vcLHC#zMmt=e9cBXpBkiHab0eGby1sC*m(hGE=>) z-H@4y@MoF#Z0pZM{4q~X8}MAN6B#K=zhrvOp$j~wzK9amLU1~Ee+5$tv=ny`$A)hV zOLCY9+Iug`oS^>vO4n=|vvl2$GI}Y9>L zV!TD@iD#r8(48L9245=&4VvdDH!{6vf%iiVd#IeiAU;Hv?dd++J{X<5H<0B_yOCQ} z*zy?Pf2xm@6{6A&)5Vm0x71e_o#v0Ka%l2y5zp()b(e^IRMr{zP>J!@3&4(|AMClhs!)a8K()>OM))grxMos$b zvDL(Hq)*&YO#)TJF>X}PPaXQy>gcrQ&y3Z#9fFIuge=X!h(=qOP$P(DL4AgDeWS8< zA+tlsd&QZ_@h-`P*nA+^rAy=HtS`v_t(qh*Un<(6>@|~`{BftjW?F3)Iv{kqN^l+S zn&m!xdpGPVpW{-A#n5}(ufyTy#HQYY5&W+@2j{o4dU+U`3bc-`xg)gIAnIFup{3bS zNxBJorZ(ALW~ub9(o~zNkIh|i*D4_To`P&RfzcD|c4}$xV(MF5YnN>)wxaOQ)N$6% z7w!x*!^qy>Cm@PdTePDON2zw|IMiVx-@=vWF=yRTxdhX1PZ50soD%p)J`AybG$uAp zgqJ9Eyy{vK9Y1o_1BI7d7Z8ili|V+jgY@z!C|Ay~>s?S{nl)N9S||JfM8w@9MS@g* z5RJH5bo?U%=tlV-$sIGr3s01;Ac_J9Lqz~$^M2S3n7Hvf-Q4OqFME)W-Gy&y(cw}X z5toE`0&A&eEqF&(93FG2neP9l?8my=K_0O8OD5Vn_AH7YBc)JEo$@(lw(C6p?U|2r zx8B_gK}X-$#+ihq;)HHZIBORZyjg`gYCK z-kl{}lNq!Nrvj5QwSpU%KMm+;(AhZRL#0Vt&dOC{iIFt3k_Kes- z1?mk@AQoj%2p9r^k?y1F0d{7DbTU49pB)TlWdZ@7ST+F347G(Zb3=74Z2n6u|3mP< zi4)-XnTh0P(e(`T3->-rN1q(r6N!}@t{W2O9q1Q|4v zftit66o_kRXlMZC(#H`^jN1*G7ZQ_-Kd;Z$Hmz)N^9l zLmRQG?vR>ZjR%X@$!Tw1Ryr|naXt4hMbo5@zmuvMRjkh}`M7UI;DpW<&G#QhJ)O@s zsXHyMp*mDGh0(|Q-+50h=EfEVrp5-nGL?uWonbzbR>!A&=2Z3%yPD>4WB-lKe8RtW zF1Y&sMr-$?rsE|7lT`jbd-m$Wo$QhWQ}aCyw~A#2mKH7dvwSV|moH&Ak*~r z8qLLu2J!~7z*Hs6$0Eie(q@*MdhE8~1Brx*(Umi9*xQP8OQ9rcW_<<&9VQWBmd2Je z#e?CZZ8iCewG_o9?dtNGL@cZ16&f3QclR5oyB4OONRHWe#q9RmtVah9x5)`Mhvx_? Lt+Bc;qMiW&Zml&K literal 0 HcmV?d00001 diff --git a/tests/unit-tests/src/test/resources/verified-client-side-truststore.jks b/tests/unit-tests/src/test/resources/verified-client-side-truststore.jks new file mode 100644 index 0000000000000000000000000000000000000000..1d992c7d999b67df2b31e4a380fea12c661c0cfc GIT binary patch literal 935 zcmezO_TO6u1_mY|W(3o$xs}KS(XY^E?~e165Y_fQ zwqvO=XVS}8b$*%kEa0N)uh2DfB}}%w(fbm}>AxuF<|ZyiM)sf6cmCv(KU)3Pvian) z&(F%@7rR<+O)7i1B}hGX_OpeT?_D}ze}mi2>q39b*<*J(LslpJ`L5@9H7de(M^(Sy zg|vy&&TRhO_OSnR?)0{MzwdZ>IVQX;JQ$Q+smJ2 zFTC7ic6i_Gp6gG_t&8_>yb`rD@60EyXA@1?o-`zVda+pN^ODMW4wY+aEc9;A-ZA0B z^4jNYi*Fq})NH(reNFt2B*9FLhi~6?{hf10`pR@$A>OUOVs@u~{<^TsDfY@4)8p3s z!DkDtcTZStDmrOK%7OR)|NZ;$r-b)r^R17*F}&8xCQiMp zP&578BDd*X)AKzR)^L{{WnUq>L;Y{C!wZgguOD!vUV7*BrAe_dJE-a^tHpfb4IY+L zd)Lf8#*y%+)NK>?_zIE37*HSn4_jwcelWM!RB47mc-Xh1pupXWWoRd literal 0 HcmV?d00001 diff --git a/tests/unit-tests/src/test/resources/verified-server-side-keystore.jceks b/tests/unit-tests/src/test/resources/verified-server-side-keystore.jceks new file mode 100644 index 0000000000000000000000000000000000000000..7a461622243ba8612098463098b83aa9efa4bb32 GIT binary patch literal 1248 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 literal 0 HcmV?d00001 diff --git a/tests/unit-tests/src/test/resources/verified-server-side-truststore.jceks b/tests/unit-tests/src/test/resources/verified-server-side-truststore.jceks new file mode 100644 index 0000000000000000000000000000000000000000..e2bd4b3be0bf9427413cf452f8466dfa264ba992 GIT binary patch literal 935 zcmX?i?%X*B1_mY|W(3o$xs}**LY@JlekVGBR?rG8iNq@)&TjF^96S2{SwT2Euq8C_HuuPtbrLq>@XR z!!fxevn&;;T)==2B*ZPu?pRcknwwc{C}bc25@8nR%*jtq%*n_vE-{c3=QT7lurM?- zv@kI;G>HOn4Gj$qpj`SmqKQ!n+4GF749rc8{0s(7j9g4jjEoE$rd&;2`snl_wwt?j zJEW@mFU9>oI$>u{0+)~x^UaKR%|W^gzpbe@(Y(E7+pOA{`-d{l_MPuZd;9N9`Q^k0 zA)Bpx1;al4j$v2(z;Nr=i!G0zhn4^0n9v?M`M%UT``{~&mCf#pUOWuTv;oq!hHBp zeUW=uht{W(FNR<8XR!$|m@4}}@|oFp#j93N!#`1st<^te=bLRDhxxZ(zq;hu{N-E9 zj~?G7e(%P{TPhm8+jUC2a}<6}Dm%En!G2?2d3z>H*Kh7Z^|qgHZLD1GS9Lvnb(e{m zk%4isqJg}DEHDCP`B=nQM9#-vbLXFNVx8gKf^K!I`E1eW!&s365txF2fyl_vv8z0@ z;Ofx{2F>w{jCIs?c-sD39sg73Q^w+QQ}VO7&Q`^O(A5piYgMOeMwG|QO*HuzB)a)w zl}y7yom$ziS5A1f+_0_h*|uh3%zESf`0odkoq{_GqvbxT-#?vI_egE|^n-efgjFBjT6Xd$IWv7(d2R@+Y8n7$kUdH7p8 literal 0 HcmV?d00001 diff --git a/tests/unit-tests/src/test/resources/verified-server-side-truststore.jks b/tests/unit-tests/src/test/resources/verified-server-side-truststore.jks new file mode 100644 index 0000000000000000000000000000000000000000..8d2288a07f559d210f881b8d9fe36214ce6fe46c GIT binary patch literal 935 zcmezO_TO6u1_mY|W(3o$xs}pyJJyFYHnt+p^$+9NQ7CKGbcYeF()IxxWqtCoY&CIz{1eT z(A?0#*dhwVH8eCdfO6^Mh$coQWY06QGB7tW@-rATF>*0AF)}i&Om{jY{pD-Ow~YLD zk*l*qc|#Yi*%n-^I11P3z6ltkLk zNWWVA$}a8;-_}uv`Ojrxl(B1h`0Cr*?(b#)j5wuKkZ+dIlW2VCThhm#4^rBlgE; z-j7q=?{AlrD?Dv&{^nk}g5P|9?)F=H(?jeH@;|%e~^}9=RTKhkcH9jchpq zzVq5HHq@(JeeyWr;g=KbdAW1)maP)Y3_q%0d#my6$;CG|uP)5ne}BvOz|h%t#!SqN z42+8v4de}Efe|Rn$0Eie5>q0vN?Ll&H$G(vwP|NN^G+wvOhOJsUNZN6 z94{qht_r>|sQdC`AzMw}3Ee3Mb620yD)L)wbDl50uGBy%Dt~t7R`!C1*tVT>jvs00 zU3Gk7Z`|qyVH#bn*80i^V-|~Sw?2J9d396Mrdyo0i`=jNIe9;|`b6l2yF1S>{?)sX}# literal 0 HcmV?d00001