From 9f0108496c9d3594e4c07bd75a4bc489f8c8e9d9 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Wed, 1 Mar 2023 16:20:23 -0600 Subject: [PATCH] Consolidate web server SSL configuration --- .../netty/NettyRSocketServerFactory.java | 6 +- .../embedded/jetty/SslServerCustomizer.java | 56 +------ .../embedded/netty/SslServerCustomizer.java | 74 +-------- .../tomcat/SslConnectorCustomizer.java | 58 +------ .../undertow/SslBuilderCustomizer.java | 75 ++------- .../AbstractConfigurableWebServerFactory.java | 4 +- .../server/JavaKeyStoreSslStoreProvider.java | 97 ++++++++++++ .../web/server/SslStoreProviderFactory.java | 41 +++++ .../jetty/SslServerCustomizerTests.java | 17 +-- .../netty/SslServerCustomizerTests.java | 22 ++- .../tomcat/SslConnectorCustomizerTests.java | 10 +- .../undertow/SslBuilderCustomizerTests.java | 25 ++- ...AbstractReactiveWebServerFactoryTests.java | 5 +- .../JavaKeyStoreSslStoreProviderTests.java | 142 ++++++++++++++++++ 14 files changed, 351 insertions(+), 281 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/JavaKeyStoreSslStoreProvider.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslStoreProviderFactory.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/JavaKeyStoreSslStoreProviderTests.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactory.java index 89f44959ed..45f6310b21 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -39,9 +39,9 @@ import org.springframework.boot.rsocket.server.ConfigurableRSocketServerFactory; import org.springframework.boot.rsocket.server.RSocketServer; import org.springframework.boot.rsocket.server.RSocketServerCustomizer; import org.springframework.boot.rsocket.server.RSocketServerFactory; -import org.springframework.boot.web.server.CertificateFileSslStoreProvider; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.SslStoreProvider; +import org.springframework.boot.web.server.SslStoreProviderFactory; import org.springframework.http.client.reactive.ReactorResourceFactory; import org.springframework.util.Assert; import org.springframework.util.unit.DataSize; @@ -202,7 +202,7 @@ public class NettyRSocketServerFactory implements RSocketServerFactory, Configur if (this.sslStoreProvider != null) { return this.sslStoreProvider; } - return CertificateFileSslStoreProvider.from(this.ssl); + return SslStoreProviderFactory.from(this.ssl); } private InetSocketAddress getListenAddress() { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java index 9ce9b4012b..11b4e68bbe 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java @@ -16,9 +16,7 @@ package org.springframework.boot.web.embedded.jetty; -import java.io.IOException; import java.net.InetSocketAddress; -import java.net.URL; import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; import org.eclipse.jetty.http.HttpVersion; @@ -32,19 +30,15 @@ import org.eclipse.jetty.server.SecureRequestCustomizer; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; -import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.springframework.boot.web.server.Http2; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.SslConfigurationValidator; import org.springframework.boot.web.server.SslStoreProvider; -import org.springframework.boot.web.server.WebServerException; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; -import org.springframework.util.ResourceUtils; -import org.springframework.util.StringUtils; /** * {@link JettyServerCustomizer} that configures SSL on the given Jetty server instance. @@ -53,6 +47,7 @@ import org.springframework.util.StringUtils; * @author Olivier Lamy * @author Chris Bono * @author Cyril Dangerville + * @author Scott Frederick */ class SslServerCustomizer implements JettyServerCustomizer { @@ -193,13 +188,9 @@ class SslServerCustomizer implements JettyServerCustomizer { factory.setTrustStore(sslStoreProvider.getTrustStore()); } catch (Exception ex) { - throw new IllegalStateException("Unable to set SSL store", ex); + throw new IllegalStateException("Unable to set SSL store: " + ex.getMessage(), ex); } } - else { - configureSslKeyStore(factory, ssl); - configureSslTrustStore(factory, ssl); - } } private void configureSslClientAuth(SslContextFactory.Server factory, Ssl ssl) { @@ -221,49 +212,6 @@ class SslServerCustomizer implements JettyServerCustomizer { } } - private void configureSslKeyStore(SslContextFactory.Server factory, Ssl ssl) { - String keystoreType = (ssl.getKeyStoreType() != null) ? ssl.getKeyStoreType() : "JKS"; - String keystoreLocation = ssl.getKeyStore(); - if (keystoreType.equalsIgnoreCase("PKCS11")) { - Assert.state(!StringUtils.hasText(keystoreLocation), - () -> "Keystore location '" + keystoreLocation + "' must be empty or null for PKCS11 key stores"); - } - else { - try { - URL url = ResourceUtils.getURL(keystoreLocation); - factory.setKeyStoreResource(Resource.newResource(url)); - } - catch (Exception ex) { - throw new WebServerException("Could not load key store '" + keystoreLocation + "'", ex); - } - } - factory.setKeyStoreType(keystoreType); - if (ssl.getKeyStoreProvider() != null) { - factory.setKeyStoreProvider(this.ssl.getKeyStoreProvider()); - } - } - - private void configureSslTrustStore(SslContextFactory.Server factory, Ssl ssl) { - if (ssl.getTrustStorePassword() != null) { - factory.setTrustStorePassword(ssl.getTrustStorePassword()); - } - if (ssl.getTrustStore() != null) { - try { - URL url = ResourceUtils.getURL(ssl.getTrustStore()); - factory.setTrustStoreResource(Resource.newResource(url)); - } - catch (IOException ex) { - throw new WebServerException("Could not find trust store '" + ssl.getTrustStore() + "'", ex); - } - } - if (ssl.getTrustStoreType() != null) { - factory.setTrustStoreType(ssl.getTrustStoreType()); - } - if (ssl.getTrustStoreProvider() != null) { - factory.setTrustStoreProvider(ssl.getTrustStoreProvider()); - } - } - /** * A {@link ServerConnector} that validates the ssl key alias on server startup. */ diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/SslServerCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/SslServerCustomizer.java index 08c220a0bc..09b42b6fa1 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/SslServerCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/SslServerCustomizer.java @@ -16,9 +16,7 @@ package org.springframework.boot.web.embedded.netty; -import java.io.InputStream; import java.net.Socket; -import java.net.URL; import java.security.InvalidAlgorithmParameterException; import java.security.KeyStore; import java.security.KeyStoreException; @@ -47,10 +45,6 @@ import org.springframework.boot.web.server.Http2; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.SslConfigurationValidator; import org.springframework.boot.web.server.SslStoreProvider; -import org.springframework.boot.web.server.WebServerException; -import org.springframework.util.Assert; -import org.springframework.util.ResourceUtils; -import org.springframework.util.StringUtils; /** * {@link NettyServerCustomizer} that configures SSL for the given Reactor Netty server @@ -60,6 +54,7 @@ import org.springframework.util.StringUtils; * @author Raheela Aslam * @author Chris Bono * @author Cyril Dangerville + * @author Scott Frederick * @since 2.0.0 * @deprecated this class is meant for Spring Boot internal use only. */ @@ -93,7 +88,7 @@ public class SslServerCustomizer implements NettyServerCustomizer { sslContextSpec = Http11SslContextSpec.forServer(getKeyManagerFactory(this.ssl, this.sslStoreProvider)); } sslContextSpec.configure((builder) -> { - builder.trustManager(getTrustManagerFactory(this.ssl, this.sslStoreProvider)); + builder.trustManager(getTrustManagerFactory(this.sslStoreProvider)); if (this.ssl.getEnabledProtocols() != null) { builder.protocols(this.ssl.getEnabledProtocols()); } @@ -112,13 +107,13 @@ public class SslServerCustomizer implements NettyServerCustomizer { KeyManagerFactory getKeyManagerFactory(Ssl ssl, SslStoreProvider sslStoreProvider) { try { - KeyStore keyStore = getKeyStore(ssl, sslStoreProvider); + KeyStore keyStore = sslStoreProvider.getKeyStore(); SslConfigurationValidator.validateKeyAlias(keyStore, ssl.getKeyAlias()); KeyManagerFactory keyManagerFactory = (ssl.getKeyAlias() == null) ? KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) : new ConfigurableAliasKeyManagerFactory(ssl.getKeyAlias(), KeyManagerFactory.getDefaultAlgorithm()); - String keyPassword = (sslStoreProvider != null) ? sslStoreProvider.getKeyPassword() : null; + String keyPassword = sslStoreProvider.getKeyPassword(); if (keyPassword == null) { keyPassword = (ssl.getKeyPassword() != null) ? ssl.getKeyPassword() : ssl.getKeyStorePassword(); } @@ -126,74 +121,21 @@ public class SslServerCustomizer implements NettyServerCustomizer { return keyManagerFactory; } catch (Exception ex) { - throw new IllegalStateException(ex); + throw new IllegalStateException("Could not load key manager factory: " + ex.getMessage(), ex); } } - private KeyStore getKeyStore(Ssl ssl, SslStoreProvider sslStoreProvider) throws Exception { - if (sslStoreProvider != null) { - return sslStoreProvider.getKeyStore(); - } - return loadKeyStore(ssl.getKeyStoreType(), ssl.getKeyStoreProvider(), ssl.getKeyStore(), - ssl.getKeyStorePassword()); - } - - TrustManagerFactory getTrustManagerFactory(Ssl ssl, SslStoreProvider sslStoreProvider) { + TrustManagerFactory getTrustManagerFactory(SslStoreProvider sslStoreProvider) { try { - KeyStore store = getTrustStore(ssl, sslStoreProvider); + KeyStore store = sslStoreProvider.getTrustStore(); TrustManagerFactory trustManagerFactory = TrustManagerFactory .getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(store); return trustManagerFactory; } catch (Exception ex) { - throw new IllegalStateException(ex); - } - } - - private KeyStore getTrustStore(Ssl ssl, SslStoreProvider sslStoreProvider) throws Exception { - if (sslStoreProvider != null) { - return sslStoreProvider.getTrustStore(); - } - return loadTrustStore(ssl.getTrustStoreType(), ssl.getTrustStoreProvider(), ssl.getTrustStore(), - ssl.getTrustStorePassword()); - } - - private KeyStore loadKeyStore(String type, String provider, String resource, String password) throws Exception { - - return loadStore(type, provider, resource, password); - } - - private KeyStore loadTrustStore(String type, String provider, String resource, String password) throws Exception { - if (resource == null) { - return null; - } - return loadStore(type, provider, resource, password); - } - - private KeyStore loadStore(String keystoreType, String provider, String keystoreLocation, String password) - throws Exception { - keystoreType = (keystoreType != null) ? keystoreType : "JKS"; - char[] passwordChars = (password != null) ? password.toCharArray() : null; - KeyStore store = (provider != null) ? KeyStore.getInstance(keystoreType, provider) - : KeyStore.getInstance(keystoreType); - if (keystoreType.equalsIgnoreCase("PKCS11")) { - Assert.state(!StringUtils.hasText(keystoreLocation), - () -> "Keystore location '" + keystoreLocation + "' must be empty or null for PKCS11 key stores"); - store.load(null, passwordChars); - } - else { - try { - URL url = ResourceUtils.getURL(keystoreLocation); - try (InputStream stream = url.openStream()) { - store.load(stream, passwordChars); - } - } - catch (Exception ex) { - throw new WebServerException("Could not load key store '" + keystoreLocation + "'", ex); - } + throw new IllegalStateException("Could not load trust manager factory: " + ex.getMessage(), ex); } - return store; } /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java index 673d7c6911..e484915f77 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -16,8 +16,6 @@ package org.springframework.boot.web.embedded.tomcat; -import java.io.FileNotFoundException; - import org.apache.catalina.connector.Connector; import org.apache.coyote.ProtocolHandler; import org.apache.coyote.http11.AbstractHttp11JsseProtocol; @@ -28,9 +26,8 @@ import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.SslStoreProvider; -import org.springframework.boot.web.server.WebServerException; +import org.springframework.boot.web.server.SslStoreProviderFactory; import org.springframework.util.Assert; -import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; /** @@ -47,6 +44,10 @@ class SslConnectorCustomizer implements TomcatConnectorCustomizer { private final SslStoreProvider sslStoreProvider; + SslConnectorCustomizer(Ssl ssl) { + this(ssl, SslStoreProviderFactory.from(ssl)); + } + SslConnectorCustomizer(Ssl ssl, SslStoreProvider sslStoreProvider) { Assert.notNull(ssl, "Ssl configuration should not be null"); this.ssl = ssl; @@ -99,10 +100,6 @@ class SslConnectorCustomizer implements TomcatConnectorCustomizer { certificate.setCertificateKeyPassword(keyPassword); } } - else { - configureSslKeyStore(certificate, ssl); - configureSslTrustStore(sslHostConfig, ssl); - } } private void configureEnabledProtocols(AbstractHttp11JsseProtocol protocol, Ssl ssl) { @@ -135,48 +132,7 @@ class SslConnectorCustomizer implements TomcatConnectorCustomizer { } } catch (Exception ex) { - throw new WebServerException("Could not load store: " + ex.getMessage(), ex); - } - } - - private void configureSslKeyStore(SSLHostConfigCertificate certificate, Ssl ssl) { - String keystoreType = (ssl.getKeyStoreType() != null) ? ssl.getKeyStoreType() : "JKS"; - String keystoreLocation = ssl.getKeyStore(); - if (keystoreType.equalsIgnoreCase("PKCS11")) { - Assert.state(!StringUtils.hasText(keystoreLocation), - () -> "Keystore location '" + keystoreLocation + "' must be empty or null for PKCS11 key stores"); - } - else { - try { - certificate.setCertificateKeystoreFile(ResourceUtils.getURL(keystoreLocation).toString()); - } - catch (Exception ex) { - throw new WebServerException("Could not load key store '" + keystoreLocation + "'", ex); - } - } - certificate.setCertificateKeystoreType(keystoreType); - if (ssl.getKeyStoreProvider() != null) { - certificate.setCertificateKeystoreProvider(ssl.getKeyStoreProvider()); - } - } - - private void configureSslTrustStore(SSLHostConfig sslHostConfig, Ssl ssl) { - if (ssl.getTrustStore() != null) { - try { - sslHostConfig.setTruststoreFile(ResourceUtils.getURL(ssl.getTrustStore()).toString()); - } - catch (FileNotFoundException ex) { - throw new WebServerException("Could not load trust store: " + ex.getMessage(), ex); - } - } - if (ssl.getTrustStorePassword() != null) { - sslHostConfig.setTruststorePassword(ssl.getTrustStorePassword()); - } - if (ssl.getTrustStoreType() != null) { - sslHostConfig.setTruststoreType(ssl.getTrustStoreType()); - } - if (ssl.getTrustStoreProvider() != null) { - sslHostConfig.setTruststoreProvider(ssl.getTrustStoreProvider()); + throw new IllegalStateException("Could not load store: " + ex.getMessage(), ex); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizer.java index 6060827395..5a88ddda27 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizer.java @@ -16,10 +16,8 @@ package org.springframework.boot.web.embedded.undertow; -import java.io.InputStream; import java.net.InetAddress; import java.net.Socket; -import java.net.URL; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.NoSuchAlgorithmException; @@ -43,8 +41,6 @@ import org.xnio.SslClientAuthMode; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.SslConfigurationValidator; import org.springframework.boot.web.server.SslStoreProvider; -import org.springframework.boot.web.server.WebServerException; -import org.springframework.util.ResourceUtils; /** * {@link UndertowBuilderCustomizer} that configures SSL on the given builder instance. @@ -52,6 +48,7 @@ import org.springframework.util.ResourceUtils; * @author Brian Clozel * @author Raheela Aslam * @author Cyril Dangerville + * @author Scott Frederick */ class SslBuilderCustomizer implements UndertowBuilderCustomizer { @@ -74,8 +71,8 @@ class SslBuilderCustomizer implements UndertowBuilderCustomizer { public void customize(Undertow.Builder builder) { try { SSLContext sslContext = SSLContext.getInstance(this.ssl.getProtocol()); - sslContext.init(getKeyManagers(this.ssl, this.sslStoreProvider), - getTrustManagers(this.ssl, this.sslStoreProvider), null); + sslContext.init(getKeyManagers(this.ssl, this.sslStoreProvider), getTrustManagers(this.sslStoreProvider), + null); builder.addHttpsListener(this.port, getListenAddress(), sslContext); builder.setSocketOption(Options.SSL_CLIENT_AUTH_MODE, getSslClientAuthMode(this.ssl)); if (this.ssl.getEnabledProtocols() != null) { @@ -107,13 +104,13 @@ class SslBuilderCustomizer implements UndertowBuilderCustomizer { return SslClientAuthMode.NOT_REQUESTED; } - private KeyManager[] getKeyManagers(Ssl ssl, SslStoreProvider sslStoreProvider) { + KeyManager[] getKeyManagers(Ssl ssl, SslStoreProvider sslStoreProvider) { try { - KeyStore keyStore = getKeyStore(ssl, sslStoreProvider); + KeyStore keyStore = sslStoreProvider.getKeyStore(); SslConfigurationValidator.validateKeyAlias(keyStore, ssl.getKeyAlias()); KeyManagerFactory keyManagerFactory = KeyManagerFactory .getInstance(KeyManagerFactory.getDefaultAlgorithm()); - String keyPassword = (sslStoreProvider != null) ? sslStoreProvider.getKeyPassword() : null; + String keyPassword = sslStoreProvider.getKeyPassword(); if (keyPassword == null) { keyPassword = (ssl.getKeyPassword() != null) ? ssl.getKeyPassword() : ssl.getKeyStorePassword(); } @@ -124,7 +121,7 @@ class SslBuilderCustomizer implements UndertowBuilderCustomizer { return keyManagerFactory.getKeyManagers(); } catch (Exception ex) { - throw new IllegalStateException(ex); + throw new IllegalStateException("Could not load key managers: " + ex.getMessage(), ex); } } @@ -138,71 +135,19 @@ class SslBuilderCustomizer implements UndertowBuilderCustomizer { return keyManagers; } - private KeyStore getKeyStore(Ssl ssl, SslStoreProvider sslStoreProvider) throws Exception { - if (sslStoreProvider != null) { - return sslStoreProvider.getKeyStore(); - } - return loadKeyStore(ssl.getKeyStoreType(), ssl.getKeyStoreProvider(), ssl.getKeyStore(), - ssl.getKeyStorePassword()); - } - - private TrustManager[] getTrustManagers(Ssl ssl, SslStoreProvider sslStoreProvider) { + TrustManager[] getTrustManagers(SslStoreProvider sslStoreProvider) { try { - KeyStore store = getTrustStore(ssl, sslStoreProvider); + KeyStore store = sslStoreProvider.getTrustStore(); TrustManagerFactory trustManagerFactory = TrustManagerFactory .getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(store); return trustManagerFactory.getTrustManagers(); } catch (Exception ex) { - throw new IllegalStateException(ex); + throw new IllegalStateException("Could not load trust managers: " + ex.getMessage(), ex); } } - private KeyStore getTrustStore(Ssl ssl, SslStoreProvider sslStoreProvider) throws Exception { - if (sslStoreProvider != null) { - return sslStoreProvider.getTrustStore(); - } - return loadTrustStore(ssl.getTrustStoreType(), ssl.getTrustStoreProvider(), ssl.getTrustStore(), - ssl.getTrustStorePassword()); - } - - private KeyStore loadKeyStore(String type, String provider, String resource, String password) throws Exception { - return loadStore(type, provider, resource, password); - } - - private KeyStore loadTrustStore(String type, String provider, String resource, String password) throws Exception { - if (resource == null) { - return null; - } - return loadStore(type, provider, resource, password); - } - - private KeyStore loadStore(String type, String provider, String resource, String password) throws Exception { - type = (type != null) ? type : "JKS"; - KeyStore store = (provider != null) ? KeyStore.getInstance(type, provider) : KeyStore.getInstance(type); - if (type.equalsIgnoreCase("PKCS11")) { - if (resource != null && !resource.isEmpty()) { - throw new IllegalArgumentException("Input keystore location is not valid for keystore type 'PKCS11': '" - + resource + "'. Must be undefined / null."); - } - store.load(null, (password != null) ? password.toCharArray() : null); - } - else { - try { - URL url = ResourceUtils.getURL(resource); - try (InputStream stream = url.openStream()) { - store.load(stream, (password != null) ? password.toCharArray() : null); - } - } - catch (Exception ex) { - throw new WebServerException("Could not load key store '" + resource + "'", ex); - } - } - - return store; - } - /** * {@link X509ExtendedKeyManager} that supports custom alias configuration. */ diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java index 1cde582ced..1e51089603 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -189,7 +189,7 @@ public abstract class AbstractConfigurableWebServerFactory implements Configurab if (this.sslStoreProvider != null) { return this.sslStoreProvider; } - return CertificateFileSslStoreProvider.from(this.ssl); + return SslStoreProviderFactory.from(this.ssl); } /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/JavaKeyStoreSslStoreProvider.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/JavaKeyStoreSslStoreProvider.java new file mode 100644 index 0000000000..c4429bb484 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/JavaKeyStoreSslStoreProvider.java @@ -0,0 +1,97 @@ +/* + * 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.web.server; + +import java.io.InputStream; +import java.net.URL; +import java.security.KeyStore; + +import org.springframework.util.Assert; +import org.springframework.util.ResourceUtils; +import org.springframework.util.StringUtils; + +/** + * An {@link SslStoreProvider} that creates key and trust stores from Java keystore files. + * + * @author Scott Frederick + */ +final class JavaKeyStoreSslStoreProvider implements SslStoreProvider { + + private final Ssl ssl; + + private JavaKeyStoreSslStoreProvider(Ssl ssl) { + this.ssl = ssl; + } + + @Override + public KeyStore getKeyStore() throws Exception { + return createKeyStore(this.ssl.getKeyStoreType(), this.ssl.getKeyStoreProvider(), this.ssl.getKeyStore(), + this.ssl.getKeyStorePassword()); + } + + @Override + public KeyStore getTrustStore() throws Exception { + if (this.ssl.getTrustStore() == null) { + return null; + } + return createKeyStore(this.ssl.getTrustStoreType(), this.ssl.getTrustStoreProvider(), this.ssl.getTrustStore(), + this.ssl.getTrustStorePassword()); + } + + @Override + public String getKeyPassword() { + return this.ssl.getKeyPassword(); + } + + private KeyStore createKeyStore(String type, String provider, String location, String password) throws Exception { + type = (type != null) ? type : "JKS"; + char[] passwordChars = (password != null) ? password.toCharArray() : null; + KeyStore store = (provider != null) ? KeyStore.getInstance(type, provider) : KeyStore.getInstance(type); + if (type.equalsIgnoreCase("PKCS11")) { + Assert.state(!StringUtils.hasText(location), + () -> "KeyStore location is '" + location + "', but must be empty or null for PKCS11 key stores"); + store.load(null, passwordChars); + } + else { + Assert.state(StringUtils.hasText(location), () -> "KeyStore location must not be empty or null"); + try { + URL url = ResourceUtils.getURL(location); + try (InputStream stream = url.openStream()) { + store.load(stream, passwordChars); + } + } + catch (Exception ex) { + throw new IllegalStateException("Could not load key store '" + location + "'", ex); + } + } + return store; + } + + /** + * Create an {@link SslStoreProvider} if the appropriate SSL properties are + * configured. + * @param ssl the SSL properties + * @return an {@code SslStoreProvider} or {@code null} + */ + static SslStoreProvider from(Ssl ssl) { + if (ssl != null && ssl.isEnabled()) { + return new JavaKeyStoreSslStoreProvider(ssl); + } + return null; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslStoreProviderFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslStoreProviderFactory.java new file mode 100644 index 0000000000..d9b9f0d1e2 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslStoreProviderFactory.java @@ -0,0 +1,41 @@ +/* + * 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.web.server; + +/** + * Creates an {@link SslStoreProvider} based on SSL configuration properties. + * + * @author Scott Frederick + * @since 3.1.0 + */ +public final class SslStoreProviderFactory { + + private SslStoreProviderFactory() { + } + + /** + * Create an {@link SslStoreProvider} if the appropriate SSL properties are + * configured. + * @param ssl the SSL properties + * @return an {@code SslStoreProvider} or {@code null} + */ + public static SslStoreProvider from(Ssl ssl) { + SslStoreProvider sslStoreProvider = CertificateFileSslStoreProvider.from(ssl); + return ((sslStoreProvider != null) ? sslStoreProvider : JavaKeyStoreSslStoreProvider.from(ssl)); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizerTests.java index bd00d201fb..6847396a9b 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizerTests.java @@ -35,10 +35,9 @@ import org.springframework.boot.web.embedded.test.MockPkcs11Security; import org.springframework.boot.web.embedded.test.MockPkcs11SecurityProvider; import org.springframework.boot.web.server.Http2; import org.springframework.boot.web.server.Ssl; -import org.springframework.boot.web.server.WebServerException; +import org.springframework.boot.web.server.SslStoreProviderFactory; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatNoException; @@ -47,6 +46,7 @@ import static org.assertj.core.api.Assertions.assertThatNoException; * * @author Andy Wilkinson * @author Cyril Dangerville + * @author Scott Frederick */ @MockPkcs11Security class SslServerCustomizerTests { @@ -92,12 +92,9 @@ class SslServerCustomizerTests { void configureSslWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsException() { Ssl ssl = new Ssl(); SslServerCustomizer customizer = new SslServerCustomizer(null, ssl, null, null); - assertThatExceptionOfType(Exception.class) - .isThrownBy(() -> customizer.configureSsl(new SslContextFactory.Server(), ssl, null)) - .satisfies((ex) -> { - assertThat(ex).isInstanceOf(WebServerException.class); - assertThat(ex).hasMessageContaining("Could not load key store 'null'"); - }); + assertThatIllegalStateException().isThrownBy( + () -> customizer.configureSsl(new SslContextFactory.Server(), ssl, SslStoreProviderFactory.from(ssl))) + .withMessageContaining("KeyStore location must not be empty or null"); } @Test @@ -108,8 +105,8 @@ class SslServerCustomizerTests { ssl.setKeyStore("src/test/resources/test.jks"); ssl.setKeyPassword("password"); SslServerCustomizer customizer = new SslServerCustomizer(null, ssl, null, null); - assertThatIllegalStateException() - .isThrownBy(() -> customizer.configureSsl(new SslContextFactory.Server(), ssl, null)) + assertThatIllegalStateException().isThrownBy( + () -> customizer.configureSsl(new SslContextFactory.Server(), ssl, SslStoreProviderFactory.from(ssl))) .withMessageContaining("must be empty or null for PKCS11 key stores"); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/SslServerCustomizerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/SslServerCustomizerTests.java index 25dda5d4be..0fc9b4942d 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/SslServerCustomizerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/SslServerCustomizerTests.java @@ -23,7 +23,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.web.embedded.test.MockPkcs11Security; import org.springframework.boot.web.embedded.test.MockPkcs11SecurityProvider; import org.springframework.boot.web.server.Ssl; -import org.springframework.boot.web.server.WebServerException; +import org.springframework.boot.web.server.SslStoreProviderFactory; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatNoException; @@ -34,6 +34,7 @@ import static org.assertj.core.api.Assertions.assertThatNoException; * @author Andy Wilkinson * @author Raheela Aslam * @author Cyril Dangerville + * @author Scott Frederick */ @SuppressWarnings("deprecation") @MockPkcs11Security @@ -46,7 +47,8 @@ class SslServerCustomizerTests { ssl.setKeyStore("src/test/resources/test.jks"); ssl.setKeyStoreProvider("com.example.KeyStoreProvider"); SslServerCustomizer customizer = new SslServerCustomizer(ssl, null, null); - assertThatIllegalStateException().isThrownBy(() -> customizer.getKeyManagerFactory(ssl, null)) + assertThatIllegalStateException() + .isThrownBy(() -> customizer.getKeyManagerFactory(ssl, SslStoreProviderFactory.from(ssl))) .withCauseInstanceOf(NoSuchProviderException.class) .withMessageContaining("com.example.KeyStoreProvider"); } @@ -58,7 +60,8 @@ class SslServerCustomizerTests { ssl.setTrustStore("src/test/resources/test.jks"); ssl.setTrustStoreProvider("com.example.TrustStoreProvider"); SslServerCustomizer customizer = new SslServerCustomizer(ssl, null, null); - assertThatIllegalStateException().isThrownBy(() -> customizer.getTrustManagerFactory(ssl, null)) + assertThatIllegalStateException() + .isThrownBy(() -> customizer.getTrustManagerFactory(SslStoreProviderFactory.from(ssl))) .withCauseInstanceOf(NoSuchProviderException.class) .withMessageContaining("com.example.TrustStoreProvider"); } @@ -67,9 +70,10 @@ class SslServerCustomizerTests { void getKeyManagerFactoryWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsException() { Ssl ssl = new Ssl(); SslServerCustomizer customizer = new SslServerCustomizer(ssl, null, null); - assertThatIllegalStateException().isThrownBy(() -> customizer.getKeyManagerFactory(ssl, null)) - .withCauseInstanceOf(WebServerException.class) - .withMessageContaining("Could not load key store 'null'"); + assertThatIllegalStateException() + .isThrownBy(() -> customizer.getKeyManagerFactory(ssl, SslStoreProviderFactory.from(ssl))) + .withCauseInstanceOf(IllegalStateException.class) + .withMessageContaining("KeyStore location must not be empty or null"); } @Test @@ -80,7 +84,8 @@ class SslServerCustomizerTests { ssl.setKeyStore("src/test/resources/test.jks"); ssl.setKeyPassword("password"); SslServerCustomizer customizer = new SslServerCustomizer(ssl, null, null); - assertThatIllegalStateException().isThrownBy(() -> customizer.getKeyManagerFactory(ssl, null)) + assertThatIllegalStateException() + .isThrownBy(() -> customizer.getKeyManagerFactory(ssl, SslStoreProviderFactory.from(ssl))) .withCauseInstanceOf(IllegalStateException.class) .withMessageContaining("must be empty or null for PKCS11 key stores"); } @@ -92,7 +97,8 @@ class SslServerCustomizerTests { ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME); ssl.setKeyStorePassword("1234"); SslServerCustomizer customizer = new SslServerCustomizer(ssl, null, null); - assertThatNoException().isThrownBy(() -> customizer.getKeyManagerFactory(ssl, null)); + assertThatNoException() + .isThrownBy(() -> customizer.getKeyManagerFactory(ssl, SslStoreProviderFactory.from(ssl))); } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java index 53c6973f45..be311fb9f0 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java @@ -41,12 +41,10 @@ import org.springframework.boot.web.embedded.test.MockPkcs11Security; import org.springframework.boot.web.embedded.test.MockPkcs11SecurityProvider; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.SslStoreProvider; -import org.springframework.boot.web.server.WebServerException; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.mockito.BDDMockito.given; @@ -184,9 +182,9 @@ class SslConnectorCustomizerTests { @Test void customizeWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsException() { - assertThatExceptionOfType(WebServerException.class) - .isThrownBy(() -> new SslConnectorCustomizer(new Ssl(), null).customize(this.tomcat.getConnector())) - .withMessageContaining("Could not load key store 'null'"); + assertThatIllegalStateException() + .isThrownBy(() -> new SslConnectorCustomizer(new Ssl()).customize(this.tomcat.getConnector())) + .withMessageContaining("KeyStore location must not be empty or null"); } @Test @@ -196,7 +194,7 @@ class SslConnectorCustomizerTests { ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME); ssl.setKeyStore("src/test/resources/test.jks"); ssl.setKeyPassword("password"); - SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl, null); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl); assertThatIllegalStateException().isThrownBy(() -> customizer.customize(this.tomcat.getConnector())) .withMessageContaining("must be empty or null for PKCS11 key stores"); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizerTests.java index d06092bb48..312c56e0f6 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizerTests.java @@ -26,8 +26,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.web.embedded.test.MockPkcs11Security; import org.springframework.boot.web.embedded.test.MockPkcs11SecurityProvider; import org.springframework.boot.web.server.Ssl; -import org.springframework.boot.web.server.WebServerException; -import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.boot.web.server.SslStoreProviderFactory; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -49,7 +48,7 @@ class SslBuilderCustomizerTests { ssl.setKeyPassword("password"); ssl.setKeyStore("src/test/resources/test.jks"); SslBuilderCustomizer customizer = new SslBuilderCustomizer(8080, InetAddress.getLocalHost(), ssl, null); - KeyManager[] keyManagers = ReflectionTestUtils.invokeMethod(customizer, "getKeyManagers", ssl, null); + KeyManager[] keyManagers = customizer.getKeyManagers(ssl, SslStoreProviderFactory.from(ssl)); Class name = Class .forName("org.springframework.boot.web.embedded.undertow.SslBuilderCustomizer$ConfigurableAliasKeyManager"); assertThat(keyManagers[0]).isNotInstanceOf(name); @@ -63,7 +62,7 @@ class SslBuilderCustomizerTests { ssl.setKeyStoreProvider("com.example.KeyStoreProvider"); SslBuilderCustomizer customizer = new SslBuilderCustomizer(8080, InetAddress.getLocalHost(), ssl, null); assertThatIllegalStateException() - .isThrownBy(() -> ReflectionTestUtils.invokeMethod(customizer, "getKeyManagers", ssl, null)) + .isThrownBy(() -> customizer.getKeyManagers(ssl, SslStoreProviderFactory.from(ssl))) .withCauseInstanceOf(NoSuchProviderException.class) .withMessageContaining("com.example.KeyStoreProvider"); } @@ -76,8 +75,7 @@ class SslBuilderCustomizerTests { ssl.setTrustStoreProvider("com.example.TrustStoreProvider"); SslBuilderCustomizer customizer = new SslBuilderCustomizer(8080, InetAddress.getLocalHost(), ssl, null); assertThatIllegalStateException() - .isThrownBy(() -> ReflectionTestUtils.invokeMethod(customizer, "getTrustManagers", ssl, null)) - .withCauseInstanceOf(NoSuchProviderException.class) + .isThrownBy(() -> customizer.getTrustManagers(SslStoreProviderFactory.from(ssl))) .withMessageContaining("com.example.TrustStoreProvider"); } @@ -86,9 +84,9 @@ class SslBuilderCustomizerTests { Ssl ssl = new Ssl(); SslBuilderCustomizer customizer = new SslBuilderCustomizer(8080, InetAddress.getLocalHost(), ssl, null); assertThatIllegalStateException() - .isThrownBy(() -> ReflectionTestUtils.invokeMethod(customizer, "getKeyManagers", ssl, null)) - .withCauseInstanceOf(WebServerException.class) - .withMessageContaining("Could not load key store 'null'"); + .isThrownBy(() -> customizer.getKeyManagers(ssl, SslStoreProviderFactory.from(ssl))) + .withCauseInstanceOf(IllegalStateException.class) + .withMessageContaining("KeyStore location must not be empty or null"); } @Test @@ -100,9 +98,9 @@ class SslBuilderCustomizerTests { ssl.setKeyPassword("password"); SslBuilderCustomizer customizer = new SslBuilderCustomizer(8080, InetAddress.getLocalHost(), ssl, null); assertThatIllegalStateException() - .isThrownBy(() -> ReflectionTestUtils.invokeMethod(customizer, "getKeyManagers", ssl, null)) - .withCauseInstanceOf(IllegalArgumentException.class) - .withMessageContaining("Input keystore location is not valid for keystore type 'PKCS11'"); + .isThrownBy(() -> customizer.getKeyManagers(ssl, SslStoreProviderFactory.from(ssl))) + .withCauseInstanceOf(IllegalStateException.class) + .withMessageContaining("must be empty or null for PKCS11 key stores"); } @Test @@ -112,8 +110,7 @@ class SslBuilderCustomizerTests { ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME); ssl.setKeyStorePassword("1234"); SslBuilderCustomizer customizer = new SslBuilderCustomizer(8080, InetAddress.getLocalHost(), ssl, null); - assertThatNoException() - .isThrownBy(() -> ReflectionTestUtils.invokeMethod(customizer, "getKeyManagers", ssl, null)); + assertThatNoException().isThrownBy(() -> customizer.getKeyManagers(ssl, SslStoreProviderFactory.from(ssl))); } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java index 3497230ce8..6751947151 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java @@ -77,6 +77,7 @@ import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientRequestException; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatThrownBy; /** @@ -400,8 +401,8 @@ public abstract class AbstractReactiveWebServerFactoryTests { @Test void whenSslIsEnabledAndNoKeyStoreIsConfiguredThenServerFailsToStart() { - assertThatThrownBy(() -> testBasicSslWithKeyStore(null, null)) - .hasMessageContaining("Could not load key store 'null'"); + assertThatIllegalStateException().isThrownBy(() -> testBasicSslWithKeyStore(null, null)) + .withMessageContaining("KeyStore location must not be empty or null"); } @Test diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/JavaKeyStoreSslStoreProviderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/JavaKeyStoreSslStoreProviderTests.java new file mode 100644 index 0000000000..d9fea419d8 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/JavaKeyStoreSslStoreProviderTests.java @@ -0,0 +1,142 @@ +/* + * 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.web.server; + +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.UnrecoverableKeyException; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.web.embedded.test.MockPkcs11Security; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link JavaKeyStoreSslStoreProvider}. + * + * @author Scott Frederick + */ +@MockPkcs11Security +class JavaKeyStoreSslStoreProviderTests { + + @Test + void fromSslWhenNullReturnsNull() { + assertThat(JavaKeyStoreSslStoreProvider.from(null)).isNull(); + } + + @Test + void fromSslWhenDisabledReturnsNull() { + Ssl ssl = new Ssl(); + ssl.setEnabled(false); + assertThat(JavaKeyStoreSslStoreProvider.from(ssl)).isNull(); + } + + @Test + void getKeyStoreWithNoLocationThrowsException() { + Ssl ssl = new Ssl(); + SslStoreProvider storeProvider = JavaKeyStoreSslStoreProvider.from(ssl); + assertThatIllegalStateException().isThrownBy(storeProvider::getKeyStore) + .withMessageContaining("KeyStore location must not be empty or null"); + } + + @Test + void getKeyStoreWithTypePKCS11AndLocationThrowsException() { + Ssl ssl = new Ssl(); + ssl.setKeyStore("test.jks"); + ssl.setKeyStoreType("PKCS11"); + SslStoreProvider storeProvider = JavaKeyStoreSslStoreProvider.from(ssl); + assertThatIllegalStateException().isThrownBy(storeProvider::getKeyStore) + .withMessageContaining("KeyStore location is 'test.jks', but must be empty or null for PKCS11 key stores"); + } + + @Test + void getKeyStoreWithLocationReturnsKeyStore() throws Exception { + Ssl ssl = new Ssl(); + ssl.setKeyStore("classpath:test.jks"); + ssl.setKeyStorePassword("secret"); + SslStoreProvider storeProvider = JavaKeyStoreSslStoreProvider.from(ssl); + assertThat(storeProvider).isNotNull(); + assertStoreContainsCertAndKey(storeProvider.getKeyStore(), "JKS", "test-alias", "password"); + } + + @Test + void getTrustStoreWithLocationsReturnsTrustStore() throws Exception { + Ssl ssl = new Ssl(); + ssl.setTrustStore("classpath:test.jks"); + ssl.setKeyStorePassword("secret"); + SslStoreProvider storeProvider = JavaKeyStoreSslStoreProvider.from(ssl); + assertThat(storeProvider).isNotNull(); + assertStoreContainsCertAndKey(storeProvider.getTrustStore(), "JKS", "test-alias", "password"); + } + + @Test + void getKeyStoreWithTypeUsesType() throws Exception { + Ssl ssl = new Ssl(); + ssl.setKeyStore("classpath:test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setKeyStoreType("PKCS12"); + SslStoreProvider storeProvider = JavaKeyStoreSslStoreProvider.from(ssl); + assertThat(storeProvider).isNotNull(); + assertStoreContainsCertAndKey(storeProvider.getKeyStore(), "PKCS12", "test-alias", "password"); + } + + @Test + void getTrustStoreWithTypeUsesType() throws Exception { + Ssl ssl = new Ssl(); + ssl.setTrustStore("classpath:test.jks"); + ssl.setKeyStorePassword("secret"); + ssl.setTrustStoreType("PKCS12"); + SslStoreProvider storeProvider = JavaKeyStoreSslStoreProvider.from(ssl); + assertThat(storeProvider).isNotNull(); + assertStoreContainsCertAndKey(storeProvider.getTrustStore(), "PKCS12", "test-alias", "password"); + } + + @Test + void getKeyStoreWithProviderUsesProvider() { + Ssl ssl = new Ssl(); + ssl.setKeyStore("classpath:test.jks"); + ssl.setKeyStoreProvider("com.example.KeyStoreProvider"); + SslStoreProvider storeProvider = JavaKeyStoreSslStoreProvider.from(ssl); + assertThatExceptionOfType(NoSuchProviderException.class).isThrownBy(storeProvider::getKeyStore) + .withMessageContaining("com.example.KeyStoreProvider"); + } + + @Test + void getTrustStoreWithProviderUsesProvider() { + Ssl ssl = new Ssl(); + ssl.setTrustStore("classpath:test.jks"); + ssl.setTrustStoreProvider("com.example.TrustStoreProvider"); + SslStoreProvider storeProvider = JavaKeyStoreSslStoreProvider.from(ssl); + assertThatExceptionOfType(NoSuchProviderException.class).isThrownBy(storeProvider::getTrustStore) + .withMessageContaining("com.example.TrustStoreProvider"); + } + + private void assertStoreContainsCertAndKey(KeyStore keyStore, String keyStoreType, String keyAlias, + String keyPassword) throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + 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(); + } + +}