Skip to content

Commit e3677f7

Browse files
scottfrederickphilwebb
authored andcommitted
Add SSL bundle support to spring-boot module
Add classes to support SSL bundles which can be used to apply SSL settings in a centralized way. An `SslBundle` can be registered with an `SslBundleRegistry` and obtained from an `SslBundles` instance. The `DefaultSslBundleRegistry` provides a default in-memory implementation. Different client libraries often configure SSL in slightly different ways. To accommodate this, the `SslBundle` provides a layered approach of obtaining SSL information: - `getStores` provides access to the key store and trust stores as well as any required key store password. - `getManagers` provides access to the `KeyManagerFactory`, `TrustManagerFactory` as well as the `KeyManger` and `TrustManager` arrays that they create. - `createSslContext` provides a convenient way to obtain a new `SSLContext` instance. In addition, the `SslBundle` also provides details about the key being used, the protocol to use and any options that should be applied to the SSL engine. See gh-34814
1 parent e61adc6 commit e3677f7

35 files changed

+2970
-0
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.ssl;
18+
19+
import java.net.Socket;
20+
import java.security.InvalidAlgorithmParameterException;
21+
import java.security.KeyStore;
22+
import java.security.KeyStoreException;
23+
import java.security.NoSuchAlgorithmException;
24+
import java.security.Principal;
25+
import java.security.PrivateKey;
26+
import java.security.UnrecoverableKeyException;
27+
import java.security.cert.X509Certificate;
28+
import java.util.Arrays;
29+
30+
import javax.net.ssl.KeyManager;
31+
import javax.net.ssl.KeyManagerFactory;
32+
import javax.net.ssl.KeyManagerFactorySpi;
33+
import javax.net.ssl.ManagerFactoryParameters;
34+
import javax.net.ssl.SSLEngine;
35+
import javax.net.ssl.X509ExtendedKeyManager;
36+
37+
/**
38+
* {@link KeyManagerFactory} that allows a configurable key alias to be used. Due to the
39+
* fact that the actual calls to retrieve the key by alias are done at request time the
40+
* approach is to wrap the actual key managers with a {@link AliasX509ExtendedKeyManager}.
41+
* The actual SPI has to be wrapped as well due to the fact that
42+
* {@link KeyManagerFactory#getKeyManagers()} is final.
43+
*
44+
* @author Scott Frederick
45+
*/
46+
final class AliasKeyManagerFactory extends KeyManagerFactory {
47+
48+
AliasKeyManagerFactory(KeyManagerFactory delegate, String alias, String algorithm) {
49+
super(new AliasKeyManagerFactorySpi(delegate, alias), delegate.getProvider(), algorithm);
50+
}
51+
52+
/**
53+
* {@link KeyManagerFactorySpi} that allows a configurable key alias to be used.
54+
*/
55+
private static final class AliasKeyManagerFactorySpi extends KeyManagerFactorySpi {
56+
57+
private final KeyManagerFactory delegate;
58+
59+
private final String alias;
60+
61+
private AliasKeyManagerFactorySpi(KeyManagerFactory delegate, String alias) {
62+
this.delegate = delegate;
63+
this.alias = alias;
64+
}
65+
66+
@Override
67+
protected void engineInit(KeyStore keyStore, char[] chars)
68+
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
69+
this.delegate.init(keyStore, chars);
70+
}
71+
72+
@Override
73+
protected void engineInit(ManagerFactoryParameters managerFactoryParameters)
74+
throws InvalidAlgorithmParameterException {
75+
throw new InvalidAlgorithmParameterException("Unsupported ManagerFactoryParameters");
76+
}
77+
78+
@Override
79+
protected KeyManager[] engineGetKeyManagers() {
80+
return Arrays.stream(this.delegate.getKeyManagers())
81+
.filter(X509ExtendedKeyManager.class::isInstance)
82+
.map(X509ExtendedKeyManager.class::cast)
83+
.map(this::wrap)
84+
.toArray(KeyManager[]::new);
85+
}
86+
87+
private AliasKeyManagerFactory.AliasX509ExtendedKeyManager wrap(X509ExtendedKeyManager keyManager) {
88+
return new AliasX509ExtendedKeyManager(keyManager, this.alias);
89+
}
90+
91+
}
92+
93+
/**
94+
* {@link X509ExtendedKeyManager} that allows a configurable key alias to be used.
95+
*/
96+
static final class AliasX509ExtendedKeyManager extends X509ExtendedKeyManager {
97+
98+
private final X509ExtendedKeyManager delegate;
99+
100+
private final String alias;
101+
102+
private AliasX509ExtendedKeyManager(X509ExtendedKeyManager keyManager, String alias) {
103+
this.delegate = keyManager;
104+
this.alias = alias;
105+
}
106+
107+
@Override
108+
public String chooseEngineClientAlias(String[] strings, Principal[] principals, SSLEngine sslEngine) {
109+
return this.delegate.chooseEngineClientAlias(strings, principals, sslEngine);
110+
}
111+
112+
@Override
113+
public String chooseEngineServerAlias(String s, Principal[] principals, SSLEngine sslEngine) {
114+
return this.alias;
115+
}
116+
117+
@Override
118+
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
119+
return this.delegate.chooseClientAlias(keyType, issuers, socket);
120+
}
121+
122+
@Override
123+
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
124+
return this.delegate.chooseServerAlias(keyType, issuers, socket);
125+
}
126+
127+
@Override
128+
public X509Certificate[] getCertificateChain(String alias) {
129+
return this.delegate.getCertificateChain(alias);
130+
}
131+
132+
@Override
133+
public String[] getClientAliases(String keyType, Principal[] issuers) {
134+
return this.delegate.getClientAliases(keyType, issuers);
135+
}
136+
137+
@Override
138+
public PrivateKey getPrivateKey(String alias) {
139+
return this.delegate.getPrivateKey(alias);
140+
}
141+
142+
@Override
143+
public String[] getServerAliases(String keyType, Principal[] issuers) {
144+
return this.delegate.getServerAliases(keyType, issuers);
145+
}
146+
147+
}
148+
149+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.ssl;
18+
19+
import java.util.Map;
20+
import java.util.concurrent.ConcurrentHashMap;
21+
22+
import org.springframework.util.Assert;
23+
24+
/**
25+
* Default {@link SslBundleRegistry} implementation.
26+
*
27+
* @author Scott Frederick
28+
* @since 3.1.0
29+
*/
30+
public class DefaultSslBundleRegistry implements SslBundleRegistry, SslBundles {
31+
32+
private final Map<String, SslBundle> bundles = new ConcurrentHashMap<>();
33+
34+
public DefaultSslBundleRegistry() {
35+
}
36+
37+
public DefaultSslBundleRegistry(String name, SslBundle bundle) {
38+
registerBundle(name, bundle);
39+
}
40+
41+
@Override
42+
public void registerBundle(String name, SslBundle bundle) {
43+
Assert.notNull(name, "Name must not be null");
44+
Assert.notNull(bundle, "Bundle must not be null");
45+
SslBundle previous = this.bundles.putIfAbsent(name, bundle);
46+
Assert.state(previous == null, () -> "Cannot replace existing SSL bundle '%s'".formatted(name));
47+
}
48+
49+
@Override
50+
public SslBundle getBundle(String name) {
51+
Assert.notNull(name, "Name must not be null");
52+
SslBundle bundle = this.bundles.get(name);
53+
if (bundle == null) {
54+
throw new NoSuchSslBundleException(name, "SSL bundle name '%s' cannot be found".formatted(name));
55+
}
56+
return bundle;
57+
}
58+
59+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.ssl;
18+
19+
import java.security.KeyStore;
20+
import java.security.NoSuchAlgorithmException;
21+
22+
import javax.net.ssl.KeyManagerFactory;
23+
import javax.net.ssl.TrustManagerFactory;
24+
25+
/**
26+
* Default implementation of {@link SslManagerBundle}.
27+
*
28+
* @author Scott Frederick
29+
* @see SslManagerBundle#from(SslStoreBundle, SslBundleKey)
30+
*/
31+
class DefaultSslManagerBundle implements SslManagerBundle {
32+
33+
private final SslStoreBundle storeBundle;
34+
35+
private final SslBundleKey key;
36+
37+
DefaultSslManagerBundle(SslStoreBundle storeBundle, SslBundleKey key) {
38+
this.storeBundle = (storeBundle != null) ? storeBundle : SslStoreBundle.NONE;
39+
this.key = (key != null) ? key : SslBundleKey.NONE;
40+
}
41+
42+
@Override
43+
public KeyManagerFactory getKeyManagerFactory() {
44+
try {
45+
KeyStore store = this.storeBundle.getKeyStore();
46+
this.key.assertContainsAlias(store);
47+
String alias = this.key.getAlias();
48+
String algorithm = KeyManagerFactory.getDefaultAlgorithm();
49+
KeyManagerFactory factory = getKeyManagerFactoryInstance(algorithm);
50+
factory = (alias != null) ? new AliasKeyManagerFactory(factory, alias, algorithm) : factory;
51+
String password = this.key.getPassword();
52+
password = (password != null) ? password : this.storeBundle.getKeyStorePassword();
53+
factory.init(store, (password != null) ? password.toCharArray() : null);
54+
return factory;
55+
}
56+
catch (RuntimeException ex) {
57+
throw ex;
58+
}
59+
catch (Exception ex) {
60+
throw new IllegalStateException("Could not load key manager factory: " + ex.getMessage(), ex);
61+
}
62+
}
63+
64+
@Override
65+
public TrustManagerFactory getTrustManagerFactory() {
66+
try {
67+
KeyStore store = this.storeBundle.getTrustStore();
68+
String algorithm = TrustManagerFactory.getDefaultAlgorithm();
69+
TrustManagerFactory factory = getTrustManagerFactoryInstance(algorithm);
70+
factory.init(store);
71+
return factory;
72+
}
73+
catch (Exception ex) {
74+
throw new IllegalStateException("Could not load trust manager factory: " + ex.getMessage(), ex);
75+
}
76+
}
77+
78+
protected KeyManagerFactory getKeyManagerFactoryInstance(String algorithm) throws NoSuchAlgorithmException {
79+
return KeyManagerFactory.getInstance(algorithm);
80+
}
81+
82+
protected TrustManagerFactory getTrustManagerFactoryInstance(String algorithm) throws NoSuchAlgorithmException {
83+
return TrustManagerFactory.getInstance(algorithm);
84+
}
85+
86+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.ssl;
18+
19+
/**
20+
* Exception indicating that an {@link SslBundle} was referenced with a name that does not
21+
* match any registered bundle.
22+
*
23+
* @author Scott Frederick
24+
* @since 3.1.0
25+
*/
26+
public class NoSuchSslBundleException extends RuntimeException {
27+
28+
private final String bundleName;
29+
30+
/**
31+
* Create a new {@code SslBundleNotFoundException} instance.
32+
* @param bundleName the name of the bundle that could not be found
33+
* @param message the exception message
34+
*/
35+
public NoSuchSslBundleException(String bundleName, String message) {
36+
this(bundleName, message, null);
37+
}
38+
39+
/**
40+
* Create a new {@code SslBundleNotFoundException} instance.
41+
* @param bundleName the name of the bundle that could not be found
42+
* @param message the exception message
43+
* @param cause the exception cause
44+
*/
45+
public NoSuchSslBundleException(String bundleName, String message, Throwable cause) {
46+
super(message, cause);
47+
this.bundleName = bundleName;
48+
}
49+
50+
/**
51+
* Return the name of the bundle that was not found.
52+
* @return the bundle name
53+
*/
54+
public String getBundleName() {
55+
return this.bundleName;
56+
}
57+
58+
}

0 commit comments

Comments
 (0)