Skip to content

Commit eda7b7c

Browse files
cbo-indeedwilkinsona
authored andcommitted
Honour SSL key alias when using Netty
See gh-19197
1 parent c6d4ff6 commit eda7b7c

File tree

2 files changed

+179
-2
lines changed

2 files changed

+179
-2
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/SslServerCustomizer.java

Lines changed: 135 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,27 @@
1616

1717
package org.springframework.boot.web.embedded.netty;
1818

19+
import java.net.Socket;
1920
import java.net.URL;
21+
import java.security.InvalidAlgorithmParameterException;
2022
import java.security.KeyStore;
23+
import java.security.KeyStoreException;
24+
import java.security.NoSuchAlgorithmException;
25+
import java.security.Principal;
26+
import java.security.PrivateKey;
27+
import java.security.Provider;
28+
import java.security.UnrecoverableKeyException;
29+
import java.security.cert.X509Certificate;
2130
import java.util.Arrays;
31+
import java.util.stream.Collectors;
2232

33+
import javax.net.ssl.KeyManager;
2334
import javax.net.ssl.KeyManagerFactory;
35+
import javax.net.ssl.KeyManagerFactorySpi;
36+
import javax.net.ssl.ManagerFactoryParameters;
37+
import javax.net.ssl.SSLEngine;
2438
import javax.net.ssl.TrustManagerFactory;
39+
import javax.net.ssl.X509ExtendedKeyManager;
2540

2641
import io.netty.handler.ssl.ClientAuth;
2742
import io.netty.handler.ssl.SslContextBuilder;
@@ -92,8 +107,10 @@ else if (this.ssl.getClientAuth() == Ssl.ClientAuth.WANT) {
92107
protected KeyManagerFactory getKeyManagerFactory(Ssl ssl, SslStoreProvider sslStoreProvider) {
93108
try {
94109
KeyStore keyStore = getKeyStore(ssl, sslStoreProvider);
95-
KeyManagerFactory keyManagerFactory = KeyManagerFactory
96-
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
110+
KeyManagerFactory keyManagerFactory = (ssl.getKeyAlias() == null)
111+
? KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
112+
: ConfigurableAliasKeyManagerFactory.instance(ssl.getKeyAlias(),
113+
KeyManagerFactory.getDefaultAlgorithm());
97114
char[] keyPassword = (ssl.getKeyPassword() != null) ? ssl.getKeyPassword().toCharArray() : null;
98115
if (keyPassword == null && ssl.getKeyStorePassword() != null) {
99116
keyPassword = ssl.getKeyStorePassword().toCharArray();
@@ -161,4 +178,120 @@ private KeyStore loadStore(String type, String provider, String resource, String
161178

162179
}
163180

181+
/**
182+
* A {@link KeyManagerFactory} that allows a configurable key alias to be used. Due to
183+
* the fact that the actual calls to retrieve the key by alias are done at request
184+
* time the approach is to wrap the actual key managers with a
185+
* {@link ConfigurableAliasKeyManager}. The actual SPI has to be wrapped as well due
186+
* to the fact that {@link KeyManagerFactory#getKeyManagers()} is final.
187+
*/
188+
private static class ConfigurableAliasKeyManagerFactory extends KeyManagerFactory {
189+
190+
static final ConfigurableAliasKeyManagerFactory instance(String alias, String algorithm)
191+
throws NoSuchAlgorithmException {
192+
KeyManagerFactory originalFactory = KeyManagerFactory.getInstance(algorithm);
193+
ConfigurableAliasKeyManagerFactorySpi spi = new ConfigurableAliasKeyManagerFactorySpi(originalFactory,
194+
alias);
195+
return new ConfigurableAliasKeyManagerFactory(spi, originalFactory.getProvider(), algorithm);
196+
}
197+
198+
ConfigurableAliasKeyManagerFactory(ConfigurableAliasKeyManagerFactorySpi spi, Provider provider,
199+
String algorithm) {
200+
super(spi, provider, algorithm);
201+
}
202+
203+
}
204+
205+
private static class ConfigurableAliasKeyManagerFactorySpi extends KeyManagerFactorySpi {
206+
207+
private KeyManagerFactory originalFactory;
208+
209+
private String alias;
210+
211+
ConfigurableAliasKeyManagerFactorySpi(KeyManagerFactory originalFactory, String alias) {
212+
this.originalFactory = originalFactory;
213+
this.alias = alias;
214+
}
215+
216+
@Override
217+
protected void engineInit(KeyStore keyStore, char[] chars)
218+
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
219+
this.originalFactory.init(keyStore, chars);
220+
}
221+
222+
@Override
223+
protected void engineInit(ManagerFactoryParameters managerFactoryParameters)
224+
throws InvalidAlgorithmParameterException {
225+
throw new InvalidAlgorithmParameterException("Unsupported ManagerFactoryParameters");
226+
}
227+
228+
@Override
229+
protected KeyManager[] engineGetKeyManagers() {
230+
return Arrays.stream(this.originalFactory.getKeyManagers()).filter(X509ExtendedKeyManager.class::isInstance)
231+
.map(X509ExtendedKeyManager.class::cast).map(this::wrapKeyManager).collect(Collectors.toList())
232+
.toArray(new KeyManager[0]);
233+
}
234+
235+
private ConfigurableAliasKeyManager wrapKeyManager(X509ExtendedKeyManager km) {
236+
return new ConfigurableAliasKeyManager(km, this.alias);
237+
}
238+
239+
}
240+
241+
private static class ConfigurableAliasKeyManager extends X509ExtendedKeyManager {
242+
243+
private final X509ExtendedKeyManager keyManager;
244+
245+
private final String alias;
246+
247+
ConfigurableAliasKeyManager(X509ExtendedKeyManager keyManager, String alias) {
248+
this.keyManager = keyManager;
249+
this.alias = alias;
250+
}
251+
252+
@Override
253+
public String chooseEngineClientAlias(String[] strings, Principal[] principals, SSLEngine sslEngine) {
254+
return this.keyManager.chooseEngineClientAlias(strings, principals, sslEngine);
255+
}
256+
257+
@Override
258+
public String chooseEngineServerAlias(String s, Principal[] principals, SSLEngine sslEngine) {
259+
if (this.alias == null) {
260+
return this.keyManager.chooseEngineServerAlias(s, principals, sslEngine);
261+
}
262+
return this.alias;
263+
}
264+
265+
@Override
266+
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
267+
return this.keyManager.chooseClientAlias(keyType, issuers, socket);
268+
}
269+
270+
@Override
271+
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
272+
return this.keyManager.chooseServerAlias(keyType, issuers, socket);
273+
}
274+
275+
@Override
276+
public X509Certificate[] getCertificateChain(String alias) {
277+
return this.keyManager.getCertificateChain(alias);
278+
}
279+
280+
@Override
281+
public String[] getClientAliases(String keyType, Principal[] issuers) {
282+
return this.keyManager.getClientAliases(keyType, issuers);
283+
}
284+
285+
@Override
286+
public PrivateKey getPrivateKey(String alias) {
287+
return this.keyManager.getPrivateKey(alias);
288+
}
289+
290+
@Override
291+
public String[] getServerAliases(String keyType, Principal[] issuers) {
292+
return this.keyManager.getServerAliases(keyType, issuers);
293+
}
294+
295+
}
296+
164297
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,25 @@
1616

1717
package org.springframework.boot.web.embedded.netty;
1818

19+
import java.time.Duration;
1920
import java.util.Arrays;
2021

22+
import javax.net.ssl.SSLHandshakeException;
23+
2124
import org.junit.Test;
2225
import org.mockito.InOrder;
26+
import reactor.core.publisher.Mono;
2327
import reactor.netty.http.server.HttpServer;
28+
import reactor.test.StepVerifier;
2429

2530
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory;
2631
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactoryTests;
2732
import org.springframework.boot.web.server.PortInUseException;
33+
import org.springframework.boot.web.server.Ssl;
34+
import org.springframework.http.MediaType;
35+
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
36+
import org.springframework.web.reactive.function.BodyInserters;
37+
import org.springframework.web.reactive.function.client.WebClient;
2838

2939
import static org.assertj.core.api.Assertions.assertThat;
3040
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -83,4 +93,38 @@ public void useForwardedHeaders() {
8393
assertForwardHeaderIsUsed(factory);
8494
}
8595

96+
@Test
97+
public void testSslWithValidAlias() {
98+
Mono<String> result = testSslWithAlias("test-alias");
99+
StepVerifier.setDefaultTimeout(Duration.ofSeconds(30));
100+
StepVerifier.create(result).expectNext("Hello World").verifyComplete();
101+
}
102+
103+
@Test
104+
public void testSslWithInvalidAlias() {
105+
Mono<String> result = testSslWithAlias("test-alias-bad");
106+
StepVerifier.setDefaultTimeout(Duration.ofSeconds(30));
107+
StepVerifier.create(result).expectErrorMatches((throwable) -> throwable instanceof SSLHandshakeException
108+
&& throwable.getMessage().contains("HANDSHAKE_FAILURE")).verify();
109+
}
110+
111+
protected Mono<String> testSslWithAlias(String alias) {
112+
String keyStore = "classpath:test.jks";
113+
String keyPassword = "password";
114+
NettyReactiveWebServerFactory factory = getFactory();
115+
Ssl ssl = new Ssl();
116+
ssl.setKeyStore(keyStore);
117+
ssl.setKeyPassword(keyPassword);
118+
ssl.setKeyAlias(alias);
119+
factory.setSsl(ssl);
120+
this.webServer = factory.getWebServer(new EchoHandler());
121+
this.webServer.start();
122+
ReactorClientHttpConnector connector = buildTrustAllSslConnector();
123+
WebClient client = WebClient.builder().baseUrl("https://localhost:" + this.webServer.getPort())
124+
.clientConnector(connector).build();
125+
return client.post().uri("/test").contentType(MediaType.TEXT_PLAIN)
126+
.body(BodyInserters.fromObject("Hello World")).exchange()
127+
.flatMap((response) -> response.bodyToMono(String.class));
128+
}
129+
86130
}

0 commit comments

Comments
 (0)