diff --git a/spring-vault-core/src/main/java/org/springframework/vault/client/ClientConfiguration.java b/spring-vault-core/src/main/java/org/springframework/vault/client/ClientConfiguration.java new file mode 100644 index 00000000..8eb76740 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/client/ClientConfiguration.java @@ -0,0 +1,447 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.client; + +import java.io.IOException; +import java.io.InputStream; +import java.net.ProxySelector; +import java.net.Socket; +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.UnrecoverableKeyException; +import java.security.cert.X509Certificate; +import java.util.List; + +import javax.net.ssl.*; + +import io.netty.channel.ChannelOption; +import io.netty.handler.ssl.SslContextBuilder; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import reactor.netty.http.client.HttpClient; + +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.lang.Nullable; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; +import org.springframework.vault.support.ClientOptions; +import org.springframework.vault.support.PemObject; +import org.springframework.vault.support.SslConfiguration; + +/** + * Shared utility class to provide client configuration regardless of the used facade + * (synchronous or reactive). + * + * @author Mark Paluch + * @since 4.0 + */ +class ClientConfiguration { + + @SuppressWarnings("FieldMayBeFinal") // allow setting via reflection. + private static Log logger = LogFactory.getLog(ClientHttpRequestFactoryFactory.class); + + static SSLContext getSSLContext(SslConfiguration sslConfiguration) throws GeneralSecurityException, IOException { + + return getSSLContext(sslConfiguration.getKeyStoreConfiguration(), sslConfiguration.getKeyConfiguration(), + getTrustManagers(sslConfiguration)); + } + + static SSLContext getSSLContext(SslConfiguration.KeyStoreConfiguration keyStoreConfiguration, + SslConfiguration.KeyConfiguration keyConfiguration, @Nullable TrustManager[] trustManagers) + throws GeneralSecurityException, IOException { + + KeyManager[] keyManagers = keyStoreConfiguration.isPresent() + ? createKeyManagerFactory(keyStoreConfiguration, keyConfiguration).getKeyManagers() : null; + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(keyManagers, trustManagers, null); + + return sslContext; + } + + static KeyManagerFactory createKeyManagerFactory(SslConfiguration.KeyStoreConfiguration keyStoreConfiguration, + SslConfiguration.KeyConfiguration keyConfiguration) throws GeneralSecurityException, IOException { + + KeyStore keyStore = getKeyStore(keyStoreConfiguration); + + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + + char[] keyPasswordToUse = keyConfiguration.getKeyPassword(); + + if (keyPasswordToUse == null) { + keyPasswordToUse = keyStoreConfiguration.getStorePassword() == null ? new char[0] + : keyStoreConfiguration.getStorePassword(); + } + + keyManagerFactory.init(keyStore, keyPasswordToUse); + + if (StringUtils.hasText(keyConfiguration.getKeyAlias())) { + return new KeySelectingKeyManagerFactory(keyManagerFactory, keyConfiguration); + } + + return keyManagerFactory; + } + + static KeyStore getKeyStore(SslConfiguration.KeyStoreConfiguration keyStoreConfiguration) + throws IOException, GeneralSecurityException { + + KeyStore keyStore = KeyStore.getInstance(getKeyStoreType(keyStoreConfiguration)); + + loadKeyStore(keyStoreConfiguration, keyStore); + return keyStore; + } + + @Nullable + static TrustManager[] getTrustManagers(SslConfiguration sslConfiguration) + throws GeneralSecurityException, IOException { + + return sslConfiguration.getTrustStoreConfiguration().isPresent() + ? createTrustManagerFactory(sslConfiguration.getTrustStoreConfiguration()).getTrustManagers() : null; + } + + private static String getKeyStoreType(SslConfiguration.KeyStoreConfiguration keyStoreConfiguration) { + + if (StringUtils.hasText(keyStoreConfiguration.getStoreType()) + && !SslConfiguration.PEM_KEYSTORE_TYPE.equalsIgnoreCase(keyStoreConfiguration.getStoreType())) { + return keyStoreConfiguration.getStoreType(); + } + + return KeyStore.getDefaultType(); + } + + static TrustManagerFactory createTrustManagerFactory(SslConfiguration.KeyStoreConfiguration keyStoreConfiguration) + throws GeneralSecurityException, IOException { + + KeyStore trustStore = getKeyStore(keyStoreConfiguration); + + TrustManagerFactory trustManagerFactory = TrustManagerFactory + .getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(trustStore); + + return trustManagerFactory; + } + + private static void loadKeyStore(SslConfiguration.KeyStoreConfiguration keyStoreConfiguration, KeyStore keyStore) + throws IOException, GeneralSecurityException { + + if (logger.isDebugEnabled()) { + logger.debug("Loading keystore from %s".formatted(keyStoreConfiguration.getResource())); + } + + InputStream inputStream = null; + try { + inputStream = keyStoreConfiguration.getResource().getInputStream(); + + if (SslConfiguration.PEM_KEYSTORE_TYPE.equalsIgnoreCase(keyStoreConfiguration.getStoreType())) { + + keyStore.load(null); + loadFromPem(keyStore, inputStream); + } + else { + keyStore.load(inputStream, keyStoreConfiguration.getStorePassword()); + } + + if (logger.isDebugEnabled()) { + logger.debug("Keystore loaded with %d entries".formatted(keyStore.size())); + } + } + finally { + if (inputStream != null) { + inputStream.close(); + } + } + } + + private static void loadFromPem(KeyStore keyStore, InputStream inputStream) throws IOException, KeyStoreException { + + List pemObjects = PemObject.parse(new String(FileCopyUtils.copyToByteArray(inputStream))); + + for (PemObject pemObject : pemObjects) { + if (pemObject.isCertificate()) { + X509Certificate cert = pemObject.getCertificate(); + String alias = cert.getSubjectX500Principal().getName(); + + if (logger.isDebugEnabled()) { + logger.debug("Adding certificate with alias %s".formatted(alias)); + } + + keyStore.setCertificateEntry(alias, cert); + } + } + } + + static boolean hasSslConfiguration(SslConfiguration sslConfiguration) { + return sslConfiguration.getTrustStoreConfiguration().isPresent() + || sslConfiguration.getKeyStoreConfiguration().isPresent(); + } + + /** + * {@link ClientHttpConnector} for Reactor Netty. + * + * @author Mark Paluch + */ + public static class ReactorNetty { + + public static HttpClient createClient(ClientOptions options, SslConfiguration sslConfiguration) { + + HttpClient client = HttpClient.create(); + + if (hasSslConfiguration(sslConfiguration)) { + + client = client.secure(builder -> { + + SslContextBuilder sslContextBuilder = SslContextBuilder.forClient(); + configureSsl(sslConfiguration, sslContextBuilder); + + try { + builder.sslContext(sslContextBuilder.build()); + } + catch (SSLException e) { + throw new RuntimeException(e); + } + }); + } + + client = client + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, + Math.toIntExact(options.getConnectionTimeout().toMillis())) + .proxyWithSystemProperties(); + + return client; + } + + private static void configureSsl(SslConfiguration sslConfiguration, SslContextBuilder sslContextBuilder) { + + try { + + if (sslConfiguration.getTrustStoreConfiguration().isPresent()) { + sslContextBuilder + .trustManager(createTrustManagerFactory(sslConfiguration.getTrustStoreConfiguration())); + } + + if (sslConfiguration.getKeyStoreConfiguration().isPresent()) { + sslContextBuilder.keyManager(createKeyManagerFactory(sslConfiguration.getKeyStoreConfiguration(), + sslConfiguration.getKeyConfiguration())); + } + + if (!sslConfiguration.getEnabledProtocols().isEmpty()) { + sslContextBuilder.protocols(sslConfiguration.getEnabledProtocols()); + } + + if (!sslConfiguration.getEnabledCipherSuites().isEmpty()) { + sslContextBuilder.ciphers(sslConfiguration.getEnabledCipherSuites()); + } + } + catch (GeneralSecurityException | IOException e) { + throw new IllegalStateException(e); + } + } + + } + + /** + * Utility methods to create {@link ClientHttpRequestFactory} using the Jetty Client. + * + * @author Mark Paluch + */ + public static class JettyClient { + + public static org.eclipse.jetty.client.HttpClient configureClient( + org.eclipse.jetty.client.HttpClient httpClient, ClientOptions options) { + + httpClient.setConnectTimeout(options.getConnectionTimeout().toMillis()); + httpClient.setAddressResolutionTimeout(options.getConnectionTimeout().toMillis()); + + return httpClient; + } + + public static org.eclipse.jetty.client.HttpClient getHttpClient(SslConfiguration sslConfiguration) + throws IOException, GeneralSecurityException { + + if (hasSslConfiguration(sslConfiguration)) { + + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + + if (sslConfiguration.getKeyStoreConfiguration().isPresent()) { + KeyStore keyStore = getKeyStore(sslConfiguration.getKeyStoreConfiguration()); + sslContextFactory.setKeyStore(keyStore); + } + + if (sslConfiguration.getTrustStoreConfiguration().isPresent()) { + KeyStore keyStore = getKeyStore(sslConfiguration.getTrustStoreConfiguration()); + sslContextFactory.setTrustStore(keyStore); + } + + SslConfiguration.KeyConfiguration keyConfiguration = sslConfiguration.getKeyConfiguration(); + + if (keyConfiguration.getKeyAlias() != null) { + sslContextFactory.setCertAlias(keyConfiguration.getKeyAlias()); + } + + if (keyConfiguration.getKeyPassword() != null) { + sslContextFactory.setKeyManagerPassword(new String(keyConfiguration.getKeyPassword())); + } + + if (!sslConfiguration.getEnabledProtocols().isEmpty()) { + sslContextFactory + .setIncludeProtocols(sslConfiguration.getEnabledProtocols().toArray(new String[0])); + } + + if (!sslConfiguration.getEnabledCipherSuites().isEmpty()) { + sslContextFactory + .setIncludeCipherSuites(sslConfiguration.getEnabledCipherSuites().toArray(new String[0])); + } + + ClientConnector connector = new ClientConnector(); + connector.setSslContextFactory(sslContextFactory); + + return new org.eclipse.jetty.client.HttpClient(new HttpClientTransportOverHTTP(connector)); + } + + return new org.eclipse.jetty.client.HttpClient(); + } + + } + + /** + * {@link ClientHttpRequestFactory} using the JDK's HttpClient. + * + * @author Mark Paluch + */ + public static class JdkHttpClient { + + public static java.net.http.HttpClient.Builder getBuilder(ClientOptions options, + SslConfiguration sslConfiguration) throws GeneralSecurityException, IOException { + + java.net.http.HttpClient.Builder builder = java.net.http.HttpClient.newBuilder(); + + if (hasSslConfiguration(sslConfiguration)) { + + SSLContext sslContext = getSSLContext(sslConfiguration); + + String[] enabledProtocols = !sslConfiguration.getEnabledProtocols().isEmpty() + ? sslConfiguration.getEnabledProtocols().toArray(new String[0]) : null; + + String[] enabledCipherSuites = !sslConfiguration.getEnabledCipherSuites().isEmpty() + ? sslConfiguration.getEnabledCipherSuites().toArray(new String[0]) : null; + + SSLParameters parameters = new SSLParameters(); + parameters.setProtocols(enabledProtocols); + parameters.setCipherSuites(enabledCipherSuites); + + builder.sslContext(sslContext).sslParameters(parameters); + } + + builder.proxy(ProxySelector.getDefault()) + .followRedirects(java.net.http.HttpClient.Redirect.ALWAYS) + .connectTimeout(options.getConnectionTimeout()); + return builder; + } + + } + + static class KeySelectingKeyManagerFactory extends KeyManagerFactory { + + KeySelectingKeyManagerFactory(KeyManagerFactory factory, SslConfiguration.KeyConfiguration keyConfiguration) { + super(new KeyManagerFactorySpi() { + @Override + protected void engineInit(KeyStore keyStore, char[] chars) + throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + factory.init(keyStore, chars); + } + + @Override + protected void engineInit(ManagerFactoryParameters managerFactoryParameters) + throws InvalidAlgorithmParameterException { + factory.init(managerFactoryParameters); + } + + @Override + protected KeyManager[] engineGetKeyManagers() { + + KeyManager[] keyManagers = factory.getKeyManagers(); + + if (keyManagers.length == 1 && keyManagers[0] instanceof X509ExtendedKeyManager) { + + return new KeyManager[] { new KeySelectingX509KeyManager( + (X509ExtendedKeyManager) keyManagers[0], keyConfiguration) }; + } + + return keyManagers; + } + }, factory.getProvider(), factory.getAlgorithm()); + } + + } + + private static class KeySelectingX509KeyManager extends X509ExtendedKeyManager { + + private final X509ExtendedKeyManager delegate; + + private final SslConfiguration.KeyConfiguration keyConfiguration; + + KeySelectingX509KeyManager(X509ExtendedKeyManager delegate, + SslConfiguration.KeyConfiguration keyConfiguration) { + this.delegate = delegate; + this.keyConfiguration = keyConfiguration; + } + + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return this.delegate.getClientAliases(keyType, issuers); + } + + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { + return this.keyConfiguration.getKeyAlias(); + } + + public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) { + return this.keyConfiguration.getKeyAlias(); + } + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return this.delegate.getServerAliases(keyType, issuers); + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + return this.delegate.chooseServerAlias(keyType, issuers, socket); + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + return this.delegate.getCertificateChain(alias); + } + + @Override + public PrivateKey getPrivateKey(String alias) { + return this.delegate.getPrivateKey(alias); + } + + } + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpConnectorFactory.java b/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpConnectorFactory.java index 1522e704..d2f490ab 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpConnectorFactory.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpConnectorFactory.java @@ -18,13 +18,9 @@ package org.springframework.vault.client; import java.io.IOException; import java.net.ProxySelector; import java.security.GeneralSecurityException; -import java.security.KeyStore; import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLParameters; -import io.netty.channel.ChannelOption; -import io.netty.handler.ssl.SslContextBuilder; import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.DefaultSchemePortResolver; @@ -33,10 +29,6 @@ import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBu import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner; import org.apache.hc.core5.http.nio.ssl.BasicClientTlsStrategy; import org.apache.hc.core5.util.Timeout; -import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP; -import org.eclipse.jetty.io.ClientConnector; -import org.eclipse.jetty.util.ssl.SslContextFactory; -import reactor.netty.http.Http11SslContextSpec; import reactor.netty.http.client.HttpClient; import org.springframework.http.client.ClientHttpRequestFactory; @@ -50,8 +42,6 @@ import org.springframework.util.ClassUtils; import org.springframework.vault.support.ClientOptions; import org.springframework.vault.support.SslConfiguration; -import static org.springframework.vault.client.ClientHttpRequestFactoryFactory.*; - /** * Factory for {@link ClientHttpConnector} that supports * {@link ReactorClientHttpConnector} and {@link JettyClientHttpConnector}. This factory @@ -85,13 +75,13 @@ public class ClientHttpConnectorFactory { Assert.notNull(sslConfiguration, "SslConfiguration must not be null"); try { - if (reactorNettyPresent) { - return ReactorNetty.usingReactorNetty(options, sslConfiguration); - } if (httpComponentsPresent) { return HttpComponents.usingHttpComponents(options, sslConfiguration); + } + if (reactorNettyPresent) { + return ReactorNetty.usingReactorNetty(options, sslConfiguration); } if (jettyPresent) { @@ -124,51 +114,7 @@ public class ClientHttpConnectorFactory { } public static HttpClient createClient(ClientOptions options, SslConfiguration sslConfiguration) { - - HttpClient client = HttpClient.create(); - - if (hasSslConfiguration(sslConfiguration)) { - - Http11SslContextSpec sslContextSpec = Http11SslContextSpec.forClient() - .configure(it -> configureSsl(sslConfiguration, it)) - .get(); - - client = client.secure(builder -> builder.sslContext(sslContextSpec)); - } - - client = client - .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, - Math.toIntExact(options.getConnectionTimeout().toMillis())) - .proxyWithSystemProperties(); - - return client; - } - - private static void configureSsl(SslConfiguration sslConfiguration, SslContextBuilder sslContextBuilder) { - - try { - - if (sslConfiguration.getTrustStoreConfiguration().isPresent()) { - sslContextBuilder - .trustManager(createTrustManagerFactory(sslConfiguration.getTrustStoreConfiguration())); - } - - if (sslConfiguration.getKeyStoreConfiguration().isPresent()) { - sslContextBuilder.keyManager(createKeyManagerFactory(sslConfiguration.getKeyStoreConfiguration(), - sslConfiguration.getKeyConfiguration())); - } - - if (!sslConfiguration.getEnabledProtocols().isEmpty()) { - sslContextBuilder.protocols(sslConfiguration.getEnabledProtocols()); - } - - if (!sslConfiguration.getEnabledCipherSuites().isEmpty()) { - sslContextBuilder.ciphers(sslConfiguration.getEnabledCipherSuites()); - } - } - catch (GeneralSecurityException | IOException e) { - throw new IllegalStateException(e); - } + return ClientConfiguration.ReactorNetty.createClient(options, sslConfiguration); } } @@ -224,9 +170,9 @@ public class ClientHttpConnectorFactory { .create() .setDefaultConnectionConfig(connectionConfig); - if (hasSslConfiguration(sslConfiguration)) { + if (ClientConfiguration.hasSslConfiguration(sslConfiguration)) { - SSLContext sslContext = getSSLContext(sslConfiguration); + SSLContext sslContext = ClientConfiguration.getSSLContext(sslConfiguration); String[] enabledProtocols = !sslConfiguration.getEnabledProtocols().isEmpty() ? sslConfiguration.getEnabledProtocols().toArray(new String[0]) : null; @@ -278,59 +224,12 @@ public class ClientHttpConnectorFactory { public static org.eclipse.jetty.client.HttpClient configureClient( org.eclipse.jetty.client.HttpClient httpClient, ClientOptions options) { - - httpClient.setConnectTimeout(options.getConnectionTimeout().toMillis()); - httpClient.setAddressResolutionTimeout(options.getConnectionTimeout().toMillis()); - - return httpClient; + return ClientConfiguration.JettyClient.configureClient(httpClient, options); } public static org.eclipse.jetty.client.HttpClient getHttpClient(SslConfiguration sslConfiguration) throws IOException, GeneralSecurityException { - - if (hasSslConfiguration(sslConfiguration)) { - - SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); - - if (sslConfiguration.getKeyStoreConfiguration().isPresent()) { - KeyStore keyStore = ClientHttpRequestFactoryFactory - .getKeyStore(sslConfiguration.getKeyStoreConfiguration()); - sslContextFactory.setKeyStore(keyStore); - } - - if (sslConfiguration.getTrustStoreConfiguration().isPresent()) { - KeyStore keyStore = ClientHttpRequestFactoryFactory - .getKeyStore(sslConfiguration.getTrustStoreConfiguration()); - sslContextFactory.setTrustStore(keyStore); - } - - SslConfiguration.KeyConfiguration keyConfiguration = sslConfiguration.getKeyConfiguration(); - - if (keyConfiguration.getKeyAlias() != null) { - sslContextFactory.setCertAlias(keyConfiguration.getKeyAlias()); - } - - if (keyConfiguration.getKeyPassword() != null) { - sslContextFactory.setKeyManagerPassword(new String(keyConfiguration.getKeyPassword())); - } - - if (!sslConfiguration.getEnabledProtocols().isEmpty()) { - sslContextFactory - .setIncludeProtocols(sslConfiguration.getEnabledProtocols().toArray(new String[0])); - } - - if (!sslConfiguration.getEnabledCipherSuites().isEmpty()) { - sslContextFactory - .setIncludeCipherSuites(sslConfiguration.getEnabledCipherSuites().toArray(new String[0])); - } - - ClientConnector connector = new ClientConnector(); - connector.setSslContextFactory(sslContextFactory); - - return new org.eclipse.jetty.client.HttpClient(new HttpClientTransportOverHTTP(connector)); - } - - return new org.eclipse.jetty.client.HttpClient(); + return ClientConfiguration.JettyClient.getHttpClient(sslConfiguration); } } @@ -360,30 +259,7 @@ public class ClientHttpConnectorFactory { public static java.net.http.HttpClient.Builder getBuilder(ClientOptions options, SslConfiguration sslConfiguration) throws GeneralSecurityException, IOException { - - java.net.http.HttpClient.Builder builder = java.net.http.HttpClient.newBuilder(); - - if (hasSslConfiguration(sslConfiguration)) { - - SSLContext sslContext = getSSLContext(sslConfiguration); - - String[] enabledProtocols = !sslConfiguration.getEnabledProtocols().isEmpty() - ? sslConfiguration.getEnabledProtocols().toArray(new String[0]) : null; - - String[] enabledCipherSuites = !sslConfiguration.getEnabledCipherSuites().isEmpty() - ? sslConfiguration.getEnabledCipherSuites().toArray(new String[0]) : null; - - SSLParameters parameters = new SSLParameters(); - parameters.setProtocols(enabledProtocols); - parameters.setCipherSuites(enabledCipherSuites); - - builder.sslContext(sslContext).sslParameters(parameters); - } - - builder.proxy(ProxySelector.getDefault()) - .followRedirects(java.net.http.HttpClient.Redirect.ALWAYS) - .connectTimeout(options.getConnectionTimeout()); - return builder; + return ClientConfiguration.JdkHttpClient.getBuilder(options, sslConfiguration); } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpRequestFactoryFactory.java b/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpRequestFactoryFactory.java index 0719fbb5..cd3540d7 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpRequestFactoryFactory.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/client/ClientHttpRequestFactoryFactory.java @@ -27,29 +27,11 @@ import javax.net.ssl.X509ExtendedKeyManager; import javax.net.ssl.X509TrustManager; import java.io.IOException; -import java.io.InputStream; import java.net.ProxySelector; -import java.net.Socket; import java.security.GeneralSecurityException; -import java.security.InvalidAlgorithmParameterException; import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.Principal; -import java.security.PrivateKey; -import java.security.UnrecoverableKeyException; -import java.security.cert.X509Certificate; -import java.util.List; -import javax.net.ssl.KeyManager; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.KeyManagerFactorySpi; -import javax.net.ssl.ManagerFactoryParameters; import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509ExtendedKeyManager; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -67,17 +49,19 @@ import org.apache.hc.core5.util.Timeout; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; -import org.springframework.http.client.SimpleClientHttpRequestFactory; -import org.springframework.lang.Nullable; +import org.springframework.http.client.JdkClientHttpRequestFactory; +import org.springframework.http.client.JettyClientHttpRequestFactory; +import org.springframework.http.client.ReactorClientHttpRequestFactory; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.http.client.reactive.JettyClientHttpConnector; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; import org.springframework.vault.support.ClientOptions; -import org.springframework.vault.support.PemObject; import org.springframework.vault.support.SslConfiguration; import org.springframework.vault.support.SslConfiguration.KeyConfiguration; -import org.springframework.vault.support.SslConfiguration.KeyStoreConfiguration; + +import static org.springframework.vault.client.ClientConfiguration.*; /** * Factory for {@link ClientHttpRequestFactory} that supports Apache HTTP Components, @@ -95,10 +79,16 @@ public class ClientHttpRequestFactoryFactory { @SuppressWarnings("FieldMayBeFinal") // allow setting via reflection. private static Log logger = LogFactory.getLog(ClientHttpRequestFactoryFactory.class); + private static final boolean reactorNettyPresent = ClassUtils.isPresent("reactor.netty.http.client.HttpClient", + ClientHttpConnectorFactory.class.getClassLoader()); + private static final boolean httpComponentsPresent = ClassUtils.isPresent( "org.apache.hc.client5.http.impl.classic.HttpClientBuilder", ClientHttpRequestFactoryFactory.class.getClassLoader()); + private static final boolean jettyPresent = ClassUtils.isPresent("org.eclipse.jetty.client.HttpClient", + ClientHttpConnectorFactory.class.getClassLoader()); + /** * Create a {@link ClientHttpRequestFactory} for the given {@link ClientOptions} and * {@link SslConfiguration}. @@ -117,151 +107,42 @@ public class ClientHttpRequestFactoryFactory { if (httpComponentsPresent) { return HttpComponents.usingHttpComponents(options, sslConfiguration); } + + if (reactorNettyPresent) { + return ReactorNetty.usingReactorNetty(options, sslConfiguration); + } + + if (jettyPresent) { + return JettyClient.usingJetty(options, sslConfiguration); + } + + return JdkHttpClient.usingJdkHttpClient(options, sslConfiguration); } catch (GeneralSecurityException | IOException e) { throw new IllegalStateException(e); } + } - if (hasSslConfiguration(sslConfiguration)) { - logger.warn("VaultProperties has SSL configured but the SSL configuration " - + "must be applied outside the Vault Client to use the JDK HTTP client"); + /** + * {@link ClientHttpConnector} for Reactor Netty. + * + * @author Mark Paluch + * @since 4.0 + */ + public static class ReactorNetty { + + /** + * Create a {@link ReactorClientHttpRequestFactory} using Reactor Netty. + * @param options must not be {@literal null} + * @param sslConfiguration must not be {@literal null} + * @return a new and configured {@link ReactorClientHttpRequestFactory} instance. + */ + public static ReactorClientHttpRequestFactory usingReactorNetty(ClientOptions options, + SslConfiguration sslConfiguration) { + return new ReactorClientHttpRequestFactory( + ClientConfiguration.ReactorNetty.createClient(options, sslConfiguration)); } - return SimpleClient.usingSimpleClientHttpRequest(options); - } - - static SSLContext getSSLContext(SslConfiguration sslConfiguration) throws GeneralSecurityException, IOException { - - return getSSLContext(sslConfiguration.getKeyStoreConfiguration(), sslConfiguration.getKeyConfiguration(), - getTrustManagers(sslConfiguration)); - } - - static SSLContext getSSLContext(KeyStoreConfiguration keyStoreConfiguration, KeyConfiguration keyConfiguration, - @Nullable TrustManager[] trustManagers) throws GeneralSecurityException, IOException { - - KeyManager[] keyManagers = keyStoreConfiguration.isPresent() - ? createKeyManagerFactory(keyStoreConfiguration, keyConfiguration).getKeyManagers() : null; - - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(keyManagers, trustManagers, null); - - return sslContext; - } - - static KeyManagerFactory createKeyManagerFactory(KeyStoreConfiguration keyStoreConfiguration, - KeyConfiguration keyConfiguration) throws GeneralSecurityException, IOException { - - KeyStore keyStore = getKeyStore(keyStoreConfiguration); - - KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - - char[] keyPasswordToUse = keyConfiguration.getKeyPassword(); - - if (keyPasswordToUse == null) { - keyPasswordToUse = keyStoreConfiguration.getStorePassword() == null ? new char[0] - : keyStoreConfiguration.getStorePassword(); - } - - keyManagerFactory.init(keyStore, keyPasswordToUse); - - if (StringUtils.hasText(keyConfiguration.getKeyAlias())) { - return new KeySelectingKeyManagerFactory(keyManagerFactory, keyConfiguration); - } - - return keyManagerFactory; - } - - static KeyStore getKeyStore(KeyStoreConfiguration keyStoreConfiguration) - throws IOException, GeneralSecurityException { - - KeyStore keyStore = KeyStore.getInstance(getKeyStoreType(keyStoreConfiguration)); - - loadKeyStore(keyStoreConfiguration, keyStore); - return keyStore; - } - - @Nullable - static TrustManager[] getTrustManagers(SslConfiguration sslConfiguration) - throws GeneralSecurityException, IOException { - - return sslConfiguration.getTrustStoreConfiguration().isPresent() - ? createTrustManagerFactory(sslConfiguration.getTrustStoreConfiguration()).getTrustManagers() : null; - } - - private static String getKeyStoreType(KeyStoreConfiguration keyStoreConfiguration) { - - if (StringUtils.hasText(keyStoreConfiguration.getStoreType()) - && !SslConfiguration.PEM_KEYSTORE_TYPE.equalsIgnoreCase(keyStoreConfiguration.getStoreType())) { - return keyStoreConfiguration.getStoreType(); - } - - return KeyStore.getDefaultType(); - } - - static TrustManagerFactory createTrustManagerFactory(KeyStoreConfiguration keyStoreConfiguration) - throws GeneralSecurityException, IOException { - - KeyStore trustStore = getKeyStore(keyStoreConfiguration); - - TrustManagerFactory trustManagerFactory = TrustManagerFactory - .getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(trustStore); - - return trustManagerFactory; - } - - private static void loadKeyStore(KeyStoreConfiguration keyStoreConfiguration, KeyStore keyStore) - throws IOException, GeneralSecurityException { - - if (logger.isDebugEnabled()) { - logger.debug("Loading keystore from %s".formatted(keyStoreConfiguration.getResource())); - } - - InputStream inputStream = null; - try { - inputStream = keyStoreConfiguration.getResource().getInputStream(); - - if (SslConfiguration.PEM_KEYSTORE_TYPE.equalsIgnoreCase(keyStoreConfiguration.getStoreType())) { - - keyStore.load(null); - loadFromPem(keyStore, inputStream); - } - else { - keyStore.load(inputStream, keyStoreConfiguration.getStorePassword()); - } - - if (logger.isDebugEnabled()) { - logger.debug("Keystore loaded with %d entries".formatted(keyStore.size())); - } - } - finally { - if (inputStream != null) { - inputStream.close(); - } - } - } - - private static void loadFromPem(KeyStore keyStore, InputStream inputStream) throws IOException, KeyStoreException { - - List pemObjects = PemObject.parse(new String(FileCopyUtils.copyToByteArray(inputStream))); - - for (PemObject pemObject : pemObjects) { - if (pemObject.isCertificate()) { - X509Certificate cert = pemObject.getCertificate(); - String alias = cert.getSubjectX500Principal().getName(); - - if (logger.isDebugEnabled()) { - logger.debug("Adding certificate with alias %s".formatted(alias)); - } - - keyStore.setCertificateEntry(alias, cert); - } - } - } - - static boolean hasSslConfiguration(SslConfiguration sslConfiguration) { - return sslConfiguration.getTrustStoreConfiguration().isPresent() - || sslConfiguration.getKeyStoreConfiguration().isPresent(); } /** @@ -318,9 +199,9 @@ public class ClientHttpRequestFactoryFactory { .setSoTimeout(readTimeout) .build()); - if (hasSslConfiguration(sslConfiguration)) { + if (ClientConfiguration.hasSslConfiguration(sslConfiguration)) { - SSLContext sslContext = getSSLContext(sslConfiguration); + SSLContext sslContext = ClientConfiguration.getSSLContext(sslConfiguration); String[] enabledProtocols = null; @@ -347,83 +228,66 @@ public class ClientHttpRequestFactoryFactory { } - static class KeySelectingKeyManagerFactory extends KeyManagerFactory { + /** + * Utility methods to create {@link ClientHttpRequestFactory} using the Jetty Client. + * + * @author Mark Paluch + * @since 4.5 + */ + public static class JettyClient { - KeySelectingKeyManagerFactory(KeyManagerFactory factory, KeyConfiguration keyConfiguration) { - super(new KeyManagerFactorySpi() { - @Override - protected void engineInit(KeyStore keyStore, char[] chars) - throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { - factory.init(keyStore, chars); - } + /** + * Create a {@link JettyClientHttpRequestFactory} using Jetty. + * @param options must not be {@literal null} + * @param sslConfiguration must not be {@literal null} + * @return a new and configured {@link JettyClientHttpConnector} instance. + * @throws GeneralSecurityException + * @throws IOException + */ + public static JettyClientHttpRequestFactory usingJetty(ClientOptions options, SslConfiguration sslConfiguration) + throws GeneralSecurityException, IOException { + return new JettyClientHttpRequestFactory(configureClient(getHttpClient(sslConfiguration), options)); + } - @Override - protected void engineInit(ManagerFactoryParameters managerFactoryParameters) - throws InvalidAlgorithmParameterException { - factory.init(managerFactoryParameters); - } + public static org.eclipse.jetty.client.HttpClient configureClient( + org.eclipse.jetty.client.HttpClient httpClient, ClientOptions options) { + return ClientConfiguration.JettyClient.configureClient(httpClient, options); + } - @Override - protected KeyManager[] engineGetKeyManagers() { - - KeyManager[] keyManagers = factory.getKeyManagers(); - - if (keyManagers.length == 1 && keyManagers[0] instanceof X509ExtendedKeyManager) { - - return new KeyManager[] { new KeySelectingX509KeyManager( - (X509ExtendedKeyManager) keyManagers[0], keyConfiguration) }; - } - - return keyManagers; - } - }, factory.getProvider(), factory.getAlgorithm()); + public static org.eclipse.jetty.client.HttpClient getHttpClient(SslConfiguration sslConfiguration) + throws IOException, GeneralSecurityException { + return ClientConfiguration.JettyClient.getHttpClient(sslConfiguration); } } - private static class KeySelectingX509KeyManager extends X509ExtendedKeyManager { + /** + * {@link ClientHttpRequestFactory} using the JDK's HttpClient. + * + * @author Mark Paluch + * @since 4.5 + */ + public static class JdkHttpClient { - private final X509ExtendedKeyManager delegate; + /** + * Create a {@link JdkClientHttpRequestFactory} using the JDK's HttpClient. + * @param options must not be {@literal null} + * @param sslConfiguration must not be {@literal null} + * @return a new and configured {@link JdkClientHttpRequestFactory} instance. + * @throws GeneralSecurityException + * @throws IOException + */ + public static JdkClientHttpRequestFactory usingJdkHttpClient(ClientOptions options, + SslConfiguration sslConfiguration) throws GeneralSecurityException, IOException { - private final KeyConfiguration keyConfiguration; + java.net.http.HttpClient.Builder builder = getBuilder(options, sslConfiguration); - KeySelectingX509KeyManager(X509ExtendedKeyManager delegate, KeyConfiguration keyConfiguration) { - this.delegate = delegate; - this.keyConfiguration = keyConfiguration; + return new JdkClientHttpRequestFactory(builder.build()); } - @Override - public String[] getClientAliases(String keyType, Principal[] issuers) { - return this.delegate.getClientAliases(keyType, issuers); - } - - @Override - public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { - return this.keyConfiguration.getKeyAlias(); - } - - public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) { - return this.keyConfiguration.getKeyAlias(); - } - - @Override - public String[] getServerAliases(String keyType, Principal[] issuers) { - return this.delegate.getServerAliases(keyType, issuers); - } - - @Override - public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { - return this.delegate.chooseServerAlias(keyType, issuers, socket); - } - - @Override - public X509Certificate[] getCertificateChain(String alias) { - return this.delegate.getCertificateChain(alias); - } - - @Override - public PrivateKey getPrivateKey(String alias) { - return this.delegate.getPrivateKey(alias); + public static java.net.http.HttpClient.Builder getBuilder(ClientOptions options, + SslConfiguration sslConfiguration) throws GeneralSecurityException, IOException { + return ClientConfiguration.JdkHttpClient.getBuilder(options, sslConfiguration); } } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/client/ClientHttpRequestFactoryFactoryIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/client/ClientHttpRequestFactoryFactoryIntegrationTests.java index 5ec517ec..77b1a13c 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/client/ClientHttpRequestFactoryFactoryIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/client/ClientHttpRequestFactoryFactoryIntegrationTests.java @@ -46,6 +46,52 @@ class ClientHttpRequestFactoryFactoryIntegrationTests { String url = new VaultEndpoint().createUriString("sys/health"); + @Test + void reactorNettyClientShouldWork() { + + ClientHttpRequestFactory factory = ClientHttpRequestFactoryFactory.ReactorNetty + .usingReactorNetty(new ClientOptions(), Settings.createSslConfiguration()); + + RestTemplate template = new RestTemplate(factory); + + String response = request(template); + + assertThat(response).isNotNull().contains("initialized"); + } + + @Test + void reactorNettyClientWithExplicitEnabledCipherSuitesShouldWork() { + + List enabledCipherSuites = new ArrayList(); + enabledCipherSuites.add("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"); + enabledCipherSuites.add("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"); + + ClientHttpRequestFactory factory = ClientHttpRequestFactoryFactory.ReactorNetty.usingReactorNetty( + new ClientOptions(), Settings.createSslConfiguration().withEnabledCipherSuites(enabledCipherSuites)); + + RestTemplate template = new RestTemplate(factory); + + String response = request(template); + + assertThat(response).isNotNull().contains("initialized"); + } + + @Test + void reactorNettyClientWithExplicitEnabledProtocolsShouldWork() { + + List enabledProtocols = new ArrayList(); + enabledProtocols.add("TLSv1.2"); + + ClientHttpRequestFactory factory = ClientHttpRequestFactoryFactory.ReactorNetty.usingReactorNetty( + new ClientOptions(), Settings.createSslConfiguration().withEnabledProtocols(enabledProtocols)); + + RestTemplate template = new RestTemplate(factory); + + String response = request(template); + + assertThat(response).isNotNull().contains("initialized"); + } + @Test void httpComponentsClientShouldWork() throws Exception { @@ -117,6 +163,65 @@ class ClientHttpRequestFactoryFactoryIntegrationTests { ((DisposableBean) factory).destroy(); } + @Test + void jettyClientShouldWork() throws Exception { + + ClientHttpRequestFactory factory = ClientHttpRequestFactoryFactory.JettyClient.usingJetty(new ClientOptions(), + Settings.createSslConfiguration()); + + RestTemplate template = new RestTemplate(factory); + + String response = request(template); + + assertThat(response).isNotNull().contains("initialized"); + } + + @Test + void jettyClientWithExplicitEnabledCipherSuitesShouldWork() throws Exception { + + List enabledCipherSuites = new ArrayList(); + enabledCipherSuites.add("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"); + enabledCipherSuites.add("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"); + + ClientHttpRequestFactory factory = ClientHttpRequestFactoryFactory.JettyClient.usingJetty(new ClientOptions(), + Settings.createSslConfiguration().withEnabledCipherSuites(enabledCipherSuites)); + + RestTemplate template = new RestTemplate(factory); + + String response = request(template); + + assertThat(response).isNotNull().contains("initialized"); + } + + @Test + void jettyClientWithExplicitEnabledProtocolsShouldWork() throws Exception { + + List enabledProtocols = new ArrayList(); + enabledProtocols.add("TLSv1.2"); + + ClientHttpRequestFactory factory = ClientHttpRequestFactoryFactory.JettyClient.usingJetty(new ClientOptions(), + Settings.createSslConfiguration().withEnabledProtocols(enabledProtocols)); + + RestTemplate template = new RestTemplate(factory); + + String response = request(template); + + assertThat(response).isNotNull().contains("initialized"); + } + + @Test + void jdkHttpClientShouldWork() throws Exception { + + ClientHttpRequestFactory factory = ClientHttpRequestFactoryFactory.JdkHttpClient + .usingJdkHttpClient(new ClientOptions(), Settings.createSslConfiguration()); + + RestTemplate template = new RestTemplate(factory); + + String response = request(template); + + assertThat(response).isNotNull().contains("initialized"); + } + private String request(RestTemplate template) { // Uninitialized and sealed can cause status 500 diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultKeyValueMetadataTemplateIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultKeyValueMetadataTemplateIntegrationTests.java index 1a185bd0..32bcb41b 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultKeyValueMetadataTemplateIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultKeyValueMetadataTemplateIntegrationTests.java @@ -76,6 +76,8 @@ class ReactiveVaultKeyValueMetadataTemplateIntegrationTests @Test void shouldReadMetadataForANewKVEntry() { + Version version = prepare().getVersion(); + vaultKeyValueMetadataOperations.get(SECRET_NAME).as(StepVerifier::create).assertNext(metadataResponse -> { assertThat(metadataResponse.getMaxVersions()).isEqualTo(0); assertThat(metadataResponse.getCurrentVersion()).isEqualTo(1); @@ -86,7 +88,7 @@ class ReactiveVaultKeyValueMetadataTemplateIntegrationTests var version1 = metadataResponse.getVersions().get(0); - if (prepare().getVersion().isGreaterThanOrEqualTo(Version.parse("1.2.0"))) { + if (version.isGreaterThanOrEqualTo(Version.parse("1.2.0"))) { assertThat(metadataResponse.getDeleteVersionAfter()).isEqualTo(Duration.ZERO); diff --git a/src/main/antora/modules/ROOT/pages/vault/client-support.adoc b/src/main/antora/modules/ROOT/pages/vault/client-support.adoc index 318df669..688004a7 100644 --- a/src/main/antora/modules/ROOT/pages/vault/client-support.adoc +++ b/src/main/antora/modules/ROOT/pages/vault/client-support.adoc @@ -8,28 +8,25 @@ that is scoped only to Spring Vault's client components. Spring Vault supports following HTTP imperative clients: -* Java's builtin `HttpURLConnection` (default client if no other is available) +* Java's builtin `HttpClient` (default client if no other is available) * Apache Http Components +* Reactor Netty +* Jetty Spring Vault's reactive integration supports the following reactive HTTP clients: * Java's builtin reactive `HttpClient` (default client if no other is available) -* Reactor Netty * Apache Http Components +* Reactor Netty * Jetty Using a specific client requires the according dependency to be available on the classpath so Spring Vault can use the available client for communicating with Vault. -== Java's builtin `HttpURLConnection` +== Java's builtin `HttpClient` -Java's builtin `HttpURLConnection` is available out-of-the-box without additional -configuration. Using `HttpURLConnection` comes with a limitation regarding SSL configuration. -Spring Vault won't apply <> as it would -require a deep reconfiguration of the JVM. This configuration would affect all -components relying on the default SSL context. Configuring SSL settings using -`HttpURLConnection` requires you providing these settings as System Properties. See -https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#InstallationAndCustomization[Customizing JSSE] for further details. +Java's builtin `HttpClient` is available out-of-the-box since Java 11 without additional +dependencies. == External Clients You can use external clients to access Vault's API. Simply add one of the following