Skip to content

Commit 3e2b466

Browse files
committed
Merge pull request #19197 from bono007
* gh-19197: Polish "Honour SSL key alias when using Netty" Honour SSL key alias when using Netty Closes gh-19197
2 parents c6d4ff6 + effdc8f commit 3e2b466

File tree

2 files changed

+181
-2
lines changed

2 files changed

+181
-2
lines changed

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

Lines changed: 136 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;
@@ -40,6 +55,7 @@
4055
*
4156
* @author Brian Clozel
4257
* @author Raheela Aslam
58+
* @author Chris Bono
4359
* @since 2.0.0
4460
*/
4561
public class SslServerCustomizer implements NettyServerCustomizer {
@@ -92,8 +108,10 @@ else if (this.ssl.getClientAuth() == Ssl.ClientAuth.WANT) {
92108
protected KeyManagerFactory getKeyManagerFactory(Ssl ssl, SslStoreProvider sslStoreProvider) {
93109
try {
94110
KeyStore keyStore = getKeyStore(ssl, sslStoreProvider);
95-
KeyManagerFactory keyManagerFactory = KeyManagerFactory
96-
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
111+
KeyManagerFactory keyManagerFactory = (ssl.getKeyAlias() == null)
112+
? KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
113+
: ConfigurableAliasKeyManagerFactory.instance(ssl.getKeyAlias(),
114+
KeyManagerFactory.getDefaultAlgorithm());
97115
char[] keyPassword = (ssl.getKeyPassword() != null) ? ssl.getKeyPassword().toCharArray() : null;
98116
if (keyPassword == null && ssl.getKeyStorePassword() != null) {
99117
keyPassword = ssl.getKeyStorePassword().toCharArray();
@@ -161,4 +179,120 @@ private KeyStore loadStore(String type, String provider, String resource, String
161179

162180
}
163181

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

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

Lines changed: 45 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;
@@ -37,6 +47,7 @@
3747
* Tests for {@link NettyReactiveWebServerFactory}.
3848
*
3949
* @author Brian Clozel
50+
* @author Chris Bono
4051
*/
4152
public class NettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactoryTests {
4253

@@ -83,4 +94,38 @@ public void useForwardedHeaders() {
8394
assertForwardHeaderIsUsed(factory);
8495
}
8596

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

0 commit comments

Comments
 (0)