From 59a243c7556918ab47ffed632a332a26a0280498 Mon Sep 17 00:00:00 2001 From: nkrivenko Date: Tue, 13 Oct 2026 21:24:58 +0300 Subject: [PATCH 1/6] Add pluggable SSLContextProvider for in-memory key and trust stores Allow applications to supply the SSLContext for SSL-enabled sessions instead of having SSLContextFactory load stores from files, e.g. when key material is delivered by a secrets manager as environment variables. - Add SSLContextProvider, keyed by SSLConfig; the default is SSLContextFactory::getInstance, so existing behaviour is unchanged. - Add setSSLContextProvider on SessionConnector and withSSLContextProvider on the connector builders. - Report provider failures and null contexts as initialization errors, honouring ContinueInitializationOnError; no fallback to file stores. - Document usage in the secure communications user manual. Relates to #1343, #678 --- .../usage/secure_communications.html | 50 ++++ .../AbstractSessionConnectorBuilder.java | 8 + .../main/java/quickfix/SocketAcceptor.java | 4 +- .../main/java/quickfix/SocketInitiator.java | 4 +- .../java/quickfix/ThreadedSocketAcceptor.java | 4 +- .../quickfix/ThreadedSocketInitiator.java | 4 +- .../java/quickfix/mina/SessionConnector.java | 29 +++ .../mina/acceptor/AbstractSocketAcceptor.java | 3 +- .../initiator/AbstractSocketInitiator.java | 2 +- .../mina/initiator/IoSessionInitiator.java | 23 +- .../quickfix/mina/ssl/SSLContextProvider.java | 33 +++ .../mina/ssl/SSLContextProviderTest.java | 221 ++++++++++++++++++ 12 files changed, 375 insertions(+), 10 deletions(-) create mode 100644 quickfixj-core/src/main/java/quickfix/mina/ssl/SSLContextProvider.java create mode 100644 quickfixj-core/src/test/java/quickfix/mina/ssl/SSLContextProviderTest.java 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 af48371862..e02a86162a 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,55 @@

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"))), null);
+
+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();
+
+ +

Connectors created with a constructor can call setSSLContextProvider before +start(). Passing null restores the default behaviour. 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 2b7e5faa23..d253530c13 100644 --- a/quickfixj-core/src/main/java/quickfix/AbstractSessionConnectorBuilder.java +++ b/quickfixj-core/src/main/java/quickfix/AbstractSessionConnectorBuilder.java @@ -1,5 +1,7 @@ package quickfix; +import quickfix.mina.ssl.SSLContextProvider; + public abstract class AbstractSessionConnectorBuilder { private final Class derived; Application application; @@ -7,6 +9,7 @@ public abstract class AbstractSessionConnectorBuilder { SessionSettings settings; LogFactory logFactory; MessageFactory messageFactory; + SSLContextProvider sslContextProvider; int queueCapacity = -1; int queueLowerWatermark = -1; @@ -41,6 +44,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"); diff --git a/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java b/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java index 23a2769448..0f4db99789 100644 --- a/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java +++ b/quickfixj-core/src/main/java/quickfix/SocketAcceptor.java @@ -56,7 +56,9 @@ private Builder() { @Override protected SocketAcceptor doBuild() throws ConfigError { - return new SocketAcceptor(this); + SocketAcceptor socketAcceptor = new SocketAcceptor(this); + socketAcceptor.setSSLContextProvider(sslContextProvider); + return socketAcceptor; } } diff --git a/quickfixj-core/src/main/java/quickfix/SocketInitiator.java b/quickfixj-core/src/main/java/quickfix/SocketInitiator.java index fca1b0d089..211d9e66d6 100644 --- a/quickfixj-core/src/main/java/quickfix/SocketInitiator.java +++ b/quickfixj-core/src/main/java/quickfix/SocketInitiator.java @@ -64,7 +64,9 @@ public Builder withReconnectThreads(int numReconnectThreads) throws ConfigError @Override protected SocketInitiator doBuild() throws ConfigError { - return new SocketInitiator(this); + SocketInitiator socketInitiator = new SocketInitiator(this); + socketInitiator.setSSLContextProvider(sslContextProvider); + return socketInitiator; } } diff --git a/quickfixj-core/src/main/java/quickfix/ThreadedSocketAcceptor.java b/quickfixj-core/src/main/java/quickfix/ThreadedSocketAcceptor.java index a1190bb585..2fee1ad65c 100644 --- a/quickfixj-core/src/main/java/quickfix/ThreadedSocketAcceptor.java +++ b/quickfixj-core/src/main/java/quickfix/ThreadedSocketAcceptor.java @@ -53,7 +53,9 @@ private Builder() { @Override protected ThreadedSocketAcceptor doBuild() throws ConfigError { - return new ThreadedSocketAcceptor(this); + ThreadedSocketAcceptor threadedSocketAcceptor = new ThreadedSocketAcceptor(this); + threadedSocketAcceptor.setSSLContextProvider(sslContextProvider); + return threadedSocketAcceptor; } } diff --git a/quickfixj-core/src/main/java/quickfix/ThreadedSocketInitiator.java b/quickfixj-core/src/main/java/quickfix/ThreadedSocketInitiator.java index 2a01b8b3c5..3edb62b4a4 100644 --- a/quickfixj-core/src/main/java/quickfix/ThreadedSocketInitiator.java +++ b/quickfixj-core/src/main/java/quickfix/ThreadedSocketInitiator.java @@ -61,7 +61,9 @@ public Builder withReconnectThreads(int numReconnectThreads) throws ConfigError @Override protected ThreadedSocketInitiator doBuild() throws ConfigError { - return new ThreadedSocketInitiator(this); + ThreadedSocketInitiator threadedSocketInitiator = new ThreadedSocketInitiator(this); + threadedSocketInitiator.setSSLContextProvider(sslContextProvider); + return threadedSocketInitiator; } } diff --git a/quickfixj-core/src/main/java/quickfix/mina/SessionConnector.java b/quickfixj-core/src/main/java/quickfix/mina/SessionConnector.java index 74cdbda99d..6c771e6161 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/SessionConnector.java +++ b/quickfixj-core/src/main/java/quickfix/mina/SessionConnector.java @@ -33,10 +33,14 @@ import quickfix.SessionID; import quickfix.SessionSettings; import quickfix.field.converter.IntConverter; +import quickfix.mina.ssl.SSLContextFactory; +import quickfix.mina.ssl.SSLContextProvider; +import javax.net.ssl.SSLContext; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.IOException; +import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -76,6 +80,7 @@ public abstract class SessionConnector implements Connector { .newSingleThreadScheduledExecutor(new QFTimerThreadFactory()); private ScheduledFuture sessionTimerFuture; private IoFilterChainBuilder ioFilterChainBuilder; + private SSLContextProvider sslContextProvider = SSLContextFactory::getInstance; protected Executor longLivedExecutor; protected Executor shortLivedExecutor; @@ -424,6 +429,30 @@ protected IoFilterChainBuilder getIoFilterChainBuilder() { return ioFilterChainBuilder; } + /** + * Sets the provider of SSL contexts, or null for the default. Must be set before the connector is started. + */ + public void setSSLContextProvider(SSLContextProvider sslContextProvider) { + this.sslContextProvider = sslContextProvider != null ? sslContextProvider : SSLContextFactory::getInstance; + } + + // Reports provider failures and missing contexts as GeneralSecurityException + protected SSLContextProvider getSSLContextProvider() { + final SSLContextProvider provider = sslContextProvider; + return sslConfig -> { + final SSLContext sslContext; + try { + sslContext = provider.getSSLContext(sslConfig); + } catch (RuntimeException e) { + throw new GeneralSecurityException("SSLContextProvider failed", e); + } + if (sslContext == null) { + throw new GeneralSecurityException("SSLContextProvider returned no SSL context"); + } + return sslContext; + }; + } + /** * Closes all managed sessions of an Initiator/Acceptor. * 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 dfb998a2e6..a609232ee4 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java +++ b/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java @@ -48,7 +48,6 @@ import quickfix.mina.message.FIXProtocolCodecFactory; import quickfix.mina.ssl.AcceptorSslFilter; import quickfix.mina.ssl.SSLConfig; -import quickfix.mina.ssl.SSLContextFactory; import quickfix.mina.ssl.SSLSupport; import javax.net.ssl.SSLContext; @@ -135,7 +134,7 @@ 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 = getSSLContextProvider().getSSLContext(sslConfig); 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 649d3fca14..44b347e263 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/initiator/AbstractSocketInitiator.java +++ b/quickfixj-core/src/main/java/quickfix/mina/initiator/AbstractSocketInitiator.java @@ -184,7 +184,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, getSSLContextProvider(), 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 84adf56e18..5ee31c772d 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,7 @@ 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); 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 0000000000..18bf74bc5e --- /dev/null +++ b/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLContextProvider.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * 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 { + + 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 0000000000..33ba25cab4 --- /dev/null +++ b/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLContextProviderTest.java @@ -0,0 +1,221 @@ +/******************************************************************************* + * 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.DefaultMessageFactory; +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.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.KeyStore; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +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(); + try { + acceptor.start(); + initiator.start(); + SSLUtil.assertAuthenticated(initiator, INITIATOR_SESSION, SERVER_SERIAL); + SSLUtil.assertAuthenticated(acceptor, ACCEPTOR_SESSION, CLIENT_SERIAL); + } finally { + initiator.stop(true); + acceptor.stop(true); + } + } + + @Test + public void shouldFailInitiatorWithoutFallbackWhenNoContextIsProvided() throws Exception { + SocketInitiator initiator = new SocketInitiator(new ApplicationAdapter(), new MemoryStoreFactory(), + settings("initiator", INITIATOR_SESSION, AvailablePortFinder.getNextAvailable()), + new ScreenLogFactory(false, false, false), new DefaultMessageFactory()); + initiator.setSSLContextProvider(sslConfig -> null); + try { + initiator.start(); + fail("Expected initiator start to fail"); + } catch (ConfigError e) { + assertTrue(e.getMessage(), e.getMessage().contains("SSLContextProvider returned no SSL context")); + } finally { + initiator.stop(true); + } + } + + @Test + public void shouldFailAcceptorWhenProviderThrows() throws Exception { + SocketAcceptor acceptor = new SocketAcceptor(new ApplicationAdapter(), new MemoryStoreFactory(), + settings("acceptor", ACCEPTOR_SESSION, AvailablePortFinder.getNextAvailable()), + new ScreenLogFactory(false, false, false), new DefaultMessageFactory()); + acceptor.setSSLContextProvider(sslConfig -> { + throw new IllegalStateException("secret unavailable"); + }); + try { + acceptor.start(); + fail("Expected acceptor start to fail"); + } catch (RuntimeError e) { + assertTrue(String.valueOf(e.getCause()), e.getCause().getMessage().contains("SSLContextProvider failed")); + } 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 = new SocketAcceptor(new ApplicationAdapter(), new MemoryStoreFactory(), + settings, new ScreenLogFactory(false, false, false), new DefaultMessageFactory()); + acceptor.setSSLContextProvider(sslConfig -> { + throw new IllegalStateException("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 = new SocketInitiator(new ApplicationAdapter(), new MemoryStoreFactory(), + settings, new ScreenLogFactory(false, false, false), new DefaultMessageFactory()); + initiator.setSSLContextProvider(sslConfig -> { + throw new IllegalStateException("secret unavailable"); + }); + try { + initiator.start(); + assertTrue(initiator.getInitiators().isEmpty()); + } finally { + initiator.stop(true); + } + } + + 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; + } +} From 23cf2c8c223fc41f0d57d5e7ca0a4d12edde8487 Mon Sep 17 00:00:00 2001 From: nkrivenko Date: Wed, 14 Oct 2026 11:40:46 +0300 Subject: [PATCH 2/6] Test SSLContextProvider with threaded connector builders and null reset --- .../mina/ssl/SSLContextProviderTest.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLContextProviderTest.java b/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLContextProviderTest.java index 33ba25cab4..085e73be4c 100644 --- a/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLContextProviderTest.java +++ b/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLContextProviderTest.java @@ -36,6 +36,8 @@ 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; @@ -47,6 +49,7 @@ import java.math.BigInteger; 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.assertTrue; @@ -104,6 +107,61 @@ public void shouldAuthenticateBothSidesWithInMemoryStores() throws Exception { } } + @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(); + try { + acceptor.start(); + initiator.start(); + SSLUtil.assertAuthenticated(initiator, INITIATOR_SESSION, SERVER_SERIAL); + SSLUtil.assertAuthenticated(acceptor, ACCEPTOR_SESSION, CLIENT_SERIAL); + } finally { + initiator.stop(true); + acceptor.stop(true); + } + } + + @Test + public void shouldLoadConfiguredStoresWhenProviderIsResetToNull() 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 = new SocketAcceptor(new ApplicationAdapter(), new MemoryStoreFactory(), + settings, new ScreenLogFactory(false, false, false), new DefaultMessageFactory()); + acceptor.setSSLContextProvider(sslConfig -> { + throw new AssertionError("provider must not be used after reset"); + }); + acceptor.setSSLContextProvider(null); + try { + acceptor.start(); + assertEquals(1, acceptor.getEndpoints().size()); + assertTrue(acceptor.getEndpoints().iterator().next().isActive()); + } finally { + acceptor.stop(true); + } + } + @Test public void shouldFailInitiatorWithoutFallbackWhenNoContextIsProvided() throws Exception { SocketInitiator initiator = new SocketInitiator(new ApplicationAdapter(), new MemoryStoreFactory(), From 62b6d7dee68c656a115bd20780b4cfcd7e6cbb7d Mon Sep 17 00:00:00 2001 From: nkrivenko Date: Wed, 14 Oct 2026 12:16:59 +0300 Subject: [PATCH 3/6] Make SSLContextProvider a final field set through the connector builders Address review: remove SessionConnector.setSSLContextProvider and the wrapping getter, keep the provider in a final, never-null field of the socket acceptor and initiator, and pass it through their constructors. The builder defaults the provider to SSLContextFactory::getInstance in build(), like the log and message factories. --- .../usage/secure_communications.html | 11 +-- .../AbstractSessionConnectorBuilder.java | 4 ++ .../main/java/quickfix/SocketAcceptor.java | 6 +- .../main/java/quickfix/SocketInitiator.java | 6 +- .../java/quickfix/ThreadedSocketAcceptor.java | 6 +- .../quickfix/ThreadedSocketInitiator.java | 6 +- .../java/quickfix/mina/SessionConnector.java | 29 -------- .../mina/acceptor/AbstractSocketAcceptor.java | 21 +++++- .../initiator/AbstractSocketInitiator.java | 22 +++++- .../quickfix/mina/ssl/SSLContextProvider.java | 5 ++ .../mina/ssl/SSLContextProviderTest.java | 70 +++++++++++-------- 11 files changed, 103 insertions(+), 83 deletions(-) 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 e02a86162a..b9820e9814 100644 --- a/quickfixj-core/src/main/doc/usermanual/usage/secure_communications.html +++ b/quickfixj-core/src/main/doc/usermanual/usage/secure_communications.html @@ -140,8 +140,9 @@

Supplying SSL contexts from the application

.build(); -

Connectors created with a constructor can call setSSLContextProvider before -start(). Passing null restores the default behaviour. The provider receives the +

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.

@@ -150,9 +151,9 @@

Supplying SSL contexts from the application

  • SocketUseSSL=Y is still required. Only the context comes from the provider; protocols, cipher suites, NeedClientAuth, SNI and endpoint identification are still applied from the settings, while store settings are not used.
  • -
  • If the provider throws or returns null, the session (initiator) or listening address -(acceptor) fails to initialize, subject to ContinueInitializationOnError. There is no -implicit fallback to the default behaviour.
  • +
  • If the provider throws a GeneralSecurityException, the session (initiator) or listening +address (acceptor) fails to initialize, subject to ContinueInitializationOnError. There is no +implicit fallback to the default behaviour. The provider must not return null.
  • 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 diff --git a/quickfixj-core/src/main/java/quickfix/AbstractSessionConnectorBuilder.java b/quickfixj-core/src/main/java/quickfix/AbstractSessionConnectorBuilder.java index d253530c13..9238036548 100644 --- a/quickfixj-core/src/main/java/quickfix/AbstractSessionConnectorBuilder.java +++ b/quickfixj-core/src/main/java/quickfix/AbstractSessionConnectorBuilder.java @@ -1,5 +1,6 @@ package quickfix; +import quickfix.mina.ssl.SSLContextFactory; import quickfix.mina.ssl.SSLContextProvider; public abstract class AbstractSessionConnectorBuilder { @@ -77,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 0f4db99789..57c28cb83d 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 @@ -56,9 +56,7 @@ private Builder() { @Override protected SocketAcceptor doBuild() throws ConfigError { - SocketAcceptor socketAcceptor = new SocketAcceptor(this); - socketAcceptor.setSSLContextProvider(sslContextProvider); - return socketAcceptor; + return new SocketAcceptor(this); } } diff --git a/quickfixj-core/src/main/java/quickfix/SocketInitiator.java b/quickfixj-core/src/main/java/quickfix/SocketInitiator.java index 211d9e66d6..864ca19ca5 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 @@ -64,9 +64,7 @@ public Builder withReconnectThreads(int numReconnectThreads) throws ConfigError @Override protected SocketInitiator doBuild() throws ConfigError { - SocketInitiator socketInitiator = new SocketInitiator(this); - socketInitiator.setSSLContextProvider(sslContextProvider); - return socketInitiator; + return new SocketInitiator(this); } } diff --git a/quickfixj-core/src/main/java/quickfix/ThreadedSocketAcceptor.java b/quickfixj-core/src/main/java/quickfix/ThreadedSocketAcceptor.java index 2fee1ad65c..66179f69a0 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 @@ -53,9 +53,7 @@ private Builder() { @Override protected ThreadedSocketAcceptor doBuild() throws ConfigError { - ThreadedSocketAcceptor threadedSocketAcceptor = new ThreadedSocketAcceptor(this); - threadedSocketAcceptor.setSSLContextProvider(sslContextProvider); - return threadedSocketAcceptor; + return new ThreadedSocketAcceptor(this); } } diff --git a/quickfixj-core/src/main/java/quickfix/ThreadedSocketInitiator.java b/quickfixj-core/src/main/java/quickfix/ThreadedSocketInitiator.java index 3edb62b4a4..32927f00a9 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 @@ -61,9 +61,7 @@ public Builder withReconnectThreads(int numReconnectThreads) throws ConfigError @Override protected ThreadedSocketInitiator doBuild() throws ConfigError { - ThreadedSocketInitiator threadedSocketInitiator = new ThreadedSocketInitiator(this); - threadedSocketInitiator.setSSLContextProvider(sslContextProvider); - return threadedSocketInitiator; + return new ThreadedSocketInitiator(this); } } diff --git a/quickfixj-core/src/main/java/quickfix/mina/SessionConnector.java b/quickfixj-core/src/main/java/quickfix/mina/SessionConnector.java index 6c771e6161..74cdbda99d 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/SessionConnector.java +++ b/quickfixj-core/src/main/java/quickfix/mina/SessionConnector.java @@ -33,14 +33,10 @@ import quickfix.SessionID; import quickfix.SessionSettings; import quickfix.field.converter.IntConverter; -import quickfix.mina.ssl.SSLContextFactory; -import quickfix.mina.ssl.SSLContextProvider; -import javax.net.ssl.SSLContext; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.IOException; -import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -80,7 +76,6 @@ public abstract class SessionConnector implements Connector { .newSingleThreadScheduledExecutor(new QFTimerThreadFactory()); private ScheduledFuture sessionTimerFuture; private IoFilterChainBuilder ioFilterChainBuilder; - private SSLContextProvider sslContextProvider = SSLContextFactory::getInstance; protected Executor longLivedExecutor; protected Executor shortLivedExecutor; @@ -429,30 +424,6 @@ protected IoFilterChainBuilder getIoFilterChainBuilder() { return ioFilterChainBuilder; } - /** - * Sets the provider of SSL contexts, or null for the default. Must be set before the connector is started. - */ - public void setSSLContextProvider(SSLContextProvider sslContextProvider) { - this.sslContextProvider = sslContextProvider != null ? sslContextProvider : SSLContextFactory::getInstance; - } - - // Reports provider failures and missing contexts as GeneralSecurityException - protected SSLContextProvider getSSLContextProvider() { - final SSLContextProvider provider = sslContextProvider; - return sslConfig -> { - final SSLContext sslContext; - try { - sslContext = provider.getSSLContext(sslConfig); - } catch (RuntimeException e) { - throw new GeneralSecurityException("SSLContextProvider failed", e); - } - if (sslContext == null) { - throw new GeneralSecurityException("SSLContextProvider returned no SSL context"); - } - return sslContext; - }; - } - /** * Closes all managed sessions of an Initiator/Acceptor. * 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 a609232ee4..5829751e1c 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java +++ b/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java @@ -48,6 +48,8 @@ import quickfix.mina.message.FIXProtocolCodecFactory; 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; @@ -59,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; @@ -70,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, @@ -89,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? @@ -134,7 +151,7 @@ private void installSSL(AcceptorSocketDescriptor descriptor, CompositeIoFilterChainBuilder ioFilterChainBuilder) throws GeneralSecurityException { log.info("Installing SSL filter for {}", descriptor.getAddress()); SSLConfig sslConfig = descriptor.getSslConfig(); - SSLContext sslContext = getSSLContextProvider().getSSLContext(sslConfig); + SSLContext sslContext = sslContextProvider.getSSLContext(sslConfig); 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 44b347e263..c0e100aac7 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, getSSLContextProvider(), + 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/ssl/SSLContextProvider.java b/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLContextProvider.java index 18bf74bc5e..0e740496f4 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLContextProvider.java +++ b/quickfixj-core/src/main/java/quickfix/mina/ssl/SSLContextProvider.java @@ -29,5 +29,10 @@ @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 index 085e73be4c..8e0efac76c 100644 --- a/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLContextProviderTest.java +++ b/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLContextProviderTest.java @@ -25,7 +25,6 @@ import org.junit.Test; import quickfix.ApplicationAdapter; import quickfix.ConfigError; -import quickfix.DefaultMessageFactory; import quickfix.FixVersions; import quickfix.MemoryStoreFactory; import quickfix.RuntimeError; @@ -47,11 +46,13 @@ 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; @@ -143,16 +144,11 @@ public void shouldAuthenticateBothSidesWithThreadedConnectorsBuiltWithProvider() } @Test - public void shouldLoadConfiguredStoresWhenProviderIsResetToNull() throws Exception { + 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 = new SocketAcceptor(new ApplicationAdapter(), new MemoryStoreFactory(), - settings, new ScreenLogFactory(false, false, false), new DefaultMessageFactory()); - acceptor.setSSLContextProvider(sslConfig -> { - throw new AssertionError("provider must not be used after reset"); - }); - acceptor.setSSLContextProvider(null); + SocketAcceptor acceptor = acceptor(settings, null); try { acceptor.start(); assertEquals(1, acceptor.getEndpoints().size()); @@ -163,16 +159,17 @@ public void shouldLoadConfiguredStoresWhenProviderIsResetToNull() throws Excepti } @Test - public void shouldFailInitiatorWithoutFallbackWhenNoContextIsProvided() throws Exception { - SocketInitiator initiator = new SocketInitiator(new ApplicationAdapter(), new MemoryStoreFactory(), - settings("initiator", INITIATOR_SESSION, AvailablePortFinder.getNextAvailable()), - new ScreenLogFactory(false, false, false), new DefaultMessageFactory()); - initiator.setSSLContextProvider(sslConfig -> null); + 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) { - assertTrue(e.getMessage(), e.getMessage().contains("SSLContextProvider returned no SSL context")); + assertSame(failure, e.getCause()); } finally { initiator.stop(true); } @@ -180,17 +177,16 @@ public void shouldFailInitiatorWithoutFallbackWhenNoContextIsProvided() throws E @Test public void shouldFailAcceptorWhenProviderThrows() throws Exception { - SocketAcceptor acceptor = new SocketAcceptor(new ApplicationAdapter(), new MemoryStoreFactory(), - settings("acceptor", ACCEPTOR_SESSION, AvailablePortFinder.getNextAvailable()), - new ScreenLogFactory(false, false, false), new DefaultMessageFactory()); - acceptor.setSSLContextProvider(sslConfig -> { - throw new IllegalStateException("secret unavailable"); - }); + 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) { - assertTrue(String.valueOf(e.getCause()), e.getCause().getMessage().contains("SSLContextProvider failed")); + assertSame(failure, e.getCause()); } finally { acceptor.stop(true); } @@ -200,10 +196,8 @@ public void shouldFailAcceptorWhenProviderThrows() throws Exception { public void shouldSkipAcceptorAddressWhenProviderThrowsAndContinuingInitialization() throws Exception { SessionSettings settings = settings("acceptor", ACCEPTOR_SESSION, AvailablePortFinder.getNextAvailable()); settings.setString(SessionFactory.SETTING_CONTINUE_INIT_ON_ERROR, "Y"); - SocketAcceptor acceptor = new SocketAcceptor(new ApplicationAdapter(), new MemoryStoreFactory(), - settings, new ScreenLogFactory(false, false, false), new DefaultMessageFactory()); - acceptor.setSSLContextProvider(sslConfig -> { - throw new IllegalStateException("secret unavailable"); + SocketAcceptor acceptor = acceptor(settings, sslConfig -> { + throw new GeneralSecurityException("secret unavailable"); }); try { acceptor.start(); @@ -220,10 +214,8 @@ public void shouldSkipAcceptorAddressWhenProviderThrowsAndContinuingInitializati public void shouldSkipInitiatorSessionWhenProviderThrowsAndContinuingInitialization() throws Exception { SessionSettings settings = settings("initiator", INITIATOR_SESSION, AvailablePortFinder.getNextAvailable()); settings.setString(SessionFactory.SETTING_CONTINUE_INIT_ON_ERROR, "Y"); - SocketInitiator initiator = new SocketInitiator(new ApplicationAdapter(), new MemoryStoreFactory(), - settings, new ScreenLogFactory(false, false, false), new DefaultMessageFactory()); - initiator.setSSLContextProvider(sslConfig -> { - throw new IllegalStateException("secret unavailable"); + SocketInitiator initiator = initiator(settings, sslConfig -> { + throw new GeneralSecurityException("secret unavailable"); }); try { initiator.start(); @@ -233,6 +225,26 @@ public void shouldSkipInitiatorSessionWhenProviderThrowsAndContinuingInitializat } } + 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); From 03fcd096104f5766ca1d6da3ce99931b531ff25c Mon Sep 17 00:00:00 2001 From: nkrivenko Date: Fri, 16 Oct 2026 12:07:12 +0300 Subject: [PATCH 4/6] Stop connectors in separate finally blocks and fix trust store example Give each connector in SSLContextProviderTest its own try/finally, so a failing stop cannot leave the other connector running. Load the trust store in the user manual example with its password: PKCS12 trust stores created by current JDKs encrypt their certificates, so loading them without a password yields an empty trust store. --- .../usage/secure_communications.html | 3 ++- .../mina/ssl/SSLContextProviderTest.java | 22 ++++++++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) 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 b9820e9814..4543be3163 100644 --- a/quickfixj-core/src/main/doc/usermanual/usage/secure_communications.html +++ b/quickfixj-core/src/main/doc/usermanual/usage/secure_communications.html @@ -125,7 +125,8 @@

    Supplying SSL contexts from the application

    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"))), null); +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); diff --git a/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLContextProviderTest.java b/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLContextProviderTest.java index 8e0efac76c..df560e7b1f 100644 --- a/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLContextProviderTest.java +++ b/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLContextProviderTest.java @@ -97,13 +97,16 @@ public void shouldAuthenticateBothSidesWithInMemoryStores() throws Exception { .withLogFactory(new ScreenLogFactory(false, false, false)) .withSSLContextProvider(sslConfig -> clientContext) .build(); + acceptor.start(); try { - acceptor.start(); initiator.start(); - SSLUtil.assertAuthenticated(initiator, INITIATOR_SESSION, SERVER_SERIAL); - SSLUtil.assertAuthenticated(acceptor, ACCEPTOR_SESSION, CLIENT_SERIAL); + try { + SSLUtil.assertAuthenticated(initiator, INITIATOR_SESSION, SERVER_SERIAL); + SSLUtil.assertAuthenticated(acceptor, ACCEPTOR_SESSION, CLIENT_SERIAL); + } finally { + initiator.stop(true); + } } finally { - initiator.stop(true); acceptor.stop(true); } } @@ -132,13 +135,16 @@ public void shouldAuthenticateBothSidesWithThreadedConnectorsBuiltWithProvider() .withLogFactory(new ScreenLogFactory(false, false, false)) .withSSLContextProvider(sslConfig -> clientContext) .build(); + acceptor.start(); try { - acceptor.start(); initiator.start(); - SSLUtil.assertAuthenticated(initiator, INITIATOR_SESSION, SERVER_SERIAL); - SSLUtil.assertAuthenticated(acceptor, ACCEPTOR_SESSION, CLIENT_SERIAL); + try { + SSLUtil.assertAuthenticated(initiator, INITIATOR_SESSION, SERVER_SERIAL); + SSLUtil.assertAuthenticated(acceptor, ACCEPTOR_SESSION, CLIENT_SERIAL); + } finally { + initiator.stop(true); + } } finally { - initiator.stop(true); acceptor.stop(true); } } From fd3831112814f963e6c63cc79a0ae0faf9b9d6da Mon Sep 17 00:00:00 2001 From: nkrivenko Date: Wed, 28 Oct 2026 13:50:03 +0200 Subject: [PATCH 5/6] Treat a null SSLContext from the provider as an initialization error A null context used to reach MINA's SslFilter and fail start() with an NPE that bypassed ContinueInitializationOnError. Report it as a GeneralSecurityException naming the session or acceptor address, so it goes through the existing initialization error handling. --- .../usage/secure_communications.html | 2 +- .../mina/acceptor/AbstractSocketAcceptor.java | 3 + .../mina/initiator/IoSessionInitiator.java | 3 + .../mina/ssl/SSLContextProviderTest.java | 59 +++++++++++++++++++ 4 files changed, 66 insertions(+), 1 deletion(-) 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 4543be3163..35f474a51e 100644 --- a/quickfixj-core/src/main/doc/usermanual/usage/secure_communications.html +++ b/quickfixj-core/src/main/doc/usermanual/usage/secure_communications.html @@ -154,7 +154,7 @@

    Supplying SSL contexts from the application

    settings, while store settings are not used.
  • If the provider throws a GeneralSecurityException, the session (initiator) or listening address (acceptor) fails to initialize, subject to ContinueInitializationOnError. There is no -implicit fallback to the default behaviour. The provider must not return null.
  • +implicit fallback to the default behaviour. Returning null is treated the same way.
  • 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 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 5829751e1c..7a9d5ae4bd 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java +++ b/quickfixj-core/src/main/java/quickfix/mina/acceptor/AbstractSocketAcceptor.java @@ -152,6 +152,9 @@ private void installSSL(AcceptorSocketDescriptor descriptor, log.info("Installing SSL filter for {}", descriptor.getAddress()); SSLConfig sslConfig = descriptor.getSslConfig(); 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/IoSessionInitiator.java b/quickfixj-core/src/main/java/quickfix/mina/initiator/IoSessionInitiator.java index 5ee31c772d..4ed0cbaa28 100644 --- a/quickfixj-core/src/main/java/quickfix/mina/initiator/IoSessionInitiator.java +++ b/quickfixj-core/src/main/java/quickfix/mina/initiator/IoSessionInitiator.java @@ -215,6 +215,9 @@ private void setupIoConnector() throws ConfigError, GeneralSecurityException { private void installSslFilter(CompositeIoFilterChainBuilder ioFilterChainBuilder) throws GeneralSecurityException { 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/test/java/quickfix/mina/ssl/SSLContextProviderTest.java b/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLContextProviderTest.java index df560e7b1f..b59384450c 100644 --- a/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLContextProviderTest.java +++ b/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLContextProviderTest.java @@ -231,6 +231,65 @@ public void shouldSkipInitiatorSessionWhenProviderThrowsAndContinuingInitializat } } + @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()) From f1e0f22ae573cc2a37bf49a9ffab9663245484cf Mon Sep 17 00:00:00 2001 From: nkrivenko Date: Wed, 28 Oct 2026 14:09:24 +0200 Subject: [PATCH 6/6] Clarify null handling of SSLContextProvider in the user manual --- .../main/doc/usermanual/usage/secure_communications.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 35f474a51e..b0a708c0d5 100644 --- a/quickfixj-core/src/main/doc/usermanual/usage/secure_communications.html +++ b/quickfixj-core/src/main/doc/usermanual/usage/secure_communications.html @@ -152,9 +152,9 @@

    Supplying SSL contexts from the application

  • SocketUseSSL=Y is still required. Only the context comes from the provider; protocols, cipher suites, NeedClientAuth, SNI and endpoint identification are still applied from the settings, while store settings are not used.
  • -
  • If the provider throws a GeneralSecurityException, the session (initiator) or listening -address (acceptor) fails to initialize, subject to ContinueInitializationOnError. There is no -implicit fallback to the default behaviour. Returning null is treated the same way.
  • +
  • If the provider throws a GeneralSecurityException or returns null, the session +(initiator) or listening address (acceptor) fails to initialize, subject to +ContinueInitializationOnError. There is no implicit fallback to the default behavior.
  • 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