diff --git a/quickfixj-core/src/main/doc/usermanual/usage/secure_communications.html b/quickfixj-core/src/main/doc/usermanual/usage/secure_communications.html index af4837186..b0a708c0d 100644 --- a/quickfixj-core/src/main/doc/usermanual/usage/secure_communications.html +++ b/quickfixj-core/src/main/doc/usermanual/usage/secure_communications.html @@ -113,5 +113,57 @@

Secure Communications with QFJ

Also see the tests in quickfix.mina.ssl.SecureSocketTest and quickfix.mina.ssl.SSLCertificateTest. +

Supplying SSL contexts from the application

+ +

By default, QuickFIX/J loads the key and trust stores named in the settings from the file system or +classpath. An application can instead supply the SSLContext itself through an +SSLContextProvider, e.g. when the stores come from a secrets manager and should stay in +memory. For example, with stores provided as Base64-encoded environment variables:

+ +
+char[] password = System.getenv("FIX_KEYSTORE_PASSWORD").toCharArray();
+KeyStore keyStore = KeyStore.getInstance("PKCS12");
+keyStore.load(new ByteArrayInputStream(Base64.getMimeDecoder().decode(System.getenv("FIX_KEYSTORE_B64"))), password);
+KeyStore trustStore = KeyStore.getInstance("PKCS12");
+trustStore.load(new ByteArrayInputStream(Base64.getMimeDecoder().decode(System.getenv("FIX_TRUSTSTORE_B64"))),
+        System.getenv("FIX_TRUSTSTORE_PASSWORD").toCharArray());
+
+KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+kmf.init(keyStore, password);
+TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+tmf.init(trustStore);
+SSLContext sslContext = SSLContext.getInstance("TLS");
+sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
+
+SocketInitiator initiator = SocketInitiator.newBuilder()
+        // ... application, store factory, settings
+        .withSSLContextProvider(sslConfig -> sslContext)
+        .build();
+
+ +

The provider is set with withSSLContextProvider on the builders of +SocketInitiator, SocketAcceptor and their threaded variants; without it, the +configured stores are loaded as before. The provider receives the +SSLConfig of the session or acceptor address, so it can return different contexts, e.g. a +different client certificate per counterparty, and delegate to +SSLContextFactory.getInstance(sslConfig) for stores that should still be loaded from files.

+ + + diff --git a/quickfixj-core/src/main/java/quickfix/AbstractSessionConnectorBuilder.java b/quickfixj-core/src/main/java/quickfix/AbstractSessionConnectorBuilder.java index 2b7e5faa2..923803654 100644 --- a/quickfixj-core/src/main/java/quickfix/AbstractSessionConnectorBuilder.java +++ b/quickfixj-core/src/main/java/quickfix/AbstractSessionConnectorBuilder.java @@ -1,5 +1,8 @@ package quickfix; +import quickfix.mina.ssl.SSLContextFactory; +import quickfix.mina.ssl.SSLContextProvider; + public abstract class AbstractSessionConnectorBuilder { private final Class derived; Application application; @@ -7,6 +10,7 @@ public abstract class AbstractSessionConnectorBuilder { SessionSettings settings; LogFactory logFactory; MessageFactory messageFactory; + SSLContextProvider sslContextProvider; int queueCapacity = -1; int queueLowerWatermark = -1; @@ -41,6 +45,11 @@ public Derived withMessageFactory(MessageFactory val) throws ConfigError { return derived.cast(this); } + public Derived withSSLContextProvider(SSLContextProvider val) { + sslContextProvider = val; + return derived.cast(this); + } + public Derived withQueueCapacity(int val) throws ConfigError { if (queueLowerWatermark >= 0) { throw new ConfigError("queue capacity and watermarks may not be configured together"); @@ -69,6 +78,9 @@ public final Product build() throws ConfigError { if (messageFactory == null) { messageFactory = new DefaultMessageFactory(); } + if (sslContextProvider == null) { + sslContextProvider = SSLContextFactory::getInstance; + } return doBuild(); } diff --git a/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java b/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java index 23a276944..57c28cb83 100644 --- a/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java +++ b/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java @@ -34,7 +34,7 @@ public class SocketAcceptor extends AbstractSocketAcceptor { private SocketAcceptor(Builder builder) throws ConfigError { super(builder.application, builder.messageStoreFactory, builder.settings, - builder.logFactory, builder.messageFactory); + builder.logFactory, builder.messageFactory, builder.sslContextProvider); if (builder.queueCapacity >= 0) { eventHandlingStrategy diff --git a/quickfixj-core/src/main/java/quickfix/SocketInitiator.java b/quickfixj-core/src/main/java/quickfix/SocketInitiator.java index fca1b0d08..864ca19ca 100644 --- a/quickfixj-core/src/main/java/quickfix/SocketInitiator.java +++ b/quickfixj-core/src/main/java/quickfix/SocketInitiator.java @@ -34,7 +34,7 @@ public class SocketInitiator extends AbstractSocketInitiator { private SocketInitiator(Builder builder) throws ConfigError { super(builder.application, builder.messageStoreFactory, builder.settings, - builder.logFactory, builder.messageFactory, builder.numReconnectThreads); + builder.logFactory, builder.messageFactory, builder.numReconnectThreads, builder.sslContextProvider); if (builder.queueCapacity >= 0) { eventHandlingStrategy diff --git a/quickfixj-core/src/main/java/quickfix/ThreadedSocketAcceptor.java b/quickfixj-core/src/main/java/quickfix/ThreadedSocketAcceptor.java index a1190bb58..66179f69a 100644 --- a/quickfixj-core/src/main/java/quickfix/ThreadedSocketAcceptor.java +++ b/quickfixj-core/src/main/java/quickfix/ThreadedSocketAcceptor.java @@ -31,7 +31,7 @@ public class ThreadedSocketAcceptor extends AbstractSocketAcceptor { private ThreadedSocketAcceptor(Builder builder) throws ConfigError { super(builder.application, builder.messageStoreFactory, builder.settings, - builder.logFactory, builder.messageFactory); + builder.logFactory, builder.messageFactory, builder.sslContextProvider); if (builder.queueCapacity >= 0) { eventHandlingStrategy diff --git a/quickfixj-core/src/main/java/quickfix/ThreadedSocketInitiator.java b/quickfixj-core/src/main/java/quickfix/ThreadedSocketInitiator.java index 2a01b8b3c..32927f00a 100644 --- a/quickfixj-core/src/main/java/quickfix/ThreadedSocketInitiator.java +++ b/quickfixj-core/src/main/java/quickfix/ThreadedSocketInitiator.java @@ -31,7 +31,7 @@ public class ThreadedSocketInitiator extends AbstractSocketInitiator { private ThreadedSocketInitiator(Builder builder) throws ConfigError { super(builder.application, builder.messageStoreFactory, builder.settings, - builder.logFactory, builder.messageFactory, builder.numReconnectThreads); + builder.logFactory, builder.messageFactory, builder.numReconnectThreads, builder.sslContextProvider); if (builder.queueCapacity >= 0) { eventHandlingStrategy diff --git a/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java b/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java index dfb998a2e..7a9d5ae4b 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java +++ b/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java @@ -49,6 +49,7 @@ import quickfix.mina.ssl.AcceptorSslFilter; import quickfix.mina.ssl.SSLConfig; import quickfix.mina.ssl.SSLContextFactory; +import quickfix.mina.ssl.SSLContextProvider; import quickfix.mina.ssl.SSLSupport; import javax.net.ssl.SSLContext; @@ -60,6 +61,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -71,13 +73,20 @@ public abstract class AbstractSocketAcceptor extends SessionConnector implements private final SessionFactory sessionFactory; private final Map socketDescriptorForAddress = new HashMap<>(); private final ConcurrentMap ioAcceptors = new ConcurrentHashMap<>(); + private final SSLContextProvider sslContextProvider; protected AbstractSocketAcceptor(SessionSettings settings, SessionFactory sessionFactory) throws ConfigError { + this(settings, sessionFactory, SSLContextFactory::getInstance); + } + + protected AbstractSocketAcceptor(SessionSettings settings, SessionFactory sessionFactory, + SSLContextProvider sslContextProvider) throws ConfigError { super(settings, sessionFactory); IoBuffer.setAllocator(new SimpleBufferAllocator()); IoBuffer.setUseDirectBuffer(false); this.sessionFactory = sessionFactory; + this.sslContextProvider = Objects.requireNonNull(sslContextProvider, "sslContextProvider"); } protected AbstractSocketAcceptor(Application application, @@ -90,8 +99,15 @@ protected AbstractSocketAcceptor(Application application, protected AbstractSocketAcceptor(Application application, MessageStoreFactory messageStoreFactory, SessionSettings settings, LogFactory logFactory, MessageFactory messageFactory) throws ConfigError { + this(application, messageStoreFactory, settings, logFactory, messageFactory, SSLContextFactory::getInstance); + } + + protected AbstractSocketAcceptor(Application application, + MessageStoreFactory messageStoreFactory, SessionSettings settings, + LogFactory logFactory, MessageFactory messageFactory, + SSLContextProvider sslContextProvider) throws ConfigError { this(settings, new DefaultSessionFactory(application, messageStoreFactory, logFactory, - messageFactory)); + messageFactory), sslContextProvider); } // TODO SYNC Does this method really need synchronization? @@ -135,7 +151,10 @@ private void installSSL(AcceptorSocketDescriptor descriptor, CompositeIoFilterChainBuilder ioFilterChainBuilder) throws GeneralSecurityException { log.info("Installing SSL filter for {}", descriptor.getAddress()); SSLConfig sslConfig = descriptor.getSslConfig(); - SSLContext sslContext = SSLContextFactory.getInstance(sslConfig); + SSLContext sslContext = sslContextProvider.getSSLContext(sslConfig); + if (sslContext == null) { + throw new GeneralSecurityException("SSLContextProvider returned null for " + descriptor.getAddress()); + } HostResolutionStrategy hostResolutionStrategy = HostResolutionStrategy.fromReverseDnsEnabled( descriptor.isReverseDnsEnabled()); SslFilter sslFilter = new AcceptorSslFilter(sslContext, hostResolutionStrategy); diff --git a/quickfixj-core/src/main/java/quickfix/mina/initiator/AbstractSocketInitiator.java b/quickfixj-core/src/main/java/quickfix/mina/initiator/AbstractSocketInitiator.java index 649d3fca1..c0e100aac 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/initiator/AbstractSocketInitiator.java +++ b/quickfixj-core/src/main/java/quickfix/mina/initiator/AbstractSocketInitiator.java @@ -43,6 +43,8 @@ import quickfix.mina.ProtocolFactory; import quickfix.mina.SessionConnector; import quickfix.mina.ssl.SSLConfig; +import quickfix.mina.ssl.SSLContextFactory; +import quickfix.mina.ssl.SSLContextProvider; import quickfix.mina.ssl.SSLSupport; import java.net.SocketAddress; @@ -51,6 +53,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; @@ -67,6 +70,7 @@ public abstract class AbstractSocketInitiator extends SessionConnector implement protected final Logger log = LoggerFactory.getLogger(getClass()); private final Set initiators = ConcurrentHashMap.newKeySet(); private final ScheduledExecutorService scheduledReconnectExecutor; + private final SSLContextProvider sslContextProvider; public static final String QFJ_RECONNECT_THREAD_PREFIX = "QFJ Reconnect Thread-"; protected AbstractSocketInitiator(Application application, @@ -84,13 +88,27 @@ protected AbstractSocketInitiator(SessionSettings settings, SessionFactory sessi protected AbstractSocketInitiator(Application application, MessageStoreFactory messageStoreFactory, SessionSettings settings, LogFactory logFactory, MessageFactory messageFactory, int numReconnectThreads) throws ConfigError { + this(application, messageStoreFactory, settings, logFactory, messageFactory, numReconnectThreads, + SSLContextFactory::getInstance); + } + + protected AbstractSocketInitiator(Application application, + MessageStoreFactory messageStoreFactory, SessionSettings settings, + LogFactory logFactory, MessageFactory messageFactory, int numReconnectThreads, + SSLContextProvider sslContextProvider) throws ConfigError { this(settings, new DefaultSessionFactory(application, messageStoreFactory, logFactory, - messageFactory), numReconnectThreads); + messageFactory), numReconnectThreads, sslContextProvider); } protected AbstractSocketInitiator(SessionSettings settings, SessionFactory sessionFactory, int numReconnectThreads) throws ConfigError { + this(settings, sessionFactory, numReconnectThreads, SSLContextFactory::getInstance); + } + + protected AbstractSocketInitiator(SessionSettings settings, SessionFactory sessionFactory, int numReconnectThreads, + SSLContextProvider sslContextProvider) throws ConfigError { super(settings, sessionFactory); + this.sslContextProvider = Objects.requireNonNull(sslContextProvider, "sslContextProvider"); IoBuffer.setAllocator(new SimpleBufferAllocator()); IoBuffer.setUseDirectBuffer(false); if (numReconnectThreads > 0) { @@ -184,7 +202,7 @@ && getSettings().isSetting(sessionID, Initiator.SETTING_PROXY_DOMAIN)) { final IoSessionInitiator ioSessionInitiator = new IoSessionInitiator(session, socketAddresses, localAddress, hostResolutionStrategy, connectTimeout, reconnectingIntervals, scheduledExecutorService, settings, networkingOptions, - getEventHandlingStrategy(), getIoFilterChainBuilder(), sslEnabled, sslConfig, + getEventHandlingStrategy(), getIoFilterChainBuilder(), sslEnabled, sslConfig, sslContextProvider, proxyType, proxyVersion, proxyHost, proxyPort, proxyUser, proxyPassword, proxyDomain, proxyWorkstation); initiators.add(ioSessionInitiator); diff --git a/quickfixj-core/src/main/java/quickfix/mina/initiator/IoSessionInitiator.java b/quickfixj-core/src/main/java/quickfix/mina/initiator/IoSessionInitiator.java index 84adf56e1..4ed0cbaa2 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/initiator/IoSessionInitiator.java +++ b/quickfixj-core/src/main/java/quickfix/mina/initiator/IoSessionInitiator.java @@ -43,6 +43,7 @@ import quickfix.mina.ssl.InitiatorSslFilter; import quickfix.mina.ssl.SSLConfig; import quickfix.mina.ssl.SSLContextFactory; +import quickfix.mina.ssl.SSLContextProvider; import quickfix.mina.ssl.SSLSupport; import javax.net.ssl.SSLContext; @@ -72,6 +73,20 @@ public class IoSessionInitiator { IoFilterChainBuilder userIoFilterChainBuilder, boolean sslEnabled, SSLConfig sslConfig, String proxyType, String proxyVersion, String proxyHost, int proxyPort, String proxyUser, String proxyPassword, String proxyDomain, String proxyWorkstation) throws ConfigError { + this(fixSession, socketAddresses, localAddress, hostResolutionStrategy, connectTimeout, + reconnectIntervalInSeconds, executor, sessionSettings, networkingOptions, eventHandlingStrategy, + userIoFilterChainBuilder, sslEnabled, sslConfig, SSLContextFactory::getInstance, proxyType, + proxyVersion, proxyHost, proxyPort, proxyUser, proxyPassword, proxyDomain, proxyWorkstation); + } + + IoSessionInitiator(Session fixSession, SocketAddress[] socketAddresses, + SocketAddress localAddress, HostResolutionStrategy hostResolutionStrategy, int connectTimeout, + int[] reconnectIntervalInSeconds, ScheduledExecutorService executor, SessionSettings sessionSettings, + NetworkingOptions networkingOptions, EventHandlingStrategy eventHandlingStrategy, + IoFilterChainBuilder userIoFilterChainBuilder, boolean sslEnabled, SSLConfig sslConfig, + SSLContextProvider sslContextProvider, String proxyType, String proxyVersion, String proxyHost, + int proxyPort, String proxyUser, String proxyPassword, String proxyDomain, String proxyWorkstation) + throws ConfigError { this.executor = executor; final long connectTimeoutMillis = connectTimeout * 1000L; @@ -83,7 +98,7 @@ public class IoSessionInitiator { try { reconnectTask = new ConnectTask(sslEnabled, socketAddresses, localAddress, userIoFilterChainBuilder, fixSession, connectTimeoutMillis, reconnectIntervalInMillis, - sessionSettings, networkingOptions, eventHandlingStrategy, sslConfig, + sessionSettings, networkingOptions, eventHandlingStrategy, sslConfig, sslContextProvider, proxyType, proxyVersion, proxyHost, proxyPort, proxyUser, proxyPassword, proxyDomain, proxyWorkstation, log, hostResolutionStrategy); } catch (GeneralSecurityException e) { @@ -106,6 +121,7 @@ private static class ConnectTask implements Runnable { private final NetworkingOptions networkingOptions; private final EventHandlingStrategy eventHandlingStrategy; private final SSLConfig sslConfig; + private final SSLContextProvider sslContextProvider; private final Logger log; private final HostResolutionStrategy hostResolutionStrategy; @@ -129,7 +145,7 @@ private static class ConnectTask implements Runnable { SocketAddress localAddress, IoFilterChainBuilder userIoFilterChainBuilder, Session fixSession, long connectTimeoutMillis, long[] reconnectIntervalInMillis, SessionSettings sessionSettings, NetworkingOptions networkingOptions, EventHandlingStrategy eventHandlingStrategy, SSLConfig sslConfig, - String proxyType, String proxyVersion, String proxyHost, + SSLContextProvider sslContextProvider, String proxyType, String proxyVersion, String proxyHost, int proxyPort, String proxyUser, String proxyPassword, String proxyDomain, String proxyWorkstation, Logger log, HostResolutionStrategy hostResolutionStrategy) throws ConfigError, GeneralSecurityException { this.sslEnabled = sslEnabled; @@ -143,6 +159,7 @@ private static class ConnectTask implements Runnable { this.networkingOptions = networkingOptions; this.eventHandlingStrategy = eventHandlingStrategy; this.sslConfig = sslConfig; + this.sslContextProvider = sslContextProvider; this.log = log; this.hostResolutionStrategy = hostResolutionStrategy; @@ -197,7 +214,10 @@ private void setupIoConnector() throws ConfigError, GeneralSecurityException { private void installSslFilter(CompositeIoFilterChainBuilder ioFilterChainBuilder) throws GeneralSecurityException { - final SSLContext sslContext = SSLContextFactory.getInstance(sslConfig); + final SSLContext sslContext = sslContextProvider.getSSLContext(sslConfig); + if (sslContext == null) { + throw new GeneralSecurityException("SSLContextProvider returned null for " + fixSession.getSessionID()); + } final SslFilter sslFilter = new InitiatorSslFilter(sslContext, getSniHostName(sslConfig), hostResolutionStrategy); sslFilter.setEnabledCipherSuites(sslConfig.getEnabledCipherSuites() != null ? sslConfig.getEnabledCipherSuites() : SSLSupport.getDefaultCipherSuites(sslContext)); diff --git a/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLContextProvider.java b/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLContextProvider.java new file mode 100644 index 000000000..0e740496f --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLContextProvider.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) quickfixengine.org All rights reserved. + * + * This file is part of the QuickFIX FIX Engine + * + * This file may be distributed under the terms of the quickfixengine.org + * license as defined by quickfixengine.org and appearing in the file + * LICENSE included in the packaging of this file. + * + * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING + * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE. + * + * See http://www.quickfixengine.org/LICENSE for licensing information. + * + * Contact ask@quickfixengine.org if any conditions of this licensing + * are not clear to you. + ******************************************************************************/ + +package quickfix.mina.ssl; + +import javax.net.ssl.SSLContext; +import java.security.GeneralSecurityException; + +/** + * Provides the {@link SSLContext} for an SSL-enabled session or acceptor address. + * The default is {@link SSLContextFactory#getInstance(SSLConfig)}. + */ +@FunctionalInterface +public interface SSLContextProvider { + + /** + * @param sslConfig the SSL configuration of the session or acceptor address + * @return an initialized SSL context, never null + * @throws GeneralSecurityException if the context cannot be created + */ + SSLContext getSSLContext(SSLConfig sslConfig) throws GeneralSecurityException; +} diff --git a/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLContextProviderTest.java b/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLContextProviderTest.java new file mode 100644 index 000000000..b59384450 --- /dev/null +++ b/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLContextProviderTest.java @@ -0,0 +1,356 @@ +/******************************************************************************* + * Copyright (c) quickfixengine.org All rights reserved. + * + * This file is part of the QuickFIX FIX Engine + * + * This file may be distributed under the terms of the quickfixengine.org + * license as defined by quickfixengine.org and appearing in the file + * LICENSE included in the packaging of this file. + * + * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING + * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE. + * + * See http://www.quickfixengine.org/LICENSE for licensing information. + * + * Contact ask@quickfixengine.org if any conditions of this licensing + * are not clear to you. + ******************************************************************************/ + +package quickfix.mina.ssl; + +import org.apache.mina.core.service.IoAcceptor; +import org.apache.mina.util.AvailablePortFinder; +import org.junit.After; +import org.junit.Test; +import quickfix.ApplicationAdapter; +import quickfix.ConfigError; +import quickfix.FixVersions; +import quickfix.MemoryStoreFactory; +import quickfix.RuntimeError; +import quickfix.ScreenLogFactory; +import quickfix.Session; +import quickfix.SessionFactory; +import quickfix.SessionID; +import quickfix.SessionSettings; +import quickfix.SocketAcceptor; +import quickfix.SocketInitiator; +import quickfix.ThreadedSocketAcceptor; +import quickfix.ThreadedSocketInitiator; +import quickfix.test.util.SSLUtil; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyStore; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class SSLContextProviderTest { + private static final SessionID ACCEPTOR_SESSION = new SessionID(FixVersions.BEGINSTRING_FIX44, "ALFA", "ZULU"); + private static final SessionID INITIATOR_SESSION = new SessionID(FixVersions.BEGINSTRING_FIX44, "ZULU", "ALFA"); + private static final BigInteger SERVER_SERIAL = new BigInteger("1449683167"); + private static final BigInteger CLIENT_SERIAL = new BigInteger("1449683336"); + private static final char[] PASSWORD = "password".toCharArray(); + + @After + public void tearDown() throws Exception { + for (SessionID sessionID : new SessionID[] { ACCEPTOR_SESSION, INITIATOR_SESSION }) { + Session session = Session.lookupSession(sessionID); + if (session != null) { + session.close(); + } + } + } + + @Test + public void shouldAuthenticateBothSidesWithInMemoryStores() throws Exception { + SSLContext serverContext = createContext("server-pkcs12.keystore", "PKCS12", + "server-jceks.truststore", "JCEKS"); + SSLContext clientContext = createContext("client-jceks.keystore", "JCEKS", + "client-jceks.keystore", "JCEKS"); + + int port = AvailablePortFinder.getNextAvailable(); + SessionSettings acceptorSettings = settings("acceptor", ACCEPTOR_SESSION, port); + acceptorSettings.setString(SSLSupport.SETTING_NEED_CLIENT_AUTH, "Y"); + SocketAcceptor acceptor = SocketAcceptor.newBuilder() + .withApplication(new ApplicationAdapter()) + .withMessageStoreFactory(new MemoryStoreFactory()) + .withSettings(acceptorSettings) + .withLogFactory(new ScreenLogFactory(false, false, false)) + .withSSLContextProvider(sslConfig -> serverContext) + .build(); + SocketInitiator initiator = SocketInitiator.newBuilder() + .withApplication(new ApplicationAdapter()) + .withMessageStoreFactory(new MemoryStoreFactory()) + .withSettings(settings("initiator", INITIATOR_SESSION, port)) + .withLogFactory(new ScreenLogFactory(false, false, false)) + .withSSLContextProvider(sslConfig -> clientContext) + .build(); + acceptor.start(); + try { + initiator.start(); + try { + SSLUtil.assertAuthenticated(initiator, INITIATOR_SESSION, SERVER_SERIAL); + SSLUtil.assertAuthenticated(acceptor, ACCEPTOR_SESSION, CLIENT_SERIAL); + } finally { + initiator.stop(true); + } + } finally { + acceptor.stop(true); + } + } + + @Test + public void shouldAuthenticateBothSidesWithThreadedConnectorsBuiltWithProvider() throws Exception { + SSLContext serverContext = createContext("server-pkcs12.keystore", "PKCS12", + "server-jceks.truststore", "JCEKS"); + SSLContext clientContext = createContext("client-jceks.keystore", "JCEKS", + "client-jceks.keystore", "JCEKS"); + + int port = AvailablePortFinder.getNextAvailable(); + SessionSettings acceptorSettings = settings("acceptor", ACCEPTOR_SESSION, port); + acceptorSettings.setString(SSLSupport.SETTING_NEED_CLIENT_AUTH, "Y"); + ThreadedSocketAcceptor acceptor = ThreadedSocketAcceptor.newBuilder() + .withApplication(new ApplicationAdapter()) + .withMessageStoreFactory(new MemoryStoreFactory()) + .withSettings(acceptorSettings) + .withLogFactory(new ScreenLogFactory(false, false, false)) + .withSSLContextProvider(sslConfig -> serverContext) + .build(); + ThreadedSocketInitiator initiator = ThreadedSocketInitiator.newBuilder() + .withApplication(new ApplicationAdapter()) + .withMessageStoreFactory(new MemoryStoreFactory()) + .withSettings(settings("initiator", INITIATOR_SESSION, port)) + .withLogFactory(new ScreenLogFactory(false, false, false)) + .withSSLContextProvider(sslConfig -> clientContext) + .build(); + acceptor.start(); + try { + initiator.start(); + try { + SSLUtil.assertAuthenticated(initiator, INITIATOR_SESSION, SERVER_SERIAL); + SSLUtil.assertAuthenticated(acceptor, ACCEPTOR_SESSION, CLIENT_SERIAL); + } finally { + initiator.stop(true); + } + } finally { + acceptor.stop(true); + } + } + + @Test + public void shouldLoadConfiguredStoresWhenNoProviderIsSet() throws Exception { + SessionSettings settings = settings("acceptor", ACCEPTOR_SESSION, AvailablePortFinder.getNextAvailable()); + settings.setString(SSLSupport.SETTING_KEY_STORE_NAME, "single-session/server.keystore"); + settings.setString(SSLSupport.SETTING_KEY_STORE_PWD, "password"); + SocketAcceptor acceptor = acceptor(settings, null); + try { + acceptor.start(); + assertEquals(1, acceptor.getEndpoints().size()); + assertTrue(acceptor.getEndpoints().iterator().next().isActive()); + } finally { + acceptor.stop(true); + } + } + + @Test + public void shouldFailInitiatorWhenProviderThrows() throws Exception { + GeneralSecurityException failure = new GeneralSecurityException("secret unavailable"); + SocketInitiator initiator = initiator( + settings("initiator", INITIATOR_SESSION, AvailablePortFinder.getNextAvailable()), sslConfig -> { + throw failure; + }); + try { + initiator.start(); + fail("Expected initiator start to fail"); + } catch (ConfigError e) { + assertSame(failure, e.getCause()); + } finally { + initiator.stop(true); + } + } + + @Test + public void shouldFailAcceptorWhenProviderThrows() throws Exception { + GeneralSecurityException failure = new GeneralSecurityException("secret unavailable"); + SocketAcceptor acceptor = acceptor( + settings("acceptor", ACCEPTOR_SESSION, AvailablePortFinder.getNextAvailable()), sslConfig -> { + throw failure; + }); + try { + acceptor.start(); + fail("Expected acceptor start to fail"); + } catch (RuntimeError e) { + assertSame(failure, e.getCause()); + } finally { + acceptor.stop(true); + } + } + + @Test + public void shouldSkipAcceptorAddressWhenProviderThrowsAndContinuingInitialization() throws Exception { + SessionSettings settings = settings("acceptor", ACCEPTOR_SESSION, AvailablePortFinder.getNextAvailable()); + settings.setString(SessionFactory.SETTING_CONTINUE_INIT_ON_ERROR, "Y"); + SocketAcceptor acceptor = acceptor(settings, sslConfig -> { + throw new GeneralSecurityException("secret unavailable"); + }); + try { + acceptor.start(); + // The endpoint is registered before SSL is installed, but its address must not be bound. + for (IoAcceptor endpoint : acceptor.getEndpoints()) { + assertFalse(endpoint.isActive()); + } + } finally { + acceptor.stop(true); + } + } + + @Test + public void shouldSkipInitiatorSessionWhenProviderThrowsAndContinuingInitialization() throws Exception { + SessionSettings settings = settings("initiator", INITIATOR_SESSION, AvailablePortFinder.getNextAvailable()); + settings.setString(SessionFactory.SETTING_CONTINUE_INIT_ON_ERROR, "Y"); + SocketInitiator initiator = initiator(settings, sslConfig -> { + throw new GeneralSecurityException("secret unavailable"); + }); + try { + initiator.start(); + assertTrue(initiator.getInitiators().isEmpty()); + } finally { + initiator.stop(true); + } + } + + @Test + public void shouldFailInitiatorWhenProviderReturnsNull() throws Exception { + SocketInitiator initiator = initiator( + settings("initiator", INITIATOR_SESSION, AvailablePortFinder.getNextAvailable()), sslConfig -> null); + try { + initiator.start(); + fail("Expected initiator start to fail"); + } catch (ConfigError e) { + assertTrue(String.valueOf(e.getCause()), e.getCause() instanceof GeneralSecurityException); + assertEquals("SSLContextProvider returned null for " + INITIATOR_SESSION, e.getCause().getMessage()); + } finally { + initiator.stop(true); + } + } + + @Test + public void shouldFailAcceptorWhenProviderReturnsNull() throws Exception { + SocketAcceptor acceptor = acceptor( + settings("acceptor", ACCEPTOR_SESSION, AvailablePortFinder.getNextAvailable()), sslConfig -> null); + try { + acceptor.start(); + fail("Expected acceptor start to fail"); + } catch (RuntimeError e) { + assertTrue(String.valueOf(e.getCause()), e.getCause() instanceof GeneralSecurityException); + assertTrue(e.getCause().getMessage(), e.getCause().getMessage().startsWith("SSLContextProvider returned null for ")); + } finally { + acceptor.stop(true); + } + } + + @Test + public void shouldSkipAcceptorAddressWhenProviderReturnsNullAndContinuingInitialization() throws Exception { + SessionSettings settings = settings("acceptor", ACCEPTOR_SESSION, AvailablePortFinder.getNextAvailable()); + settings.setString(SessionFactory.SETTING_CONTINUE_INIT_ON_ERROR, "Y"); + SocketAcceptor acceptor = acceptor(settings, sslConfig -> null); + try { + acceptor.start(); + // The endpoint is registered before SSL is installed, but its address must not be bound. + for (IoAcceptor endpoint : acceptor.getEndpoints()) { + assertFalse(endpoint.isActive()); + } + } finally { + acceptor.stop(true); + } + } + + @Test + public void shouldSkipInitiatorSessionWhenProviderReturnsNullAndContinuingInitialization() throws Exception { + SessionSettings settings = settings("initiator", INITIATOR_SESSION, AvailablePortFinder.getNextAvailable()); + settings.setString(SessionFactory.SETTING_CONTINUE_INIT_ON_ERROR, "Y"); + SocketInitiator initiator = initiator(settings, sslConfig -> null); + try { + initiator.start(); + assertTrue(initiator.getInitiators().isEmpty()); + } finally { + initiator.stop(true); + } + } + + private static SocketAcceptor acceptor(SessionSettings settings, SSLContextProvider provider) throws ConfigError { + return SocketAcceptor.newBuilder() + .withApplication(new ApplicationAdapter()) + .withMessageStoreFactory(new MemoryStoreFactory()) + .withSettings(settings) + .withLogFactory(new ScreenLogFactory(false, false, false)) + .withSSLContextProvider(provider) + .build(); + } + + private static SocketInitiator initiator(SessionSettings settings, SSLContextProvider provider) throws ConfigError { + return SocketInitiator.newBuilder() + .withApplication(new ApplicationAdapter()) + .withMessageStoreFactory(new MemoryStoreFactory()) + .withSettings(settings) + .withLogFactory(new ScreenLogFactory(false, false, false)) + .withSSLContextProvider(provider) + .build(); + } + + private static SessionSettings settings(String connectionType, SessionID sessionID, int port) { + SessionSettings settings = new SessionSettings(); + settings.setString("ConnectionType", connectionType); + settings.setString("StartTime", "00:00:00"); + settings.setString("EndTime", "00:00:00"); + settings.setString("HeartBtInt", "30"); + settings.setString("ReconnectInterval", "1"); + settings.setString("UseDataDictionary", "N"); + settings.setString("SocketAcceptAddress", "127.0.0.1"); + settings.setString("SocketAcceptPort", Integer.toString(port)); + settings.setString("SocketConnectHost", "127.0.0.1"); + settings.setString("SocketConnectPort", Integer.toString(port)); + settings.setString(SSLSupport.SETTING_USE_SSL, "Y"); + settings.setString(sessionID, "BeginString", sessionID.getBeginString()); + return settings; + } + + /** Builds a context from stores held in memory, the way an application would after decoding a secret. */ + private static SSLContext createContext(String keyStore, String keyStoreType, + String trustStore, String trustStoreType) throws Exception { + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(loadFromBytes(keyStore, keyStoreType), PASSWORD); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(loadFromBytes(trustStore, trustStoreType)); + SSLContext context = SSLContext.getInstance("TLS"); + context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + return context; + } + + private static KeyStore loadFromBytes(String resource, String type) throws Exception { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (InputStream in = SSLContextProviderTest.class.getResourceAsStream("/single-session/" + resource)) { + assertNotNull(resource, in); + byte[] buffer = new byte[4096]; + int n; + while ((n = in.read(buffer)) != -1) { + bytes.write(buffer, 0, n); + } + } + KeyStore store = KeyStore.getInstance(type); + store.load(new ByteArrayInputStream(bytes.toByteArray()), PASSWORD); + return store; + } +}