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-34814pull/35107/head
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…
Reference in New Issue