Add SSL bundle support to spring-boot module

Add classes to support SSL bundles which can be used to apply SSL
settings in a centralized way. An `SslBundle` can be registered with
an `SslBundleRegistry` and obtained from an `SslBundles` instance. The
`DefaultSslBundleRegistry` provides a default in-memory implementation.

Different client libraries often configure SSL in slightly different
ways. To accommodate this, the `SslBundle` provides a layered approach
of obtaining SSL information:

	- `getStores` provides access to the key store and trust stores
	  as well as any required key store password.

	- `getManagers` provides access to the `KeyManagerFactory`,
	  `TrustManagerFactory` as well as the `KeyManger` and
	  `TrustManager` arrays that they create.

	- `createSslContext` provides a convenient way to obtain a new
	  `SSLContext` instance.

In addition, the `SslBundle` also provides details about the key being
used, the protocol to use and any options that should be applied to the
SSL engine.

See gh-34814
pull/35107/head
Scott Frederick 2 years ago committed by Phillip Webb
parent e61adc6cbf
commit e3677f7ff6

@ -0,0 +1,149 @@
/*
* Copyright 2012-2023 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.boot.ssl;
import java.net.Socket;
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.Arrays;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.KeyManagerFactorySpi;
import javax.net.ssl.ManagerFactoryParameters;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.X509ExtendedKeyManager;
/**
* {@link KeyManagerFactory} that allows a configurable key alias to be used. Due to the
* fact that the actual calls to retrieve the key by alias are done at request time the
* approach is to wrap the actual key managers with a {@link AliasX509ExtendedKeyManager}.
* The actual SPI has to be wrapped as well due to the fact that
* {@link KeyManagerFactory#getKeyManagers()} is final.
*
* @author Scott Frederick
*/
final class AliasKeyManagerFactory extends KeyManagerFactory {
AliasKeyManagerFactory(KeyManagerFactory delegate, String alias, String algorithm) {
super(new AliasKeyManagerFactorySpi(delegate, alias), delegate.getProvider(), algorithm);
}
/**
* {@link KeyManagerFactorySpi} that allows a configurable key alias to be used.
*/
private static final class AliasKeyManagerFactorySpi extends KeyManagerFactorySpi {
private final KeyManagerFactory delegate;
private final String alias;
private AliasKeyManagerFactorySpi(KeyManagerFactory delegate, String alias) {
this.delegate = delegate;
this.alias = alias;
}
@Override
protected void engineInit(KeyStore keyStore, char[] chars)
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
this.delegate.init(keyStore, chars);
}
@Override
protected void engineInit(ManagerFactoryParameters managerFactoryParameters)
throws InvalidAlgorithmParameterException {
throw new InvalidAlgorithmParameterException("Unsupported ManagerFactoryParameters");
}
@Override
protected KeyManager[] engineGetKeyManagers() {
return Arrays.stream(this.delegate.getKeyManagers())
.filter(X509ExtendedKeyManager.class::isInstance)
.map(X509ExtendedKeyManager.class::cast)
.map(this::wrap)
.toArray(KeyManager[]::new);
}
private AliasKeyManagerFactory.AliasX509ExtendedKeyManager wrap(X509ExtendedKeyManager keyManager) {
return new AliasX509ExtendedKeyManager(keyManager, this.alias);
}
}
/**
* {@link X509ExtendedKeyManager} that allows a configurable key alias to be used.
*/
static final class AliasX509ExtendedKeyManager extends X509ExtendedKeyManager {
private final X509ExtendedKeyManager delegate;
private final String alias;
private AliasX509ExtendedKeyManager(X509ExtendedKeyManager keyManager, String alias) {
this.delegate = keyManager;
this.alias = alias;
}
@Override
public String chooseEngineClientAlias(String[] strings, Principal[] principals, SSLEngine sslEngine) {
return this.delegate.chooseEngineClientAlias(strings, principals, sslEngine);
}
@Override
public String chooseEngineServerAlias(String s, Principal[] principals, SSLEngine sslEngine) {
return this.alias;
}
@Override
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
return this.delegate.chooseClientAlias(keyType, issuers, socket);
}
@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 String[] getClientAliases(String keyType, Principal[] issuers) {
return this.delegate.getClientAliases(keyType, issuers);
}
@Override
public PrivateKey getPrivateKey(String alias) {
return this.delegate.getPrivateKey(alias);
}
@Override
public String[] getServerAliases(String keyType, Principal[] issuers) {
return this.delegate.getServerAliases(keyType, issuers);
}
}
}

@ -0,0 +1,59 @@
/*
* Copyright 2012-2023 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.boot.ssl;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.util.Assert;
/**
* Default {@link SslBundleRegistry} implementation.
*
* @author Scott Frederick
* @since 3.1.0
*/
public class DefaultSslBundleRegistry implements SslBundleRegistry, SslBundles {
private final Map<String, SslBundle> bundles = new ConcurrentHashMap<>();
public DefaultSslBundleRegistry() {
}
public DefaultSslBundleRegistry(String name, SslBundle bundle) {
registerBundle(name, bundle);
}
@Override
public void registerBundle(String name, SslBundle bundle) {
Assert.notNull(name, "Name must not be null");
Assert.notNull(bundle, "Bundle must not be null");
SslBundle previous = this.bundles.putIfAbsent(name, bundle);
Assert.state(previous == null, () -> "Cannot replace existing SSL bundle '%s'".formatted(name));
}
@Override
public SslBundle getBundle(String name) {
Assert.notNull(name, "Name must not be null");
SslBundle bundle = this.bundles.get(name);
if (bundle == null) {
throw new NoSuchSslBundleException(name, "SSL bundle name '%s' cannot be found".formatted(name));
}
return bundle;
}
}

@ -0,0 +1,86 @@
/*
* Copyright 2012-2023 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.boot.ssl;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
/**
* Default implementation of {@link SslManagerBundle}.
*
* @author Scott Frederick
* @see SslManagerBundle#from(SslStoreBundle, SslBundleKey)
*/
class DefaultSslManagerBundle implements SslManagerBundle {
private final SslStoreBundle storeBundle;
private final SslBundleKey key;
DefaultSslManagerBundle(SslStoreBundle storeBundle, SslBundleKey key) {
this.storeBundle = (storeBundle != null) ? storeBundle : SslStoreBundle.NONE;
this.key = (key != null) ? key : SslBundleKey.NONE;
}
@Override
public KeyManagerFactory getKeyManagerFactory() {
try {
KeyStore store = this.storeBundle.getKeyStore();
this.key.assertContainsAlias(store);
String alias = this.key.getAlias();
String algorithm = KeyManagerFactory.getDefaultAlgorithm();
KeyManagerFactory factory = getKeyManagerFactoryInstance(algorithm);
factory = (alias != null) ? new AliasKeyManagerFactory(factory, alias, algorithm) : factory;
String password = this.key.getPassword();
password = (password != null) ? password : this.storeBundle.getKeyStorePassword();
factory.init(store, (password != null) ? password.toCharArray() : null);
return factory;
}
catch (RuntimeException ex) {
throw ex;
}
catch (Exception ex) {
throw new IllegalStateException("Could not load key manager factory: " + ex.getMessage(), ex);
}
}
@Override
public TrustManagerFactory getTrustManagerFactory() {
try {
KeyStore store = this.storeBundle.getTrustStore();
String algorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory factory = getTrustManagerFactoryInstance(algorithm);
factory.init(store);
return factory;
}
catch (Exception ex) {
throw new IllegalStateException("Could not load trust manager factory: " + ex.getMessage(), ex);
}
}
protected KeyManagerFactory getKeyManagerFactoryInstance(String algorithm) throws NoSuchAlgorithmException {
return KeyManagerFactory.getInstance(algorithm);
}
protected TrustManagerFactory getTrustManagerFactoryInstance(String algorithm) throws NoSuchAlgorithmException {
return TrustManagerFactory.getInstance(algorithm);
}
}

@ -0,0 +1,58 @@
/*
* Copyright 2012-2023 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.boot.ssl;
/**
* Exception indicating that an {@link SslBundle} was referenced with a name that does not
* match any registered bundle.
*
* @author Scott Frederick
* @since 3.1.0
*/
public class NoSuchSslBundleException extends RuntimeException {
private final String bundleName;
/**
* Create a new {@code SslBundleNotFoundException} instance.
* @param bundleName the name of the bundle that could not be found
* @param message the exception message
*/
public NoSuchSslBundleException(String bundleName, String message) {
this(bundleName, message, null);
}
/**
* Create a new {@code SslBundleNotFoundException} instance.
* @param bundleName the name of the bundle that could not be found
* @param message the exception message
* @param cause the exception cause
*/
public NoSuchSslBundleException(String bundleName, String message, Throwable cause) {
super(message, cause);
this.bundleName = bundleName;
}
/**
* Return the name of the bundle that was not found.
* @return the bundle name
*/
public String getBundleName() {
return this.bundleName;
}
}

@ -0,0 +1,166 @@
/*
* Copyright 2012-2023 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.boot.ssl;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import org.springframework.util.StringUtils;
/**
* A bundle of trust material that can be used to establish an SSL connection.
*
* @author Scott Frederick
* @since 3.1.0
*/
public interface SslBundle {
/**
* The default protocol to use.
*/
String DEFAULT_PROTOCOL = "TLS";
/**
* Return the {@link SslStoreBundle} that can be used to access this bundle's key and
* trust stores.
* @return the {@code SslStoreBundle} instance for this bundle
*/
SslStoreBundle getStores();
/**
* Return a reference to the key that should be used for this bundle or
* {@link SslBundleKey#NONE}.
* @return a reference to the SSL key that should be used
*/
SslBundleKey getKey();
/**
* Return {@link SslOptions} that should be applied when establishing the SSL
* connection.
* @return the options that should be applied
*/
SslOptions getOptions();
/**
* Return the protocol to use when establishing the connection. Values should be
* supported by {@link SSLContext#getInstance(String)}.
* @return the SSL protocol
* @see SSLContext#getInstance(String)
*/
String getProtocol();
/**
* Return the {@link SslManagerBundle} that can be used to access this bundle's
* {@link KeyManager key} and {@link TrustManager trust} managers.
* @return the {@code SslManagerBundle} instance for this bundle
*/
SslManagerBundle getManagers();
/**
* Factory method to create a new {@link SSLContext} for this bundle.
* @return a new {@link SSLContext} instance
*/
default SSLContext createSslContext() {
return getManagers().createSslContext(getProtocol());
}
/**
* Factory method to create a new {@link SslBundle} instance.
* @param stores the stores or {@code null}
* @return a new {@link SslBundle} instance
*/
static SslBundle of(SslStoreBundle stores) {
return of(stores, null, null);
}
/**
* Factory method to create a new {@link SslBundle} instance.
* @param stores the stores or {@code null}
* @param key the key or {@code null}
* @return a new {@link SslBundle} instance
*/
static SslBundle of(SslStoreBundle stores, SslBundleKey key) {
return of(stores, key, null);
}
/**
* Factory method to create a new {@link SslBundle} instance.
* @param stores the stores or {@code null}
* @param key the key or {@code null}
* @param options the options or {@code null}
* @return a new {@link SslBundle} instance
*/
static SslBundle of(SslStoreBundle stores, SslBundleKey key, SslOptions options) {
return of(stores, key, options, null);
}
/**
* Factory method to create a new {@link SslBundle} instance.
* @param stores the stores or {@code null}
* @param key the key or {@code null}
* @param options the options or {@code null}
* @param protocol the protocol or {@code null}
* @return a new {@link SslBundle} instance
*/
static SslBundle of(SslStoreBundle stores, SslBundleKey key, SslOptions options, String protocol) {
return of(stores, key, options, protocol, null);
}
/**
* Factory method to create a new {@link SslBundle} instance.
* @param stores the stores or {@code null}
* @param key the key or {@code null}
* @param options the options or {@code null}
* @param protocol the protocol or {@code null}
* @param managers the managers or {@code null}
* @return a new {@link SslBundle} instance
*/
static SslBundle of(SslStoreBundle stores, SslBundleKey key, SslOptions options, String protocol,
SslManagerBundle managers) {
SslManagerBundle managersToUse = (managers != null) ? managers : SslManagerBundle.from(stores, key);
return new SslBundle() {
@Override
public SslStoreBundle getStores() {
return (stores != null) ? stores : SslStoreBundle.NONE;
}
@Override
public SslBundleKey getKey() {
return (key != null) ? key : SslBundleKey.NONE;
}
@Override
public SslOptions getOptions() {
return (options != null) ? options : SslOptions.NONE;
}
@Override
public String getProtocol() {
return (!StringUtils.hasText(protocol)) ? DEFAULT_PROTOCOL : protocol;
}
@Override
public SslManagerBundle getManagers() {
return managersToUse;
}
};
}
}

@ -0,0 +1,100 @@
/*
* Copyright 2012-2023 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.boot.ssl;
import java.security.KeyStore;
import java.security.KeyStoreException;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* A reference to a single key obtained via {@link SslBundle}.
*
* @author Phillip Webb
* @since 3.1.0
*/
public interface SslBundleKey {
/**
* {@link SslBundleKey} that returns no values.
*/
SslBundleKey NONE = of(null, null);
/**
* Return the password that should be used to access the key or {@code null} if no
* password is required.
* @return the key password
*/
String getPassword();
/**
* Return the alias of the key or {@code null} if the key has no alias.
* @return the key alias
*/
String getAlias();
/**
* Assert that the alias is contained in the given keystore.
* @param keyStore the keystore to check
*/
default void assertContainsAlias(KeyStore keyStore) {
String alias = getAlias();
if (StringUtils.hasLength(alias) && keyStore != null) {
try {
Assert.state(keyStore.containsAlias(alias),
() -> String.format("Keystore does not contain alias '%s'", alias));
}
catch (KeyStoreException ex) {
throw new IllegalStateException(
String.format("Could not determine if keystore contains alias '%s'", alias), ex);
}
}
}
/**
* Factory method to create a new {@link SslBundleKey} instance.
* @param password the password used to access the key
* @return a new {@link SslBundleKey} instance
*/
static SslBundleKey of(String password) {
return of(password, null);
}
/**
* Factory method to create a new {@link SslBundleKey} instance.
* @param password the password used to access the key
* @param alias the alias of the key
* @return a new {@link SslBundleKey} instance
*/
static SslBundleKey of(String password, String alias) {
return new SslBundleKey() {
@Override
public String getPassword() {
return password;
}
@Override
public String getAlias() {
return alias;
}
};
}
}

@ -0,0 +1,34 @@
/*
* Copyright 2012-2023 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.boot.ssl;
/**
* Interface that can be used to register an {@link SslBundle} for a given name.
*
* @author Scott Frederick
* @since 3.1.0
*/
public interface SslBundleRegistry {
/**
* Register a named {@link SslBundle}.
* @param name the bundle name
* @param bundle the bundle
*/
void registerBundle(String name, SslBundle bundle);
}

@ -0,0 +1,35 @@
/*
* Copyright 2012-2023 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.boot.ssl;
/**
* A managed set of {@link SslBundle} instances that can be retrieved by name.
*
* @author Scott Frederick
* @since 3.1.0
*/
public interface SslBundles {
/**
* Return an {@link SslBundle} with the provided name.
* @param bundleName the bundle name
* @return the bundle
* @throws NoSuchSslBundleException if a bundle with the provided name does not exist
*/
SslBundle getBundle(String bundleName) throws NoSuchSslBundleException;
}

@ -0,0 +1,121 @@
/*
* Copyright 2012-2023 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.boot.ssl;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import org.springframework.util.Assert;
/**
* A bundle of key and trust managers that can be used to establish an SSL connection.
* Instances are usually created {@link #from(SslStoreBundle, SslBundleKey) from} an
* {@link SslStoreBundle}.
*
* @author Scott Frederick
* @since 3.1.0
* @see SslStoreBundle
* @see SslBundle#getManagers()
*/
public interface SslManagerBundle {
/**
* Return the {@code KeyManager} instances used to establish identity.
* @return the key managers
*/
default KeyManager[] getKeyManagers() {
return getKeyManagerFactory().getKeyManagers();
}
/**
* Return the {@code KeyManagerFactory} used to establish identity.
* @return the key manager factory
*/
KeyManagerFactory getKeyManagerFactory();
/**
* Return the {@link TrustManager} instances used to establish trust.
* @return the trust managers
*/
default TrustManager[] getTrustManagers() {
return getTrustManagerFactory().getTrustManagers();
}
/**
* Return the {@link TrustManagerFactory} used to establish trust.
* @return the trust manager factory
*/
TrustManagerFactory getTrustManagerFactory();
/**
* Factory method to create a new {@link SSLContext} for the {@link #getKeyManagers()
* key managers} and {@link #getTrustManagers() trust managers} managed by this
* instance.
* @param protocol the standard name of the SSL protocol. See
* {@link SSLContext#getInstance(String)}
* @return a new {@link SSLContext} instance
*/
default SSLContext createSslContext(String protocol) {
try {
SSLContext sslContext = SSLContext.getInstance(protocol);
sslContext.init(getKeyManagers(), getTrustManagers(), null);
return sslContext;
}
catch (Exception ex) {
throw new IllegalStateException("Could not load SSL context: " + ex.getMessage(), ex);
}
}
/**
* Factory method to create a new {@link SslManagerBundle} instance.
* @param keyManagerFactory the key manager factory
* @param trustManagerFactory the trust manager factory
* @return a new {@link SslManagerBundle} instance
*/
static SslManagerBundle of(KeyManagerFactory keyManagerFactory, TrustManagerFactory trustManagerFactory) {
Assert.notNull(keyManagerFactory, "KeyManagerFactory must not be null");
Assert.notNull(trustManagerFactory, "TrustManagerFactory must not be null");
return new SslManagerBundle() {
@Override
public KeyManagerFactory getKeyManagerFactory() {
return keyManagerFactory;
}
@Override
public TrustManagerFactory getTrustManagerFactory() {
return trustManagerFactory;
}
};
}
/**
* Factory method to create a new {@link SslManagerBundle} backed by the given
* {@link SslBundle} and {@link SslBundleKey}.
* @param storeBundle the SSL store bundle
* @param key the key reference
* @return a new {@link SslManagerBundle} instance
*/
static SslManagerBundle from(SslStoreBundle storeBundle, SslBundleKey key) {
return new DefaultSslManagerBundle(storeBundle, key);
}
}

@ -0,0 +1,93 @@
/*
* Copyright 2012-2023 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.boot.ssl;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.net.ssl.SSLEngine;
/**
* Configuration options that should be applied when establishing an SSL connection.
*
* @author Scott Frederick
* @since 3.1.0
* @see SslBundle#getOptions()
*/
public interface SslOptions {
/**
* {@link SslOptions} that returns no values.
*/
SslOptions NONE = of(Collections.emptySet(), Collections.emptySet());
/**
* Return the ciphers that can be used or an empty set. The cipher names in this set
* should be compatible with those supported by
* {@link SSLEngine#getSupportedCipherSuites()}.
* @return the ciphers that can be used
*/
Set<String> getCiphers();
/**
* Return the protocols that should be enabled or an empty set. The protocols names in
* this set should be compatible with those supported by
* {@link SSLEngine#getSupportedProtocols()}.
* @return the protocols to enable
*/
Set<String> getEnabledProtocols();
/**
* Factory method to create a new {@link SslOptions} instance.
* @param ciphers the ciphers
* @param enabledProtocols the enabled protocols
* @return a new {@link SslOptions} instance
*/
static SslOptions of(String[] ciphers, String[] enabledProtocols) {
return of(asSet(ciphers), asSet(enabledProtocols));
}
/**
* Factory method to create a new {@link SslOptions} instance.
* @param ciphers the ciphers
* @param enabledProtocols the enabled protocols
* @return a new {@link SslOptions} instance
*/
static SslOptions of(Set<String> ciphers, Set<String> enabledProtocols) {
return new SslOptions() {
@Override
public Set<String> getCiphers() {
return (ciphers != null) ? ciphers : Collections.emptySet();
}
@Override
public Set<String> getEnabledProtocols() {
return (enabledProtocols != null) ? enabledProtocols : Collections.emptySet();
}
};
}
private static Set<String> asSet(String[] array) {
return (array != null) ? Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(array))) : null;
}
}

@ -0,0 +1,81 @@
/*
* Copyright 2012-2023 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.boot.ssl;
import java.security.KeyStore;
/**
* A bundle of key and trust stores that can be used to establish an SSL connection.
*
* @author Scott Frederick
* @since 3.1.0
* @see SslBundle#getStores()
*/
public interface SslStoreBundle {
/**
* {@link SslStoreBundle} that returns {@code null} for each method.
*/
SslStoreBundle NONE = of(null, null, null);
/**
* Return a key store generated from the trust material or {@code null}.
* @return the key store
*/
KeyStore getKeyStore();
/**
* Return the password for the key in the key store or {@code null}.
* @return the key password
*/
String getKeyStorePassword();
/**
* Return a trust store generated from the trust material or {@code null}.
* @return the trust store
*/
KeyStore getTrustStore();
/**
* Factory method to create a new {@link SslStoreBundle} instance.
* @param keyStore the key store or {@code null}
* @param keyStorePassword the key store password or {@code null}
* @param trustStore the trust store or {@code null}
* @return a new {@link SslStoreBundle} instance
*/
static SslStoreBundle of(KeyStore keyStore, String keyStorePassword, KeyStore trustStore) {
return new SslStoreBundle() {
@Override
public KeyStore getKeyStore() {
return keyStore;
}
@Override
public KeyStore getTrustStore() {
return trustStore;
}
@Override
public String getKeyStorePassword() {
return keyStorePassword;
}
};
}
}

@ -0,0 +1,122 @@
/*
* Copyright 2012-2023 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.boot.ssl.jks;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.CertificateException;
import org.springframework.boot.ssl.SslStoreBundle;
import org.springframework.util.Assert;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
/**
* {@link SslStoreBundle} backed by a Java keystore.
*
* @author Scott Frederick
* @author Phillip Webb
* @since 3.1.0
*/
public class JksSslStoreBundle implements SslStoreBundle {
private final JksSslStoreDetails keyStoreDetails;
private final JksSslStoreDetails trustStoreDetails;
/**
* Create a new {@link JksSslStoreBundle} instance.
* @param keyStoreDetails the key store details
* @param trustStoreDetails the trust store details
*/
public JksSslStoreBundle(JksSslStoreDetails keyStoreDetails, JksSslStoreDetails trustStoreDetails) {
this.keyStoreDetails = keyStoreDetails;
this.trustStoreDetails = trustStoreDetails;
}
@Override
public KeyStore getKeyStore() {
return createKeyStore("key", this.keyStoreDetails);
}
@Override
public String getKeyStorePassword() {
return (this.keyStoreDetails != null) ? this.keyStoreDetails.password() : null;
}
@Override
public KeyStore getTrustStore() {
return createKeyStore("trust", this.trustStoreDetails);
}
private KeyStore createKeyStore(String name, JksSslStoreDetails details) {
if (details == null || details.isEmpty()) {
return null;
}
try {
String type = (!StringUtils.hasText(details.type())) ? KeyStore.getDefaultType() : details.type();
char[] password = (details.password() != null) ? details.password().toCharArray() : null;
String location = details.location();
KeyStore store = getKeyStoreInstance(type, details.provider());
if (isHardwareKeystoreType(type)) {
loadHardwareKeyStore(store, location, password);
}
else {
loadKeyStore(store, location, password);
}
return store;
}
catch (Exception ex) {
throw new IllegalStateException("Unable to create %s store: %s".formatted(name, ex.getMessage()), ex);
}
}
private KeyStore getKeyStoreInstance(String type, String provider)
throws KeyStoreException, NoSuchProviderException {
return (!StringUtils.hasText(provider)) ? KeyStore.getInstance(type) : KeyStore.getInstance(type, provider);
}
private boolean isHardwareKeystoreType(String type) {
return type.equalsIgnoreCase("PKCS11");
}
private void loadHardwareKeyStore(KeyStore store, String location, char[] password)
throws IOException, NoSuchAlgorithmException, CertificateException {
Assert.state(!StringUtils.hasText(location),
() -> "Location is '%s', but must be empty or null for PKCS11 hardware key stores".formatted(location));
store.load(null, password);
}
private void loadKeyStore(KeyStore store, String location, char[] password) {
Assert.state(StringUtils.hasText(location), () -> "Location must not be empty or null");
try {
URL url = ResourceUtils.getURL(location);
try (InputStream stream = url.openStream()) {
store.load(stream, password);
}
}
catch (Exception ex) {
throw new IllegalStateException("Could not load store from '" + location + "'", ex);
}
}
}

@ -0,0 +1,65 @@
/*
* Copyright 2012-2023 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.boot.ssl.jks;
import java.security.KeyStore;
import org.springframework.util.StringUtils;
/**
* Details for an individual trust or key store in a {@link JksSslStoreBundle}.
*
* @param type the key store type, for example {@code JKS} or {@code PKCS11}. A
* {@code null} value will use {@link KeyStore#getDefaultType()}).
* @param provider the name of the key store provider
* @param location the location of the key store file or {@code null} if using a
* {@code PKCS11} hardware store
* @param password the password used to unlock the store or {@code null}
* @author Scott Frederick
* @author Phillip Webb
* @since 3.1.0
*/
public record JksSslStoreDetails(String type, String provider, String location, String password) {
/**
* Return a new {@link JksSslStoreDetails} instance with a new password.
* @param password the new password
* @return a new {@link JksSslStoreDetails} instance
*/
public JksSslStoreDetails withPassword(String password) {
return new JksSslStoreDetails(this.type, this.provider, this.location, password);
}
boolean isEmpty() {
return isEmpty(this.type) && isEmpty(this.provider) && isEmpty(this.location);
}
private boolean isEmpty(String value) {
return !StringUtils.hasText(value);
}
/**
* Factory method to create a new {@link JksSslStoreDetails} instance for the given
* location.
* @param location the location
* @return a new {@link JksSslStoreDetails} instance.
*/
public static JksSslStoreDetails forLocation(String location) {
return new JksSslStoreDetails(null, null, location, null);
}
}

@ -0,0 +1,20 @@
/*
* Copyright 2012-2023 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.
*/
/**
* SSL trust material provider for Java KeyStores.
*/
package org.springframework.boot.ssl.jks;

@ -0,0 +1,20 @@
/*
* Copyright 2012-2023 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.
*/
/**
* Management of trust material that can be used to establish an SSL connection.
*/
package org.springframework.boot.ssl;

@ -0,0 +1,95 @@
/*
* Copyright 2012-2023 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.boot.ssl.pem;
import java.io.ByteArrayInputStream;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Parser for X.509 certificates in PEM format.
*
* @author Scott Frederick
* @author Phillip Webb
*/
final class PemCertificateParser {
private static final String HEADER = "-+BEGIN\\s+.*CERTIFICATE[^-]*-+(?:\\s|\\r|\\n)+";
private static final String BASE64_TEXT = "([a-z0-9+/=\\r\\n]+)";
private static final String FOOTER = "-+END\\s+.*CERTIFICATE[^-]*-+";
private static final Pattern PATTERN = Pattern.compile(HEADER + BASE64_TEXT + FOOTER, Pattern.CASE_INSENSITIVE);
private PemCertificateParser() {
}
/**
* Parse certificates from the specified string.
* @param certificates the certificates to parse
* @return the parsed certificates
*/
static X509Certificate[] parse(String certificates) {
if (certificates == null) {
return null;
}
CertificateFactory factory = getCertificateFactory();
List<X509Certificate> certs = new ArrayList<>();
readCertificates(certificates, factory, certs::add);
return (!certs.isEmpty()) ? certs.toArray(X509Certificate[]::new) : null;
}
private static CertificateFactory getCertificateFactory() {
try {
return CertificateFactory.getInstance("X.509");
}
catch (CertificateException ex) {
throw new IllegalStateException("Unable to get X.509 certificate factory", ex);
}
}
private static void readCertificates(String text, CertificateFactory factory, Consumer<X509Certificate> consumer) {
try {
Matcher matcher = PATTERN.matcher(text);
while (matcher.find()) {
String encodedText = matcher.group(1);
byte[] decodedBytes = decodeBase64(encodedText);
ByteArrayInputStream inputStream = new ByteArrayInputStream(decodedBytes);
while (inputStream.available() > 0) {
consumer.accept((X509Certificate) factory.generateCertificate(inputStream));
}
}
}
catch (CertificateException ex) {
throw new IllegalStateException("Error reading certificate: " + ex.getMessage(), ex);
}
}
private static byte[] decodeBase64(String content) {
byte[] bytes = content.replaceAll("\r", "").replaceAll("\n", "").getBytes();
return Base64.getDecoder().decode(bytes);
}
}

@ -0,0 +1,64 @@
/*
* Copyright 2012-2023 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.boot.ssl.pem;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.ResourceUtils;
/**
* Utility to load PEM content.
*
* @author Scott Frederick
* @author Phillip Webb
*/
final class PemContent {
private static final Pattern PEM_HEADER = Pattern.compile("-+BEGIN\\s+[^-]*-+", Pattern.CASE_INSENSITIVE);
private static final Pattern PEM_FOOTER = Pattern.compile("-+END\\s+[^-]*-+", Pattern.CASE_INSENSITIVE);
private PemContent() {
}
static String load(String content) {
if (content == null || isPemContent(content)) {
return content;
}
try {
URL url = ResourceUtils.getURL(content);
try (Reader reader = new InputStreamReader(url.openStream(), StandardCharsets.UTF_8)) {
return FileCopyUtils.copyToString(reader);
}
}
catch (IOException ex) {
throw new IllegalStateException(
"Error reading certificate or key from file '" + content + "':" + ex.getMessage(), ex);
}
}
private static boolean isPemContent(String content) {
return content != null && PEM_HEADER.matcher(content).find() && PEM_FOOTER.matcher(content).find();
}
}

@ -0,0 +1,245 @@
/*
* Copyright 2012-2023 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.boot.ssl.pem;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Parser for PKCS private key files in PEM format.
*
* @author Scott Frederick
* @author Phillip Webb
*/
final class PemPrivateKeyParser {
private static final String PKCS1_HEADER = "-+BEGIN\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+";
private static final String PKCS1_FOOTER = "-+END\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+";
private static final String PKCS8_HEADER = "-+BEGIN\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+";
private static final String PKCS8_FOOTER = "-+END\\s+PRIVATE\\s+KEY[^-]*-+";
private static final String EC_HEADER = "-+BEGIN\\s+EC\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+";
private static final String EC_FOOTER = "-+END\\s+EC\\s+PRIVATE\\s+KEY[^-]*-+";
private static final String BASE64_TEXT = "([a-z0-9+/=\\r\\n]+)";
private static final List<PemParser> PEM_PARSERS;
static {
List<PemParser> parsers = new ArrayList<>();
parsers.add(new PemParser(PKCS1_HEADER, PKCS1_FOOTER, "RSA", PemPrivateKeyParser::createKeySpecForPkcs1));
parsers.add(new PemParser(EC_HEADER, EC_FOOTER, "EC", PemPrivateKeyParser::createKeySpecForEc));
parsers.add(new PemParser(PKCS8_HEADER, PKCS8_FOOTER, "RSA", PKCS8EncodedKeySpec::new));
PEM_PARSERS = Collections.unmodifiableList(parsers);
}
/**
* ASN.1 encoded object identifier {@literal 1.2.840.113549.1.1.1}.
*/
private static final int[] RSA_ALGORITHM = { 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01 };
/**
* ASN.1 encoded object identifier {@literal 1.2.840.10045.2.1}.
*/
private static final int[] EC_ALGORITHM = { 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01 };
/**
* ASN.1 encoded object identifier {@literal 1.3.132.0.34}.
*/
private static final int[] EC_PARAMETERS = { 0x2b, 0x81, 0x04, 0x00, 0x22 };
private PemPrivateKeyParser() {
}
private static PKCS8EncodedKeySpec createKeySpecForPkcs1(byte[] bytes) {
return createKeySpecForAlgorithm(bytes, RSA_ALGORITHM, null);
}
private static PKCS8EncodedKeySpec createKeySpecForEc(byte[] bytes) {
return createKeySpecForAlgorithm(bytes, EC_ALGORITHM, EC_PARAMETERS);
}
private static PKCS8EncodedKeySpec createKeySpecForAlgorithm(byte[] bytes, int[] algorithm, int[] parameters) {
try {
DerEncoder encoder = new DerEncoder();
encoder.integer(0x00); // Version 0
DerEncoder algorithmIdentifier = new DerEncoder();
algorithmIdentifier.objectIdentifier(algorithm);
algorithmIdentifier.objectIdentifier(parameters);
byte[] byteArray = algorithmIdentifier.toByteArray();
encoder.sequence(byteArray);
encoder.octetString(bytes);
return new PKCS8EncodedKeySpec(encoder.toSequence());
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
/**
* Parse a private key from the specified string.
* @param key the private key to parse
* @return the parsed private key
*/
static PrivateKey parse(String key) {
if (key == null) {
return null;
}
try {
for (PemParser pemParser : PEM_PARSERS) {
PrivateKey privateKey = pemParser.parse(key);
if (privateKey != null) {
return privateKey;
}
}
throw new IllegalStateException("Unrecognized private key format");
}
catch (Exception ex) {
throw new IllegalStateException("Error loading private key file: " + ex.getMessage(), ex);
}
}
/**
* Parser for a specific PEM format.
*/
private static class PemParser {
private final Pattern pattern;
private final String algorithm;
private final Function<byte[], PKCS8EncodedKeySpec> keySpecFactory;
PemParser(String header, String footer, String algorithm,
Function<byte[], PKCS8EncodedKeySpec> keySpecFactory) {
this.pattern = Pattern.compile(header + BASE64_TEXT + footer, Pattern.CASE_INSENSITIVE);
this.algorithm = algorithm;
this.keySpecFactory = keySpecFactory;
}
PrivateKey parse(String text) {
Matcher matcher = this.pattern.matcher(text);
return (!matcher.find()) ? null : parse(decodeBase64(matcher.group(1)));
}
private static byte[] decodeBase64(String content) {
byte[] contentBytes = content.replaceAll("\r", "").replaceAll("\n", "").getBytes();
return Base64.getDecoder().decode(contentBytes);
}
private PrivateKey parse(byte[] bytes) {
try {
PKCS8EncodedKeySpec keySpec = this.keySpecFactory.apply(bytes);
KeyFactory keyFactory = KeyFactory.getInstance(this.algorithm);
return keyFactory.generatePrivate(keySpec);
}
catch (GeneralSecurityException ex) {
throw new IllegalArgumentException("Unexpected key format", ex);
}
}
}
/**
* Simple ASN.1 DER encoder.
*/
static class DerEncoder {
private final ByteArrayOutputStream stream = new ByteArrayOutputStream();
void objectIdentifier(int... encodedObjectIdentifier) throws IOException {
int code = (encodedObjectIdentifier != null) ? 0x06 : 0x05;
codeLengthBytes(code, bytes(encodedObjectIdentifier));
}
void integer(int... encodedInteger) throws IOException {
codeLengthBytes(0x02, bytes(encodedInteger));
}
void octetString(byte[] bytes) throws IOException {
codeLengthBytes(0x04, bytes);
}
void sequence(int... elements) throws IOException {
sequence(bytes(elements));
}
void sequence(byte[] bytes) throws IOException {
codeLengthBytes(0x30, bytes);
}
void codeLengthBytes(int code, byte[] bytes) throws IOException {
this.stream.write(code);
int length = (bytes != null) ? bytes.length : 0;
if (length <= 127) {
this.stream.write(length & 0xFF);
}
else {
ByteArrayOutputStream lengthStream = new ByteArrayOutputStream();
while (length != 0) {
lengthStream.write(length & 0xFF);
length = length >> 8;
}
byte[] lengthBytes = lengthStream.toByteArray();
this.stream.write(0x80 | lengthBytes.length);
for (int i = lengthBytes.length - 1; i >= 0; i--) {
this.stream.write(lengthBytes[i]);
}
}
if (bytes != null) {
this.stream.write(bytes);
}
}
private static byte[] bytes(int... elements) {
if (elements == null) {
return null;
}
byte[] result = new byte[elements.length];
for (int i = 0; i < elements.length; i++) {
result[i] = (byte) elements[i];
}
return result;
}
byte[] toSequence() throws IOException {
DerEncoder sequenceEncoder = new DerEncoder();
sequenceEncoder.sequence(toByteArray());
return sequenceEncoder.toByteArray();
}
byte[] toByteArray() {
return this.stream.toByteArray();
}
}
}

@ -0,0 +1,116 @@
/*
* Copyright 2012-2023 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.boot.ssl.pem;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import org.springframework.boot.ssl.SslStoreBundle;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link SslStoreBundle} backed by PEM-encoded certificates and private keys.
*
* @author Scott Frederick
* @author Phillip Webb
* @since 3.1.0
*/
public class PemSslStoreBundle implements SslStoreBundle {
private static final String DEFAULT_KEY_ALIAS = "ssl";
private final PemSslStoreDetails keyStoreDetails;
private final PemSslStoreDetails trustStoreDetails;
private final String keyAlias;
/**
* Create a new {@link PemSslStoreBundle} instance.
* @param keyStoreDetails the key store details
* @param trustStoreDetails the trust store details
*/
public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails) {
this(keyStoreDetails, trustStoreDetails, null);
}
/**
* Create a new {@link PemSslStoreBundle} instance.
* @param keyStoreDetails the key store details
* @param trustStoreDetails the trust store details
* @param keyAlias the key alias to use or {@code null} to use a default alias
*/
public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails,
String keyAlias) {
this.keyAlias = keyAlias;
this.keyStoreDetails = keyStoreDetails;
this.trustStoreDetails = trustStoreDetails;
}
@Override
public KeyStore getKeyStore() {
return createKeyStore("key", this.keyStoreDetails);
}
@Override
public String getKeyStorePassword() {
return null;
}
@Override
public KeyStore getTrustStore() {
return createKeyStore("trust", this.trustStoreDetails);
}
private KeyStore createKeyStore(String name, PemSslStoreDetails details) {
if (details == null || details.isEmpty()) {
return null;
}
try {
Assert.notNull(details.certificate(), "CertificateContent must not be null");
String type = (!StringUtils.hasText(details.type())) ? KeyStore.getDefaultType() : details.type();
KeyStore store = KeyStore.getInstance(type);
store.load(null);
String certificateContent = PemContent.load(details.certificate());
String privateKeyContent = PemContent.load(details.privateKey());
X509Certificate[] certificates = PemCertificateParser.parse(certificateContent);
PrivateKey privateKey = PemPrivateKeyParser.parse(privateKeyContent);
addCertificates(store, certificates, privateKey);
return store;
}
catch (Exception ex) {
throw new IllegalStateException("Unable to create %s store: %s".formatted(name, ex.getMessage()), ex);
}
}
private void addCertificates(KeyStore keyStore, X509Certificate[] certificates, PrivateKey privateKey)
throws KeyStoreException {
String alias = (this.keyAlias != null) ? this.keyAlias : DEFAULT_KEY_ALIAS;
if (privateKey != null) {
keyStore.setKeyEntry(alias, privateKey, null, certificates);
}
else {
for (int index = 0; index < certificates.length; index++) {
keyStore.setCertificateEntry(alias + "-" + index, certificates[index]);
}
}
}
}

@ -0,0 +1,66 @@
/*
* Copyright 2012-2023 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.boot.ssl.pem;
import java.security.KeyStore;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
/**
* Details for an individual trust or key store in a {@link PemSslStoreBundle}.
*
* @param type the key store type, for example {@code JKS} or {@code PKCS11}. A
* {@code null} value will use {@link KeyStore#getDefaultType()}).
* @param certificate the certificate content (either the PEM content itself or something
* that can be loaded by {@link ResourceUtils#getURL})
* @param privateKey the private key content (either the PEM content itself or something
* that can be loaded by {@link ResourceUtils#getURL})
* @author Scott Frederick
* @author Phillip Webb
* @since 3.1.0
*/
public record PemSslStoreDetails(String type, String certificate, String privateKey) {
/**
* Return a new {@link PemSslStoreDetails} instance with a new private key.
* @param privateKey the new private key
* @return a new {@link PemSslStoreDetails} instance
*/
public PemSslStoreDetails withPrivateKey(String privateKey) {
return new PemSslStoreDetails(this.type, this.certificate, privateKey);
}
boolean isEmpty() {
return isEmpty(this.type) && isEmpty(this.certificate) && isEmpty(this.privateKey);
}
private boolean isEmpty(String value) {
return !StringUtils.hasText(value);
}
/**
* Factory method to create a new {@link PemSslStoreDetails} instance for the given
* certificate.
* @param certificate the certificate
* @return a new {@link PemSslStoreDetails} instance.
*/
public static PemSslStoreDetails forCertificate(String certificate) {
return new PemSslStoreDetails(null, certificate, null);
}
}

@ -0,0 +1,20 @@
/*
* Copyright 2012-2023 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.
*/
/**
* SSL trust material provider for PEM-encoded certificates.
*/
package org.springframework.boot.ssl.pem;

@ -0,0 +1,54 @@
/*
* Copyright 2012-2023 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.boot.ssl;
import java.util.Arrays;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.X509ExtendedKeyManager;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link AliasKeyManagerFactory}.
*
* @author Phillip Webb
*/
class AliasKeyManagerFactoryTests {
@Test
void chooseEngineServerAliasReturnsAlias() throws Exception {
KeyManagerFactory delegate = mock(KeyManagerFactory.class);
given(delegate.getKeyManagers()).willReturn(new KeyManager[] { mock(X509ExtendedKeyManager.class) });
AliasKeyManagerFactory factory = new AliasKeyManagerFactory(delegate, "test-alias",
KeyManagerFactory.getDefaultAlgorithm());
factory.init(null, null);
KeyManager[] keyManagers = factory.getKeyManagers();
X509ExtendedKeyManager x509KeyManager = (X509ExtendedKeyManager) Arrays.stream(keyManagers)
.filter(X509ExtendedKeyManager.class::isInstance)
.findAny()
.get();
String chosenAlias = x509KeyManager.chooseEngineServerAlias(null, null, null);
assertThat(chosenAlias).isEqualTo("test-alias");
}
}

@ -0,0 +1,92 @@
/*
* Copyright 2012-2023 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.boot.ssl;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link DefaultSslBundleRegistry}.
*
* @author Phillip Webb
*/
class DefaultSslBundleRegistryTests {
private SslBundle bundle1 = mock(SslBundle.class);
private SslBundle bundle2 = mock(SslBundle.class);
private DefaultSslBundleRegistry registry = new DefaultSslBundleRegistry();
@Test
void createWithNameAndBundleRegistersBundle() {
DefaultSslBundleRegistry registry = new DefaultSslBundleRegistry("test", this.bundle1);
assertThat(registry.getBundle("test")).isSameAs(this.bundle1);
}
@Test
void registerBundleWhenNameIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.registry.registerBundle(null, this.bundle1))
.withMessage("Name must not be null");
}
@Test
void registerBundleWhenBundleIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.registry.registerBundle("test", null))
.withMessage("Bundle must not be null");
}
@Test
void registerBundleWhenNameIsTakenThrowsException() {
this.registry.registerBundle("test", this.bundle1);
assertThatIllegalStateException().isThrownBy(() -> this.registry.registerBundle("test", this.bundle2))
.withMessage("Cannot replace existing SSL bundle 'test'");
}
@Test
void registerBundleRegistersBundle() {
this.registry.registerBundle("test", this.bundle1);
assertThat(this.registry.getBundle("test")).isSameAs(this.bundle1);
}
@Test
void getBundleWhenNameIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.registry.getBundle(null))
.withMessage("Name must not be null");
}
@Test
void getBundleWhenNoSuchBundleThrowsException() {
this.registry.registerBundle("test", this.bundle1);
assertThatExceptionOfType(NoSuchSslBundleException.class).isThrownBy(() -> this.registry.getBundle("missing"))
.satisfies((ex) -> assertThat(ex.getBundleName()).isEqualTo("missing"));
}
@Test
void getBundleReturnsBundle() {
this.registry.registerBundle("test1", this.bundle1);
this.registry.registerBundle("test2", this.bundle2);
assertThat(this.registry.getBundle("test1")).isSameAs(this.bundle1);
assertThat(this.registry.getBundle("test2")).isSameAs(this.bundle2);
}
}

@ -0,0 +1,156 @@
/*
* Copyright 2012-2023 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.boot.ssl;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link DefaultSslManagerBundle}.
*
* @author Phillip Webb
*/
class DefaultSslManagerBundleTests {
private KeyManagerFactory keyManagerFactory = mock(KeyManagerFactory.class);
private TrustManagerFactory trustManagerFactory = mock(TrustManagerFactory.class);
@Test
void getKeyManagerFactoryWhenStoreBundleIsNull() throws Exception {
DefaultSslManagerBundle bundle = new TestDefaultSslManagerBundle(null, SslBundleKey.NONE);
KeyManagerFactory result = bundle.getKeyManagerFactory();
assertThat(result).isNotNull();
then(this.keyManagerFactory).should().init(null, null);
}
@Test
void getKeyManagerFactoryWhenKeyIsNull() throws Exception {
DefaultSslManagerBundle bundle = new TestDefaultSslManagerBundle(SslStoreBundle.NONE, null);
KeyManagerFactory result = bundle.getKeyManagerFactory();
assertThat(result).isSameAs(this.keyManagerFactory);
then(this.keyManagerFactory).should().init(null, null);
}
@Test
void getKeyManagerFactoryWhenHasKeyAliasReturnsWrapped() {
DefaultSslManagerBundle bundle = new TestDefaultSslManagerBundle(null, SslBundleKey.of("secret", "alias"));
KeyManagerFactory result = bundle.getKeyManagerFactory();
assertThat(result).isInstanceOf(AliasKeyManagerFactory.class);
}
@Test
void getKeyManagerFactoryWhenHasKeyPassword() throws Exception {
DefaultSslManagerBundle bundle = new TestDefaultSslManagerBundle(null, SslBundleKey.of("secret"));
KeyManagerFactory result = bundle.getKeyManagerFactory();
assertThat(result).isSameAs(this.keyManagerFactory);
then(this.keyManagerFactory).should().init(null, "secret".toCharArray());
}
@Test
void getKeyManagerFactoryWhenHasKeyStorePassword() throws Exception {
SslStoreBundle storeBundle = SslStoreBundle.of(null, "secret", null);
DefaultSslManagerBundle bundle = new TestDefaultSslManagerBundle(storeBundle, null);
KeyManagerFactory result = bundle.getKeyManagerFactory();
assertThat(result).isSameAs(this.keyManagerFactory);
then(this.keyManagerFactory).should().init(null, "secret".toCharArray());
}
@Test
void getKeyManagerFactoryWhenHasAliasNotInStoreThrowsException() throws Exception {
KeyStore keyStore = mock(KeyStore.class);
given(keyStore.containsAlias("alias")).willReturn(false);
SslStoreBundle storeBundle = SslStoreBundle.of(keyStore, null, null);
DefaultSslManagerBundle bundle = new TestDefaultSslManagerBundle(storeBundle,
SslBundleKey.of("secret", "alias"));
assertThatIllegalStateException().isThrownBy(() -> bundle.getKeyManagerFactory())
.withMessage("Keystore does not contain alias 'alias'");
}
@Test
void getKeyManagerFactoryWhenHasAliasNotDeterminedInStoreThrowsException() throws Exception {
KeyStore keyStore = mock(KeyStore.class);
given(keyStore.containsAlias("alias")).willThrow(KeyStoreException.class);
SslStoreBundle storeBundle = SslStoreBundle.of(keyStore, null, null);
DefaultSslManagerBundle bundle = new TestDefaultSslManagerBundle(storeBundle,
SslBundleKey.of("secret", "alias"));
assertThatIllegalStateException().isThrownBy(() -> bundle.getKeyManagerFactory())
.withMessage("Could not determine if keystore contains alias 'alias'");
}
@Test
void getKeyManagerFactoryWhenHasStore() throws Exception {
KeyStore keyStore = mock(KeyStore.class);
SslStoreBundle storeBundle = SslStoreBundle.of(keyStore, null, null);
DefaultSslManagerBundle bundle = new TestDefaultSslManagerBundle(storeBundle, null);
KeyManagerFactory result = bundle.getKeyManagerFactory();
assertThat(result).isSameAs(this.keyManagerFactory);
then(this.keyManagerFactory).should().init(keyStore, null);
}
@Test
void getTrustManagerFactoryWhenStoreBundleIsNull() throws Exception {
DefaultSslManagerBundle bundle = new TestDefaultSslManagerBundle(null, null);
TrustManagerFactory result = bundle.getTrustManagerFactory();
assertThat(result).isSameAs(this.trustManagerFactory);
then(this.trustManagerFactory).should().init((KeyStore) null);
}
@Test
void getTrustManagerFactoryWhenHasStore() throws Exception {
KeyStore trustStore = mock(KeyStore.class);
SslStoreBundle storeBundle = SslStoreBundle.of(null, null, trustStore);
DefaultSslManagerBundle bundle = new TestDefaultSslManagerBundle(storeBundle, null);
TrustManagerFactory result = bundle.getTrustManagerFactory();
assertThat(result).isSameAs(this.trustManagerFactory);
then(this.trustManagerFactory).should().init(trustStore);
}
/**
* Test version of {@link DefaultSslManagerBundle}.
*/
class TestDefaultSslManagerBundle extends DefaultSslManagerBundle {
TestDefaultSslManagerBundle(SslStoreBundle storeBundle, SslBundleKey key) {
super(storeBundle, key);
}
@Override
protected KeyManagerFactory getKeyManagerFactoryInstance(String algorithm) throws NoSuchAlgorithmException {
return DefaultSslManagerBundleTests.this.keyManagerFactory;
}
@Override
protected TrustManagerFactory getTrustManagerFactoryInstance(String algorithm) throws NoSuchAlgorithmException {
return DefaultSslManagerBundleTests.this.trustManagerFactory;
}
}
}

@ -0,0 +1,38 @@
/*
* Copyright 2012-2023 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.boot.ssl;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link NoSuchSslBundleException}.
*
* @author Phillip Webb
*/
class NoSuchSslBundleExceptionTests {
@Test
void createCreatesException() {
Throwable cause = new RuntimeException();
NoSuchSslBundleException exception = new NoSuchSslBundleException("name", "badness", cause);
assertThat(exception).hasMessage("badness").hasCause(cause);
assertThat(exception.getBundleName()).isEqualTo("name");
}
}

@ -0,0 +1,75 @@
/*
* Copyright 2012-2023 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.boot.ssl;
import java.security.KeyStore;
import java.security.KeyStoreException;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link SslBundleKey}.
*
* @author Phillip Webb
*/
class SslBundleKeyTests {
@Test
void noneHasNoValues() {
SslBundleKey keyReference = SslBundleKey.NONE;
assertThat(keyReference.getPassword()).isNull();
assertThat(keyReference.getAlias()).isNull();
}
@Test
void ofCreatesWithPasswordSslKeyReference() {
SslBundleKey keyReference = SslBundleKey.of("password");
assertThat(keyReference.getPassword()).isEqualTo("password");
assertThat(keyReference.getAlias()).isNull();
}
@Test
void ofCreatesWithPasswordAndAliasSslKeyReference() {
SslBundleKey keyReference = SslBundleKey.of("password", "alias");
assertThat(keyReference.getPassword()).isEqualTo("password");
assertThat(keyReference.getAlias()).isEqualTo("alias");
}
@Test
void getKeyManagerFactoryWhenHasAliasNotInStoreThrowsException() throws Exception {
KeyStore keyStore = mock(KeyStore.class);
given(keyStore.containsAlias("alias")).willReturn(false);
SslBundleKey key = SslBundleKey.of("secret", "alias");
assertThatIllegalStateException().isThrownBy(() -> key.assertContainsAlias(keyStore))
.withMessage("Keystore does not contain alias 'alias'");
}
@Test
void getKeyManagerFactoryWhenHasAliasNotDeterminedInStoreThrowsException() throws Exception {
KeyStore keyStore = mock(KeyStore.class);
given(keyStore.containsAlias("alias")).willThrow(KeyStoreException.class);
SslBundleKey key = SslBundleKey.of("secret", "alias");
assertThatIllegalStateException().isThrownBy(() -> key.assertContainsAlias(keyStore))
.withMessage("Could not determine if keystore contains alias 'alias'");
}
}

@ -0,0 +1,55 @@
/*
* Copyright 2012-2023 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.boot.ssl;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link SslBundle}.
*
* @author Phillip Webb
*/
class SslBundleTests {
@Test
void createSslContextDelegatesToManagers() {
SslManagerBundle managers = mock(SslManagerBundle.class);
SslBundle bundle = SslBundle.of(null, null, null, "testprotocol", managers);
bundle.createSslContext();
then(managers).should().createSslContext("testprotocol");
}
@Test
void ofCreatesSslBundle() {
SslStoreBundle stores = mock(SslStoreBundle.class);
SslBundleKey key = mock(SslBundleKey.class);
SslOptions options = mock(SslOptions.class);
String protocol = "test";
SslManagerBundle managers = mock(SslManagerBundle.class);
SslBundle bundle = SslBundle.of(stores, key, options, protocol, managers);
assertThat(bundle.getStores()).isSameAs(stores);
assertThat(bundle.getKey()).isSameAs(key);
assertThat(bundle.getOptions()).isSameAs(options);
assertThat(bundle.getProtocol()).isSameAs(protocol);
assertThat(bundle.getManagers()).isSameAs(managers);
}
}

@ -0,0 +1,88 @@
/*
* Copyright 2012-2023 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.boot.ssl;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link SslManagerBundle}.
*
* @author Phillip Webb
*/
class SslManagerBundleTests {
private KeyManagerFactory keyManagerFactory = mock(KeyManagerFactory.class);
private TrustManagerFactory trustManagerFactory = mock(TrustManagerFactory.class);
@Test
void getKeyManagersDelegatesToFactory() {
SslManagerBundle bundle = SslManagerBundle.of(this.keyManagerFactory, this.trustManagerFactory);
bundle.getKeyManagers();
then(this.keyManagerFactory).should().getKeyManagers();
}
@Test
void getTrustManagersDelegatesToFactory() {
SslManagerBundle bundle = SslManagerBundle.of(this.keyManagerFactory, this.trustManagerFactory);
bundle.getTrustManagers();
then(this.trustManagerFactory).should().getTrustManagers();
}
@Test
void createSslContextCreatesInitializedSslContext() {
SslManagerBundle bundle = SslManagerBundle.of(this.keyManagerFactory, this.trustManagerFactory);
SSLContext sslContext = bundle.createSslContext("TLS");
assertThat(sslContext).isNotNull();
assertThat(sslContext.getProtocol()).isEqualTo("TLS");
}
@Test
void ofWhenKeyManagerFactoryIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> SslManagerBundle.of(null, this.trustManagerFactory))
.withMessage("KeyManagerFactory must not be null");
}
@Test
void ofWhenTrustManagerFactoryIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> SslManagerBundle.of(this.keyManagerFactory, null))
.withMessage("TrustManagerFactory must not be null");
}
@Test
void ofCreatesSslManagerBundle() {
SslManagerBundle bundle = SslManagerBundle.of(this.keyManagerFactory, this.trustManagerFactory);
assertThat(bundle.getKeyManagerFactory()).isSameAs(this.keyManagerFactory);
assertThat(bundle.getTrustManagerFactory()).isSameAs(this.trustManagerFactory);
}
@Test
void fromCreatesDefaultSslManagerBundle() {
SslManagerBundle bundle = SslManagerBundle.from(SslStoreBundle.NONE, SslBundleKey.NONE);
assertThat(bundle).isInstanceOf(DefaultSslManagerBundle.class);
}
}

@ -0,0 +1,75 @@
/*
* Copyright 2012-2023 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.boot.ssl;
import java.util.Set;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SslOptions}.
*
* @author Phillip Webb
*/
class SslOptionsTests {
@Test
void noneReturnsEmptyCollections() {
SslOptions options = SslOptions.NONE;
assertThat(options.getCiphers()).isEmpty();
assertThat(options.getEnabledProtocols()).isEmpty();
}
@Test
void ofWithArrayCreatesSslOptions() {
String[] ciphers = { "a", "b", "c" };
String[] enabledProtocols = { "d", "e", "f" };
SslOptions options = SslOptions.of(ciphers, enabledProtocols);
assertThat(options.getCiphers()).containsExactly(ciphers);
assertThat(options.getEnabledProtocols()).containsExactly(enabledProtocols);
}
@Test
void ofWithNullArraysCreatesSslOptions() {
String[] ciphers = null;
String[] enabledProtocols = null;
SslOptions options = SslOptions.of(ciphers, enabledProtocols);
assertThat(options.getCiphers()).isEmpty();
assertThat(options.getEnabledProtocols()).isEmpty();
}
@Test
void ofWithSetCreatesSslOptions() {
Set<String> ciphers = Set.of("a", "b", "c");
Set<String> enabledProtocols = Set.of("d", "e", "f");
SslOptions options = SslOptions.of(ciphers, enabledProtocols);
assertThat(options.getCiphers()).isEqualTo(ciphers);
assertThat(options.getEnabledProtocols()).isEqualTo(enabledProtocols);
}
@Test
void ofWithNullSetCreatesSslOptions() {
Set<String> ciphers = null;
Set<String> enabledProtocols = null;
SslOptions options = SslOptions.of(ciphers, enabledProtocols);
assertThat(options.getCiphers()).isEmpty();
assertThat(options.getEnabledProtocols()).isEmpty();
}
}

@ -0,0 +1,52 @@
/*
* Copyright 2012-2023 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.boot.ssl;
import java.security.KeyStore;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link SslStoreBundle}
*
* @author Phillip Webb
*/
class SslStoreBundleTests {
@Test
void noneReturnsEmptySslStoreBundle() {
SslStoreBundle bundle = SslStoreBundle.NONE;
assertThat(bundle.getKeyStore()).isNull();
assertThat(bundle.getKeyStorePassword()).isNull();
assertThat(bundle.getTrustStore()).isNull();
}
@Test
void ofCreatesStoreBundle() {
KeyStore keyStore = mock(KeyStore.class);
String keyStorePassword = "secret";
KeyStore trustStore = mock(KeyStore.class);
SslStoreBundle bundle = SslStoreBundle.of(keyStore, keyStorePassword, trustStore);
assertThat(bundle.getKeyStore()).isSameAs(keyStore);
assertThat(bundle.getKeyStorePassword()).isEqualTo(keyStorePassword);
assertThat(bundle.getTrustStore()).isSameAs(trustStore);
}
}

@ -0,0 +1,137 @@
/*
* Copyright 2012-2023 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.boot.ssl.jks;
import java.security.KeyStore;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.boot.web.embedded.test.MockPkcs11Security;
import org.springframework.util.function.ThrowingConsumer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link JksSslStoreBundle}.
*
* @author Scott Frederick
* @author Phillip Webb
*/
@MockPkcs11Security
class JksSslStoreBundleTests {
@Test
void whenNullStores() {
JksSslStoreDetails keyStoreDetails = null;
JksSslStoreDetails trustStoreDetails = null;
JksSslStoreBundle bundle = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getKeyStore()).isNull();
assertThat(bundle.getKeyStorePassword()).isNull();
assertThat(bundle.getTrustStore()).isNull();
}
@Test
void whenStoresHaveNoValues() {
JksSslStoreDetails keyStoreDetails = JksSslStoreDetails.forLocation(null);
JksSslStoreDetails trustStoreDetails = JksSslStoreDetails.forLocation(null);
JksSslStoreBundle bundle = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getKeyStore()).isNull();
assertThat(bundle.getKeyStorePassword()).isNull();
assertThat(bundle.getTrustStore()).isNull();
}
@Test
void whenTypePKCS11AndLocationThrowsException() {
JksSslStoreDetails keyStoreDetails = new JksSslStoreDetails("PKCS11", null, "test.jks", null);
JksSslStoreDetails trustStoreDetails = null;
JksSslStoreBundle bundle = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThatIllegalStateException().isThrownBy(bundle::getKeyStore)
.withMessageContaining(
"Unable to create key store: Location is 'test.jks', but must be empty or null for PKCS11 hardware key stores");
}
@Test
void whenHasKeyStoreLocation() {
JksSslStoreDetails keyStoreDetails = JksSslStoreDetails.forLocation("classpath:test.jks")
.withPassword("secret");
JksSslStoreDetails trustStoreDetails = null;
JksSslStoreBundle bundle = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("test-alias", "password"));
}
@Test
void getTrustStoreWithLocations() {
JksSslStoreDetails keyStoreDetails = null;
JksSslStoreDetails trustStoreDetails = JksSslStoreDetails.forLocation("classpath:test.jks")
.withPassword("secret");
JksSslStoreBundle bundle = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("test-alias", "password"));
}
@Test
void whenHasKeyStoreType() {
JksSslStoreDetails keyStoreDetails = new JksSslStoreDetails("jks", null, "classpath:test.jks", "secret");
JksSslStoreDetails trustStoreDetails = null;
JksSslStoreBundle bundle = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("jks", "test-alias", "password"));
}
@Test
void whenHasTrustStoreType() {
JksSslStoreDetails keyStoreDetails = null;
JksSslStoreDetails trustStoreDetails = new JksSslStoreDetails("jks", null, "classpath:test.jks", "secret");
JksSslStoreBundle bundle = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("jks", "test-alias", "password"));
}
@Test
void whenHasKeyStoreProvider() {
JksSslStoreDetails keyStoreDetails = new JksSslStoreDetails(null, "com.example.KeyStoreProvider",
"classpath:test.jks", "secret");
JksSslStoreDetails trustStoreDetails = null;
JksSslStoreBundle bundle = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThatIllegalStateException().isThrownBy(bundle::getKeyStore)
.withMessageContaining("com.example.KeyStoreProvider");
}
@Test
void whenHasTrustStoreProvider() {
JksSslStoreDetails keyStoreDetails = null;
JksSslStoreDetails trustStoreDetails = new JksSslStoreDetails(null, "com.example.KeyStoreProvider",
"classpath:test.jks", "secret");
JksSslStoreBundle bundle = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThatIllegalStateException().isThrownBy(bundle::getTrustStore)
.withMessageContaining("com.example.KeyStoreProvider");
}
private Consumer<KeyStore> storeContainingCertAndKey(String keyAlias, String keyPassword) {
return storeContainingCertAndKey(KeyStore.getDefaultType(), keyAlias, keyPassword);
}
private Consumer<KeyStore> storeContainingCertAndKey(String keyStoreType, String keyAlias, String keyPassword) {
return ThrowingConsumer.of((keyStore) -> {
assertThat(keyStore).isNotNull();
assertThat(keyStore.getType()).isEqualTo(keyStoreType);
assertThat(keyStore.containsAlias(keyAlias)).isTrue();
assertThat(keyStore.getCertificate(keyAlias)).isNotNull();
assertThat(keyStore.getKey(keyAlias, keyPassword.toCharArray())).isNotNull();
});
}
}

@ -0,0 +1,57 @@
/*
* Copyright 2012-2023 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.boot.ssl.pem;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link PemCertificateParser}.
*
* @author Scott Frederick
*/
class PemCertificateParserTests {
@Test
void parseCertificate() throws Exception {
X509Certificate[] certificates = PemCertificateParser.parse(read("test-cert.pem"));
assertThat(certificates).isNotNull();
assertThat(certificates).hasSize(1);
assertThat(certificates[0].getType()).isEqualTo("X.509");
}
@Test
void parseCertificateChain() throws Exception {
X509Certificate[] certificates = PemCertificateParser.parse(read("test-cert-chain.pem"));
assertThat(certificates).isNotNull();
assertThat(certificates).hasSize(2);
assertThat(certificates[0].getType()).isEqualTo("X.509");
assertThat(certificates[1].getType()).isEqualTo("X.509");
}
private String read(String path) throws IOException {
return new ClassPathResource(path).getContentAsString(StandardCharsets.UTF_8);
}
}

@ -0,0 +1,77 @@
/*
* Copyright 2012-2023 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.boot.ssl.pem;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link PemContent}.
*
* @author Phillip Webb
*/
class PemContentTests {
@Test
void loadWhenContentIsNullReturnsNull() {
assertThat(PemContent.load(null)).isNull();
}
@Test
void loadWhenContentIsPemContentReturnsContent() {
String content = """
-----BEGIN CERTIFICATE-----
MIICpDCCAYwCCQCDOqHKPjAhCTANBgkqhkiG9w0BAQUFADAUMRIwEAYDVQQDDAls
b2NhbGhvc3QwHhcNMTQwOTEwMjE0MzA1WhcNMTQxMDEwMjE0MzA1WjAUMRIwEAYD
VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDR
0KfxUw7MF/8RB5/YXOM7yLnoHYb/M/6dyoulMbtEdKKhQhU28o5FiDkHcEG9PJQL
gqrRgAjl3VmCC9omtfZJQ2EpfkTttkJjnKOOroXhYE51/CYSckapBYCVh8GkjUEJ
uEfnp07cTfYZFqViIgIWPZyjkzl3w4girS7kCuzNdDntVJVx5F/EsFwMA8n3C0Qa
zHQoM5s00Fer6aTwd6AW0JD5QkADavpfzZ554e4HrVGwHlM28WKQQkFzzGu44FFX
yVuEF3HeyVPug8GRHAc8UU7ijVgJB5TmbvRGYowIErD5i4VvGLuOv9mgR3aVyN0S
dJ1N7aJnXpeSQjAgf03jAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAE4yvwhbPldg
Bpl7sBw/m2B3bfiNeSqa4tII1PQ7ysgWVb9HbFNKkriScwDWlqo6ljZfJ+SDFCoj
bQz4fOFdMAOzRnpTrG2NAKMoJLY0/g/p7XO00PiC8T3h3BOJ5SHuW3gUyfGXmAYs
DnJxJOrwPzj57xvNXjNSbDOJ3DRfCbB0CWBexOeGDiUokoEq3Gnz04Q4ZfHyAcpZ
3deMw8Od5p9WAoCh3oClpFyOSzXYKZd+3ppMMtfc4wnbfocnfSFxj0UCpOEJw4Ez
+lGuHKdhNOVW9CmqPD1y76o6c8PQKuF7KZEoY2jvy3GeIfddBvqXgZ4PbWvFz1jO
32C9XWHwRA4=
-----END CERTIFICATE-----""";
assertThat(PemContent.load(content)).isEqualTo(content);
}
@Test
void loadWhenClasspathLocationReturnsContent() throws IOException {
String actual = PemContent.load("classpath:test-cert.pem");
String expected = new ClassPathResource("test-cert.pem").getContentAsString(StandardCharsets.UTF_8);
assertThat(actual).isEqualTo(expected);
}
@Test
void loadWhenFileLocationReturnsContent() throws IOException {
String actual = PemContent.load("src/test/resources/test-cert.pem");
String expected = new ClassPathResource("test-cert.pem").getContentAsString(StandardCharsets.UTF_8);
assertThat(actual).isEqualTo(expected);
}
}

@ -0,0 +1,62 @@
/*
* Copyright 2012-2023 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.boot.ssl.pem;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link PemPrivateKeyParser}.
*
* @author Scott Frederick
*/
class PemPrivateKeyParserTests {
@Test
void parsePkcs8KeyFile() throws Exception {
PrivateKey privateKey = PemPrivateKeyParser.parse(read("test-key.pem"));
assertThat(privateKey).isNotNull();
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
assertThat(privateKey.getAlgorithm()).isEqualTo("RSA");
}
@Test
void parsePkcs8KeyFileWithEcdsa() throws Exception {
PrivateKey privateKey = PemPrivateKeyParser.parse(read("test-ec-key.pem"));
assertThat(privateKey).isNotNull();
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
assertThat(privateKey.getAlgorithm()).isEqualTo("EC");
}
@Test
void parseWithNonKeyTextWillThrowException() {
assertThatIllegalStateException().isThrownBy(() -> PemPrivateKeyParser.parse(read("test-banner.txt")));
}
private String read(String path) throws IOException {
return new ClassPathResource(path).getContentAsString(StandardCharsets.UTF_8);
}
}

@ -0,0 +1,137 @@
/*
* Copyright 2012-2023 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.boot.ssl.pem;
import java.security.KeyStore;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.util.function.ThrowingConsumer;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link PemSslStoreBundle}.
*
* @author Scott Frederick
* @author Phillip Webb
*/
class PemSslStoreBundleTests {
@Test
void whenNullStores() {
PemSslStoreDetails keyStoreDetails = null;
PemSslStoreDetails trustStoreDetails = null;
PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getKeyStore()).isNull();
assertThat(bundle.getKeyStorePassword()).isNull();
assertThat(bundle.getTrustStore()).isNull();
}
@Test
void whenStoresHaveNoValues() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate(null);
PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate(null);
PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getKeyStore()).isNull();
assertThat(bundle.getKeyStorePassword()).isNull();
assertThat(bundle.getTrustStore()).isNull();
}
@Test
void whenHasKeyStoreDetailsCertAndKey() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem")
.withPrivateKey("classpath:test-key.pem");
PemSslStoreDetails trustStoreDetails = null;
PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("ssl"));
assertThat(bundle.getTrustStore()).isNull();
}
@Test
void whenHasKeyStoreDetailsAndTrustStoreDetailsWithoutKey() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem")
.withPrivateKey("classpath:test-key.pem");
PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem");
PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("ssl"));
assertThat(bundle.getTrustStore()).satisfies(storeContainingCert("ssl-0"));
}
@Test
void whenHasKeyStoreDetailsAndTrustStoreDetails() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem")
.withPrivateKey("classpath:test-key.pem");
PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem")
.withPrivateKey("classpath:test-key.pem");
PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("ssl"));
assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("ssl"));
}
@Test
void whenHasKeyStoreDetailsAndTrustStoreDetailsAndAlias() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem")
.withPrivateKey("classpath:test-key.pem");
PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem")
.withPrivateKey("classpath:test-key.pem");
PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, "test-alias");
assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("test-alias"));
assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("test-alias"));
}
@Test
void whenHasStoreType() {
PemSslStoreDetails keyStoreDetails = new PemSslStoreDetails("PKCS12", "classpath:test-cert.pem",
"classpath:test-key.pem");
PemSslStoreDetails trustStoreDetails = new PemSslStoreDetails("PKCS12", "classpath:test-cert.pem",
"classpath:test-key.pem");
PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("PKCS12", "ssl"));
assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("PKCS12", "ssl"));
}
private Consumer<KeyStore> storeContainingCert(String keyAlias) {
return storeContainingCert(KeyStore.getDefaultType(), keyAlias);
}
private Consumer<KeyStore> storeContainingCert(String keyStoreType, String keyAlias) {
return ThrowingConsumer.of((keyStore) -> {
assertThat(keyStore).isNotNull();
assertThat(keyStore.getType()).isEqualTo(keyStoreType);
assertThat(keyStore.containsAlias(keyAlias)).isTrue();
assertThat(keyStore.getCertificate(keyAlias)).isNotNull();
assertThat(keyStore.getKey(keyAlias, new char[] {})).isNull();
});
}
private Consumer<KeyStore> storeContainingCertAndKey(String keyAlias) {
return storeContainingCertAndKey(KeyStore.getDefaultType(), keyAlias);
}
private Consumer<KeyStore> storeContainingCertAndKey(String keyStoreType, String keyAlias) {
return ThrowingConsumer.of((keyStore) -> {
assertThat(keyStore).isNotNull();
assertThat(keyStore.getType()).isEqualTo(keyStoreType);
assertThat(keyStore.containsAlias(keyAlias)).isTrue();
assertThat(keyStore.getCertificate(keyAlias)).isNotNull();
assertThat(keyStore.getKey(keyAlias, new char[] {})).isNotNull();
});
}
}
Loading…
Cancel
Save