diff --git a/spring-vault-core/src/main/java/org/springframework/vault/config/ClientHttpConnectorFactory.java b/spring-vault-core/src/main/java/org/springframework/vault/config/ClientHttpConnectorFactory.java index 7b8e8367..cf88409f 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/config/ClientHttpConnectorFactory.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/config/ClientHttpConnectorFactory.java @@ -81,8 +81,9 @@ public class ClientHttpConnectorFactory { } if (sslConfiguration.getKeyStoreConfiguration().isPresent()) { - sslContextBuilder.keyManager(createKeyManagerFactory(sslConfiguration - .getKeyStoreConfiguration())); + sslContextBuilder.keyManager(createKeyManagerFactory( + sslConfiguration.getKeyStoreConfiguration(), + sslConfiguration.getKeyConfiguration())); } } catch (GeneralSecurityException | IOException e) { diff --git a/spring-vault-core/src/main/java/org/springframework/vault/config/ClientHttpRequestFactoryFactory.java b/spring-vault-core/src/main/java/org/springframework/vault/config/ClientHttpRequestFactoryFactory.java index 7bb59925..fb3206ca 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/config/ClientHttpRequestFactoryFactory.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/config/ClientHttpRequestFactoryFactory.java @@ -18,18 +18,29 @@ package org.springframework.vault.config; 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.CertificateException; +import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.concurrent.TimeUnit; 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 javax.net.ssl.X509TrustManager; import io.netty.handler.ssl.SslContextBuilder; @@ -57,6 +68,8 @@ import org.springframework.vault.support.ClientOptions; import org.springframework.vault.support.SslConfiguration; import org.springframework.vault.support.SslConfiguration.KeyStoreConfiguration; +import static org.springframework.vault.support.SslConfiguration.KeyConfiguration; + /** * Factory for {@link ClientHttpRequestFactory} that supports Apache HTTP Components, * OkHttp, Netty and the JDK HTTP client (in that order). This factory configures a @@ -95,6 +108,7 @@ public class ClientHttpRequestFactoryFactory { return true; } + /** * Create a {@link ClientHttpRequestFactory} for the given {@link ClientOptions} and * {@link SslConfiguration}. @@ -140,12 +154,13 @@ public class ClientHttpRequestFactoryFactory { } static SSLContext getSSLContext(SslConfiguration sslConfiguration, - TrustManager[] trustManagers) - throws GeneralSecurityException, IOException { + TrustManager[] trustManagers) throws GeneralSecurityException, IOException { + KeyConfiguration keyConfiguration = sslConfiguration.getKeyConfiguration(); KeyManager[] keyManagers = sslConfiguration.getKeyStoreConfiguration() .isPresent() ? createKeyManagerFactory( - sslConfiguration.getKeyStoreConfiguration()).getKeyManagers() : null; + sslConfiguration.getKeyStoreConfiguration(), keyConfiguration) + .getKeyManagers() : null; SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(keyManagers, trustManagers, null); @@ -162,8 +177,8 @@ public class ClientHttpRequestFactoryFactory { } static KeyManagerFactory createKeyManagerFactory( - KeyStoreConfiguration keyStoreConfiguration) throws GeneralSecurityException, - IOException { + KeyStoreConfiguration keyStoreConfiguration, KeyConfiguration keyConfiguration) + throws GeneralSecurityException, IOException { KeyStore keyStore = KeyStore.getInstance(StringUtils .hasText(keyStoreConfiguration.getStoreType()) ? keyStoreConfiguration @@ -173,9 +188,19 @@ public class ClientHttpRequestFactoryFactory { KeyManagerFactory keyManagerFactory = KeyManagerFactory .getInstance(KeyManagerFactory.getDefaultAlgorithm()); - keyManagerFactory.init(keyStore, - keyStoreConfiguration.getStorePassword() == null ? new char[0] - : keyStoreConfiguration.getStorePassword()); + + 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; } @@ -325,8 +350,9 @@ public class ClientHttpRequestFactoryFactory { } if (sslConfiguration.getKeyStoreConfiguration().isPresent()) { - sslContextBuilder.keyManager(createKeyManagerFactory(sslConfiguration - .getKeyStoreConfiguration())); + sslContextBuilder.keyManager(createKeyManagerFactory( + sslConfiguration.getKeyStoreConfiguration(), + sslConfiguration.getKeyConfiguration())); } requestFactory.setSslContext(sslContextBuilder.sslProvider( @@ -341,4 +367,89 @@ public class ClientHttpRequestFactoryFactory { return requestFactory; } } + + static class KeySelectingKeyManagerFactory extends KeyManagerFactory { + + KeySelectingKeyManagerFactory(KeyManagerFactory factory, + 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 KeyConfiguration keyConfiguration; + + KeySelectingX509KeyManager(X509ExtendedKeyManager delegate, + KeyConfiguration keyConfiguration) { + this.delegate = delegate; + this.keyConfiguration = keyConfiguration; + } + + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return delegate.getClientAliases(keyType, issuers); + } + + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, + Socket socket) { + return keyConfiguration.getKeyAlias(); + } + + public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, + SSLEngine engine) { + return keyConfiguration.getKeyAlias(); + } + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return delegate.getServerAliases(keyType, issuers); + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + return delegate.chooseServerAlias(keyType, issuers, socket); + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + return delegate.getCertificateChain(alias); + } + + @Override + public PrivateKey getPrivateKey(String alias) { + return delegate.getPrivateKey(alias); + } + } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/SslConfiguration.java b/spring-vault-core/src/main/java/org/springframework/vault/support/SslConfiguration.java index c5895c99..edff86ac 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/SslConfiguration.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/SslConfiguration.java @@ -46,6 +46,8 @@ public class SslConfiguration { private final KeyStoreConfiguration trustStoreConfiguration; + private final KeyConfiguration keyConfiguration; + /** * Create a new {@link SslConfiguration} with the default {@link KeyStore} type. * @@ -78,12 +80,32 @@ public class SslConfiguration { */ public SslConfiguration(KeyStoreConfiguration keyStoreConfiguration, KeyStoreConfiguration trustStoreConfiguration) { + this(keyStoreConfiguration, KeyConfiguration.unconfigured(), + trustStoreConfiguration); + } + + /** + * Create a new {@link SslConfiguration}. + * + * @param keyStoreConfiguration the key store configuration, must not be + * {@literal null}. + * @param keyConfiguration the configuration for a specific key in + * {@code keyStoreConfiguration} to use. + * @param trustStoreConfiguration the trust store configuration, must not be + * {@literal null}. + * @since 2.2 + */ + public SslConfiguration(KeyStoreConfiguration keyStoreConfiguration, + KeyConfiguration keyConfiguration, + KeyStoreConfiguration trustStoreConfiguration) { Assert.notNull(keyStoreConfiguration, "KeyStore configuration must not be null"); + Assert.notNull(keyConfiguration, "KeyConfiguration must not be null"); Assert.notNull(trustStoreConfiguration, "TrustStore configuration must not be null"); this.keyStoreConfiguration = keyStoreConfiguration; + this.keyConfiguration = keyConfiguration; this.trustStoreConfiguration = trustStoreConfiguration; } @@ -123,9 +145,9 @@ public class SslConfiguration { Assert.isTrue(trustStore.exists(), () -> String.format("TrustStore %s does not exist", trustStore)); - return new SslConfiguration(KeyStoreConfiguration.UNCONFIGURED, - new KeyStoreConfiguration(trustStore, trustStorePassword, - DEFAULT_KEYSTORE_TYPE)); + return new SslConfiguration(KeyStoreConfiguration.unconfigured(), + KeyConfiguration.unconfigured(), new KeyStoreConfiguration(trustStore, + trustStorePassword, DEFAULT_KEYSTORE_TYPE)); } /** @@ -157,15 +179,35 @@ public class SslConfiguration { * @return the created {@link SslConfiguration}. * @see java.security.KeyStore */ - public static SslConfiguration forKeyStore(@Nullable Resource keyStore, + public static SslConfiguration forKeyStore(Resource keyStore, @Nullable char[] keyStorePassword) { + return forKeyStore(keyStore, keyStorePassword, KeyConfiguration.unconfigured()); + } + + /** + * Create a new {@link SslConfiguration} for the given key store with the default + * {@link KeyStore} type. + * + * @param keyStore resource pointing to an existing key store, must not be + * {@literal null}. + * @param keyStorePassword may be {@literal null}. + * @param keyConfiguration the configuration for a specific key in + * {@code keyStoreConfiguration} to use. + * @return the created {@link SslConfiguration}. + * @since 2.2 + * @see java.security.KeyStore + */ + public static SslConfiguration forKeyStore(Resource keyStore, + @Nullable char[] keyStorePassword, KeyConfiguration keyConfiguration) { Assert.notNull(keyStore, "KeyStore must not be null"); Assert.isTrue(keyStore.exists(), () -> String.format("KeyStore %s does not exist", keyStore)); + Assert.notNull(keyConfiguration, "KeyConfiguration must not be null"); return new SslConfiguration(new KeyStoreConfiguration(keyStore, keyStorePassword, - DEFAULT_KEYSTORE_TYPE), KeyStoreConfiguration.UNCONFIGURED); + DEFAULT_KEYSTORE_TYPE), keyConfiguration, + KeyStoreConfiguration.unconfigured()); } /** @@ -259,6 +301,14 @@ public class SslConfiguration { return keyStoreConfiguration; } + /** + * @return the key configuration. + * @since 2.2 + */ + public KeyConfiguration getKeyConfiguration() { + return keyConfiguration; + } + /** * Create a new {@link SslConfiguration} with {@link KeyStoreConfiguration} applied * retaining the {@link #getTrustStoreConfiguration() trust store} configuration. @@ -268,7 +318,25 @@ public class SslConfiguration { * @since 2.0 */ public SslConfiguration withKeyStore(KeyStoreConfiguration configuration) { - return new SslConfiguration(configuration, this.trustStoreConfiguration); + return withKeyStore(configuration, KeyConfiguration.unconfigured()); + } + + /** + * Create a new {@link SslConfiguration} with {@link KeyStoreConfiguration} and + * {@link KeyConfiguration} applied retaining the + * {@link #getTrustStoreConfiguration() trust store} configuration. + * + * @param configuration must not be {@literal null}. + * @param keyConfiguration the configuration for a specific key in + * {@code keyStoreConfiguration} to use. + * @return a new {@link SslConfiguration} with {@link KeyStoreConfiguration} and + * {@link KeyConfiguration} applied. + * @since 2.2 + */ + public SslConfiguration withKeyStore(KeyStoreConfiguration configuration, + KeyConfiguration keyConfiguration) { + return new SslConfiguration(configuration, keyConfiguration, + this.trustStoreConfiguration); } /** @@ -310,7 +378,8 @@ public class SslConfiguration { * @since 2.0 */ public SslConfiguration withTrustStore(KeyStoreConfiguration configuration) { - return new SslConfiguration(this.keyStoreConfiguration, configuration); + return new SslConfiguration(this.keyStoreConfiguration, this.keyConfiguration, + configuration); } @Nullable @@ -441,6 +510,74 @@ public class SslConfiguration { } } + /** + * Configuration for a key in a keystore. + * + * @author Mark Paluch + * @since 2.2 + */ + public static class KeyConfiguration { + + private static final KeyConfiguration UNCONFIGURED = new KeyConfiguration(null, + null); + + private final @Nullable char[] keyPassword; + + private final @Nullable String keyAlias; + + private KeyConfiguration(@Nullable char[] keyPassword, @Nullable String keyAlias) { + + if (keyPassword == null) { + this.keyPassword = null; + } + else { + this.keyPassword = Arrays.copyOf(keyPassword, keyPassword.length); + } + + this.keyAlias = keyAlias; + } + + /** + * Create an unconfigured, empty {@link KeyConfiguration}. + * + * @return unconfigured, empty {@link KeyConfiguration}. + */ + public static KeyConfiguration unconfigured() { + return UNCONFIGURED; + } + + /** + * Create a {@link KeyConfiguration} to configure a specific key within a + * {@link KeyStore}. + * + * @param keyPassword the key password to use. Uses + * {@link KeyStoreConfiguration#getStorePassword()} if left {@code null}. + * @param keyAlias the key alias to use. Uses the first alias if left {@code null} + * . + * @return the {@link KeyConfiguration}. + */ + public static KeyConfiguration of(@Nullable char[] keyPassword, + @Nullable String keyAlias) { + return new KeyConfiguration(keyPassword, keyAlias); + } + + /** + * @return the key password to use. + */ + @Nullable + public char[] getKeyPassword() { + return keyPassword; + } + + /** + * @return key alias to use. + */ + @Nullable + public String getKeyAlias() { + return keyAlias; + } + } + static class AbsentResource extends AbstractResource { static final AbsentResource INSTANCE = new AbsentResource(); diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTestBase.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTestBase.java index 3b45f42e..6cd4ce03 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTestBase.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTestBase.java @@ -61,11 +61,17 @@ public abstract class ClientCertificateAuthenticationIntegrationTestBase extends } static SslConfiguration prepareCertAuthenticationMethod() { + return prepareCertAuthenticationMethod(SslConfiguration.KeyConfiguration + .unconfigured()); + } + + static SslConfiguration prepareCertAuthenticationMethod( + SslConfiguration.KeyConfiguration keyConfiguration) { SslConfiguration original = createSslConfiguration(); return new SslConfiguration(KeyStoreConfiguration.of(new FileSystemResource( new File(findWorkDir(), "client-cert.jks")), "changeit".toCharArray()), - original.getTrustStoreConfiguration()); + keyConfiguration, original.getTrustStoreConfiguration()); } } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTests.java index e8745069..934ffee8 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTests.java @@ -22,6 +22,7 @@ import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.vault.client.VaultClients; import org.springframework.vault.config.ClientHttpRequestFactoryFactory; import org.springframework.vault.support.ClientOptions; +import org.springframework.vault.support.SslConfiguration; import org.springframework.vault.support.VaultToken; import org.springframework.vault.util.Settings; import org.springframework.vault.util.TestRestTemplateFactory; @@ -52,6 +53,46 @@ public class ClientCertificateAuthenticationIntegrationTests extends assertThat(login.getToken()).isNotEmpty(); } + @Test + public void shouldSelectKey() { + + ClientHttpRequestFactory clientHttpRequestFactory = ClientHttpRequestFactoryFactory + .create(new ClientOptions(), + prepareCertAuthenticationMethod(SslConfiguration.KeyConfiguration + .of("changeit".toCharArray(), "1"))); + + RestTemplate restTemplate = VaultClients.createRestTemplate( + TestRestTemplateFactory.TEST_VAULT_ENDPOINT, clientHttpRequestFactory); + ClientCertificateAuthentication authentication = new ClientCertificateAuthentication( + restTemplate); + VaultToken login = authentication.login(); + + assertThat(login.getToken()).isNotEmpty(); + } + + @Test(expected = NestedRuntimeException.class) + public void shouldSelectInvalidKey() { + + ClientHttpRequestFactory clientHttpRequestFactory = ClientHttpRequestFactoryFactory + .create(new ClientOptions(), + prepareCertAuthenticationMethod(SslConfiguration.KeyConfiguration + .of("changeit".toCharArray(), "2"))); + + RestTemplate restTemplate = VaultClients.createRestTemplate( + TestRestTemplateFactory.TEST_VAULT_ENDPOINT, clientHttpRequestFactory); + ClientCertificateAuthentication authentication = new ClientCertificateAuthentication( + restTemplate); + authentication.login(); + } + + @Test(expected = IllegalStateException.class) + public void shouldProvideInvalidKeyPassword() { + + ClientHttpRequestFactoryFactory.create(new ClientOptions(), + prepareCertAuthenticationMethod(SslConfiguration.KeyConfiguration.of( + "wrong".toCharArray(), "1"))); + } + // Compatibility for Vault 0.6.0 and below. Vault 0.6.1 fixed that issue and we // receive a VaultException here. @Test(expected = NestedRuntimeException.class) diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationOperatorIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationOperatorIntegrationTests.java index fc51909c..f710e40f 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationOperatorIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationOperatorIntegrationTests.java @@ -15,10 +15,10 @@ */ package org.springframework.vault.authentication; -import org.junit.Ignore; import org.junit.Test; import reactor.test.StepVerifier; +import org.springframework.vault.support.SslConfiguration; import org.springframework.vault.util.TestWebClientFactory; import org.springframework.web.reactive.function.client.WebClient; @@ -28,7 +28,6 @@ import org.springframework.web.reactive.function.client.WebClient; * * @author Mark Paluch */ -@Ignore("See https://github.com/spring-projects/spring-vault/issues/281 and https://github.com/reactor/reactor-netty/issues/407") public class ClientCertificateAuthenticationOperatorIntegrationTests extends ClientCertificateAuthenticationIntegrationTestBase { @@ -43,4 +42,39 @@ public class ClientCertificateAuthenticationOperatorIntegrationTests extends StepVerifier.create(operator.getVaultToken()).expectNextCount(1).verifyComplete(); } + + @Test + public void shouldSelectKey() { + + WebClient webClient = TestWebClientFactory + .create(prepareCertAuthenticationMethod(SslConfiguration.KeyConfiguration + .of("changeit".toCharArray(), "1"))); + + AuthenticationStepsOperator operator = new AuthenticationStepsOperator( + ClientCertificateAuthentication.createAuthenticationSteps(), webClient); + + StepVerifier.create(operator.getVaultToken()).expectNextCount(1).verifyComplete(); + } + + @Test + public void shouldSelectInvalidKey() { + + WebClient webClient = TestWebClientFactory + .create(prepareCertAuthenticationMethod(SslConfiguration.KeyConfiguration + .of("changeit".toCharArray(), "2"))); + + AuthenticationStepsOperator operator = new AuthenticationStepsOperator( + ClientCertificateAuthentication.createAuthenticationSteps(), webClient); + + StepVerifier.create(operator.getVaultToken()).verifyError( + VaultLoginException.class); + } + + @Test(expected = IllegalStateException.class) + public void shouldProvideInvalidKeyPassword() { + + TestWebClientFactory + .create(prepareCertAuthenticationMethod(SslConfiguration.KeyConfiguration + .of("wrong".toCharArray(), "1"))); + } } diff --git a/src/main/asciidoc/reference/client-support.adoc b/src/main/asciidoc/reference/client-support.adoc index 3fd534e7..c721b586 100644 --- a/src/main/asciidoc/reference/client-support.adoc +++ b/src/main/asciidoc/reference/client-support.adoc @@ -78,18 +78,24 @@ to set SSL settings only for Spring Vault. ---- SslConfiguration sslConfiguration = new SslConfiguration( <1> - new FileSystemResource("client-cert.jks"), "changeit", - new FileSystemResource("truststore.jks"), "changeit"); + new FileSystemResource("client-cert.jks"), "changeit".toCharArray(), + new FileSystemResource("truststore.jks"), "changeit".toCharArray()); SslConfiguration.forTrustStore(new FileSystemResource("keystore.jks"), <2> "changeit") SslConfiguration.forKeyStore(new FileSystemResource("keystore.jks"), <3> - "changeit") + "changeit".toCharArray()) + +SslConfiguration.forKeyStore(new FileSystemResource("keystore.jks"), <4> + "changeit".toCharArray() + KeyConfiguration.of("key-password".toCharArray(), + "my-key-alias")) ---- <1> Full configuration. <2> Configuring only trust store settings. <3> Configuring only key store settings. +<3> Configuring only key store settings with providing a key-configuration. ==== Please note that providing `SslConfiguration` can be only