Skip to content

Pluggable SSLContextProvider for in-memory key and trust stores - #1344

Open
nkrivenko wants to merge 4 commits into
quickfix-j:masterfrom
nkrivenko:feature/1343-ssl-context-provider
Open

nkrivenko wants to merge 4 commits into
quickfix-j:masterfrom
nkrivenko:feature/1343-ssl-context-provider

Conversation

@nkrivenko

@nkrivenko nkrivenko commented Oct 14, 2026 •

Copy link
Copy Markdown

Summary

Adds a pluggable SSLContextProvider so applications can supply the SSLContext for SSL-enabled sessions instead of having SSLContextFactory load key and trust stores from files. This lets key material held in memory, e.g. delivered by a secrets manager as environment variables, be used without writing it to disk.

Closes #1343. Follows the approach suggested in #678 / discussion #679.

Changes

  • SSLContextProvider (quickfix.mina.ssl): a functional interface, SSLContext getSSLContext(SSLConfig). The default is SSLContextFactory::getInstance, so existing behaviour is unchanged.
  • withSSLContextProvider(...) on the builders of SocketInitiator, SocketAcceptor and their threaded variants. Without it (or with null), the builder uses SSLContextFactory::getInstance.
  • The provider is held in a final, never-null field of AbstractSocketAcceptor / AbstractSocketInitiator, set through new constructor overloads; the existing constructors use the default.
  • Failure handling: if the provider throws GeneralSecurityException, the session (initiator) or listening address (acceptor) fails to initialize, honouring ContinueInitializationOnError. The provider must not return null. There is no silent fallback to file stores.
  • Only the SSLContext comes from the provider; protocols, cipher suites, NeedClientAuth, SNI and endpoint identification are still applied from the settings.
  • User manual: new "Supplying SSL contexts from the application" section in secure_communications.html.

Usage

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 receives the SSLConfig of each session or acceptor address, so it can also return different contexts per session, or delegate to SSLContextFactory.getInstance(sslConfig) for stores that should still be loaded from files.

Compatibility

  • No behaviour change for existing users; all new API is additive. SSLContextFactory and SessionConnector are unchanged, and connectors created with the existing constructors keep loading stores from files.
  • The existing package-private IoSessionInitiator constructor is kept and delegates to the new one.
  • Java 8 compatible, no new dependencies.

Testing

New SSLContextProviderTest:

  • mutual TLS logon with contexts built from in-memory PKCS12/JCEKS stores, using both the plain and the threaded connector builders;
  • without a provider, the configured stores are loaded from files as before;
  • initiator and acceptor start fail when the provider throws GeneralSecurityException;
  • with ContinueInitializationOnError=Y, a failing provider skips the acceptor address and the initiator session.

Existing SSLCertificateTest, SecureSocketTest, IoSessionInitiatorTest, SocketInitiatorTest and SocketAcceptorTest pass unchanged.

@nkrivenko
nkrivenko marked this pull request as ready for review October 14, 2026 07:20
@codecov

codecov Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.95238% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.51%. Comparing base (149869e) to head (9774ec4).

Files with missing lines Patch % Lines
...quickfix/mina/acceptor/AbstractSocketAcceptor.java 66.66% 2 Missing ⚠️
...ickfix/mina/initiator/AbstractSocketInitiator.java 66.66% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #1344      +/-   ##
============================================
+ Coverage     70.79%   71.51%   +0.72%     
- Complexity     2270     2297      +27     
============================================
  Files           161      161              
  Lines          9077     9094      +17     
  Branches       1192     1193       +1     
============================================
+ Hits           6426     6504      +78     
+ Misses         2198     2139      -59     
+ Partials        453      451       -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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 quickfix-j#1343, quickfix-j#678
@nkrivenko
nkrivenko force-pushed the feature/1343-ssl-context-provider branch from d4dd222 to 93736f6 Compare October 14, 2026 08:44

@the-thing the-thing left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Useful feature. Can we just make the field final and if specific provider logic is required can we make it in the builder?

log.info("Installing SSL filter for {}", descriptor.getAddress());
SSLConfig sslConfig = descriptor.getSslConfig();
SSLContext sslContext = SSLContextFactory.getInstance(sslConfig);
SSLContext sslContext = getSSLContextProvider().getSSLContext(sslConfig);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably shouldn't be a method, but just a final field that is never null.

.newSingleThreadScheduledExecutor(new QFTimerThreadFactory());
private ScheduledFuture<?> sessionTimerFuture;
private IoFilterChainBuilder ioFilterChainBuilder;
private SSLContextProvider sslContextProvider = SSLContextFactory::getInstance;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of initializing it like that this should be created via builder pattern.

/**
* Sets the provider of SSL contexts, or null for the default. Must be set before the connector is started.
*/
public void setSSLContextProvider(SSLContextProvider sslContextProvider) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mentioned before. Can we remove it and make the field final?

@Override
protected SocketAcceptor doBuild() throws ConfigError {
return new SocketAcceptor(this);
SocketAcceptor socketAcceptor = new SocketAcceptor(this);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This kinds of break builder pattern. The field should be set to final field in the socket acceptor constructor.

@Override
protected SocketInitiator doBuild() throws ConfigError {
return new SocketInitiator(this);
SocketInitiator socketInitiator = new SocketInitiator(this);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. This method shouldn't change.

@Override
protected ThreadedSocketAcceptor doBuild() throws ConfigError {
return new ThreadedSocketAcceptor(this);
ThreadedSocketAcceptor threadedSocketAcceptor = new ThreadedSocketAcceptor(this);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here as well.

@Override
protected ThreadedSocketInitiator doBuild() throws ConfigError {
return new ThreadedSocketInitiator(this);
ThreadedSocketInitiator threadedSocketInitiator = new ThreadedSocketInitiator(this);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here.

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.
@nkrivenko

Copy link
Copy Markdown
Author

Hello @the-thing , thank you for your comments, moved everything to builders and made the field final. Could you please review again when you have a chance?

@the-thing

Copy link
Copy Markdown
Collaborator

Will do. It might take a few days, but I will. Stay tuned.

@the-thing the-thing left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

SSLContextProviderTest start / stop could have been wrapped in separate try / finally blocks.

Not for this PR, but I have a feeling that these overloaded super constructors are too large and probably every one of them should have a single parameter builder and take whatever they need from there.

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.
@nkrivenko

Copy link
Copy Markdown
Author

Thank you @the-thing , addressed this comment:

SSLContextProviderTest start / stop could have been wrapped in separate try / finally blocks.

Also will take a look whether we can replace the ctors' parameters with builders, will create an issue for that if this is feasible.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pluggable SSLContextProvider for in-memory keys and/or truststores

2 participants