diff --git a/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLCertificateTest.java b/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLCertificateTest.java index 4bf176e092..a7998e05d1 100644 --- a/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLCertificateTest.java +++ b/quickfixj-core/src/test/java/quickfix/mina/ssl/SSLCertificateTest.java @@ -20,10 +20,11 @@ package quickfix.mina.ssl; import org.apache.mina.core.filterchain.IoFilterAdapter; -import org.apache.mina.core.filterchain.IoFilterChain; import org.apache.mina.core.session.IoSession; -import org.apache.mina.filter.ssl.SslFilter; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import quickfix.Acceptor; @@ -42,17 +43,18 @@ import quickfix.SessionSettings; import quickfix.ThreadedSocketAcceptor; import quickfix.ThreadedSocketInitiator; -import quickfix.mina.IoSessionResponder; import quickfix.mina.ProtocolFactory; import quickfix.mina.SessionConnector; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLHandshakeException; -import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; -import java.lang.reflect.Field; import java.math.BigInteger; +import java.security.Principal; import java.security.cert.Certificate; import java.security.cert.X509Certificate; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Properties; @@ -63,14 +65,27 @@ import org.apache.mina.util.AvailablePortFinder; import org.junit.After; import quickfix.mina.SocksProxyServer; +import quickfix.test.util.SSLUtil; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +@RunWith(Parameterized.class) public class SSLCertificateTest { + @Parameters + public static List parameters() { + return Arrays.asList(new String[][] {{"TLS_RSA_WITH_AES_128_CBC_SHA", "TLSv1.2"}, {"TLS_AES_256_GCM_SHA384", "TLSv1.3"}}); + } + // Note: To diagnose cipher suite errors, run with -Djavax.net.debug=ssl:handshake - private static final String CIPHER_SUITES_TLS = "TLS_RSA_WITH_AES_128_CBC_SHA"; + private final String enabledCipherSuites; + private final String enabledProtocols; + + public SSLCertificateTest(String enabledCipherSuites, String enabledProtocols) { + this.enabledCipherSuites = enabledCipherSuites; + this.enabledProtocols = enabledProtocols; + } @After public void cleanup() { @@ -86,14 +101,14 @@ public void cleanup() { public void shouldAuthenticateServerCertificate() throws Exception { int freePort = AvailablePortFinder.getNextAvailable(); TestAcceptor acceptor = new TestAcceptor(createAcceptorSettings("single-session/server.keystore", false, - "single-session/empty.keystore", CIPHER_SUITES_TLS, "TLSv1.2", "JKS", "JKS", freePort)); + "single-session/empty.keystore", enabledCipherSuites, enabledProtocols, "JKS", "JKS", freePort)); try { acceptor.start(); TestInitiator initiator = new TestInitiator( createInitiatorSettings("single-session/empty.keystore", "single-session/client.truststore", - CIPHER_SUITES_TLS, "TLSv1.2", "ZULU", "ALFA", Integer.toString(freePort), "JKS", "JKS")); + enabledCipherSuites, enabledProtocols, "ZULU", "ALFA", Integer.toString(freePort), "JKS", "JKS")); try { initiator.start(); @@ -105,7 +120,7 @@ public void shouldAuthenticateServerCertificate() throws Exception { acceptor.assertNoSslExceptionThrown(); acceptor.assertLoggedOn(new SessionID(FixVersions.BEGINSTRING_FIX44, "ALFA", "ZULU")); - acceptor.assertNotAuthenticated(new SessionID(FixVersions.BEGINSTRING_FIX44, "ALFA", "ZULU")); + acceptor.assertNotAuthenticated(new SessionID(FixVersions.BEGINSTRING_FIX44, "ALFA", "ZULU"), false); } finally { initiator.stop(); } @@ -138,13 +153,13 @@ public void shouldAuthenticateServerCertificateViaSocksProxy(String proxyVersion try { int port = AvailablePortFinder.getNextAvailable(); TestAcceptor acceptor = new TestAcceptor(createAcceptorSettings("single-session/server.keystore", false, - "single-session/empty.keystore", CIPHER_SUITES_TLS, "TLSv1.2", "JKS", "JKS", port)); + "single-session/empty.keystore", enabledCipherSuites, enabledProtocols, "JKS", "JKS", port)); try { acceptor.start(); SessionSettings initiatorSettings = createInitiatorSettings("single-session/empty.keystore", "single-session/client.truststore", - CIPHER_SUITES_TLS, "TLSv1.2", "ZULU", "ALFA", Integer.toString(port), "JKS", "JKS"); + enabledCipherSuites, enabledProtocols, "ZULU", "ALFA", Integer.toString(port), "JKS", "JKS"); Properties defaults = initiatorSettings.getDefaultProperties(); @@ -166,7 +181,7 @@ public void shouldAuthenticateServerCertificateViaSocksProxy(String proxyVersion acceptor.assertNoSslExceptionThrown(); acceptor.assertLoggedOn(new SessionID(FixVersions.BEGINSTRING_FIX44, "ALFA", "ZULU")); - acceptor.assertNotAuthenticated(new SessionID(FixVersions.BEGINSTRING_FIX44, "ALFA", "ZULU")); + acceptor.assertNotAuthenticated(new SessionID(FixVersions.BEGINSTRING_FIX44, "ALFA", "ZULU"), false); } finally { initiator.stop(); } @@ -185,14 +200,14 @@ public void shouldAuthenticateServerCertificateViaSocksProxy(String proxyVersion public void shouldAuthenticateServerNameUsingServerCommonName() throws Exception { int freePort = AvailablePortFinder.getNextAvailable(); TestAcceptor acceptor = new TestAcceptor(createAcceptorSettings("single-session/server-cn.keystore", false, - "single-session/empty.keystore", CIPHER_SUITES_TLS, "TLSv1.2", "JKS", "JKS", freePort)); + "single-session/empty.keystore", enabledCipherSuites, enabledProtocols, "JKS", "JKS", freePort)); try { acceptor.start(); TestInitiator initiator = new TestInitiator( createInitiatorSettings("single-session/empty.keystore", "single-session/client-cn.truststore", - CIPHER_SUITES_TLS, "TLSv1.2", "ZULU", "ALFA", Integer.toString(freePort), "JKS", "JKS", "HTTPS")); + enabledCipherSuites, enabledProtocols, "ZULU", "ALFA", Integer.toString(freePort), "JKS", "JKS", "HTTPS")); try { initiator.start(); @@ -204,7 +219,7 @@ public void shouldAuthenticateServerNameUsingServerCommonName() throws Exception acceptor.assertNoSslExceptionThrown(); acceptor.assertLoggedOn(new SessionID(FixVersions.BEGINSTRING_FIX44, "ALFA", "ZULU")); - acceptor.assertNotAuthenticated(new SessionID(FixVersions.BEGINSTRING_FIX44, "ALFA", "ZULU")); + acceptor.assertNotAuthenticated(new SessionID(FixVersions.BEGINSTRING_FIX44, "ALFA", "ZULU"), false); } finally { initiator.stop(); } @@ -220,14 +235,14 @@ public void shouldAuthenticateServerNameUsingServerCommonName() throws Exception public void shouldAuthenticateServerNameUsingSNIExtension() throws Exception { int freePort = AvailablePortFinder.getNextAvailable(); TestAcceptor acceptor = new TestAcceptor(createAcceptorSettings("single-session/server-sni.keystore", false, - "single-session/empty.keystore", CIPHER_SUITES_TLS, "TLSv1.2", "JKS", "JKS", freePort)); + "single-session/empty.keystore", enabledCipherSuites, enabledProtocols, "JKS", "JKS", freePort)); try { acceptor.start(); TestInitiator initiator = new TestInitiator( createInitiatorSettings("single-session/empty.keystore", "single-session/client-sni.truststore", - CIPHER_SUITES_TLS, "TLSv1.2", "ZULU", "ALFA", Integer.toString(freePort), "JKS", "JKS", "HTTPS")); + enabledCipherSuites, enabledProtocols, "ZULU", "ALFA", Integer.toString(freePort), "JKS", "JKS", "HTTPS")); try { initiator.start(); @@ -239,7 +254,7 @@ public void shouldAuthenticateServerNameUsingSNIExtension() throws Exception { acceptor.assertNoSslExceptionThrown(); acceptor.assertLoggedOn(new SessionID(FixVersions.BEGINSTRING_FIX44, "ALFA", "ZULU")); - acceptor.assertNotAuthenticated(new SessionID(FixVersions.BEGINSTRING_FIX44, "ALFA", "ZULU")); + acceptor.assertNotAuthenticated(new SessionID(FixVersions.BEGINSTRING_FIX44, "ALFA", "ZULU"), false); } finally { initiator.stop(); } @@ -256,14 +271,14 @@ public void shouldFailWhenHostnameDoesNotMatchServerName() throws Exception { int freePort = AvailablePortFinder.getNextAvailable(); TestAcceptor acceptor = new TestAcceptor(createAcceptorSettings("single-session/server-bad-cn.keystore", false, - "single-session/empty.keystore", CIPHER_SUITES_TLS, "TLSv1.2", "JKS", "JKS", freePort)); + "single-session/empty.keystore", enabledCipherSuites, enabledProtocols, "JKS", "JKS", freePort)); try { acceptor.start(); TestInitiator initiator = new TestInitiator( createInitiatorSettings("single-session/empty.keystore", "single-session/client-bad-cn.truststore", - CIPHER_SUITES_TLS, "TLSv1.2", "ZULU", "ALFA", Integer.toString(freePort), "JKS", "JKS", "HTTPS")); + enabledCipherSuites, enabledProtocols, "ZULU", "ALFA", Integer.toString(freePort), "JKS", "JKS", "HTTPS")); try { initiator.start(); @@ -287,14 +302,14 @@ public void shouldFailWhenHostnameDoesNotMatchServerName() throws Exception { public void shouldAuthenticateServerAndClientCertificates() throws Exception { int freePort = AvailablePortFinder.getNextAvailable(); TestAcceptor acceptor = new TestAcceptor(createAcceptorSettings("single-session/server.keystore", true, - "single-session/server.truststore", CIPHER_SUITES_TLS, "TLSv1.2", "JKS", "JKS", freePort)); + "single-session/server.truststore", enabledCipherSuites, enabledProtocols, "JKS", "JKS", freePort)); try { acceptor.start(); TestInitiator initiator = new TestInitiator( createInitiatorSettings("single-session/client.keystore", "single-session/client.truststore", - CIPHER_SUITES_TLS, "TLSv1.2", "ZULU", "ALFA", Integer.toString(freePort), "JKS", "JKS")); + enabledCipherSuites, enabledProtocols, "ZULU", "ALFA", Integer.toString(freePort), "JKS", "JKS")); try { initiator.start(); @@ -320,14 +335,14 @@ public void shouldAuthenticateServerAndClientCertificates() throws Exception { public void shouldAuthenticateServerAndClientCertificatesWhenUsingDifferentKeystoreFormats() throws Exception { int freePort = AvailablePortFinder.getNextAvailable(); TestAcceptor acceptor = new TestAcceptor(createAcceptorSettings("single-session/server-pkcs12.keystore", true, - "single-session/server-jceks.truststore", CIPHER_SUITES_TLS, "TLSv1.2", "PKCS12", + "single-session/server-jceks.truststore", enabledCipherSuites, enabledProtocols, "PKCS12", "JCEKS", freePort)); try { acceptor.start(); TestInitiator initiator = new TestInitiator(createInitiatorSettings("single-session/client-jceks.keystore", - "single-session/client-jceks.keystore", CIPHER_SUITES_TLS, "TLSv1.2", "ZULU", "ALFA", + "single-session/client-jceks.keystore", enabledCipherSuites, enabledProtocols, "ZULU", "ALFA", Integer.toString(freePort), "JCEKS", "JCEKS")); try { @@ -355,20 +370,20 @@ public void shouldAuthenticateServerAndClientCertificatesForIndividualSessions() TestAcceptor acceptor = new TestAcceptor(createMultiSessionAcceptorSettings( "multi-session/server.keystore", true, new String[] { "multi-session/server1.truststore", "multi-session/server2.truststore", "multi-session/server3.truststore" }, - CIPHER_SUITES_TLS, "TLSv1.2")); + enabledCipherSuites, enabledProtocols)); try { acceptor.start(); TestInitiator initiator1 = new TestInitiator( createInitiatorSettings("multi-session/client1.keystore", "multi-session/client1.keystore", - CIPHER_SUITES_TLS, "TLSv1.2", "ZULU0", "ALFA0", "12340", "JKS", "JKS")); + enabledCipherSuites, enabledProtocols, "ZULU0", "ALFA0", "12340", "JKS", "JKS")); TestInitiator initiator2 = new TestInitiator( createInitiatorSettings("multi-session/client2.keystore", "multi-session/client2.keystore", - CIPHER_SUITES_TLS, "TLSv1.2", "ZULU1", "ALFA1", "12341", "JKS", "JKS")); + enabledCipherSuites, enabledProtocols, "ZULU1", "ALFA1", "12341", "JKS", "JKS")); TestInitiator initiator3 = new TestInitiator( createInitiatorSettings("multi-session/client3.keystore", "multi-session/client3.keystore", - CIPHER_SUITES_TLS, "TLSv1.2", "ZULU2", "ALFA2", "12342", "JKS", "JKS")); + enabledCipherSuites, enabledProtocols, "ZULU2", "ALFA2", "12342", "JKS", "JKS")); try { initiator1.start(); @@ -416,20 +431,20 @@ public void shouldFailIndividualSessionsWhenInvalidCertificatesUsed() throws Exc TestAcceptor acceptor = new TestAcceptor(createMultiSessionAcceptorSettings( "multi-session/server.keystore", true, new String[] { "multi-session/server1.truststore", "multi-session/server2.truststore", "multi-session/server3.truststore" }, - CIPHER_SUITES_TLS, "TLSv1.2")); + enabledCipherSuites, enabledProtocols)); try { acceptor.start(); TestInitiator initiator1 = new TestInitiator( createInitiatorSettings("multi-session/client2.keystore", "multi-session/client2.keystore", - CIPHER_SUITES_TLS, "TLSv1.2", "ZULU0", "ALFA0", "12340", "JKS", "JKS")); + enabledCipherSuites, enabledProtocols, "ZULU0", "ALFA0", "12340", "JKS", "JKS")); TestInitiator initiator2 = new TestInitiator( createInitiatorSettings("multi-session/client1.keystore", "multi-session/client1.keystore", - CIPHER_SUITES_TLS, "TLSv1.2", "ZULU1", "ALFA1", "12341", "JKS", "JKS")); + enabledCipherSuites, enabledProtocols, "ZULU1", "ALFA1", "12341", "JKS", "JKS")); TestInitiator initiator3 = new TestInitiator( createInitiatorSettings("multi-session/client3.keystore", "multi-session/client3.keystore", - CIPHER_SUITES_TLS, "TLSv1.2", "ZULU2", "ALFA2", "12342", "JKS", "JKS")); + enabledCipherSuites, enabledProtocols, "ZULU2", "ALFA2", "12342", "JKS", "JKS")); try { initiator1.start(); @@ -501,14 +516,14 @@ public void shouldFailWhenUsingEmptyServerKeyStore() throws Exception { public void shouldFailWhenUsingEmptyClientTruststore() throws Exception { int freePort = AvailablePortFinder.getNextAvailable(); TestAcceptor acceptor = new TestAcceptor(createAcceptorSettings("single-session/server.keystore", false, - "single-session/empty.keystore", CIPHER_SUITES_TLS, "TLSv1.2", "JKS", "JKS", freePort)); + "single-session/empty.keystore", enabledCipherSuites, enabledProtocols, "JKS", "JKS", freePort)); try { acceptor.start(); TestInitiator initiator = new TestInitiator( createInitiatorSettings("single-session/empty.keystore", "single-session/empty.keystore", - CIPHER_SUITES_TLS, "TLSv1.2", "ZULU", "ALFA", Integer.toString(freePort), "JKS", "JKS")); + enabledCipherSuites, enabledProtocols, "ZULU", "ALFA", Integer.toString(freePort), "JKS", "JKS")); try { initiator.start(); @@ -532,14 +547,14 @@ public void shouldFailWhenUsingEmptyClientTruststore() throws Exception { public void shouldFailWhenUsingEmptyServerTrustore() throws Exception { int freePort = AvailablePortFinder.getNextAvailable(); TestAcceptor acceptor = new TestAcceptor(createAcceptorSettings("single-session/server.keystore", true, - "single-session/empty.keystore", CIPHER_SUITES_TLS, "TLSv1.2", "JKS", "JKS", freePort)); + "single-session/empty.keystore", enabledCipherSuites, enabledProtocols, "JKS", "JKS", freePort)); try { acceptor.start(); TestInitiator initiator = new TestInitiator( createInitiatorSettings("single-session/client.keystore", "single-session/client.truststore", - CIPHER_SUITES_TLS, "TLSv1.2", "ZULU", "ALFA", Integer.toString(freePort), "JKS", "JKS")); + enabledCipherSuites, enabledProtocols, "ZULU", "ALFA", Integer.toString(freePort), "JKS", "JKS")); try { initiator.start(); @@ -563,14 +578,14 @@ public void shouldFailWhenUsingEmptyServerTrustore() throws Exception { public void shouldFailWhenUsingBadClientCertificate() throws Exception { int freePort = AvailablePortFinder.getNextAvailable(); TestAcceptor acceptor = new TestAcceptor(createAcceptorSettings("single-session/server.keystore", true, - "single-session/server.truststore", CIPHER_SUITES_TLS, "TLSv1.2", "JKS", "JKS", freePort)); + "single-session/server.truststore", enabledCipherSuites, enabledProtocols, "JKS", "JKS", freePort)); try { acceptor.start(); TestInitiator initiator = new TestInitiator( createInitiatorSettings("single-session/server.keystore", "single-session/client.truststore", - CIPHER_SUITES_TLS, "TLSv1.2", "ZULU", "ALFA", Integer.toString(freePort), "JKS", "JKS")); + enabledCipherSuites, enabledProtocols, "ZULU", "ALFA", Integer.toString(freePort), "JKS", "JKS")); try { initiator.start(); @@ -594,14 +609,14 @@ public void shouldFailWhenUsingBadClientCertificate() throws Exception { public void shouldFailWhenUsingBadServerCertificate() throws Exception { int freePort = AvailablePortFinder.getNextAvailable(); TestAcceptor acceptor = new TestAcceptor(createAcceptorSettings("single-session/client.keystore", false, - "single-session/empty.keystore", CIPHER_SUITES_TLS, "TLSv1.2", "JKS", "JKS", freePort)); + "single-session/empty.keystore", enabledCipherSuites, enabledProtocols, "JKS", "JKS", freePort)); try { acceptor.start(); TestInitiator initiator = new TestInitiator( createInitiatorSettings("single-session/empty.keystore", "single-session/client.truststore", - CIPHER_SUITES_TLS, "TLSv1.2", "ZULU", "ALFA", Integer.toString(freePort), "JKS", "JKS")); + enabledCipherSuites, enabledProtocols, "ZULU", "ALFA", Integer.toString(freePort), "JKS", "JKS")); try { initiator.start(); @@ -632,7 +647,7 @@ public void shouldConnectDifferentTypesOfSessions() throws Exception { TestInitiator sslInitiator = new TestInitiator( createInitiatorSettings("single-session/client.keystore", "single-session/client.truststore", - CIPHER_SUITES_TLS, "TLSv1.2", "ZULU_SSL", "ALFA_SSL", Integer.toString(sslPort), "JKS", "JKS")); + enabledCipherSuites, enabledProtocols, "ZULU_SSL", "ALFA_SSL", Integer.toString(sslPort), "JKS", "JKS")); TestInitiator nonSslInitiator = new TestInitiator(createInitiatorSettings("ZULU_NON_SSL", "ALFA_NON_SSL", nonSslPort)); @@ -647,7 +662,7 @@ public void shouldConnectDifferentTypesOfSessions() throws Exception { acceptor.assertNoSslExceptionThrown(); acceptor.assertLoggedOn(new SessionID(FixVersions.BEGINSTRING_FIX44, "ALFA_SSL", "ZULU_SSL")); - acceptor.assertNotAuthenticated(new SessionID(FixVersions.BEGINSTRING_FIX44, "ALFA_SSL", "ZULU_SSL")); + acceptor.assertNotAuthenticated(new SessionID(FixVersions.BEGINSTRING_FIX44, "ALFA_SSL", "ZULU_SSL"), false); nonSslInitiator.assertNoSslExceptionThrown(); nonSslInitiator.assertLoggedOn(new SessionID(FixVersions.BEGINSTRING_FIX44, "ZULU_NON_SSL", "ALFA_NON_SSL")); @@ -697,21 +712,6 @@ public void exceptionCaught(NextFilter nextFilter, IoSession session, Throwable public abstract SessionConnector createConnector(SessionSettings sessionSettings) throws ConfigError; - private SSLSession findSSLSession(Session session) throws Exception { - IoSession ioSession = findIoSession(session); - - if (ioSession == null) - return null; - - IoFilterChain filterChain = ioSession.getFilterChain(); - SslFilter sslFilter = (SslFilter) filterChain.get(SSLSupport.FILTER_NAME); - - if (sslFilter == null) - return null; - - return (SSLSession) ioSession.getAttribute(SslFilter.SSL_SECURED); - } - private Session findSession(SessionID sessionID) { for (Session session : connector.getManagedSessions()) { if (session.getSessionID().equals(sessionID)) @@ -721,23 +721,19 @@ private Session findSession(SessionID sessionID) { return null; } - private IoSession findIoSession(Session session) throws Exception { - IoSessionResponder ioSessionResponder = (IoSessionResponder) session.getResponder(); - - if (ioSessionResponder == null) - return null; - - Field field = IoSessionResponder.class.getDeclaredField("ioSession"); - field.setAccessible(true); + public void assertAuthenticated(SessionID sessionID, BigInteger serialNumber) { + Session session = findSession(sessionID); + SSLSession sslSession = SSLUtil.findSSLSession(session); - return (IoSession) field.get(ioSessionResponder); - } + if (sslSession == null) { + throw new AssertionError("No SSL session found: " + sessionID); + } - public void assertAuthenticated(SessionID sessionID, BigInteger serialNumber) throws Exception { - Session session = findSession(sessionID); - SSLSession sslSession = findSSLSession(session); + Certificate[] peerCertificates = SSLUtil.getPeerCertificates(sslSession); - Certificate[] peerCertificates = sslSession.getPeerCertificates(); + if (peerCertificates == null || peerCertificates.length == 0) { + throw new AssertionError("Session was not authenticated: " + sslSession); + } for (Certificate peerCertificate : peerCertificates) { if (!(peerCertificate instanceof X509Certificate)) { @@ -752,35 +748,85 @@ public void assertAuthenticated(SessionID sessionID, BigInteger serialNumber) th throw new AssertionError("Certificate with serial number " + serialNumber + " was not authenticated"); } - public void assertNotAuthenticated(SessionID sessionID) throws Exception { + public void assertNotAuthenticated(SessionID sessionID) { + assertNotAuthenticated(sessionID, true); + } + + /** + * Asserts that the session associated with the given {@code sessionID} is not authenticated. + * The behavior of this method depends on the {@code authOn} parameter: + * + * + * + * @param sessionID the session ID to check for authentication status + * @param authOn a flag indicating whether authentication is currently enabled + * @throws AssertionError if the session is still authenticated after the timeout period + * (when {@code authOn} is {@code true}) or if peer certificates are found + * (when {@code authOn} is {@code false}) + */ + public void assertNotAuthenticated(SessionID sessionID, boolean authOn) { Session session = findSession(sessionID); - SSLSession sslSession = findSSLSession(session); + SSLSession sslSession = SSLUtil.findSSLSession(session); - if (sslSession == null) + if (sslSession == null) { return; + } - try { - Certificate[] peerCertificates = sslSession.getPeerCertificates(); + if (authOn) { + // when authentication is on, the SSL session maybe still be alive (invalid) for some time + long startTime = System.nanoTime(); + + while (SSLUtil.findSSLSession(session) != null) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Thread was interrupted", e); + } + + if (TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startTime) >= TIMEOUT_SECONDS) { + throw new AssertionError("SSL session still exists for session: " + sessionID); + } + } + } else { + // when authentication is off, there must be no peer certificates + Certificate[] peerCertificates = SSLUtil.getPeerCertificates(sslSession); if (peerCertificates != null && peerCertificates.length > 0) { throw new AssertionError("Certificate was authenticated"); } - } catch (SSLPeerUnverifiedException e) { } } public void assertLoggedOn(SessionID sessionID) { Session session = findSession(sessionID); - if (!session.isLoggedOn()) - throw new AssertionError("Session is not logged on"); + if (session == null) { + throw new AssertionError("No session found: " + sessionID); + } + + if (!session.isLoggedOn()) { + throw new AssertionError("Session is not logged on: " + session); + } } public void assertNotLoggedOn(SessionID sessionID) { Session session = findSession(sessionID); - if (session.isLoggedOn()) - throw new AssertionError("Session is logged on"); + if (session == null) { + throw new AssertionError("No session found: " + sessionID); + } + + if (session.isLoggedOn()) { + throw new AssertionError("Session is logged on: " + session); + } } public void assertSslExceptionThrown() throws Exception { @@ -829,7 +875,7 @@ public void stop() { connector.stop(); } - private void logSSLInfo() throws Exception { + private void logSSLInfo() { List sessionsIDs = connector.getSessions(); LOGGER.info("All session IDs: {}", sessionsIDs); @@ -841,7 +887,7 @@ private void logSSLInfo() throws Exception { continue; } - SSLSession sslSession = findSSLSession(session); + SSLSession sslSession = SSLUtil.findSSLSession(session); if (sslSession == null) { LOGGER.info("No SSL session found for session: {}", session); @@ -851,9 +897,13 @@ private void logSSLInfo() throws Exception { Throwable exception = this.exception.get(); String exceptionMessage = exception != null ? exception.getMessage() : null; Class exceptionType = exception != null ? exception.getClass() : null; + Certificate[] peerCertificates = SSLUtil.getPeerCertificates(sslSession); + Principal peerPrincipal = SSLUtil.getPeerPrincipal(sslSession); + SSLEngine sslEngine = SSLUtil.getSSLEngine(session); + SSLEngineResult.HandshakeStatus handshakeStatus = sslEngine != null ? sslEngine.getHandshakeStatus() : null; - LOGGER.info("SSL session info [sessionID={},isLoggedOn={},sslSession={},peerCertificates={},localCertificates={},peerPrincipal={},exceptionMessage={},exceptionType={}]", - sessionID, session.isLoggedOn(), sslSession, sslSession.getPeerCertificates(), sslSession.getLocalCertificates(), sslSession.getPeerPrincipal(), exceptionMessage, exceptionType); + LOGGER.info("SSL session info [sessionID={},isLoggedOn={},sslSession={},sslSession.valid={},peerCertificates={},peerPrincipal={},exceptionMessage={},exceptionType={},handshakeStatus={}]", + sessionID, session.isLoggedOn(), sslSession, sslSession.isValid(), peerCertificates, peerPrincipal, exceptionMessage, exceptionType, handshakeStatus); } } } @@ -1075,15 +1125,7 @@ private SessionSettings createInitiatorSettings(String keyStoreName, String trus } private SessionSettings createInitiatorSettings(String senderId, String targetId, int port) { - HashMap defaults = new HashMap<>(); - defaults.put(SessionFactory.SETTING_CONNECTION_TYPE, "initiator"); - defaults.put(Initiator.SETTING_SOCKET_CONNECT_PROTOCOL, ProtocolFactory.getTypeString(ProtocolFactory.SOCKET)); - defaults.put(Initiator.SETTING_SOCKET_CONNECT_HOST, "localhost"); - defaults.put(Initiator.SETTING_SOCKET_CONNECT_PORT, Integer.toString(port)); - defaults.put(Initiator.SETTING_RECONNECT_INTERVAL, "2"); - defaults.put(Session.SETTING_START_TIME, "00:00:00"); - defaults.put(Session.SETTING_END_TIME, "00:00:00"); - defaults.put(Session.SETTING_HEARTBTINT, "30"); + HashMap defaults = createDefaults(port); SessionID sessionID = new SessionID(FixVersions.BEGINSTRING_FIX44, senderId, targetId); @@ -1096,4 +1138,17 @@ private SessionSettings createInitiatorSettings(String senderId, String targetId return sessionSettings; } + + private static HashMap createDefaults(int port) { + HashMap defaults = new HashMap<>(); + defaults.put(SessionFactory.SETTING_CONNECTION_TYPE, "initiator"); + defaults.put(Initiator.SETTING_SOCKET_CONNECT_PROTOCOL, ProtocolFactory.getTypeString(ProtocolFactory.SOCKET)); + defaults.put(Initiator.SETTING_SOCKET_CONNECT_HOST, "localhost"); + defaults.put(Initiator.SETTING_SOCKET_CONNECT_PORT, Integer.toString(port)); + defaults.put(Initiator.SETTING_RECONNECT_INTERVAL, "2"); + defaults.put(Session.SETTING_START_TIME, "00:00:00"); + defaults.put(Session.SETTING_END_TIME, "00:00:00"); + defaults.put(Session.SETTING_HEARTBTINT, "30"); + return defaults; + } } diff --git a/quickfixj-core/src/test/java/quickfix/test/util/SSLUtil.java b/quickfixj-core/src/test/java/quickfix/test/util/SSLUtil.java new file mode 100644 index 0000000000..9b99add52e --- /dev/null +++ b/quickfixj-core/src/test/java/quickfix/test/util/SSLUtil.java @@ -0,0 +1,171 @@ +package quickfix.test.util; + +import org.apache.mina.core.filterchain.IoFilterChain; +import org.apache.mina.core.session.AttributeKey; +import org.apache.mina.core.session.IoSession; +import org.apache.mina.filter.ssl.SslFilter; +import org.apache.mina.filter.ssl.SslHandler; +import quickfix.Session; +import quickfix.mina.IoSessionResponder; +import quickfix.mina.ssl.SSLSupport; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import java.lang.reflect.Field; +import java.security.Principal; +import java.security.cert.Certificate; + +/** + * A utility class for working with SSL/TLS sessions and retrieving SSL-related information from a {@link Session}. + * This class provides methods to find the underlying {@link SSLSession}, retrieve peer certificates, and get the peer principal etc. + */ +public final class SSLUtil { + + private static final String IO_SESSION_FIELD_NAME = "ioSession"; + private static final String SSL_ENGINE_FIELD_NAME = "mEngine"; + private static final AttributeKey SSL_HANDLER_ATTRIBUTE_KEY = new AttributeKey(SslHandler.class, "handler"); + private static final Field IO_SESSION_FIELD; + private static final Field SSL_ENGINE_FIELD; + + static { + try { + IO_SESSION_FIELD = IoSessionResponder.class.getDeclaredField(IO_SESSION_FIELD_NAME); + IO_SESSION_FIELD.setAccessible(true); + } catch (NoSuchFieldException e) { + throw new RuntimeException("Unable to get field: " + IO_SESSION_FIELD_NAME, e); + } + + try { + SSL_ENGINE_FIELD = SslHandler.class.getDeclaredField(SSL_ENGINE_FIELD_NAME); + SSL_ENGINE_FIELD.setAccessible(true); + } catch (NoSuchFieldException e) { + throw new RuntimeException("Unable to get field: " + SSL_ENGINE_FIELD_NAME, e); + } + } + + private SSLUtil() { + } + + /** + * Retrieves the {@link SSLSession} associated with the given {@link Session}. + * + * @param session the session from which to retrieve the {@link SSLSession}. + * @return the {@link SSLSession} if found, or {@code null} if no SSL session is available. + */ + public static SSLSession findSSLSession(Session session) { + IoSession ioSession = findIoSession(session); + + if (ioSession == null) { + return null; + } + + IoFilterChain filterChain = ioSession.getFilterChain(); + SslFilter sslFilter = (SslFilter) filterChain.get(SSLSupport.FILTER_NAME); + + if (sslFilter == null) { + return null; + } + + return (SSLSession) ioSession.getAttribute(SslFilter.SSL_SECURED); + } + + /** + * Retrieves the {@link IoSession} associated with the given {@link Session}. + * + * @param session the session from which to retrieve the {@link IoSession}. + * @return the {@link IoSession} if found, or {@code null} if no underlying session is available. + */ + public static IoSession findIoSession(Session session) { + IoSessionResponder ioSessionResponder = (IoSessionResponder) session.getResponder(); + + if (ioSessionResponder == null) { + return null; + } + + try { + return (IoSession) IO_SESSION_FIELD.get(ioSessionResponder); + } catch (IllegalAccessException e) { + throw new RuntimeException("Failed to get IO session field", e); + } + } + + /** + * Retrieves the {@link SslHandler} associated with the given {@link Session}. + * This method first finds the corresponding {@link IoSession} for the provided session, + * then retrieves the {@link SslFilter} from the session's filter chain. + * If the filter is found, it returns the {@link SslHandler} stored as an attribute in the {@link IoSession}. + * + * @param session The session for which to retrieve the {@link SslHandler}. + * @return The {@link SslHandler} associated with the session, or {@code null} if either + * the {@link IoSession} or the {@link SslFilter} is not found. + */ + public static SslHandler getSSLHandler(Session session) { + IoSession ioSession = findIoSession(session); + + if (ioSession == null) { + return null; + } + + IoFilterChain filterChain = ioSession.getFilterChain(); + SslFilter sslFilter = (SslFilter) filterChain.get(SSLSupport.FILTER_NAME); + + if (sslFilter == null) { + return null; + } + + return (SslHandler) ioSession.getAttribute(SSL_HANDLER_ATTRIBUTE_KEY); + } + + /** + * Retrieves the {@link SSLEngine} associated with the given {@link Session}. + * This method first retrieves the {@link SslHandler} using {@link #getSSLHandler(Session)}, + * and then attempts to access the {@link SSLEngine} stored within the {@link SslHandler} + * using reflection. + * + * @param session The session for which to retrieve the {@link SSLEngine}. + * @return The {@link SSLEngine} associated with the session, or {@code null} if the + * {@link SslHandler} is not found. + */ + public static SSLEngine getSSLEngine(Session session) { + SslHandler sslHandler = getSSLHandler(session); + + if (sslHandler == null) { + return null; + } + + try { + return (SSLEngine) SSL_ENGINE_FIELD.get(sslHandler); + } catch (IllegalAccessException e) { + throw new RuntimeException("Failed to get SSL engine field", e); + } + } + + /** + * Retrieves the peer certificates from the given {@link SSLSession}. + * + * @param sslSession the SSL session from which to retrieve the peer certificates. + * @return an array of {@link Certificate} objects representing the peer certificates, or {@code null} if the peer is unverified. + */ + public static Certificate[] getPeerCertificates(SSLSession sslSession) { + try { + return sslSession.getPeerCertificates(); + } catch (SSLPeerUnverifiedException e) { + return null; + } + } + + /** + * Retrieves the peer principal from the given {@link SSLSession}. + * + * @param sslSession the SSL session from which to retrieve the peer principal. + * @return the {@link Principal} representing the peer, or {@code null} if the peer is unverified. + */ + public static Principal getPeerPrincipal(SSLSession sslSession) { + try { + return sslSession.getPeerPrincipal(); + } catch (SSLPeerUnverifiedException e) { + return null; + } + } +}