Allow configuration of a key alias and key password.

SslConfiguration now allows configuration of the key alias to use if a keystore contains multiple keys and setting the key password.

KeyManagerFactory and X509ExtendedKeyManager are adapted internally to provide the appropriate Key alias.

Resolves gh-416.
This commit is contained in:
Mark Paluch
2019-05-01 22:40:56 +02:00
parent b303702d21
commit 0070b78d55
7 changed files with 361 additions and 25 deletions

View File

@@ -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) {

View File

@@ -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);
}
}
}

View File

@@ -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();

View File

@@ -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());
}
}

View File

@@ -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)

View File

@@ -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")));
}
}

View File

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