From 8222234737d7e24dc17d54bbfaa67ace69f12b95 Mon Sep 17 00:00:00 2001 From: Timothy Bish Date: Tue, 19 Mar 2024 17:41:04 -0400 Subject: [PATCH] PROTON-2727 Allow reading key and trust store files from class path Use prefix notation to indicate that a store is to be found on the class path, prefix is 'classpath:' and add some tests for that case. --- .../client/transport/netty4/SslSupport.java | 34 +++++++++++++-- .../client/transport/netty5/SslSupport.java | 31 +++++++++++++- .../client/impl/SslConnectionTest.java | 25 ++++++++--- .../transport/netty4/SslTransportTest.java | 42 +++++++++++++++++++ .../transport/netty5/SslTransportTest.java | 42 +++++++++++++++++++ 5 files changed, 164 insertions(+), 10 deletions(-) diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/netty4/SslSupport.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/netty4/SslSupport.java index 5dd5c23de..02f4e868c 100644 --- a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/netty4/SslSupport.java +++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/netty4/SslSupport.java @@ -16,8 +16,8 @@ */ package org.apache.qpid.protonj2.client.transport.netty4; -import java.io.File; import java.io.FileInputStream; +import java.io.IOException; import java.io.InputStream; import java.security.KeyStore; import java.security.KeyStoreException; @@ -56,6 +56,10 @@ public final class SslSupport { private static final Logger LOG = LoggerFactory.getLogger(SslSupport.class); + public static final String FROM_CLASSPATH_PREFIX = "classpath:"; + public static final String FROM_FILE_PREFIX = "file:"; + public static final String FROM_FILE_URL_PREFIX = "file://"; + /** * Determines if Netty OpenSSL support is available and applicable based on the configuration * in the given TransportOptions instance. @@ -437,11 +441,35 @@ private static void validateAlias(KeyStore store, String alias) throws IllegalAr } private static KeyStore loadStore(String storePath, final String password, String storeType) throws Exception { - KeyStore store = KeyStore.getInstance(storeType); - try (InputStream in = new FileInputStream(new File(storePath));) { + final KeyStore store = KeyStore.getInstance(storeType); + + try (InputStream in = openStoreAtLocation(storePath)) { store.load(in, password != null ? password.toCharArray() : null); + } catch (Exception ex) { + LOG.trace("Caught Error loading store: {}", ex.getMessage(), ex); + throw ex; } return store; } + + private static InputStream openStoreAtLocation(final String storePath) throws IOException { + final InputStream stream; + + if (storePath.startsWith(FROM_CLASSPATH_PREFIX)) { + stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(storePath.substring(FROM_CLASSPATH_PREFIX.length())); + } else if (storePath.startsWith(FROM_FILE_URL_PREFIX)) { + stream = new FileInputStream(storePath.substring(FROM_FILE_URL_PREFIX.length())); + } else if (storePath.startsWith(FROM_FILE_PREFIX)) { + stream = new FileInputStream(storePath.substring(FROM_FILE_PREFIX.length())); + } else { + stream = new FileInputStream(storePath); + } + + if (stream == null) { + throw new IOException("Could no locate KeyStore at location: " + storePath); + } + + return stream; + } } diff --git a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/netty5/SslSupport.java b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/netty5/SslSupport.java index 88acd3451..4d1ffe3c3 100644 --- a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/netty5/SslSupport.java +++ b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/transport/netty5/SslSupport.java @@ -16,8 +16,8 @@ */ package org.apache.qpid.protonj2.client.transport.netty5; -import java.io.File; import java.io.FileInputStream; +import java.io.IOException; import java.io.InputStream; import java.security.KeyStore; import java.security.KeyStoreException; @@ -56,6 +56,10 @@ public final class SslSupport { private static final Logger LOG = LoggerFactory.getLogger(SslSupport.class); + public static final String FROM_CLASSPATH_PREFIX = "classpath:"; + public static final String FROM_FILE_PREFIX = "file:"; + public static final String FROM_FILE_URL_PREFIX = "file://"; + /** * Determines if Netty OpenSSL support is available and applicable based on the configuration * in the given TransportOptions instance. @@ -438,10 +442,33 @@ private static void validateAlias(KeyStore store, String alias) throws IllegalAr private static KeyStore loadStore(String storePath, final String password, String storeType) throws Exception { KeyStore store = KeyStore.getInstance(storeType); - try (InputStream in = new FileInputStream(new File(storePath));) { + try (InputStream in = openStoreAtLocation(storePath)) { store.load(in, password != null ? password.toCharArray() : null); + } catch (Exception ex) { + LOG.trace("Caught Error loading store: {}", ex.getMessage(), ex); + throw ex; } return store; } + + private static InputStream openStoreAtLocation(final String storePath) throws IOException { + final InputStream stream; + + if (storePath.startsWith(FROM_CLASSPATH_PREFIX)) { + stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(storePath.substring(FROM_CLASSPATH_PREFIX.length())); + } else if (storePath.startsWith(FROM_FILE_URL_PREFIX)) { + stream = new FileInputStream(storePath.substring(FROM_FILE_URL_PREFIX.length())); + } else if (storePath.startsWith(FROM_FILE_PREFIX)) { + stream = new FileInputStream(storePath.substring(FROM_FILE_PREFIX.length())); + } else { + stream = new FileInputStream(storePath); + } + + if (stream == null) { + throw new IOException("Could no locate KeyStore at location: " + storePath); + } + + return stream; + } } diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/SslConnectionTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/SslConnectionTest.java index c0f61a9c2..93c5c3953 100644 --- a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/SslConnectionTest.java +++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/SslConnectionTest.java @@ -66,6 +66,7 @@ public class SslConnectionTest extends ImperativeClientTestCase { private static final String BROKER_PKCS12_TRUSTSTORE = "src/test/resources/broker-pkcs12.truststore"; private static final String CLIENT_MULTI_KEYSTORE = "src/test/resources/client-multiple-keys-jks.keystore"; private static final String CLIENT_JKS_TRUSTSTORE = "src/test/resources/client-jks.truststore"; + private static final String CLIENT_JKS_TRUSTSTORE_CLASSPATH = "classpath:client-jks.truststore"; private static final String CLIENT_PKCS12_TRUSTSTORE = "src/test/resources/client-pkcs12.truststore"; private static final String OTHER_CA_TRUSTSTORE = "src/test/resources/other-ca-jks.truststore"; private static final String CLIENT_JKS_KEYSTORE = "src/test/resources/client-jks.keystore"; @@ -100,7 +101,12 @@ protected ConnectionOptions connectionOptions() { @Test public void testCreateAndCloseSslConnectionJDK() throws Exception { - testCreateAndCloseSslConnection(false); + testCreateAndCloseSslConnection(false, false); + } + + @Test + public void testCreateAndCloseSslConnectionJDKTrustStoreOnClasspath() throws Exception { + testCreateAndCloseSslConnection(false, true); } @Test @@ -108,10 +114,18 @@ public void testCreateAndCloseSslConnectionOpenSSL() throws Exception { assumeTrue(OpenSsl.isAvailable()); assumeTrue(OpenSsl.supportsKeyManagerFactory()); - testCreateAndCloseSslConnection(true); + testCreateAndCloseSslConnection(true, false); } - private void testCreateAndCloseSslConnection(boolean openSSL) throws Exception { + @Test + public void testCreateAndCloseSslConnectionOpenSSLTrustStoreOnClasspath() throws Exception { + assumeTrue(OpenSsl.isAvailable()); + assumeTrue(OpenSsl.supportsKeyManagerFactory()); + + testCreateAndCloseSslConnection(true, true); + } + + private void testCreateAndCloseSslConnection(boolean openSSL, boolean storeFromClassPath) throws Exception { ProtonTestServerOptions serverOptions = serverOptions(); serverOptions.setSecure(true); serverOptions.setKeyStoreLocation(BROKER_JKS_KEYSTORE); @@ -124,11 +138,12 @@ private void testCreateAndCloseSslConnection(boolean openSSL) throws Exception { peer.expectClose().respond(); peer.start(); - URI remoteURI = peer.getServerURI(); + final URI remoteURI = peer.getServerURI(); + final String storeLocation = storeFromClassPath ? CLIENT_JKS_TRUSTSTORE_CLASSPATH : CLIENT_JKS_TRUSTSTORE; ConnectionOptions clientOptions = connectionOptions(); clientOptions.sslOptions() - .trustStoreLocation(CLIENT_JKS_TRUSTSTORE) + .trustStoreLocation(storeLocation) .trustStorePassword(PASSWORD) .allowNativeSSL(openSSL); diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/netty4/SslTransportTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/netty4/SslTransportTest.java index 42401424b..cc8d3ed3d 100644 --- a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/netty4/SslTransportTest.java +++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/netty4/SslTransportTest.java @@ -52,6 +52,8 @@ public class SslTransportTest extends TcpTransportTest { public static final String CLIENT_MULTI_KEYSTORE = "src/test/resources/client-multiple-keys-jks.keystore"; public static final String CLIENT_TRUSTSTORE = "src/test/resources/client-jks.truststore"; public static final String OTHER_CA_TRUSTSTORE = "src/test/resources/other-ca-jks.truststore"; + public static final String SERVER_CLASSPATH_KEYSTORE = "classpath:broker-jks.keystore"; + public static final String SERVER_CLASSPATH_TRUSTSTORE = "classpath:broker-jks.truststore"; public static final String CLIENT_KEY_ALIAS = "client"; public static final String CLIENT_DN = "O=Client,CN=client"; @@ -210,6 +212,31 @@ public void testConnectToServerClientTrustsAll() throws Exception { assertTrue(exceptions.isEmpty()); } + @Test + public void testConnectToServerWithServerClasspathStores() throws Exception { + try (NettyEchoServer server = createEchoServer()) { + server.start(); + + final int port = server.getServerPort(); + + Transport transport = createTransport(createTransportOptions(), createServerClasspathSSLOptions()); + try { + transport.connect(HOSTNAME, port, testListener).awaitConnect(); + LOG.info("Connection established to test server: {}:{}", HOSTNAME, port); + } catch (Exception e) { + fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e); + } + + assertTrue(transport.isConnected()); + assertTrue(transport.isSecure()); + + transport.close(); + } + + logTransportErrors(); + assertTrue(exceptions.isEmpty()); + } + @Test public void testConnectWithNeedClientAuth() throws Exception { try (NettyEchoServer server = createEchoServer(true)) { @@ -386,4 +413,19 @@ protected SslOptions createServerSSLOptions() { return options; } + + protected SslOptions createServerClasspathSSLOptions() { + SslOptions options = new SslOptions(); + + // Run the server in JDK mode for now to validate cross compatibility + options.sslEnabled(true); + options.keyStoreLocation(SERVER_CLASSPATH_KEYSTORE); + options.keyStorePassword(PASSWORD); + options.trustStoreLocation(SERVER_CLASSPATH_TRUSTSTORE); + options.trustStorePassword(PASSWORD); + options.storeType(KEYSTORE_TYPE); + options.verifyHost(false); + + return options; + } } diff --git a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/netty5/SslTransportTest.java b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/netty5/SslTransportTest.java index c7e0c1180..6083305ff 100644 --- a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/netty5/SslTransportTest.java +++ b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/transport/netty5/SslTransportTest.java @@ -52,6 +52,8 @@ public class SslTransportTest extends TcpTransportTest { public static final String CLIENT_MULTI_KEYSTORE = "src/test/resources/client-multiple-keys-jks.keystore"; public static final String CLIENT_TRUSTSTORE = "src/test/resources/client-jks.truststore"; public static final String OTHER_CA_TRUSTSTORE = "src/test/resources/other-ca-jks.truststore"; + public static final String SERVER_CLASSPATH_KEYSTORE = "classpath:broker-jks.keystore"; + public static final String SERVER_CLASSPATH_TRUSTSTORE = "classpath:broker-jks.truststore"; public static final String CLIENT_KEY_ALIAS = "client"; public static final String CLIENT_DN = "O=Client,CN=client"; @@ -210,6 +212,31 @@ public void testConnectToServerClientTrustsAll() throws Exception { assertTrue(exceptions.isEmpty()); } + @Test + public void testConnectToServerWithServerClasspathStores() throws Exception { + try (NettyEchoServer server = createEchoServer()) { + server.start(); + + final int port = server.getServerPort(); + + Transport transport = createTransport(createTransportOptions(), createServerClasspathSSLOptions()); + try { + transport.connect(HOSTNAME, port, testListener).awaitConnect(); + LOG.info("Connection established to test server: {}:{}", HOSTNAME, port); + } catch (Exception e) { + fail("Should not have failed to connect to the server at " + HOSTNAME + ":" + port + " but got exception: " + e); + } + + assertTrue(transport.isConnected()); + assertTrue(transport.isSecure()); + + transport.close(); + } + + logTransportErrors(); + assertTrue(exceptions.isEmpty()); + } + @Test public void testConnectWithNeedClientAuth() throws Exception { try (NettyEchoServer server = createEchoServer(true)) { @@ -390,4 +417,19 @@ protected SslOptions createServerSSLOptions() { return options; } + + protected SslOptions createServerClasspathSSLOptions() { + SslOptions options = new SslOptions(); + + // Run the server in JDK mode for now to validate cross compatibility + options.sslEnabled(true); + options.keyStoreLocation(SERVER_CLASSPATH_KEYSTORE); + options.keyStorePassword(PASSWORD); + options.trustStoreLocation(SERVER_CLASSPATH_TRUSTSTORE); + options.trustStorePassword(PASSWORD); + options.storeType(KEYSTORE_TYPE); + options.verifyHost(false); + + return options; + } }