diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/AcceptorControl.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/AcceptorControl.java index 97e49875cc..45df0dff94 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/AcceptorControl.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/core/management/AcceptorControl.java @@ -41,4 +41,10 @@ public interface AcceptorControl extends ActiveMQComponentControl { */ @Attribute(desc = "parameters used to configure this acceptor") Map getParameters(); + + /** + * Re-create the acceptor with the existing configuration values. Useful, for example, for reloading key/trust + * stores on acceptors which support SSL. + */ + void reload(); } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/AcceptorControlImpl.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/AcceptorControlImpl.java index ac6950d5c1..ae7660b667 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/AcceptorControlImpl.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/AcceptorControlImpl.java @@ -83,6 +83,17 @@ public class AcceptorControlImpl extends AbstractControl implements AcceptorCont } } + @Override + public void reload() { + clearIO(); + try { + acceptor.reload(); + } + finally { + blockOnIO(); + } + } + @Override public boolean isStarted() { clearIO(); diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/remoting/impl/invm/InVMAcceptor.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/remoting/impl/invm/InVMAcceptor.java index 2967b2a3ef..190a5d1718 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/remoting/impl/invm/InVMAcceptor.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/remoting/impl/invm/InVMAcceptor.java @@ -247,6 +247,11 @@ public final class InVMAcceptor extends AbstractAcceptor { return true; } + @Override + public void reload() { + throw new UnsupportedOperationException(); + } + @Override public void setDefaultActiveMQPrincipal(ActiveMQPrincipal defaultActiveMQPrincipal) { this.defaultActiveMQPrincipal = defaultActiveMQPrincipal; 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 3da612f226..48922aa7b0 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 @@ -32,7 +32,6 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; @@ -121,7 +120,8 @@ public class NettyAcceptor extends AbstractAcceptor { private final String keyStoreProvider; - private final String keyStorePath; + // non-final for testing purposes + private String keyStorePath; private final String keyStorePassword; @@ -282,87 +282,13 @@ public class NettyAcceptor extends AbstractAcceptor { bootstrap = new ServerBootstrap(); bootstrap.group(eventLoopGroup); bootstrap.channel(channelClazz); - final SSLContext context; - if (sslEnabled) { - try { - if (keyStorePath == null && TransportConstants.DEFAULT_TRUSTSTORE_PROVIDER.equals(keyStoreProvider)) - throw new IllegalArgumentException("If \"" + TransportConstants.SSL_ENABLED_PROP_NAME + - "\" is true then \"" + TransportConstants.KEYSTORE_PATH_PROP_NAME + "\" must be non-null " + - "unless an alternative \"" + TransportConstants.KEYSTORE_PROVIDER_PROP_NAME + "\" has been specified."); - context = SSLSupport.createContext(keyStoreProvider, keyStorePath, keyStorePassword, trustStoreProvider, trustStorePath, trustStorePassword); - } - catch (Exception e) { - IllegalStateException ise = new IllegalStateException("Unable to create NettyAcceptor for " + host + - ":" + port); - ise.initCause(e); - throw ise; - } - } - else { - context = null; // Unused - } - - final AtomicBoolean warningPrinted = new AtomicBoolean(false); ChannelInitializer factory = new ChannelInitializer() { @Override public void initChannel(Channel channel) throws Exception { ChannelPipeline pipeline = channel.pipeline(); if (sslEnabled) { - SSLEngine engine = context.createSSLEngine(); - - engine.setUseClientMode(false); - - if (needClientAuth) - engine.setNeedClientAuth(true); - - // setting the enabled cipher suites resets the enabled protocols so we need - // to save the enabled protocols so that after the customer cipher suite is enabled - // we can reset the enabled protocols if a customer protocol isn't specified - String[] originalProtocols = engine.getEnabledProtocols(); - - if (enabledCipherSuites != null) { - try { - engine.setEnabledCipherSuites(SSLSupport.parseCommaSeparatedListIntoArray(enabledCipherSuites)); - } - catch (IllegalArgumentException e) { - ActiveMQServerLogger.LOGGER.invalidCipherSuite(SSLSupport.parseArrayIntoCommandSeparatedList(engine.getSupportedCipherSuites())); - throw e; - } - } - - if (enabledProtocols != null) { - try { - engine.setEnabledProtocols(SSLSupport.parseCommaSeparatedListIntoArray(enabledProtocols)); - } - catch (IllegalArgumentException e) { - ActiveMQServerLogger.LOGGER.invalidProtocol(SSLSupport.parseArrayIntoCommandSeparatedList(engine.getSupportedProtocols())); - throw e; - } - } - else { - engine.setEnabledProtocols(originalProtocols); - } - - // Strip "SSLv3" from the current enabled protocols to address the POODLE exploit. - // This recommendation came from http://www.oracle.com/technetwork/java/javase/documentation/cve-2014-3566-2342133.html - String[] protocols = engine.getEnabledProtocols(); - Set set = new HashSet<>(); - for (String s : protocols) { - if (s.equals("SSLv3") || s.equals("SSLv2Hello")) { - if (!warningPrinted.get()) { - ActiveMQServerLogger.LOGGER.disallowedProtocol(s); - } - continue; - } - set.add(s); - } - warningPrinted.set(true); - engine.setEnabledProtocols(set.toArray(new String[set.size()])); - - SslHandler handler = new SslHandler(engine); - - pipeline.addLast("ssl", handler); + pipeline.addLast("ssl", getSslHandler()); } pipeline.addLast(protocolHandler.getProtocolDecoder()); } @@ -421,6 +347,11 @@ public class NettyAcceptor extends AbstractAcceptor { return name; } + // only for testing purposes + public void setKeyStorePath(String keyStorePath) { + this.keyStorePath = keyStorePath; + } + /** * Transfers the Netty channel that has been created outside of this NettyAcceptor * to control it and configure it according to this NettyAcceptor setting. @@ -434,6 +365,77 @@ public class NettyAcceptor extends AbstractAcceptor { channel.pipeline().addLast(protocolHandler.getProtocolDecoder()); } + public void reload() { + serverChannelGroup.disconnect(); + serverChannelGroup.clear(); + startServerChannels(); + } + + public synchronized SslHandler getSslHandler() throws Exception { + final SSLContext context; + try { + if (keyStorePath == null && TransportConstants.DEFAULT_TRUSTSTORE_PROVIDER.equals(keyStoreProvider)) + throw new IllegalArgumentException("If \"" + TransportConstants.SSL_ENABLED_PROP_NAME + + "\" is true then \"" + TransportConstants.KEYSTORE_PATH_PROP_NAME + "\" must be non-null " + + "unless an alternative \"" + TransportConstants.KEYSTORE_PROVIDER_PROP_NAME + "\" has been specified."); + context = SSLSupport.createContext(keyStoreProvider, keyStorePath, keyStorePassword, trustStoreProvider, trustStorePath, trustStorePassword); + } + catch (Exception e) { + IllegalStateException ise = new IllegalStateException("Unable to create NettyAcceptor for " + host + ":" + port); + ise.initCause(e); + throw ise; + } + SSLEngine engine = context.createSSLEngine(); + + engine.setUseClientMode(false); + + if (needClientAuth) + engine.setNeedClientAuth(true); + + // setting the enabled cipher suites resets the enabled protocols so we need + // to save the enabled protocols so that after the customer cipher suite is enabled + // we can reset the enabled protocols if a customer protocol isn't specified + String[] originalProtocols = engine.getEnabledProtocols(); + + if (enabledCipherSuites != null) { + try { + engine.setEnabledCipherSuites(SSLSupport.parseCommaSeparatedListIntoArray(enabledCipherSuites)); + } + catch (IllegalArgumentException e) { + ActiveMQServerLogger.LOGGER.invalidCipherSuite(SSLSupport.parseArrayIntoCommandSeparatedList(engine.getSupportedCipherSuites())); + throw e; + } + } + + if (enabledProtocols != null) { + try { + engine.setEnabledProtocols(SSLSupport.parseCommaSeparatedListIntoArray(enabledProtocols)); + } + catch (IllegalArgumentException e) { + ActiveMQServerLogger.LOGGER.invalidProtocol(SSLSupport.parseArrayIntoCommandSeparatedList(engine.getSupportedProtocols())); + throw e; + } + } + else { + engine.setEnabledProtocols(originalProtocols); + } + + // Strip "SSLv3" from the current enabled protocols to address the POODLE exploit. + // This recommendation came from http://www.oracle.com/technetwork/java/javase/documentation/cve-2014-3566-2342133.html + String[] protocols = engine.getEnabledProtocols(); + Set set = new HashSet<>(); + for (String s : protocols) { + if (s.equalsIgnoreCase("SSLv3") || s.equals("SSLv2Hello")) { + ActiveMQServerLogger.LOGGER.disallowedProtocol(s); + continue; + } + set.add(s); + } + + engine.setEnabledProtocols(set.toArray(new String[set.size()])); + return new SslHandler(engine); + } + private void startServerChannels() { String[] hosts = TransportConfiguration.splitHosts(host); for (String h : hosts) { diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/remoting/Acceptor.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/remoting/Acceptor.java index b4c195282a..c0da381d46 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/remoting/Acceptor.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/remoting/Acceptor.java @@ -70,4 +70,10 @@ public interface Acceptor extends ActiveMQComponent { * @throws java.lang.IllegalStateException if false @setDefaultActiveMQPrincipal */ boolean isUnsecurable(); + + /** + * Re-create the acceptor with the existing configuration values. Useful, for example, for reloading key/trust + * stores on acceptors which support SSL. + */ + void reload(); } diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/management/AcceptorControlUsingCoreTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/management/AcceptorControlUsingCoreTest.java index d5c7b65e94..8039e98bec 100644 --- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/management/AcceptorControlUsingCoreTest.java +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/management/AcceptorControlUsingCoreTest.java @@ -68,6 +68,16 @@ public class AcceptorControlUsingCoreTest extends AcceptorControlTest { return (Map) proxy.retrieveAttributeValue("parameters"); } + @Override + public void reload() { + try { + proxy.invokeOperation("reload"); + } + catch (Exception e) { + e.printStackTrace(); + } + } + @Override public boolean isStarted() { return (Boolean) proxy.retrieveAttributeValue("started"); 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 f23e58006d..50ac9980ca 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 @@ -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.integration.IntegrationTestLogger; import org.apache.activemq.artemis.tests.util.ActiveMQTestBase; import org.apache.activemq.artemis.utils.RandomUtil; @@ -73,10 +74,18 @@ public class CoreClientOverOneWaySSLTest extends ActiveMQTestBase { * keytool -export -keystore server-side-keystore.jks -file activemq-jks.cer -storepass secureexample * keytool -import -keystore client-side-truststore.jks -file activemq-jks.cer -storepass secureexample -keypass secureexample -noprompt * + * keytool -genkey -keystore other-server-side-keystore.jks -storepass secureexample -keypass secureexample -dname "CN=Other ActiveMQ Artemis Server, OU=Artemis, O=ActiveMQ, L=AMQ, S=AMQ, C=AMQ" -keyalg RSA + * 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 + * * 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 * keytool -import -keystore client-side-truststore.jceks -storetype JCEKS -file activemq-jceks.cer -storepass secureexample -keypass secureexample -noprompt + * + * 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 */ private String storeType; private String SERVER_SIDE_KEYSTORE; @@ -114,6 +123,67 @@ public class CoreClientOverOneWaySSLTest extends ActiveMQTestBase { Assert.assertEquals(text, m.getBodyBuffer().readString()); } + @Test + public void testOneWaySSLReloaded() throws Exception { + createCustomSslServer(); + server.createQueue(CoreClientOverOneWaySSLTest.QUEUE, CoreClientOverOneWaySSLTest.QUEUE, null, false, false); + String text = RandomUtil.randomString(); + + // create a valid SSL connection and keep it for use later + 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); + + ServerLocator existingLocator = addServerLocator(ActiveMQClient.createServerLocatorWithoutHA(tc)); + existingLocator.setCallTimeout(3000); + ClientSessionFactory existingSessionFactory = addSessionFactory(createSessionFactory(existingLocator)); + ClientSession existingSession = addClientSession(existingSessionFactory.createSession(false, true, true)); + ClientConsumer existingConsumer = addClientConsumer(existingSession.createConsumer(CoreClientOverOneWaySSLTest.QUEUE)); + + // create an invalid SSL connection + 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, "other-client-side-truststore." + storeType.toLowerCase()); + tc.getParams().put(TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME, PASSWORD); + + ServerLocator locator = addServerLocator(ActiveMQClient.createServerLocatorWithoutHA(tc)).setCallTimeout(3000); + try { + addSessionFactory(createSessionFactory(locator)); + fail("Creating session here should fail due to SSL handshake problems."); + } + catch (Exception e) { + // ignore + } + + // reload the acceptor to reload the SSL stores + NettyAcceptor acceptor = (NettyAcceptor) server.getRemotingService().getAcceptor("nettySSL"); + acceptor.setKeyStorePath("other-server-side-keystore." + storeType.toLowerCase()); + acceptor.reload(); + + // create a session with the locator which failed previously proving that the SSL stores have been reloaded + ClientSessionFactory sf = addSessionFactory(createSessionFactory(locator)); + ClientSession session = addClientSession(sf.createSession(false, true, true)); + ClientProducer producer = addClientProducer(session.createProducer(CoreClientOverOneWaySSLTest.QUEUE)); + + ClientMessage message = createTextMessage(session, text); + producer.send(message); + 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()); + consumer.close(); + + // use the existing connection to prove it wasn't lost when the acceptor was reloaded + existingSession.start(); + m = existingConsumer.receive(1000); + Assert.assertNotNull(m); + Assert.assertEquals(text, m.getBodyBuffer().readString()); + } + @Test public void testOneWaySSLWithBadClientCipherSuite() throws Exception { createCustomSslServer(); @@ -533,7 +603,7 @@ public class CoreClientOverOneWaySSLTest extends ActiveMQTestBase { params.put(TransportConstants.ENABLED_PROTOCOLS_PROP_NAME, protocols); } - 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/java/org/apache/activemq/artemis/tests/unit/core/remoting/server/impl/fake/FakeAcceptorFactory.java b/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/core/remoting/server/impl/fake/FakeAcceptorFactory.java index 1c15c692d3..b2305ec1f8 100644 --- a/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/core/remoting/server/impl/fake/FakeAcceptorFactory.java +++ b/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/core/remoting/server/impl/fake/FakeAcceptorFactory.java @@ -89,6 +89,11 @@ public class FakeAcceptorFactory implements AcceptorFactory { return false; } + @Override + public void reload() { + + } + @Override public void start() throws Exception { started = true; diff --git a/tests/unit-tests/src/test/resources/other-client-side-truststore.jceks b/tests/unit-tests/src/test/resources/other-client-side-truststore.jceks new file mode 100644 index 0000000000..91c4caf234 Binary files /dev/null and b/tests/unit-tests/src/test/resources/other-client-side-truststore.jceks differ diff --git a/tests/unit-tests/src/test/resources/other-client-side-truststore.jks b/tests/unit-tests/src/test/resources/other-client-side-truststore.jks new file mode 100644 index 0000000000..9098d3bf25 Binary files /dev/null and b/tests/unit-tests/src/test/resources/other-client-side-truststore.jks differ diff --git a/tests/unit-tests/src/test/resources/other-server-side-keystore.jceks b/tests/unit-tests/src/test/resources/other-server-side-keystore.jceks new file mode 100644 index 0000000000..be26131e7e Binary files /dev/null and b/tests/unit-tests/src/test/resources/other-server-side-keystore.jceks differ diff --git a/tests/unit-tests/src/test/resources/other-server-side-keystore.jks b/tests/unit-tests/src/test/resources/other-server-side-keystore.jks new file mode 100644 index 0000000000..8e8d1c9f62 Binary files /dev/null and b/tests/unit-tests/src/test/resources/other-server-side-keystore.jks differ