Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,57 @@ <h2>Secure Communications with QFJ</h2>

Also see the tests in <code>quickfix.mina.ssl.SecureSocketTest</code> and <code>quickfix.mina.ssl.SSLCertificateTest</code>.

<h3>Supplying SSL contexts from the application</h3>

<p>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 <code>SSLContext</code> itself through an
<code>SSLContextProvider</code>, 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:</p>

<pre class="code">
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 -&gt; sslContext)
.build();
</pre>

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

<ul>
<li><code>SocketUseSSL=Y</code> is still required. Only the context comes from the provider; protocols,
cipher suites, <code>NeedClientAuth</code>, SNI and endpoint identification are still applied from the
settings, while store settings are not used.</li>
<li>If the provider throws a <code>GeneralSecurityException</code> or returns <code>null</code>, the session
(initiator) or listening address (acceptor) fails to initialize, subject to
<code>ContinueInitializationOnError</code>. There is no implicit fallback to the default behavior.</li>
<li>The provider is called whenever an SSL filter is set up: once per listening address when an
acceptor starts, and once per session when an initiator starts (and again if its I/O connector is
reset). Return a new context to pick up rotated credentials on the next start, or supply a key or
trust manager that reloads its material to rotate without restarting.</li>
<li>Contexts created by <code>SSLContextFactory</code> have their trust managers wrapped so that a
<code>RuntimeException</code> thrown during certificate validation is reported as a
<code>CertificateException</code>. A supplied context is used as is; wrap its trust managers with
<code>X509TrustManagerWrapper.wrap(...)</code> to get the same behaviour.</li>
</ul>

</body>
</html>
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package quickfix;

import quickfix.mina.ssl.SSLContextFactory;
import quickfix.mina.ssl.SSLContextProvider;

public abstract class AbstractSessionConnectorBuilder<Derived, Product> {
private final Class<Derived> derived;
Application application;
MessageStoreFactory messageStoreFactory;
SessionSettings settings;
LogFactory logFactory;
MessageFactory messageFactory;
SSLContextProvider sslContextProvider;

int queueCapacity = -1;
int queueLowerWatermark = -1;
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -69,6 +78,9 @@ public final Product build() throws ConfigError {
if (messageFactory == null) {
messageFactory = new DefaultMessageFactory();
}
if (sslContextProvider == null) {
sslContextProvider = SSLContextFactory::getInstance;
}
return doBuild();
}

Expand Down
2 changes: 1 addition & 1 deletion quickfixj-core/src/main/java/quickfix/SocketAcceptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion quickfixj-core/src/main/java/quickfix/SocketInitiator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -71,13 +73,20 @@ public abstract class AbstractSocketAcceptor extends SessionConnector implements
private final SessionFactory sessionFactory;
private final Map<SocketAddress, AcceptorSocketDescriptor> socketDescriptorForAddress = new HashMap<>();
private final ConcurrentMap<AcceptorSocketDescriptor, IoAcceptor> 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,
Expand All @@ -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?
Expand Down Expand Up @@ -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);
Comment thread
chrjohn marked this conversation as resolved.
Resolved
if (sslContext == null) {
throw new GeneralSecurityException("SSLContextProvider returned null for " + descriptor.getAddress());
}
HostResolutionStrategy hostResolutionStrategy = HostResolutionStrategy.fromReverseDnsEnabled(
descriptor.isReverseDnsEnabled());
SslFilter sslFilter = new AcceptorSslFilter(sslContext, hostResolutionStrategy);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -67,6 +70,7 @@ public abstract class AbstractSocketInitiator extends SessionConnector implement
protected final Logger log = LoggerFactory.getLogger(getClass());
private final Set<IoSessionInitiator> 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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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;

Expand All @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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);
Comment thread
chrjohn marked this conversation as resolved.
Resolved
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));
Expand Down
Loading
Loading