diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index ffe5c1d479..9a15c30b96 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -2064,6 +2064,10 @@ "level": "error" } }, + { + "name": "management.server.ssl.bundle", + "description": "The name of a configured SSL bundle." + }, { "name": "management.server.ssl.certificate", "description": "Path to a PEM-encoded SSL certificate file." diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfiguration.java index b913a28db5..96bbbc454b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfiguration.java @@ -37,6 +37,7 @@ import org.springframework.boot.rsocket.context.RSocketServerBootstrap; import org.springframework.boot.rsocket.netty.NettyRSocketServerFactory; import org.springframework.boot.rsocket.server.RSocketServerCustomizer; import org.springframework.boot.rsocket.server.RSocketServerFactory; +import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -54,6 +55,7 @@ import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHa * server port is configured, a new standalone RSocket server is created. * * @author Brian Clozel + * @author Scott Frederick * @since 2.2.0 */ @AutoConfiguration(after = RSocketStrategiesAutoConfiguration.class) @@ -85,7 +87,7 @@ public class RSocketServerAutoConfiguration { @Bean @ConditionalOnMissingBean RSocketServerFactory rSocketServerFactory(RSocketProperties properties, ReactorResourceFactory resourceFactory, - ObjectProvider customizers) { + ObjectProvider customizers, ObjectProvider sslBundles) { NettyRSocketServerFactory factory = new NettyRSocketServerFactory(); factory.setResourceFactory(resourceFactory); factory.setTransport(properties.getServer().getTransport()); @@ -94,6 +96,7 @@ public class RSocketServerAutoConfiguration { map.from(properties.getServer().getPort()).to(factory::setPort); map.from(properties.getServer().getFragmentSize()).to(factory::setFragmentSize); map.from(properties.getServer().getSsl()).to(factory::setSsl); + factory.setSslBundles(sslBundles.getIfAvailable()); factory.setRSocketServerCustomizers(customizers.orderedStream().toList()); return factory; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/JksSslBundleProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/JksSslBundleProperties.java new file mode 100644 index 0000000000..ca89bde4a8 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/JksSslBundleProperties.java @@ -0,0 +1,108 @@ +/* + * 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.autoconfigure.ssl; + +import org.springframework.boot.ssl.jks.JksSslStoreBundle; + +/** + * {@link SslBundleProperties} for Java keystores. + * + * @author Scott Frederick + * @author Phillip Webb + * @since 3.1.0 + * @see JksSslStoreBundle + */ +public class JksSslBundleProperties extends SslBundleProperties { + + /** + * Keystore properties. + */ + private final Store keystore = new Store(); + + /** + * Truststore properties. + */ + private final Store truststore = new Store(); + + public Store getKeystore() { + return this.keystore; + } + + public Store getTruststore() { + return this.truststore; + } + + /** + * Store properties. + */ + public static class Store { + + /** + * Type of the store to create, e.g. JKS. + */ + private String type; + + /** + * Provider for the store. + */ + private String provider; + + /** + * Location of the resource containing the store content. + */ + private String location; + + /** + * Password used to access the store. + */ + private String password; + + public String getType() { + return this.type; + } + + public void setType(String type) { + this.type = type; + } + + public String getProvider() { + return this.provider; + } + + public void setProvider(String provider) { + this.provider = provider; + } + + public String getLocation() { + return this.location; + } + + public void setLocation(String location) { + this.location = location; + } + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemSslBundleProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemSslBundleProperties.java new file mode 100644 index 0000000000..d296ffdec3 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemSslBundleProperties.java @@ -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.autoconfigure.ssl; + +import org.springframework.boot.ssl.pem.PemSslStoreBundle; + +/** + * {@link SslBundleProperties} for PEM-encoded certificates and private keys. + * + * @author Scott Frederick + * @author Phillip Webb + * @since 3.1.0 + * @see PemSslStoreBundle + */ +public class PemSslBundleProperties extends SslBundleProperties { + + /** + * Keystore properties. + */ + private Store keystore = new Store(); + + /** + * Truststore properties. + */ + private Store truststore = new Store(); + + public Store getKeystore() { + return this.keystore; + } + + public Store getTruststore() { + return this.truststore; + } + + /** + * Store properties. + */ + public static class Store { + + /** + * Type of the store to create, e.g. JKS. + */ + String type; + + /** + * Location or content of the certificate in PEM format. + */ + String certificate; + + /** + * Location or content of the private key in PEM format. + */ + String privateKey; + + public String getType() { + return this.type; + } + + public void setType(String type) { + this.type = type; + } + + public String getCertificate() { + return this.certificate; + } + + public void setCertificate(String certificate) { + this.certificate = certificate; + } + + public String getPrivateKey() { + return this.privateKey; + } + + public void setPrivateKey(String privateKey) { + this.privateKey = privateKey; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java new file mode 100644 index 0000000000..337db88f13 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java @@ -0,0 +1,131 @@ +/* + * 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.autoconfigure.ssl; + +import org.springframework.boot.autoconfigure.ssl.SslBundleProperties.Key; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundleKey; +import org.springframework.boot.ssl.SslManagerBundle; +import org.springframework.boot.ssl.SslOptions; +import org.springframework.boot.ssl.SslStoreBundle; +import org.springframework.boot.ssl.jks.JksSslStoreBundle; +import org.springframework.boot.ssl.jks.JksSslStoreDetails; +import org.springframework.boot.ssl.pem.PemSslStoreBundle; +import org.springframework.boot.ssl.pem.PemSslStoreDetails; + +/** + * {@link SslBundle} backed by {@link JksSslBundleProperties} or + * {@link PemSslBundleProperties}. + * + * @author Scott Frederick + * @author Phillip Webb + * @since 3.1.0 + */ +public final class PropertiesSslBundle implements SslBundle { + + private final SslStoreBundle stores; + + private final SslBundleKey key; + + private final SslOptions options; + + private final String protocol; + + private final SslManagerBundle managers; + + private PropertiesSslBundle(SslStoreBundle stores, SslBundleProperties properties) { + this.stores = stores; + this.key = asSslKeyReference(properties.getKey()); + this.options = asSslOptions(properties.getOptions()); + this.protocol = properties.getProtocol(); + this.managers = SslManagerBundle.from(this.stores, this.key); + } + + private static SslBundleKey asSslKeyReference(Key key) { + return (key != null) ? SslBundleKey.of(key.getPassword(), key.getAlias()) : SslBundleKey.NONE; + } + + private static SslOptions asSslOptions(SslBundleProperties.Options properties) { + return (properties != null) ? SslOptions.of(properties.getCiphers(), properties.getEnabledProtocols()) + : SslOptions.NONE; + } + + @Override + public SslStoreBundle getStores() { + return this.stores; + } + + @Override + public SslBundleKey getKey() { + return this.key; + } + + @Override + public SslOptions getOptions() { + return this.options; + } + + @Override + public String getProtocol() { + return this.protocol; + } + + @Override + public SslManagerBundle getManagers() { + return this.managers; + } + + /** + * Get an {@link SslBundle} for the given {@link PemSslBundleProperties}. + * @param properties the source properties + * @return an {@link SslBundle} instance + */ + public static SslBundle get(PemSslBundleProperties properties) { + return new PropertiesSslBundle(asSslStoreBundle(properties), properties); + } + + /** + * Get an {@link SslBundle} for the given {@link JksSslBundleProperties}. + * @param properties the source properties + * @return an {@link SslBundle} instance + */ + public static SslBundle get(JksSslBundleProperties properties) { + return new PropertiesSslBundle(asSslStoreBundle(properties), properties); + } + + private static SslStoreBundle asSslStoreBundle(PemSslBundleProperties properties) { + PemSslStoreDetails keyStoreDetails = asStoreDetails(properties.getKeystore()); + PemSslStoreDetails trustStoreDetails = asStoreDetails(properties.getTruststore()); + return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, properties.getKey().getAlias()); + } + + private static PemSslStoreDetails asStoreDetails(PemSslBundleProperties.Store properties) { + return new PemSslStoreDetails(properties.getType(), properties.getCertificate(), properties.getPrivateKey()); + } + + private static SslStoreBundle asSslStoreBundle(JksSslBundleProperties properties) { + JksSslStoreDetails keyStoreDetails = asStoreDetails(properties.getKeystore()); + JksSslStoreDetails trustStoreDetails = asStoreDetails(properties.getTruststore()); + return new JksSslStoreBundle(keyStoreDetails, trustStoreDetails); + } + + private static JksSslStoreDetails asStoreDetails(JksSslBundleProperties.Store properties) { + return new JksSslStoreDetails(properties.getType(), properties.getProvider(), properties.getLocation(), + properties.getPassword()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.java new file mode 100644 index 0000000000..12b856c8a0 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.java @@ -0,0 +1,56 @@ +/* + * 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.autoconfigure.ssl; + +import java.util.List; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.ssl.DefaultSslBundleRegistry; +import org.springframework.boot.ssl.SslBundleRegistry; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.context.annotation.Bean; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for SSL. + * + * @author Scott Frederick + * @since 3.1.0 + */ +@AutoConfiguration +@EnableConfigurationProperties(SslProperties.class) +public class SslAutoConfiguration { + + SslAutoConfiguration() { + } + + @Bean + public SslPropertiesBundleRegistrar sslPropertiesSslBundleRegistrar(SslProperties sslProperties) { + return new SslPropertiesBundleRegistrar(sslProperties); + } + + @Bean + @ConditionalOnMissingBean({ SslBundleRegistry.class, SslBundles.class }) + public DefaultSslBundleRegistry sslBundleRegistry(List sslBundleRegistrars) { + DefaultSslBundleRegistry registry = new DefaultSslBundleRegistry(); + sslBundleRegistrars.forEach((registrar) -> registrar.registerBundles(registry)); + return registry; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslBundleProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslBundleProperties.java new file mode 100644 index 0000000000..e8b9fd1a4c --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslBundleProperties.java @@ -0,0 +1,124 @@ +/* + * 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.autoconfigure.ssl; + +import java.util.Set; + +import org.springframework.boot.ssl.SslBundle; + +/** + * Base class for SSL Bundle properties. + * + * @author Scott Frederick + * @author Phillip Webb + * @since 3.1.0 + * @see SslBundle + */ +public abstract class SslBundleProperties { + + /** + * Key details for the bundle. + */ + private final Key key = new Key(); + + /** + * Options for the SLL connection. + */ + private final Options options = new Options(); + + /** + * SSL Protocol to use. + */ + private String protocol = SslBundle.DEFAULT_PROTOCOL; + + public Key getKey() { + return this.key; + } + + public Options getOptions() { + return this.options; + } + + public String getProtocol() { + return this.protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public static class Options { + + /** + * Supported SSL ciphers. + */ + private Set ciphers; + + /** + * Enabled SSL protocols. + */ + private Set enabledProtocols; + + public Set getCiphers() { + return this.ciphers; + } + + public void setCiphers(Set ciphers) { + this.ciphers = ciphers; + } + + public Set getEnabledProtocols() { + return this.enabledProtocols; + } + + public void setEnabledProtocols(Set enabledProtocols) { + this.enabledProtocols = enabledProtocols; + } + + } + + public static class Key { + + /** + * The password used to access the key in the key store. + */ + private String password; + + /** + * The alias that identifies the key in the key store. + */ + private String alias; + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getAlias() { + return this.alias; + } + + public void setAlias(String alias) { + this.alias = alias; + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslStoreProviderFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslBundleRegistrar.java similarity index 52% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslStoreProviderFactory.java rename to spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslBundleRegistrar.java index d9b9f0d1e2..ab75cc0a51 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslStoreProviderFactory.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslBundleRegistrar.java @@ -14,28 +14,26 @@ * limitations under the License. */ -package org.springframework.boot.web.server; +package org.springframework.boot.autoconfigure.ssl; + +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundleRegistry; /** - * Creates an {@link SslStoreProvider} based on SSL configuration properties. + * Interface to be implemented by types that register {@link SslBundle} instances with an + * {@link SslBundleRegistry}. * * @author Scott Frederick * @since 3.1.0 */ -public final class SslStoreProviderFactory { - - private SslStoreProviderFactory() { - } +@FunctionalInterface +public interface SslBundleRegistrar { /** - * Create an {@link SslStoreProvider} if the appropriate SSL properties are - * configured. - * @param ssl the SSL properties - * @return an {@code SslStoreProvider} or {@code null} + * Callback method for registering {@link SslBundle}s with an + * {@link SslBundleRegistry}. + * @param registry the registry that accepts {@code SslBundle}s */ - public static SslStoreProvider from(Ssl ssl) { - SslStoreProvider sslStoreProvider = CertificateFileSslStoreProvider.from(ssl); - return ((sslStoreProvider != null) ? sslStoreProvider : JavaKeyStoreSslStoreProvider.from(ssl)); - } + void registerBundles(SslBundleRegistry registry); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslProperties.java new file mode 100644 index 0000000000..ee17a4a64f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslProperties.java @@ -0,0 +1,70 @@ +/* + * 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.autoconfigure.ssl; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +/** + * Properties for centralized SSL trust material configuration. + * + * @author Scott Frederick + * @since 3.1.0 + */ +@ConfigurationProperties(prefix = "spring.ssl") +public class SslProperties { + + /** + * SSL bundles. + */ + private final Bundles bundle = new Bundles(); + + public Bundles getBundle() { + return this.bundle; + } + + /** + * Properties to define SSL Bundles. + */ + public static class Bundles { + + /** + * PEM-encoded SSL trust material. + */ + @NestedConfigurationProperty + private final Map pem = new LinkedHashMap<>(); + + /** + * Java keystore SSL trust material. + */ + @NestedConfigurationProperty + private final Map jks = new LinkedHashMap<>(); + + public Map getPem() { + return this.pem; + } + + public Map getJks() { + return this.jks; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java new file mode 100644 index 0000000000..89a3e7c126 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java @@ -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.autoconfigure.ssl; + +import java.util.Map; +import java.util.function.Function; + +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundleRegistry; + +/** + * A {@link SslBundleRegistrar} that registers SSL bundles based + * {@link SslProperties#getBundle() configuration properties}. + * + * @author Scott Frederick + * @author Phillip Webb + */ +class SslPropertiesBundleRegistrar implements SslBundleRegistrar { + + private final SslProperties.Bundles properties; + + SslPropertiesBundleRegistrar(SslProperties properties) { + this.properties = properties.getBundle(); + } + + @Override + public void registerBundles(SslBundleRegistry registry) { + registerBundles(registry, this.properties.getPem(), PropertiesSslBundle::get); + registerBundles(registry, this.properties.getJks(), PropertiesSslBundle::get); + } + + private

void registerBundles(SslBundleRegistry registry, Map properties, + Function bundleFactory) { + properties.forEach((bundleName, bundleProperties) -> registry.registerBundle(bundleName, + bundleFactory.apply(bundleProperties))); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/package-info.java new file mode 100644 index 0000000000..c068bf9ff3 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/package-info.java @@ -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. + */ + +/** + * Auto-configuration for SSL bundles. + */ +package org.springframework.boot.autoconfigure.ssl; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfiguration.java index 7e621f0947..18ba60c405 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfiguration.java @@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure.web.reactive; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; @@ -31,6 +32,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; @@ -45,6 +47,7 @@ import org.springframework.web.server.adapter.ForwardedHeaderTransformer; * {@link EnableAutoConfiguration Auto-configuration} for a reactive web server. * * @author Brian Clozel + * @author Scott Frederick * @since 2.0.0 */ @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) @@ -60,8 +63,9 @@ import org.springframework.web.server.adapter.ForwardedHeaderTransformer; public class ReactiveWebServerFactoryAutoConfiguration { @Bean - public ReactiveWebServerFactoryCustomizer reactiveWebServerFactoryCustomizer(ServerProperties serverProperties) { - return new ReactiveWebServerFactoryCustomizer(serverProperties); + public ReactiveWebServerFactoryCustomizer reactiveWebServerFactoryCustomizer(ServerProperties serverProperties, + ObjectProvider sslBundles) { + return new ReactiveWebServerFactoryCustomizer(serverProperties, sslBundles.getIfAvailable()); } @Bean diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryCustomizer.java index 8d15e4db4a..1b1a1c7882 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 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. @@ -18,6 +18,7 @@ package org.springframework.boot.autoconfigure.web.reactive; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.web.reactive.server.ConfigurableReactiveWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.core.Ordered; @@ -28,6 +29,7 @@ import org.springframework.core.Ordered; * * @author Brian Clozel * @author Yunkun Huang + * @author Scott Frederick * @since 2.0.0 */ public class ReactiveWebServerFactoryCustomizer @@ -35,8 +37,25 @@ public class ReactiveWebServerFactoryCustomizer private final ServerProperties serverProperties; + private final SslBundles sslBundles; + + /** + * Create a new {@link ReactiveWebServerFactoryCustomizer} instance. + * @param serverProperties the server properties + */ public ReactiveWebServerFactoryCustomizer(ServerProperties serverProperties) { + this(serverProperties, null); + } + + /** + * Create a new {@link ReactiveWebServerFactoryCustomizer} instance. + * @param serverProperties the server properties + * @param sslBundles the SSL bundles + * @since 3.1.0 + */ + public ReactiveWebServerFactoryCustomizer(ServerProperties serverProperties, SslBundles sslBundles) { this.serverProperties = serverProperties; + this.sslBundles = sslBundles; } @Override @@ -53,6 +72,7 @@ public class ReactiveWebServerFactoryCustomizer map.from(this.serverProperties::getCompression).to(factory::setCompression); map.from(this.serverProperties::getHttp2).to(factory::setHttp2); map.from(this.serverProperties.getShutdown()).to(factory::setShutdown); + map.from(() -> this.sslBundles).to(factory::setSslBundles); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration.java index 88c5e0d5e6..5f4a23e374 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration.java @@ -33,8 +33,10 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.web.server.ErrorPageRegistrarBeanPostProcessor; import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor; import org.springframework.boot.web.servlet.FilterRegistrationBean; @@ -57,9 +59,10 @@ import org.springframework.web.filter.ForwardedHeaderFilter; * @author Ivan Sopov * @author Brian Clozel * @author Stephane Nicoll + * @author Scott Frederick * @since 2.0.0 */ -@AutoConfiguration +@AutoConfiguration(after = SslAutoConfiguration.class) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) @ConditionalOnClass(ServletRequest.class) @ConditionalOnWebApplication(type = Type.SERVLET) @@ -73,9 +76,9 @@ public class ServletWebServerFactoryAutoConfiguration { @Bean public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties, ObjectProvider webListenerRegistrars, - ObjectProvider cookieSameSiteSuppliers) { + ObjectProvider cookieSameSiteSuppliers, ObjectProvider sslBundles) { return new ServletWebServerFactoryCustomizer(serverProperties, webListenerRegistrars.orderedStream().toList(), - cookieSameSiteSuppliers.orderedStream().toList()); + cookieSameSiteSuppliers.orderedStream().toList(), sslBundles.getIfAvailable()); } @Bean diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizer.java index f3f007cc6a..70706f9397 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 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. @@ -21,6 +21,7 @@ import java.util.List; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.boot.web.servlet.WebListenerRegistrar; import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; @@ -36,6 +37,7 @@ import org.springframework.util.CollectionUtils; * @author Stephane Nicoll * @author Olivier Lamy * @author Yunkun Huang + * @author Scott Frederick * @since 2.0.0 */ public class ServletWebServerFactoryCustomizer @@ -47,20 +49,24 @@ public class ServletWebServerFactoryCustomizer private final List cookieSameSiteSuppliers; + private final SslBundles sslBundles; + public ServletWebServerFactoryCustomizer(ServerProperties serverProperties) { this(serverProperties, Collections.emptyList()); } public ServletWebServerFactoryCustomizer(ServerProperties serverProperties, List webListenerRegistrars) { - this(serverProperties, webListenerRegistrars, null); + this(serverProperties, webListenerRegistrars, null, null); } ServletWebServerFactoryCustomizer(ServerProperties serverProperties, - List webListenerRegistrars, List cookieSameSiteSuppliers) { + List webListenerRegistrars, List cookieSameSiteSuppliers, + SslBundles sslBundles) { this.serverProperties = serverProperties; this.webListenerRegistrars = webListenerRegistrars; this.cookieSameSiteSuppliers = cookieSameSiteSuppliers; + this.sslBundles = sslBundles; } @Override @@ -84,12 +90,11 @@ public class ServletWebServerFactoryCustomizer map.from(this.serverProperties::getServerHeader).to(factory::setServerHeader); map.from(this.serverProperties.getServlet()::getContextParameters).to(factory::setInitParameters); map.from(this.serverProperties.getShutdown()).to(factory::setShutdown); - for (WebListenerRegistrar registrar : this.webListenerRegistrars) { - registrar.register(factory); - } - if (!CollectionUtils.isEmpty(this.cookieSameSiteSuppliers)) { - factory.setCookieSameSiteSuppliers(this.cookieSameSiteSuppliers); - } + map.from(() -> this.sslBundles).to(factory::setSslBundles); + map.from(() -> this.cookieSameSiteSuppliers) + .whenNot(CollectionUtils::isEmpty) + .to(factory::setCookieSameSiteSuppliers); + this.webListenerRegistrars.forEach((registrar) -> registrar.register(factory)); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index c56fa356ab..d6b3ab6a24 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -262,6 +262,10 @@ "name": "server.shutdown", "defaultValue": "immediate" }, + { + "name": "server.ssl.bundle", + "description": "The name of a configured SSL bundle." + }, { "name": "server.ssl.certificate", "description": "Path to a PEM-encoded SSL certificate file." @@ -2635,6 +2639,10 @@ "level": "error" } }, + { + "name": "spring.rsocket.server.ssl.bundle", + "description": "The name of a configured SSL bundle." + }, { "name": "spring.rsocket.server.ssl.certificate", "description": "Path to a PEM-encoded SSL certificate file." diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index f688b1590d..f001840697 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -116,6 +116,7 @@ org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.Reactiv org.springframework.boot.autoconfigure.security.oauth2.server.servlet.OAuth2AuthorizationServerAutoConfiguration org.springframework.boot.autoconfigure.security.oauth2.server.servlet.OAuth2AuthorizationServerJwtAutoConfiguration org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration +org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfigurationTests.java index ace10a95e7..0ddf63903d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfigurationTests.java @@ -16,13 +16,16 @@ package org.springframework.boot.autoconfigure.rsocket; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; import org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer; import org.springframework.boot.rsocket.context.RSocketServerBootstrap; import org.springframework.boot.rsocket.server.RSocketServerCustomizer; import org.springframework.boot.rsocket.server.RSocketServerFactory; +import org.springframework.boot.ssl.NoSuchSslBundleException; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; @@ -44,6 +47,7 @@ import static org.mockito.Mockito.mock; * * @author Brian Clozel * @author Verónica Vásquez + * @author Scott Frederick */ class RSocketServerAutoConfigurationTests { @@ -134,6 +138,32 @@ class RSocketServerAutoConfigurationTests { .hasFieldOrPropertyWithValue("ssl.keyPassword", "password")); } + @Test + @Disabled + void shouldUseSslWhenRocketServerSslIsConfiguredWithSslBundle() { + reactiveWebContextRunner() + .withPropertyValues("spring.rsocket.server.port=0", "spring.rsocket.server.ssl.bundle=test-bundle", + "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:rsocket/test.jks", + "spring.ssl.bundle.jks.test-bundle.key.password=password") + .run((context) -> assertThat(context).hasSingleBean(RSocketServerFactory.class) + .hasSingleBean(RSocketServerBootstrap.class) + .hasSingleBean(RSocketServerCustomizer.class) + .getBean(RSocketServerFactory.class) + .hasFieldOrPropertyWithValue("sslBundle.details.keyStore", "classpath:rsocket/test.jks") + .hasFieldOrPropertyWithValue("sslBundle.details.keyPassword", "password")); + } + + @Test + void shouldFailWhenSslIsConfiguredWithMissingBundle() { + reactiveWebContextRunner() + .withPropertyValues("spring.rsocket.server.port=0", "spring.rsocket.server.ssl.bundle=test-bundle") + .run((context) -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).hasRootCauseInstanceOf(NoSuchSslBundleException.class) + .withFailMessage("SSL bundle name 'test-bundle' is not valid"); + }); + } + @Test void shouldUseCustomServerBootstrap() { contextRunner().withUserConfiguration(CustomServerBootstrapConfig.class) @@ -164,7 +194,7 @@ class RSocketServerAutoConfigurationTests { private ReactiveWebApplicationContextRunner reactiveWebContextRunner() { return new ReactiveWebApplicationContextRunner().withUserConfiguration(BaseConfiguration.class) - .withConfiguration(AutoConfigurations.of(RSocketServerAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(RSocketServerAutoConfiguration.class, SslAutoConfiguration.class)); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfigurationTests.java new file mode 100644 index 0000000000..5717fedfbd --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfigurationTests.java @@ -0,0 +1,145 @@ +/* + * 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.autoconfigure.ssl; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundleRegistry; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SslAutoConfiguration}. + * + * @author Scott Frederick + * @author Phillip Webb + */ +class SslAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(SslAutoConfiguration.class)); + + @Test + void sslBundlesCreatedWithNoConfiguration() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(SslBundleRegistry.class)); + } + + @Test + void sslBundlesCreatedWithCertificates() { + List propertyValues = new ArrayList<>(); + propertyValues.add("spring.ssl.bundle.pem.first.key.alias=alias1"); + propertyValues.add("spring.ssl.bundle.pem.first.key.password=secret1"); + propertyValues.add("spring.ssl.bundle.pem.first.keystore.certificate=cert1.pem"); + propertyValues.add("spring.ssl.bundle.pem.first.keystore.private-key=key1.pem"); + propertyValues.add("spring.ssl.bundle.pem.first.keystore.type=JKS"); + propertyValues.add("spring.ssl.bundle.pem.first.truststore.type=PKCS12"); + propertyValues.add("spring.ssl.bundle.pem.second.key.alias=alias2"); + propertyValues.add("spring.ssl.bundle.pem.second.key.password=secret2"); + propertyValues.add("spring.ssl.bundle.pem.second.keystore.certificate=cert2.pem"); + propertyValues.add("spring.ssl.bundle.pem.second.keystore.private-key=key2.pem"); + propertyValues.add("spring.ssl.bundle.pem.second.keystore.type=PKCS12"); + propertyValues.add("spring.ssl.bundle.pem.second.truststore.certificate=ca.pem"); + propertyValues.add("spring.ssl.bundle.pem.second.truststore.private-key=ca-key.pem"); + propertyValues.add("spring.ssl.bundle.pem.second.truststore.type=JKS"); + this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> { + assertThat(context).hasSingleBean(SslBundles.class); + SslBundles bundles = context.getBean(SslBundles.class); + SslBundle first = bundles.getBundle("first"); + assertThat(first).isNotNull(); + assertThat(first.getStores()).isNotNull(); + assertThat(first.getManagers()).isNotNull(); + assertThat(first.getKey().getAlias()).isEqualTo("alias1"); + assertThat(first.getKey().getPassword()).isEqualTo("secret1"); + assertThat(first.getStores()).extracting("keyStoreDetails").extracting("type").isEqualTo("JKS"); + assertThat(first.getStores()).extracting("trustStoreDetails").extracting("type").isEqualTo("PKCS12"); + SslBundle second = bundles.getBundle("second"); + assertThat(second).isNotNull(); + assertThat(second.getStores()).isNotNull(); + assertThat(second.getManagers()).isNotNull(); + assertThat(second.getKey().getAlias()).isEqualTo("alias2"); + assertThat(second.getKey().getPassword()).isEqualTo("secret2"); + assertThat(second.getStores()).extracting("keyStoreDetails").extracting("type").isEqualTo("PKCS12"); + assertThat(second.getStores()).extracting("trustStoreDetails").extracting("type").isEqualTo("JKS"); + }); + } + + @Test + void sslBundlesCreatedWithCustomSslBundle() { + List propertyValues = new ArrayList<>(); + propertyValues.add("custom.ssl.key.alias=alias1"); + propertyValues.add("custom.ssl.key.password=secret1"); + propertyValues.add("custom.ssl.keystore.type=JKS"); + propertyValues.add("custom.ssl.truststore.type=PKCS12"); + this.contextRunner.withUserConfiguration(CustomSslBundleConfiguration.class) + .withPropertyValues(propertyValues.toArray(String[]::new)) + .run((context) -> { + assertThat(context).hasSingleBean(SslBundles.class); + SslBundles bundles = context.getBean(SslBundles.class); + SslBundle first = bundles.getBundle("custom"); + assertThat(first).isNotNull(); + assertThat(first.getStores()).isNotNull(); + assertThat(first.getManagers()).isNotNull(); + assertThat(first.getKey().getAlias()).isEqualTo("alias1"); + assertThat(first.getKey().getPassword()).isEqualTo("secret1"); + assertThat(first.getStores()).extracting("keyStoreDetails").extracting("type").isEqualTo("JKS"); + assertThat(first.getStores()).extracting("trustStoreDetails").extracting("type").isEqualTo("PKCS12"); + }); + } + + @Configuration + @EnableConfigurationProperties(CustomSslProperties.class) + public static class CustomSslBundleConfiguration { + + @Bean + public SslBundleRegistrar customSslBundlesRegistrar(CustomSslProperties properties) { + return new CustomSslBundlesRegistrar(properties); + } + + } + + @ConfigurationProperties("custom.ssl") + static class CustomSslProperties extends PemSslBundleProperties { + + } + + static class CustomSslBundlesRegistrar implements SslBundleRegistrar { + + private final CustomSslProperties properties; + + CustomSslBundlesRegistrar(CustomSslProperties properties) { + this.properties = properties; + } + + @Override + public void registerBundles(SslBundleRegistry registry) { + registry.registerBundle("custom", PropertiesSslBundle.get(this.properties)); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfigurationTests.java index 460083cfaa..7b1b38cf26 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfigurationTests.java @@ -26,6 +26,8 @@ import org.junit.jupiter.api.Test; import reactor.netty.http.server.HttpServer; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; +import org.springframework.boot.ssl.NoSuchSslBundleException; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories; @@ -64,13 +66,15 @@ import static org.mockito.Mockito.mock; * @author Brian Clozel * @author Raheela Aslam * @author Madhura Bhave + * @author Scott Frederick */ @DirtiesUrlFactories class ReactiveWebServerFactoryAutoConfigurationTests { private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner( AnnotationConfigReactiveWebServerApplicationContext::new) - .withConfiguration(AutoConfigurations.of(ReactiveWebServerFactoryAutoConfiguration.class)); + .withConfiguration( + AutoConfigurations.of(ReactiveWebServerFactoryAutoConfiguration.class, SslAutoConfiguration.class)); @Test void createFromConfigClass() { @@ -118,6 +122,17 @@ class ReactiveWebServerFactoryAutoConfigurationTests { .isInstanceOf(TomcatReactiveWebServerFactory.class)); } + @Test + void webServerFailsWithInvalidSslBundle() { + this.contextRunner.withUserConfiguration(HttpHandlerConfiguration.class) + .withPropertyValues("server.port=0", "server.ssl.bundle=test-bundle") + .run((context) -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure().getCause()).isInstanceOf(NoSuchSslBundleException.class) + .withFailMessage("test"); + }); + } + @Test void tomcatConnectorCustomizerBeanIsAddedToFactory() { ReactiveWebApplicationContextRunner runner = new ReactiveWebApplicationContextRunner( diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryCustomizerTests.java index 7eb1cd5f04..75deae2c70 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryCustomizerTests.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. @@ -23,6 +23,8 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.ssl.DefaultSslBundleRegistry; +import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.web.reactive.server.ConfigurableReactiveWebServerFactory; import org.springframework.boot.web.server.Shutdown; import org.springframework.boot.web.server.Ssl; @@ -36,16 +38,19 @@ import static org.mockito.Mockito.mock; * * @author Brian Clozel * @author Yunkun Huang + * @author Scott Frederick */ class ReactiveWebServerFactoryCustomizerTests { private final ServerProperties properties = new ServerProperties(); + private final SslBundles sslBundles = new DefaultSslBundleRegistry(); + private ReactiveWebServerFactoryCustomizer customizer; @BeforeEach void setup() { - this.customizer = new ReactiveWebServerFactoryCustomizer(this.properties); + this.customizer = new ReactiveWebServerFactoryCustomizer(this.properties, this.sslBundles); } @Test @@ -72,6 +77,7 @@ class ReactiveWebServerFactoryCustomizerTests { this.properties.setSsl(ssl); this.customizer.customize(factory); then(factory).should().setSsl(ssl); + then(factory).should().setSslBundles(this.sslBundles); } @Test diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features.adoc index d42c2951d2..2f75b39902 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features.adoc @@ -30,4 +30,6 @@ include::features/developing-auto-configuration.adoc[] include::features/kotlin.adoc[] +include::features/ssl.adoc[] + include::features/whats-next.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/ssl.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/ssl.adoc new file mode 100644 index 0000000000..de53ec48de --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/ssl.adoc @@ -0,0 +1,99 @@ +[[features.ssl]] +== SSL +Spring Boot provides the ability to configure SSL trust material that can be applied to several types of connections in order to support secure communications. +Configuration properties with the prefix `spring.ssl.bundle` can be used to specify named sets of trust material and associated information. + + + +[[features.ssl.jks]] +=== Configuring SSL With Java KeyStore Files +Configuration properties with the prefix `spring.ssl.bundle.jks` can be used to configure bundles of trust material created with the Java `keytool` utility and stored in Java KeyStore files in the JKS or PKCS12 format. +Each bundle has a user-provided name that can be used to reference the bundle. + +When used to secure an embedded web server, a `keystore` is typically configured with a Java KeyStore containing a certificate and private key as shown in this example: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + spring: + ssl: + bundle: + jks: + mybundle: + key: + alias: "application" + keystore: + location: "classpath:application.p12" + password: "secret" + type: "PKCS12" +---- + +When used to secure a client-side connection, a `truststore` is typically configured with a Java KeyStore containing the server certificate as shown in this example: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + spring: + ssl: + bundle: + jks: + mybundle: + truststore: + location: "classpath:server.p12" + password: "secret" +---- + +See {spring-boot-autoconfigure-module-code}/ssl/JksSslBundleProperties.java[JksSslBundleProperties] for the full set of supported properties. + + + +[[features.ssl.pem]] +=== Configuring SSL With PEM-encoded Certificates +Configuration properties with the prefix `spring.ssl.bundle.pem` can be used to configure bundles of trust material in the form of PEM-encoded text. +Each bundle has a user-provided name that can be used to reference the bundle. + +When used to secure an embedded web server, a `keystore` is typically configured with a certificate and private key as shown in this example: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + spring: + ssl: + bundle: + pem: + mybundle: + keystore: + certificate: "classpath:application.crt" + private-key: "classpath:application.key" +---- + +When used to secure an embedded web server, a `truststore` is typically configured with the server certificate as shown in this example: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + spring: + ssl: + bundle: + pem: + mybundle: + truststore: + certificate: "classpath:server.crt" +---- + +See {spring-boot-autoconfigure-module-code}/ssl/PemSslBundleProperties.java[PemSslBundleProperties] for the full set of supported properties. + + + +[[features.ssl.applying]] +=== Applying SSL Bundles +Once configured using properties, SSL bundles can be referred to by name in configuration properties for various types of connections that are auto-configured by Spring Boot. +See the sections on <> and <> for further information. + + + +[[features.ssl.bundles]] +=== Using SSL Bundles +Spring Boot auto-configures a bean of type `SslBundles` that provides access to each of the named bundles configured using the `spring.ssl.bundle` properties. +An `SslBundle` can be retrieved from the auto-configured `SslBundles` bean and used to create a `javax.net.ssl.SSLContext` or objects of other types from the `java.net.ssl` package that are typically used to configure SSL connectivity in other APIs. + +The following example shows retrieving an `SslBundle` and using it to create an `SSLContext`: + +include::code:MyComponent[] + diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/webserver.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/webserver.adoc index b012866c03..19d7625cf2 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/webserver.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/webserver.adoc @@ -207,6 +207,16 @@ The following example shows setting SSL properties using PEM-encoded certificate trust-certificate: "classpath:ca-cert.crt" ---- +Alternatively, the SSL trust material can be configured in an <> and applied to the web server as shown in this example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + server: + port: 8443 + ssl: + bundle: "example" +---- + See {spring-boot-module-code}/web/server/Ssl.java[`Ssl`] for details of all of the supported properties. Using configuration such as the preceding example means the application no longer supports a plain HTTP connector at port 8080. diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/ssl/bundles/MyComponent.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/ssl/bundles/MyComponent.java new file mode 100644 index 0000000000..a7e99fdbb3 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/ssl/bundles/MyComponent.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2019 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.docs.features.ssl.bundles; + +import javax.net.ssl.SSLContext; + +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.stereotype.Component; + +@Component +public class MyComponent { + + @SuppressWarnings("unused") + public MyComponent(SslBundles sslBundles) { + SslBundle sslBundle = sslBundles.getBundle("mybundle"); + SSLContext sslContext = sslBundle.createSslContext(); + // do something with the created sslContext + } + +} 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 45f6310b21..bd1d642835 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 @@ -39,9 +39,12 @@ 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.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.web.embedded.netty.SslServerCustomizer; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.SslStoreProvider; -import org.springframework.boot.web.server.SslStoreProviderFactory; +import org.springframework.boot.web.server.WebServerSslBundle; import org.springframework.http.client.reactive.ReactorResourceFactory; import org.springframework.util.Assert; import org.springframework.util.unit.DataSize; @@ -55,6 +58,7 @@ import org.springframework.util.unit.DataSize; * @author Scott Frederick * @since 2.2.0 */ +@SuppressWarnings("removal") public class NettyRSocketServerFactory implements RSocketServerFactory, ConfigurableRSocketServerFactory { private int port = 9898; @@ -75,6 +79,8 @@ public class NettyRSocketServerFactory implements RSocketServerFactory, Configur private SslStoreProvider sslStoreProvider; + private SslBundles sslBundles; + @Override public void setPort(int port) { this.port = port; @@ -105,6 +111,11 @@ public class NettyRSocketServerFactory implements RSocketServerFactory, Configur this.sslStoreProvider = sslStoreProvider; } + @Override + public void setSslBundles(SslBundles sslBundles) { + this.sslBundles = sslBundles; + } + /** * Set the {@link ReactorResourceFactory} to get the shared resources from. * @param resourceFactory the server resources @@ -172,17 +183,14 @@ public class NettyRSocketServerFactory implements RSocketServerFactory, Configur if (this.resourceFactory != null) { httpServer = httpServer.runOn(this.resourceFactory.getLoopResources()); } - if (this.ssl != null && this.ssl.isEnabled()) { + if (Ssl.isEnabled(this.ssl)) { httpServer = customizeSslConfiguration(httpServer); } return WebsocketServerTransport.create(httpServer.bindAddress(this::getListenAddress)); } - @SuppressWarnings("deprecation") private HttpServer customizeSslConfiguration(HttpServer httpServer) { - org.springframework.boot.web.embedded.netty.SslServerCustomizer sslServerCustomizer = new org.springframework.boot.web.embedded.netty.SslServerCustomizer( - this.ssl, null, getOrCreateSslStoreProvider()); - return sslServerCustomizer.apply(httpServer); + return new SslServerCustomizer(null, this.ssl.getClientAuth(), getSslBundle()).apply(httpServer); } private ServerTransport createTcpTransport() { @@ -190,19 +198,15 @@ public class NettyRSocketServerFactory implements RSocketServerFactory, Configur if (this.resourceFactory != null) { tcpServer = tcpServer.runOn(this.resourceFactory.getLoopResources()); } - if (this.ssl != null && this.ssl.isEnabled()) { - TcpSslServerCustomizer sslServerCustomizer = new TcpSslServerCustomizer(this.ssl, - getOrCreateSslStoreProvider()); - tcpServer = sslServerCustomizer.apply(tcpServer); + if (Ssl.isEnabled(this.ssl)) { + tcpServer = new TcpSslServerCustomizer(this.ssl.getClientAuth(), getSslBundle()).apply(tcpServer); } return TcpServerTransport.create(tcpServer.bindAddress(this::getListenAddress)); } - private SslStoreProvider getOrCreateSslStoreProvider() { - if (this.sslStoreProvider != null) { - return this.sslStoreProvider; - } - return SslStoreProviderFactory.from(this.ssl); + @SuppressWarnings("deprecation") + private SslBundle getSslBundle() { + return WebServerSslBundle.get(this.ssl, this.sslBundles, this.sslStoreProvider); } private InetSocketAddress getListenAddress() { @@ -212,12 +216,11 @@ public class NettyRSocketServerFactory implements RSocketServerFactory, Configur return new InetSocketAddress(this.port); } - @SuppressWarnings("deprecation") private static final class TcpSslServerCustomizer extends org.springframework.boot.web.embedded.netty.SslServerCustomizer { - private TcpSslServerCustomizer(Ssl ssl, SslStoreProvider sslStoreProvider) { - super(ssl, null, sslStoreProvider); + private TcpSslServerCustomizer(Ssl.ClientAuth clientAuth, SslBundle sslBundle) { + super(null, clientAuth, sslBundle); } private TcpServer apply(TcpServer server) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/server/ConfigurableRSocketServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/server/ConfigurableRSocketServerFactory.java index 3f7d9b2c38..eb48a9ef47 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/server/ConfigurableRSocketServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/server/ConfigurableRSocketServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 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. @@ -18,6 +18,7 @@ package org.springframework.boot.rsocket.server; import java.net.InetAddress; +import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.SslStoreProvider; import org.springframework.util.unit.DataSize; @@ -26,6 +27,7 @@ import org.springframework.util.unit.DataSize; * A configurable {@link RSocketServerFactory}. * * @author Brian Clozel + * @author Scott Frederick * @since 2.2.0 */ public interface ConfigurableRSocketServerFactory { @@ -66,7 +68,18 @@ public interface ConfigurableRSocketServerFactory { /** * Sets a provider that will be used to obtain SSL stores. * @param sslStoreProvider the SSL store provider + * @deprecated since 3.1.0 for removal in 3.3.0 in favor of + * {@link #setSslBundles(SslBundles)} */ + @SuppressWarnings("removal") + @Deprecated(since = "3.1.0", forRemoval = true) void setSslStoreProvider(SslStoreProvider sslStoreProvider); + /** + * Sets an SSL bundle that can be used to get SSL configuration. + * @param sslBundles the SSL bundles + * @since 3.1.0 + */ + void setSslBundles(SslBundles sslBundles); + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/AliasKeyManagerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/AliasKeyManagerFactory.java new file mode 100644 index 0000000000..41f431b6db --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/AliasKeyManagerFactory.java @@ -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); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/DefaultSslBundleRegistry.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/DefaultSslBundleRegistry.java new file mode 100644 index 0000000000..fa79265755 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/DefaultSslBundleRegistry.java @@ -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 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; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/DefaultSslManagerBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/DefaultSslManagerBundle.java new file mode 100644 index 0000000000..e449ab657a --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/DefaultSslManagerBundle.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/NoSuchSslBundleException.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/NoSuchSslBundleException.java new file mode 100644 index 0000000000..c4c66140f8 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/NoSuchSslBundleException.java @@ -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; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundle.java new file mode 100644 index 0000000000..790b225df7 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundle.java @@ -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; + } + + }; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundleKey.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundleKey.java new file mode 100644 index 0000000000..cf94130200 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundleKey.java @@ -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; + } + + }; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundleRegistry.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundleRegistry.java new file mode 100644 index 0000000000..990a481066 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundleRegistry.java @@ -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); + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundles.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundles.java new file mode 100644 index 0000000000..ed8a0ea9cd --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundles.java @@ -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; + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslManagerBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslManagerBundle.java new file mode 100644 index 0000000000..87e82a5374 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslManagerBundle.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslOptions.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslOptions.java new file mode 100644 index 0000000000..9ebc13f8d0 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslOptions.java @@ -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 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 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 ciphers, Set enabledProtocols) { + return new SslOptions() { + + @Override + public Set getCiphers() { + return (ciphers != null) ? ciphers : Collections.emptySet(); + } + + @Override + public Set getEnabledProtocols() { + return (enabledProtocols != null) ? enabledProtocols : Collections.emptySet(); + } + + }; + + } + + private static Set asSet(String[] array) { + return (array != null) ? Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(array))) : null; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslStoreBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslStoreBundle.java new file mode 100644 index 0000000000..a5f5a0e7d8 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslStoreBundle.java @@ -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; + } + + }; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/jks/JksSslStoreBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/jks/JksSslStoreBundle.java new file mode 100644 index 0000000000..8deb079a5c --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/jks/JksSslStoreBundle.java @@ -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); + } + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/jks/JksSslStoreDetails.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/jks/JksSslStoreDetails.java new file mode 100644 index 0000000000..f2679e56ae --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/jks/JksSslStoreDetails.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/jks/package-info.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/jks/package-info.java new file mode 100644 index 0000000000..d50e7e85c0 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/jks/package-info.java @@ -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; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/package-info.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/package-info.java new file mode 100644 index 0000000000..21eaefbfd2 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/package-info.java @@ -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; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/CertificateParser.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemCertificateParser.java similarity index 66% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/CertificateParser.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemCertificateParser.java index 23f30baa12..327dcc94a1 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/CertificateParser.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemCertificateParser.java @@ -14,13 +14,9 @@ * limitations under the License. */ -package org.springframework.boot.web.server; +package org.springframework.boot.ssl.pem; import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; -import java.net.URL; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; @@ -31,16 +27,13 @@ import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.springframework.util.FileCopyUtils; -import org.springframework.util.ResourceUtils; - /** * Parser for X.509 certificates in PEM format. * * @author Scott Frederick * @author Phillip Webb */ -final class CertificateParser { +final class PemCertificateParser { private static final String HEADER = "-+BEGIN\\s+.*CERTIFICATE[^-]*-+(?:\\s|\\r|\\n)+"; @@ -50,19 +43,22 @@ final class CertificateParser { private static final Pattern PATTERN = Pattern.compile(HEADER + BASE64_TEXT + FOOTER, Pattern.CASE_INSENSITIVE); - private CertificateParser() { + private PemCertificateParser() { } /** - * Load certificates from the specified resource. - * @param path the certificate to parse + * Parse certificates from the specified string. + * @param certificates the certificates to parse * @return the parsed certificates */ - static X509Certificate[] parse(String path) { + static X509Certificate[] parse(String certificates) { + if (certificates == null) { + return null; + } CertificateFactory factory = getCertificateFactory(); - List certificates = new ArrayList<>(); - readCertificates(path, factory, certificates::add); - return certificates.toArray(new X509Certificate[0]); + List certs = new ArrayList<>(); + readCertificates(certificates, factory, certs::add); + return (!certs.isEmpty()) ? certs.toArray(X509Certificate[]::new) : null; } private static CertificateFactory getCertificateFactory() { @@ -74,10 +70,8 @@ final class CertificateParser { } } - private static void readCertificates(String resource, CertificateFactory factory, - Consumer consumer) { + private static void readCertificates(String text, CertificateFactory factory, Consumer consumer) { try { - String text = readText(resource); Matcher matcher = PATTERN.matcher(text); while (matcher.find()) { String encodedText = matcher.group(1); @@ -88,16 +82,8 @@ final class CertificateParser { } } } - catch (CertificateException | IOException ex) { - throw new IllegalStateException("Error reading certificate from '" + resource + "' : " + ex.getMessage(), - ex); - } - } - - private static String readText(String resource) throws IOException { - URL url = ResourceUtils.getURL(resource); - try (Reader reader = new InputStreamReader(url.openStream())) { - return FileCopyUtils.copyToString(reader); + catch (CertificateException ex) { + throw new IllegalStateException("Error reading certificate: " + ex.getMessage(), ex); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java new file mode 100644 index 0000000000..3178285750 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java @@ -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(); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/PrivateKeyParser.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java similarity index 88% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/PrivateKeyParser.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java index 45635bae38..b1129f06b3 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/PrivateKeyParser.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java @@ -14,13 +14,10 @@ * limitations under the License. */ -package org.springframework.boot.web.server; +package org.springframework.boot.ssl.pem; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; -import java.net.URL; import java.security.GeneralSecurityException; import java.security.KeyFactory; import java.security.PrivateKey; @@ -33,16 +30,13 @@ import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.springframework.util.FileCopyUtils; -import org.springframework.util.ResourceUtils; - /** * Parser for PKCS private key files in PEM format. * * @author Scott Frederick * @author Phillip Webb */ -final class PrivateKeyParser { +final class PemPrivateKeyParser { private static final String PKCS1_HEADER = "-+BEGIN\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+"; @@ -61,8 +55,8 @@ final class PrivateKeyParser { private static final List PEM_PARSERS; static { List parsers = new ArrayList<>(); - parsers.add(new PemParser(PKCS1_HEADER, PKCS1_FOOTER, "RSA", PrivateKeyParser::createKeySpecForPkcs1)); - parsers.add(new PemParser(EC_HEADER, EC_FOOTER, "EC", PrivateKeyParser::createKeySpecForEc)); + 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); } @@ -82,7 +76,7 @@ final class PrivateKeyParser { */ private static final int[] EC_PARAMETERS = { 0x2b, 0x81, 0x04, 0x00, 0x22 }; - private PrivateKeyParser() { + private PemPrivateKeyParser() { } private static PKCS8EncodedKeySpec createKeySpecForPkcs1(byte[] bytes) { @@ -111,15 +105,17 @@ final class PrivateKeyParser { } /** - * Load a private key from the specified resource. - * @param resource the private key to parse + * Parse a private key from the specified string. + * @param key the private key to parse * @return the parsed private key */ - static PrivateKey parse(String resource) { + static PrivateKey parse(String key) { + if (key == null) { + return null; + } try { - String text = readText(resource); for (PemParser pemParser : PEM_PARSERS) { - PrivateKey privateKey = pemParser.parse(text); + PrivateKey privateKey = pemParser.parse(key); if (privateKey != null) { return privateKey; } @@ -127,14 +123,7 @@ final class PrivateKeyParser { throw new IllegalStateException("Unrecognized private key format"); } catch (Exception ex) { - throw new IllegalStateException("Error loading private key file " + resource, ex); - } - } - - private static String readText(String resource) throws IOException { - URL url = ResourceUtils.getURL(resource); - try (Reader reader = new InputStreamReader(url.openStream())) { - return FileCopyUtils.copyToString(reader); + throw new IllegalStateException("Error loading private key file: " + ex.getMessage(), ex); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java new file mode 100644 index 0000000000..dc857bf66f --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java @@ -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]); + } + } + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java new file mode 100644 index 0000000000..978d14a1b4 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/package-info.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/package-info.java new file mode 100644 index 0000000000..50e912f18a --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/package-info.java @@ -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; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java index 223058f720..3e8049f4d7 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.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. @@ -43,6 +43,7 @@ import org.eclipse.jetty.util.thread.ThreadPool; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory; import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; import org.springframework.boot.web.server.Shutdown; +import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.WebServer; import org.springframework.http.client.reactive.JettyResourceFactory; import org.springframework.http.server.reactive.HttpHandler; @@ -179,7 +180,7 @@ public class JettyReactiveWebServerFactory extends AbstractReactiveWebServerFact contextHandler.addServlet(servletHolder, "/"); server.setHandler(addHandlerWrappers(contextHandler)); JettyReactiveWebServerFactory.logger.info("Server initialized with port: " + port); - if (getSsl() != null && getSsl().isEnabled()) { + if (Ssl.isEnabled(getSsl())) { customizeSsl(server, address); } for (JettyServerCustomizer customizer : getServerCustomizers()) { @@ -236,7 +237,7 @@ public class JettyReactiveWebServerFactory extends AbstractReactiveWebServerFact } private void customizeSsl(Server server, InetSocketAddress address) { - new SslServerCustomizer(address, getSsl(), getOrCreateSslStoreProvider(), getHttp2()).customize(server); + new SslServerCustomizer(getHttp2(), address, getSsl().getClientAuth(), getSslBundle()).customize(server); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java index 7c6e82fbed..e060a372f0 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java @@ -74,6 +74,7 @@ import org.springframework.boot.web.server.Cookie.SameSite; import org.springframework.boot.web.server.ErrorPage; import org.springframework.boot.web.server.MimeMappings; import org.springframework.boot.web.server.Shutdown; +import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.servlet.ServletContextInitializer; import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory; @@ -162,7 +163,7 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor configureWebAppContext(context, initializers); server.setHandler(addHandlerWrappers(context)); this.logger.info("Server initialized with port: " + port); - if (getSsl() != null && getSsl().isEnabled()) { + if (Ssl.isEnabled(getSsl())) { customizeSsl(server, address); } for (JettyServerCustomizer customizer : getServerCustomizers()) { @@ -220,7 +221,7 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor } private void customizeSsl(Server server, InetSocketAddress address) { - new SslServerCustomizer(address, getSsl(), getOrCreateSslStoreProvider(), getHttp2()).customize(server); + new SslServerCustomizer(getHttp2(), address, getSsl().getClientAuth(), getSslBundle()).customize(server); } /** 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 11b4e68bbe..4d1a5b6866 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 @@ -32,12 +32,15 @@ import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundleKey; +import org.springframework.boot.ssl.SslOptions; +import org.springframework.boot.ssl.SslStoreBundle; 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.Ssl.ClientAuth; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; /** @@ -51,18 +54,18 @@ import org.springframework.util.ObjectUtils; */ class SslServerCustomizer implements JettyServerCustomizer { - private final InetSocketAddress address; - - private final Ssl ssl; - - private final SslStoreProvider sslStoreProvider; - private final Http2 http2; - SslServerCustomizer(InetSocketAddress address, Ssl ssl, SslStoreProvider sslStoreProvider, Http2 http2) { + private final InetSocketAddress address; + + private final ClientAuth clientAuth; + + private final SslBundle sslBundle; + + SslServerCustomizer(Http2 http2, InetSocketAddress address, ClientAuth clientAuth, SslBundle sslBundle) { this.address = address; - this.ssl = ssl; - this.sslStoreProvider = sslStoreProvider; + this.clientAuth = clientAuth; + this.sslBundle = sslBundle; this.http2 = http2; } @@ -70,41 +73,42 @@ class SslServerCustomizer implements JettyServerCustomizer { public void customize(Server server) { SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); sslContextFactory.setEndpointIdentificationAlgorithm(null); - configureSsl(sslContextFactory, this.ssl, this.sslStoreProvider); - ServerConnector connector = createConnector(server, sslContextFactory, this.address); + configureSsl(sslContextFactory, this.clientAuth); + ServerConnector connector = createConnector(server, sslContextFactory); server.setConnectors(new Connector[] { connector }); } - private ServerConnector createConnector(Server server, SslContextFactory.Server sslContextFactory, - InetSocketAddress address) { + private ServerConnector createConnector(Server server, SslContextFactory.Server sslContextFactory) { HttpConfiguration config = new HttpConfiguration(); config.setSendServerVersion(false); config.setSecureScheme("https"); - config.setSecurePort(address.getPort()); + config.setSecurePort(this.address.getPort()); config.addCustomizer(new SecureRequestCustomizer()); ServerConnector connector = createServerConnector(server, sslContextFactory, config); - connector.setPort(address.getPort()); - connector.setHost(address.getHostString()); + connector.setPort(this.address.getPort()); + connector.setHost(this.address.getHostString()); return connector; } private ServerConnector createServerConnector(Server server, SslContextFactory.Server sslContextFactory, HttpConfiguration config) { if (this.http2 == null || !this.http2.isEnabled()) { - return createHttp11ServerConnector(server, config, sslContextFactory); + return createHttp11ServerConnector(config, sslContextFactory, server); } Assert.state(isJettyAlpnPresent(), () -> "An 'org.eclipse.jetty:jetty-alpn-*-server' dependency is required for HTTP/2 support."); Assert.state(isJettyHttp2Present(), () -> "The 'org.eclipse.jetty.http2:http2-server' dependency is required for HTTP/2 support."); - return createHttp2ServerConnector(server, config, sslContextFactory); + return createHttp2ServerConnector(config, sslContextFactory, server); } - private ServerConnector createHttp11ServerConnector(Server server, HttpConfiguration config, - SslContextFactory.Server sslContextFactory) { + private ServerConnector createHttp11ServerConnector(HttpConfiguration config, + SslContextFactory.Server sslContextFactory, Server server) { + SslConnectionFactory sslConnectionFactory = createSslConnectionFactory(sslContextFactory, + HttpVersion.HTTP_1_1.asString()); HttpConnectionFactory connectionFactory = new HttpConnectionFactory(config); - return new SslValidatingServerConnector(server, sslContextFactory, this.ssl.getKeyAlias(), - createSslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()), connectionFactory); + return new SslValidatingServerConnector(this.sslBundle.getKey(), sslContextFactory, server, + sslConnectionFactory, connectionFactory); } private SslConnectionFactory createSslConnectionFactory(SslContextFactory.Server sslContextFactory, @@ -132,8 +136,8 @@ class SslServerCustomizer implements JettyServerCustomizer { return ClassUtils.isPresent("org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory", null); } - private ServerConnector createHttp2ServerConnector(Server server, HttpConfiguration config, - SslContextFactory.Server sslContextFactory) { + private ServerConnector createHttp2ServerConnector(HttpConfiguration config, + SslContextFactory.Server sslContextFactory, Server server) { HttpConnectionFactory http = new HttpConnectionFactory(config); HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(config); ALPNServerConnectionFactory alpn = createAlpnServerConnectionFactory(); @@ -141,8 +145,9 @@ class SslServerCustomizer implements JettyServerCustomizer { if (isConscryptPresent()) { sslContextFactory.setProvider("Conscrypt"); } - SslConnectionFactory ssl = createSslConnectionFactory(sslContextFactory, alpn.getProtocol()); - return new SslValidatingServerConnector(server, sslContextFactory, this.ssl.getKeyAlias(), ssl, alpn, h2, http); + SslConnectionFactory sslConnectionFactory = createSslConnectionFactory(sslContextFactory, alpn.getProtocol()); + return new SslValidatingServerConnector(this.sslBundle.getKey(), sslContextFactory, server, + sslConnectionFactory, alpn, h2, http); } private ALPNServerConnectionFactory createAlpnServerConnectionFactory() { @@ -163,53 +168,40 @@ class SslServerCustomizer implements JettyServerCustomizer { /** * Configure the SSL connection. * @param factory the Jetty {@link Server SslContextFactory.Server}. - * @param ssl the ssl details. - * @param sslStoreProvider the ssl store provider + * @param clientAuth the client authentication mode */ - protected void configureSsl(SslContextFactory.Server factory, Ssl ssl, SslStoreProvider sslStoreProvider) { - factory.setProtocol(ssl.getProtocol()); - configureSslClientAuth(factory, ssl); - configureSslPasswords(factory, ssl); - factory.setCertAlias(ssl.getKeyAlias()); - if (!ObjectUtils.isEmpty(ssl.getCiphers())) { - factory.setIncludeCipherSuites(ssl.getCiphers()); + protected void configureSsl(SslContextFactory.Server factory, ClientAuth clientAuth) { + SslBundleKey key = this.sslBundle.getKey(); + SslOptions options = this.sslBundle.getOptions(); + SslStoreBundle stores = this.sslBundle.getStores(); + factory.setProtocol(this.sslBundle.getProtocol()); + configureSslClientAuth(factory, clientAuth); + if (stores.getKeyStorePassword() != null) { + factory.setKeyStorePassword(stores.getKeyStorePassword()); + } + factory.setCertAlias(key.getAlias()); + if (!ObjectUtils.isEmpty(options.getCiphers())) { + factory.setIncludeCipherSuites(options.getCiphers().toArray(String[]::new)); factory.setExcludeCipherSuites(); } - if (ssl.getEnabledProtocols() != null) { - factory.setIncludeProtocols(ssl.getEnabledProtocols()); + if (!CollectionUtils.isEmpty(options.getEnabledProtocols())) { + factory.setIncludeProtocols(options.getEnabledProtocols().toArray(String[]::new)); } - if (sslStoreProvider != null) { - try { - String keyPassword = sslStoreProvider.getKeyPassword(); - if (keyPassword != null) { - factory.setKeyManagerPassword(keyPassword); - } - factory.setKeyStore(sslStoreProvider.getKeyStore()); - factory.setTrustStore(sslStoreProvider.getTrustStore()); - } - catch (Exception ex) { - throw new IllegalStateException("Unable to set SSL store: " + ex.getMessage(), ex); + try { + if (key.getPassword() != null) { + factory.setKeyManagerPassword(key.getPassword()); } + factory.setKeyStore(stores.getKeyStore()); + factory.setTrustStore(stores.getTrustStore()); + } + catch (Exception ex) { + throw new IllegalStateException("Unable to set SSL store: " + ex.getMessage(), ex); } } - private void configureSslClientAuth(SslContextFactory.Server factory, Ssl ssl) { - if (ssl.getClientAuth() == Ssl.ClientAuth.NEED) { - factory.setNeedClientAuth(true); - factory.setWantClientAuth(true); - } - else if (ssl.getClientAuth() == Ssl.ClientAuth.WANT) { - factory.setWantClientAuth(true); - } - } - - private void configureSslPasswords(SslContextFactory.Server factory, Ssl ssl) { - if (ssl.getKeyStorePassword() != null) { - factory.setKeyStorePassword(ssl.getKeyStorePassword()); - } - if (ssl.getKeyPassword() != null) { - factory.setKeyManagerPassword(ssl.getKeyPassword()); - } + private void configureSslClientAuth(SslContextFactory.Server factory, ClientAuth clientAuth) { + factory.setWantClientAuth(clientAuth == ClientAuth.WANT || clientAuth == ClientAuth.NEED); + factory.setNeedClientAuth(clientAuth == ClientAuth.NEED); } /** @@ -217,28 +209,28 @@ class SslServerCustomizer implements JettyServerCustomizer { */ static class SslValidatingServerConnector extends ServerConnector { + private final SslBundleKey key; + private final SslContextFactory sslContextFactory; - private final String keyAlias; - - SslValidatingServerConnector(Server server, SslContextFactory sslContextFactory, String keyAlias, + SslValidatingServerConnector(SslBundleKey key, SslContextFactory sslContextFactory, Server server, SslConnectionFactory sslConnectionFactory, HttpConnectionFactory connectionFactory) { super(server, sslConnectionFactory, connectionFactory); + this.key = key; this.sslContextFactory = sslContextFactory; - this.keyAlias = keyAlias; } - SslValidatingServerConnector(Server server, SslContextFactory sslContextFactory, String keyAlias, + SslValidatingServerConnector(SslBundleKey keyAlias, SslContextFactory sslContextFactory, Server server, ConnectionFactory... factories) { super(server, factories); + this.key = keyAlias; this.sslContextFactory = sslContextFactory; - this.keyAlias = keyAlias; } @Override protected void doStart() throws Exception { super.doStart(); - SslConfigurationValidator.validateKeyAlias(this.sslContextFactory.getKeyStore(), this.keyAlias); + this.key.assertContainsAlias(this.sslContextFactory.getKeyStore()); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java index 3ada700326..f351da622f 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.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. @@ -32,6 +32,7 @@ import reactor.netty.resources.LoopResources; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory; import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; import org.springframework.boot.web.server.Shutdown; +import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.WebServer; import org.springframework.http.client.reactive.ReactorResourceFactory; import org.springframework.http.server.reactive.HttpHandler; @@ -166,7 +167,7 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact else { server = server.bindAddress(this::getListenAddress); } - if (getSsl() != null && getSsl().isEnabled()) { + if (Ssl.isEnabled(getSsl())) { server = customizeSslConfiguration(server); } if (getCompression() != null && getCompression().getEnabled()) { @@ -177,11 +178,8 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact return applyCustomizers(server); } - @SuppressWarnings("deprecation") private HttpServer customizeSslConfiguration(HttpServer httpServer) { - SslServerCustomizer sslServerCustomizer = new SslServerCustomizer(getSsl(), getHttp2(), - getOrCreateSslStoreProvider()); - return sslServerCustomizer.apply(httpServer); + return new SslServerCustomizer(getHttp2(), getSsl().getClientAuth(), getSslBundle()).apply(httpServer); } private HttpProtocol[] listProtocols() { 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 09b42b6fa1..f764d8273a 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,35 +16,17 @@ package org.springframework.boot.web.embedded.netty; -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.TrustManagerFactory; -import javax.net.ssl.X509ExtendedKeyManager; - import io.netty.handler.ssl.ClientAuth; import reactor.netty.http.Http11SslContextSpec; import reactor.netty.http.Http2SslContextSpec; import reactor.netty.http.server.HttpServer; import reactor.netty.tcp.AbstractProtocolSslContextSpec; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslOptions; 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.util.CollectionUtils; /** * {@link NettyServerCustomizer} that configures SSL for the given Reactor Netty server @@ -56,21 +38,19 @@ import org.springframework.boot.web.server.SslStoreProvider; * @author Cyril Dangerville * @author Scott Frederick * @since 2.0.0 - * @deprecated this class is meant for Spring Boot internal use only. */ -@Deprecated(since = "2.0.0", forRemoval = false) public class SslServerCustomizer implements NettyServerCustomizer { - private final Ssl ssl; - private final Http2 http2; - private final SslStoreProvider sslStoreProvider; + private final Ssl.ClientAuth clientAuth; - public SslServerCustomizer(Ssl ssl, Http2 http2, SslStoreProvider sslStoreProvider) { - this.ssl = ssl; + private final SslBundle sslBundle; + + public SslServerCustomizer(Http2 http2, Ssl.ClientAuth clientAuth, SslBundle sslBundle) { this.http2 = http2; - this.sslStoreProvider = sslStoreProvider; + this.clientAuth = clientAuth; + this.sslBundle = sslBundle; } @Override @@ -80,172 +60,22 @@ public class SslServerCustomizer implements NettyServerCustomizer { } protected AbstractProtocolSslContextSpec createSslContextSpec() { - AbstractProtocolSslContextSpec sslContextSpec; - if (this.http2 != null && this.http2.isEnabled()) { - sslContextSpec = Http2SslContextSpec.forServer(getKeyManagerFactory(this.ssl, this.sslStoreProvider)); - } - else { - sslContextSpec = Http11SslContextSpec.forServer(getKeyManagerFactory(this.ssl, this.sslStoreProvider)); - } + AbstractProtocolSslContextSpec sslContextSpec = (this.http2 != null && this.http2.isEnabled()) + ? Http2SslContextSpec.forServer(this.sslBundle.getManagers().getKeyManagerFactory()) + : Http11SslContextSpec.forServer(this.sslBundle.getManagers().getKeyManagerFactory()); sslContextSpec.configure((builder) -> { - builder.trustManager(getTrustManagerFactory(this.sslStoreProvider)); - if (this.ssl.getEnabledProtocols() != null) { - builder.protocols(this.ssl.getEnabledProtocols()); + builder.trustManager(this.sslBundle.getManagers().getTrustManagerFactory()); + SslOptions options = this.sslBundle.getOptions(); + if (!CollectionUtils.isEmpty(options.getEnabledProtocols())) { + builder.protocols(options.getEnabledProtocols()); } - if (this.ssl.getCiphers() != null) { - builder.ciphers(Arrays.asList(this.ssl.getCiphers())); - } - if (this.ssl.getClientAuth() == Ssl.ClientAuth.NEED) { - builder.clientAuth(ClientAuth.REQUIRE); - } - else if (this.ssl.getClientAuth() == Ssl.ClientAuth.WANT) { - builder.clientAuth(ClientAuth.OPTIONAL); + if (!CollectionUtils.isEmpty(options.getCiphers())) { + builder.ciphers(options.getCiphers()); } + builder.clientAuth(org.springframework.boot.web.server.Ssl.ClientAuth.map(this.clientAuth, ClientAuth.NONE, + ClientAuth.OPTIONAL, ClientAuth.REQUIRE)); }); return sslContextSpec; } - KeyManagerFactory getKeyManagerFactory(Ssl ssl, SslStoreProvider sslStoreProvider) { - try { - 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.getKeyPassword(); - if (keyPassword == null) { - keyPassword = (ssl.getKeyPassword() != null) ? ssl.getKeyPassword() : ssl.getKeyStorePassword(); - } - keyManagerFactory.init(keyStore, (keyPassword != null) ? keyPassword.toCharArray() : null); - return keyManagerFactory; - } - catch (Exception ex) { - throw new IllegalStateException("Could not load key manager factory: " + ex.getMessage(), ex); - } - } - - TrustManagerFactory getTrustManagerFactory(SslStoreProvider sslStoreProvider) { - try { - KeyStore store = sslStoreProvider.getTrustStore(); - TrustManagerFactory trustManagerFactory = TrustManagerFactory - .getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(store); - return trustManagerFactory; - } - catch (Exception ex) { - throw new IllegalStateException("Could not load trust manager factory: " + ex.getMessage(), ex); - } - } - - /** - * A {@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 ConfigurableAliasKeyManager}. The actual SPI has to be wrapped as well due - * to the fact that {@link KeyManagerFactory#getKeyManagers()} is final. - */ - private static final class ConfigurableAliasKeyManagerFactory extends KeyManagerFactory { - - private ConfigurableAliasKeyManagerFactory(String alias, String algorithm) throws NoSuchAlgorithmException { - this(KeyManagerFactory.getInstance(algorithm), alias, algorithm); - } - - private ConfigurableAliasKeyManagerFactory(KeyManagerFactory delegate, String alias, String algorithm) { - super(new ConfigurableAliasKeyManagerFactorySpi(delegate, alias), delegate.getProvider(), algorithm); - } - - } - - private static final class ConfigurableAliasKeyManagerFactorySpi extends KeyManagerFactorySpi { - - private final KeyManagerFactory delegate; - - private final String alias; - - private ConfigurableAliasKeyManagerFactorySpi(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 ConfigurableAliasKeyManager wrap(X509ExtendedKeyManager keyManager) { - return new ConfigurableAliasKeyManager(keyManager, this.alias); - } - - } - - private static final class ConfigurableAliasKeyManager extends X509ExtendedKeyManager { - - private final X509ExtendedKeyManager delegate; - - private final String alias; - - private ConfigurableAliasKeyManager(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 != null) ? this.alias : this.delegate.chooseEngineServerAlias(s, principals, sslEngine); - } - - @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); - } - - } - } 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 e484915f77..912c039b99 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 @@ -24,10 +24,13 @@ import org.apache.tomcat.util.net.SSLHostConfig; import org.apache.tomcat.util.net.SSLHostConfigCertificate; 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.SslStoreProviderFactory; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundleKey; +import org.springframework.boot.ssl.SslOptions; +import org.springframework.boot.ssl.SslStoreBundle; +import org.springframework.boot.web.server.Ssl.ClientAuth; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** @@ -40,18 +43,13 @@ import org.springframework.util.StringUtils; */ class SslConnectorCustomizer implements TomcatConnectorCustomizer { - private final Ssl ssl; + private final ClientAuth clientAuth; - private final SslStoreProvider sslStoreProvider; + private final SslBundle sslBundle; - 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; - this.sslStoreProvider = sslStoreProvider; + SslConnectorCustomizer(ClientAuth clientAuth, SslBundle sslBundle) { + this.clientAuth = clientAuth; + this.sslBundle = sslBundle; } @Override @@ -59,7 +57,7 @@ class SslConnectorCustomizer implements TomcatConnectorCustomizer { ProtocolHandler handler = connector.getProtocolHandler(); Assert.state(handler instanceof AbstractHttp11JsseProtocol, "To use SSL, the connector's protocol handler must be an AbstractHttp11JsseProtocol subclass"); - configureSsl((AbstractHttp11JsseProtocol) handler, this.ssl, this.sslStoreProvider); + configureSsl((AbstractHttp11JsseProtocol) handler); connector.setScheme("https"); connector.setSecure(true); } @@ -67,68 +65,59 @@ class SslConnectorCustomizer implements TomcatConnectorCustomizer { /** * Configure Tomcat's {@link AbstractHttp11JsseProtocol} for SSL. * @param protocol the protocol - * @param ssl the ssl details - * @param sslStoreProvider the ssl store provider */ - protected void configureSsl(AbstractHttp11JsseProtocol protocol, Ssl ssl, SslStoreProvider sslStoreProvider) { + void configureSsl(AbstractHttp11JsseProtocol protocol) { + SslBundleKey key = this.sslBundle.getKey(); + SslStoreBundle stores = this.sslBundle.getStores(); + SslOptions options = this.sslBundle.getOptions(); protocol.setSSLEnabled(true); SSLHostConfig sslHostConfig = new SSLHostConfig(); sslHostConfig.setHostName(protocol.getDefaultSSLHostConfigName()); - sslHostConfig.setSslProtocol(ssl.getProtocol()); + sslHostConfig.setSslProtocol(this.sslBundle.getProtocol()); protocol.addSslHostConfig(sslHostConfig); - configureSslClientAuth(sslHostConfig, ssl); + configureSslClientAuth(sslHostConfig); SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, Type.UNDEFINED); - if (ssl.getKeyStorePassword() != null) { - certificate.setCertificateKeystorePassword(ssl.getKeyStorePassword()); + String keystorePassword = (stores.getKeyStorePassword() != null) ? stores.getKeyStorePassword() : ""; + certificate.setCertificateKeystorePassword(keystorePassword); + if (key.getPassword() != null) { + certificate.setCertificateKeyPassword(key.getPassword()); } - if (ssl.getKeyPassword() != null) { - certificate.setCertificateKeyPassword(ssl.getKeyPassword()); - } - if (ssl.getKeyAlias() != null) { - certificate.setCertificateKeyAlias(ssl.getKeyAlias()); + if (key.getAlias() != null) { + certificate.setCertificateKeyAlias(key.getAlias()); } sslHostConfig.addCertificate(certificate); - String ciphers = StringUtils.arrayToCommaDelimitedString(ssl.getCiphers()); - if (StringUtils.hasText(ciphers)) { + if (!CollectionUtils.isEmpty(options.getCiphers())) { + String ciphers = StringUtils.collectionToCommaDelimitedString(options.getCiphers()); sslHostConfig.setCiphers(ciphers); } - configureEnabledProtocols(protocol, ssl); - if (sslStoreProvider != null) { - configureSslStoreProvider(protocol, sslHostConfig, certificate, sslStoreProvider); - String keyPassword = sslStoreProvider.getKeyPassword(); - if (keyPassword != null) { - certificate.setCertificateKeyPassword(keyPassword); - } - } + configureEnabledProtocols(protocol); + configureSslStoreProvider(protocol, sslHostConfig, certificate); } - private void configureEnabledProtocols(AbstractHttp11JsseProtocol protocol, Ssl ssl) { - if (ssl.getEnabledProtocols() != null) { + private void configureEnabledProtocols(AbstractHttp11JsseProtocol protocol) { + SslOptions options = this.sslBundle.getOptions(); + if (!CollectionUtils.isEmpty(options.getEnabledProtocols())) { for (SSLHostConfig sslHostConfig : protocol.findSslHostConfigs()) { - sslHostConfig.setProtocols(StringUtils.arrayToCommaDelimitedString(ssl.getEnabledProtocols())); + sslHostConfig.setProtocols(StringUtils.collectionToCommaDelimitedString(options.getEnabledProtocols())); } } } - private void configureSslClientAuth(SSLHostConfig config, Ssl ssl) { - if (ssl.getClientAuth() == Ssl.ClientAuth.NEED) { - config.setCertificateVerification("required"); - } - else if (ssl.getClientAuth() == Ssl.ClientAuth.WANT) { - config.setCertificateVerification("optional"); - } + private void configureSslClientAuth(SSLHostConfig config) { + config.setCertificateVerification(ClientAuth.map(this.clientAuth, "none", "optional", "required")); } protected void configureSslStoreProvider(AbstractHttp11JsseProtocol protocol, SSLHostConfig sslHostConfig, - SSLHostConfigCertificate certificate, SslStoreProvider sslStoreProvider) { + SSLHostConfigCertificate certificate) { Assert.isInstanceOf(Http11NioProtocol.class, protocol, "SslStoreProvider can only be used with Http11NioProtocol"); try { - if (sslStoreProvider.getKeyStore() != null) { - certificate.setCertificateKeystore(sslStoreProvider.getKeyStore()); + SslStoreBundle stores = this.sslBundle.getStores(); + if (stores.getKeyStore() != null) { + certificate.setCertificateKeystore(stores.getKeyStore()); } - if (sslStoreProvider.getTrustStore() != null) { - sslHostConfig.setTrustStore(sslStoreProvider.getTrustStore()); + if (stores.getTrustStore() != null) { + sslHostConfig.setTrustStore(stores.getTrustStore()); } } catch (Exception ex) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java index 5f3fcfba64..ab31ecce73 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java @@ -44,6 +44,7 @@ import org.apache.tomcat.util.scan.StandardJarScanFilter; import org.springframework.boot.util.LambdaSafe; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory; import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; +import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.WebServer; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.TomcatHttpHandlerAdapter; @@ -199,7 +200,7 @@ public class TomcatReactiveWebServerFactory extends AbstractReactiveWebServerFac if (getHttp2() != null && getHttp2().isEnabled()) { connector.addUpgradeProtocol(new Http2Protocol()); } - if (getSsl() != null && getSsl().isEnabled()) { + if (Ssl.isEnabled(getSsl())) { customizeSsl(connector); } TomcatConnectorCustomizer compression = new CompressionConnectorCustomizer(getCompression()); @@ -223,7 +224,7 @@ public class TomcatReactiveWebServerFactory extends AbstractReactiveWebServerFac } private void customizeSsl(Connector connector) { - new SslConnectorCustomizer(getSsl(), getOrCreateSslStoreProvider()).customize(connector); + new SslConnectorCustomizer(getSsl().getClientAuth(), getSslBundle()).customize(connector); } @Override diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java index 746b6bd1bc..ca5e6fa1d4 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java @@ -71,6 +71,7 @@ import org.springframework.boot.util.LambdaSafe; import org.springframework.boot.web.server.Cookie.SameSite; import org.springframework.boot.web.server.ErrorPage; import org.springframework.boot.web.server.MimeMappings; +import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.servlet.ServletContextInitializer; import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory; @@ -339,7 +340,7 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto if (getHttp2() != null && getHttp2().isEnabled()) { connector.addUpgradeProtocol(new Http2Protocol()); } - if (getSsl() != null && getSsl().isEnabled()) { + if (Ssl.isEnabled(getSsl())) { customizeSsl(connector); } TomcatConnectorCustomizer compression = new CompressionConnectorCustomizer(getCompression()); @@ -363,7 +364,7 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto } private void customizeSsl(Connector connector) { - new SslConnectorCustomizer(getSsl(), getOrCreateSslStoreProvider()).customize(connector); + new SslConnectorCustomizer(getSsl().getClientAuth(), getSslBundle()).customize(connector); } /** 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 5a88ddda27..fe14fe96ac 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 @@ -17,30 +17,18 @@ package org.springframework.boot.web.embedded.undertow; import java.net.InetAddress; -import java.net.Socket; -import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.NoSuchAlgorithmException; -import java.security.Principal; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import javax.net.ssl.KeyManager; -import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509ExtendedKeyManager; import io.undertow.Undertow; import org.xnio.Options; import org.xnio.Sequence; 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.ssl.SslBundle; +import org.springframework.boot.ssl.SslOptions; +import org.springframework.boot.web.server.Ssl.ClientAuth; +import org.springframework.util.CollectionUtils; /** * {@link UndertowBuilderCustomizer} that configures SSL on the given builder instance. @@ -56,34 +44,29 @@ class SslBuilderCustomizer implements UndertowBuilderCustomizer { private final InetAddress address; - private final Ssl ssl; + private final ClientAuth clientAuth; - private final SslStoreProvider sslStoreProvider; + private final SslBundle sslBundle; - SslBuilderCustomizer(int port, InetAddress address, Ssl ssl, SslStoreProvider sslStoreProvider) { + SslBuilderCustomizer(int port, InetAddress address, ClientAuth clientAuth, SslBundle sslBundle) { this.port = port; this.address = address; - this.ssl = ssl; - this.sslStoreProvider = sslStoreProvider; + this.clientAuth = clientAuth; + this.sslBundle = sslBundle; } @Override public void customize(Undertow.Builder builder) { - try { - SSLContext sslContext = SSLContext.getInstance(this.ssl.getProtocol()); - 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) { - builder.setSocketOption(Options.SSL_ENABLED_PROTOCOLS, Sequence.of(this.ssl.getEnabledProtocols())); - } - if (this.ssl.getCiphers() != null) { - builder.setSocketOption(Options.SSL_ENABLED_CIPHER_SUITES, Sequence.of(this.ssl.getCiphers())); - } + SslOptions options = this.sslBundle.getOptions(); + SSLContext sslContext = this.sslBundle.createSslContext(); + builder.addHttpsListener(this.port, getListenAddress(), sslContext); + builder.setSocketOption(Options.SSL_CLIENT_AUTH_MODE, ClientAuth.map(this.clientAuth, + SslClientAuthMode.NOT_REQUESTED, SslClientAuthMode.REQUESTED, SslClientAuthMode.REQUIRED)); + if (!CollectionUtils.isEmpty(options.getEnabledProtocols())) { + builder.setSocketOption(Options.SSL_ENABLED_PROTOCOLS, Sequence.of(options.getEnabledProtocols())); } - catch (NoSuchAlgorithmException | KeyManagementException ex) { - throw new IllegalStateException(ex); + if (!CollectionUtils.isEmpty(options.getCiphers())) { + builder.setSocketOption(Options.SSL_ENABLED_CIPHER_SUITES, Sequence.of(options.getCiphers())); } } @@ -94,117 +77,4 @@ class SslBuilderCustomizer implements UndertowBuilderCustomizer { return this.address.getHostAddress(); } - private SslClientAuthMode getSslClientAuthMode(Ssl ssl) { - if (ssl.getClientAuth() == Ssl.ClientAuth.NEED) { - return SslClientAuthMode.REQUIRED; - } - if (ssl.getClientAuth() == Ssl.ClientAuth.WANT) { - return SslClientAuthMode.REQUESTED; - } - return SslClientAuthMode.NOT_REQUESTED; - } - - KeyManager[] getKeyManagers(Ssl ssl, SslStoreProvider sslStoreProvider) { - try { - KeyStore keyStore = sslStoreProvider.getKeyStore(); - SslConfigurationValidator.validateKeyAlias(keyStore, ssl.getKeyAlias()); - KeyManagerFactory keyManagerFactory = KeyManagerFactory - .getInstance(KeyManagerFactory.getDefaultAlgorithm()); - String keyPassword = sslStoreProvider.getKeyPassword(); - if (keyPassword == null) { - keyPassword = (ssl.getKeyPassword() != null) ? ssl.getKeyPassword() : ssl.getKeyStorePassword(); - } - keyManagerFactory.init(keyStore, (keyPassword != null) ? keyPassword.toCharArray() : null); - if (ssl.getKeyAlias() != null) { - return getConfigurableAliasKeyManagers(ssl, keyManagerFactory.getKeyManagers()); - } - return keyManagerFactory.getKeyManagers(); - } - catch (Exception ex) { - throw new IllegalStateException("Could not load key managers: " + ex.getMessage(), ex); - } - } - - private KeyManager[] getConfigurableAliasKeyManagers(Ssl ssl, KeyManager[] keyManagers) { - for (int i = 0; i < keyManagers.length; i++) { - if (keyManagers[i] instanceof X509ExtendedKeyManager) { - keyManagers[i] = new ConfigurableAliasKeyManager((X509ExtendedKeyManager) keyManagers[i], - ssl.getKeyAlias()); - } - } - return keyManagers; - } - - TrustManager[] getTrustManagers(SslStoreProvider sslStoreProvider) { - try { - KeyStore store = sslStoreProvider.getTrustStore(); - TrustManagerFactory trustManagerFactory = TrustManagerFactory - .getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(store); - return trustManagerFactory.getTrustManagers(); - } - catch (Exception ex) { - throw new IllegalStateException("Could not load trust managers: " + ex.getMessage(), ex); - } - } - - /** - * {@link X509ExtendedKeyManager} that supports custom alias configuration. - */ - private static class ConfigurableAliasKeyManager extends X509ExtendedKeyManager { - - private final X509ExtendedKeyManager keyManager; - - private final String alias; - - ConfigurableAliasKeyManager(X509ExtendedKeyManager keyManager, String alias) { - this.keyManager = keyManager; - this.alias = alias; - } - - @Override - public String chooseEngineClientAlias(String[] strings, Principal[] principals, SSLEngine sslEngine) { - return this.keyManager.chooseEngineClientAlias(strings, principals, sslEngine); - } - - @Override - public String chooseEngineServerAlias(String s, Principal[] principals, SSLEngine sslEngine) { - if (this.alias == null) { - return this.keyManager.chooseEngineServerAlias(s, principals, sslEngine); - } - return this.alias; - } - - @Override - public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { - return this.keyManager.chooseClientAlias(keyType, issuers, socket); - } - - @Override - public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { - return this.keyManager.chooseServerAlias(keyType, issuers, socket); - } - - @Override - public X509Certificate[] getCertificateChain(String alias) { - return this.keyManager.getCertificateChain(alias); - } - - @Override - public String[] getClientAliases(String keyType, Principal[] issuers) { - return this.keyManager.getClientAliases(keyType, issuers); - } - - @Override - public PrivateKey getPrivateKey(String alias) { - return this.keyManager.getPrivateKey(alias); - } - - @Override - public String[] getServerAliases(String keyType, Principal[] issuers) { - return this.keyManager.getServerAliases(keyType, issuers); - } - - } - } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactory.java index 2840d36edf..75b3ab9966 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactory.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. @@ -137,7 +137,7 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF @Override public WebServer getWebServer(org.springframework.http.server.reactive.HttpHandler httpHandler) { - Undertow.Builder builder = this.delegate.createBuilder(this); + Undertow.Builder builder = this.delegate.createBuilder(this, this::getSslBundle); List httpHandlerFactories = this.delegate.createHttpHandlerFactories(this, (next) -> new UndertowHttpHandlerAdapter(httpHandler)); return new UndertowWebServer(builder, httpHandlerFactories, getPort() >= 0); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java index e457f75544..ae43901c8a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java @@ -294,7 +294,7 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac @Override public WebServer getWebServer(ServletContextInitializer... initializers) { - Builder builder = this.delegate.createBuilder(this); + Builder builder = this.delegate.createBuilder(this, this::getSslBundle); DeploymentManager manager = createManager(initializers); return getUndertowWebServer(builder, manager, getPort()); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServerFactoryDelegate.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServerFactoryDelegate.java index 084dbde662..742e332f14 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServerFactoryDelegate.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServerFactoryDelegate.java @@ -24,12 +24,14 @@ import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.function.Supplier; import io.undertow.Handlers; import io.undertow.Undertow; import io.undertow.Undertow.Builder; import io.undertow.UndertowOptions; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.web.server.AbstractConfigurableWebServerFactory; import org.springframework.boot.web.server.Compression; import org.springframework.boot.web.server.Http2; @@ -141,8 +143,7 @@ class UndertowWebServerFactoryDelegate { return this.useForwardHeaders; } - Builder createBuilder(AbstractConfigurableWebServerFactory factory) { - Ssl ssl = factory.getSsl(); + Builder createBuilder(AbstractConfigurableWebServerFactory factory, Supplier sslBundleSupplier) { InetAddress address = factory.getAddress(); int port = factory.getPort(); Builder builder = Undertow.builder(); @@ -162,8 +163,9 @@ class UndertowWebServerFactoryDelegate { if (http2 != null) { builder.setServerOption(UndertowOptions.ENABLE_HTTP2, http2.isEnabled()); } - if (ssl != null && ssl.isEnabled()) { - new SslBuilderCustomizer(factory.getPort(), address, ssl, factory.getOrCreateSslStoreProvider()) + Ssl ssl = factory.getSsl(); + if (Ssl.isEnabled(ssl)) { + new SslBuilderCustomizer(factory.getPort(), address, ssl.getClientAuth(), sslBundleSupplier.get()) .customize(builder); } else { 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 1e51089603..4a23afd753 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 @@ -24,6 +24,8 @@ import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Set; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; import org.springframework.util.Assert; /** @@ -49,8 +51,11 @@ public abstract class AbstractConfigurableWebServerFactory implements Configurab private Ssl ssl; + @SuppressWarnings("removal") private SslStoreProvider sslStoreProvider; + private SslBundles sslBundles; + private Http2 http2; private Compression compression; @@ -130,15 +135,22 @@ public abstract class AbstractConfigurableWebServerFactory implements Configurab this.ssl = ssl; } + @SuppressWarnings("removal") public SslStoreProvider getSslStoreProvider() { return this.sslStoreProvider; } @Override + @SuppressWarnings("removal") public void setSslStoreProvider(SslStoreProvider sslStoreProvider) { this.sslStoreProvider = sslStoreProvider; } + @Override + public void setSslBundles(SslBundles sslBundles) { + this.sslBundles = sslBundles; + } + public Http2 getHttp2() { return this.http2; } @@ -184,12 +196,24 @@ public abstract class AbstractConfigurableWebServerFactory implements Configurab * Return the provided {@link SslStoreProvider} or create one using {@link Ssl} * properties. * @return the {@code SslStoreProvider} + * @deprecated since 3.1.0 for removal in 3.3.0 in favor of {@link #getSslBundle()} */ + @Deprecated(since = "3.1.0", forRemoval = true) + @SuppressWarnings("removal") public final SslStoreProvider getOrCreateSslStoreProvider() { if (this.sslStoreProvider != null) { return this.sslStoreProvider; } - return SslStoreProviderFactory.from(this.ssl); + return CertificateFileSslStoreProvider.from(this.ssl); + } + + /** + * Return the {@link SslBundle} that should be used with this server. + * @return the SSL bundle + */ + @SuppressWarnings("removal") + protected final SslBundle getSslBundle() { + return WebServerSslBundle.get(this.ssl, this.sslBundles, this.sslStoreProvider); } /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/CertificateFileSslStoreProvider.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/CertificateFileSslStoreProvider.java index 2041759e54..d3ed1a091e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/CertificateFileSslStoreProvider.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/CertificateFileSslStoreProvider.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,12 +16,10 @@ package org.springframework.boot.web.server; -import java.io.IOException; -import java.security.GeneralSecurityException; import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; + +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.pem.PemSslStoreBundle; /** * An {@link SslStoreProvider} that creates key and trust stores from certificate and @@ -29,82 +27,32 @@ import java.security.cert.X509Certificate; * * @author Scott Frederick * @since 2.7.0 + * @deprecated since 3.1.0 for removal in 3.3.0 in favor of registering a + * {@link SslBundle} backed by a {@link PemSslStoreBundle}. */ +@Deprecated(since = "3.1.0", forRemoval = true) +@SuppressWarnings({ "deprecation", "removal" }) public final class CertificateFileSslStoreProvider implements SslStoreProvider { - /** - * The password of the private key entry in the {@link #getKeyStore provided - * KeyStore}. - */ - private static final String KEY_PASSWORD = ""; + private final SslBundle delegate; - private static final String DEFAULT_KEY_ALIAS = "spring-boot-web"; - - private final Ssl ssl; - - private CertificateFileSslStoreProvider(Ssl ssl) { - this.ssl = ssl; + private CertificateFileSslStoreProvider(SslBundle delegate) { + this.delegate = delegate; } @Override public KeyStore getKeyStore() throws Exception { - return createKeyStore(this.ssl.getCertificate(), this.ssl.getCertificatePrivateKey(), - this.ssl.getKeyStoreType(), this.ssl.getKeyAlias()); + return this.delegate.getStores().getKeyStore(); } @Override public KeyStore getTrustStore() throws Exception { - if (this.ssl.getTrustCertificate() == null) { - return null; - } - return createKeyStore(this.ssl.getTrustCertificate(), this.ssl.getTrustCertificatePrivateKey(), - this.ssl.getTrustStoreType(), this.ssl.getKeyAlias()); + return this.delegate.getStores().getTrustStore(); } @Override public String getKeyPassword() { - return KEY_PASSWORD; - } - - /** - * Create a new {@link KeyStore} populated with the certificate stored at the - * specified file path and an optional private key. - * @param certPath the path to the certificate authority file - * @param keyPath the path to the private file - * @param storeType the {@code KeyStore} type to create - * @param keyAlias the alias to use when adding keys to the {@code KeyStore} - * @return the {@code KeyStore} - */ - private KeyStore createKeyStore(String certPath, String keyPath, String storeType, String keyAlias) { - try { - KeyStore keyStore = KeyStore.getInstance((storeType != null) ? storeType : KeyStore.getDefaultType()); - keyStore.load(null); - X509Certificate[] certificates = CertificateParser.parse(certPath); - PrivateKey privateKey = (keyPath != null) ? PrivateKeyParser.parse(keyPath) : null; - try { - addCertificates(keyStore, certificates, privateKey, keyAlias); - } - catch (KeyStoreException ex) { - throw new IllegalStateException("Error adding certificates to KeyStore: " + ex.getMessage(), ex); - } - return keyStore; - } - catch (GeneralSecurityException | IOException ex) { - throw new IllegalStateException("Error creating KeyStore: " + ex.getMessage(), ex); - } - } - - private void addCertificates(KeyStore keyStore, X509Certificate[] certificates, PrivateKey privateKey, - String keyAlias) throws KeyStoreException { - String alias = (keyAlias != null) ? keyAlias : DEFAULT_KEY_ALIAS; - if (privateKey != null) { - keyStore.setKeyEntry(alias, privateKey, KEY_PASSWORD.toCharArray(), certificates); - } - else { - for (int index = 0; index < certificates.length; index++) { - keyStore.setCertificateEntry(alias + "-" + index, certificates[index]); - } - } + return this.delegate.getKey().getPassword(); } /** @@ -114,12 +62,8 @@ public final class CertificateFileSslStoreProvider implements SslStoreProvider { * @return an {@code SslStoreProvider} or {@code null} */ public static SslStoreProvider from(Ssl ssl) { - if (ssl != null && ssl.isEnabled()) { - if (ssl.getCertificate() != null && ssl.getCertificatePrivateKey() != null) { - return new CertificateFileSslStoreProvider(ssl); - } - } - return null; + SslBundle delegate = WebServerSslBundle.createCertificateFileSslStoreProviderDelegate(ssl); + return (delegate != null) ? new CertificateFileSslStoreProvider(delegate) : null; } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/ConfigurableWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/ConfigurableWebServerFactory.java index ba3914d812..c10580aa3d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/ConfigurableWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/ConfigurableWebServerFactory.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. @@ -19,11 +19,14 @@ package org.springframework.boot.web.server; import java.net.InetAddress; import java.util.Set; +import org.springframework.boot.ssl.SslBundles; + /** * A configurable {@link WebServerFactory}. * * @author Phillip Webb * @author Brian Clozel + * @author Scott Frederick * @since 2.0.0 * @see ErrorPageRegistry */ @@ -58,9 +61,20 @@ public interface ConfigurableWebServerFactory extends WebServerFactory, ErrorPag /** * Sets a provider that will be used to obtain SSL stores. * @param sslStoreProvider the SSL store provider + * @deprecated since 3.1.0 for removal in 3.3.0, in favor of + * {@link #setSslBundles(SslBundles)} */ + @Deprecated(since = "3.1.0", forRemoval = true) + @SuppressWarnings("removal") void setSslStoreProvider(SslStoreProvider sslStoreProvider); + /** + * Sets the SSL bundles that can be used to configure SSL connections. + * @param sslBundles the SSL bundles + * @since 3.1.0 + */ + void setSslBundles(SslBundles sslBundles); + /** * Sets the HTTP/2 configuration that will be applied to the server. * @param http2 the HTTP/2 configuration 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 deleted file mode 100644 index c4429bb484..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/JavaKeyStoreSslStoreProvider.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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/Ssl.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Ssl.java index af4400717c..bb17e6a26c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Ssl.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Ssl.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. @@ -29,6 +29,8 @@ public class Ssl { private boolean enabled = true; + private String bundle; + private ClientAuth clientAuth; private String[] ciphers; @@ -77,6 +79,24 @@ public class Ssl { this.enabled = enabled; } + /** + * Return the name of the SSL bundle to use. + * @return the SSL bundle name + * @since 3.1.0 + */ + public String getBundle() { + return this.bundle; + } + + /** + * Set the name of the SSL bundle to use. + * @param bundle the SSL bundle name + * @since 3.1.0 + */ + public void setBundle(String bundle) { + this.bundle = bundle; + } + /** * Return Whether client authentication is not wanted ("none"), wanted ("want") or * needed ("need"). Requires a trust store. @@ -295,6 +315,28 @@ public class Ssl { this.protocol = protocol; } + /** + * Returns if SSL is enabled for the given instance. + * @param ssl the {@link Ssl SSL} instance or {@code null} + * @return {@code true} is SSL is enabled + * @since 3.1.0 + */ + public static boolean isEnabled(Ssl ssl) { + return (ssl != null) && ssl.isEnabled(); + } + + /** + * Factory method to create an {@link Ssl} instance for a specific bundle name. + * @param bundle the name of the bundle + * @return a new {@link Ssl} instance with the bundle set + * @since 3.1.0 + */ + public static Ssl forBundle(String bundle) { + Ssl ssl = new Ssl(); + ssl.setBundle(bundle); + return ssl; + } + /** * Client authentication types. */ @@ -313,7 +355,25 @@ public class Ssl { /** * Client authentication is needed and mandatory. */ - NEED + NEED; + + /** + * Map an optional {@link ClientAuth} value to a different type. + * @param the result type + * @param clientAuth the client auth to map (may be {@code null}) + * @param none the value for {@link ClientAuth#NONE} or {@code null} + * @param want the value for {@link ClientAuth#WANT} + * @param need the value for {@link ClientAuth#NEED} + * @return the mapped value + * @since 3.1.0 + */ + public static R map(ClientAuth clientAuth, R none, R want, R need) { + return switch ((clientAuth != null) ? clientAuth : NONE) { + case NONE -> none; + case WANT -> want; + case NEED -> need; + }; + } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslConfigurationValidator.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslConfigurationValidator.java index 88fff39537..8bf44d6db5 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslConfigurationValidator.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslConfigurationValidator.java @@ -17,33 +17,25 @@ package org.springframework.boot.web.server; import java.security.KeyStore; -import java.security.KeyStoreException; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; +import org.springframework.boot.ssl.SslBundleKey; /** * Provides utilities around SSL. * * @author Chris Bono * @since 2.1.13 + * @deprecated since 3.1.0 for removal in 3.3.0 in favor of + * {@link SslBundleKey#assertContainsAlias(KeyStore)} */ +@Deprecated(since = "3.1.0", forRemoval = true) public final class SslConfigurationValidator { private SslConfigurationValidator() { } public static void validateKeyAlias(KeyStore keyStore, String keyAlias) { - if (StringUtils.hasLength(keyAlias)) { - try { - Assert.state(keyStore.containsAlias(keyAlias), - () -> String.format("Keystore does not contain specified alias '%s'", keyAlias)); - } - catch (KeyStoreException ex) { - throw new IllegalStateException( - String.format("Could not determine if keystore contains alias '%s'", keyAlias), ex); - } - } + SslBundleKey.of(null, keyAlias).assertContainsAlias(keyStore); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslStoreProvider.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslStoreProvider.java index e5bb107e23..31f2de86de 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslStoreProvider.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslStoreProvider.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. @@ -18,13 +18,18 @@ package org.springframework.boot.web.server; import java.security.KeyStore; +import org.springframework.boot.ssl.SslBundle; + /** * Interface to provide SSL key stores for an {@link WebServer} to use. Can be used when * file based key stores cannot be used. * * @author Phillip Webb * @since 2.0.0 + * @deprecated since 3.1.0 for removal in 3.3.0 in favor of registering an + * {@link SslBundle}. */ +@Deprecated(since = "3.1.0", forRemoval = true) public interface SslStoreProvider { /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java new file mode 100644 index 0000000000..e722830aa4 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java @@ -0,0 +1,214 @@ +/* + * 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 org.springframework.boot.ssl.NoSuchSslBundleException; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundleKey; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.ssl.SslManagerBundle; +import org.springframework.boot.ssl.SslOptions; +import org.springframework.boot.ssl.SslStoreBundle; +import org.springframework.boot.ssl.jks.JksSslStoreBundle; +import org.springframework.boot.ssl.jks.JksSslStoreDetails; +import org.springframework.boot.ssl.pem.PemSslStoreBundle; +import org.springframework.boot.ssl.pem.PemSslStoreDetails; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.util.function.ThrowingSupplier; + +/** + * {@link SslBundle} backed by {@link Ssl} or an {@link SslStoreProvider}. + * + * @author Scott Frederick + * @author Phillip Webb + * @since 3.1.0 + */ +public final class WebServerSslBundle implements SslBundle { + + private final SslStoreBundle stores; + + private final SslBundleKey key; + + private final SslOptions options; + + private final String protocol; + + private final SslManagerBundle managers; + + private WebServerSslBundle(SslStoreBundle stores, String keyPassword, Ssl ssl) { + this.stores = stores; + this.key = SslBundleKey.of(keyPassword, ssl.getKeyAlias()); + this.protocol = ssl.getProtocol(); + this.options = SslOptions.of(ssl.getCiphers(), ssl.getEnabledProtocols()); + this.managers = SslManagerBundle.from(this.stores, this.key); + } + + private static SslStoreBundle createPemStoreBundle(Ssl ssl) { + PemSslStoreDetails keyStoreDetails = new PemSslStoreDetails(ssl.getKeyStoreType(), ssl.getCertificate(), + ssl.getCertificatePrivateKey()); + PemSslStoreDetails trustStoreDetails = new PemSslStoreDetails(ssl.getTrustStoreType(), + ssl.getTrustCertificate(), ssl.getTrustCertificatePrivateKey()); + return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, ssl.getKeyAlias()); + } + + private static SslStoreBundle createJksStoreBundle(Ssl ssl) { + JksSslStoreDetails keyStoreDetails = new JksSslStoreDetails(ssl.getKeyStoreType(), ssl.getKeyStoreProvider(), + ssl.getKeyStore(), ssl.getKeyStorePassword()); + JksSslStoreDetails trustStoreDetails = new JksSslStoreDetails(ssl.getTrustStoreType(), + ssl.getTrustStoreProvider(), ssl.getTrustStore(), ssl.getTrustStorePassword()); + return new JksSslStoreBundle(keyStoreDetails, trustStoreDetails); + } + + @Override + public SslStoreBundle getStores() { + return this.stores; + } + + @Override + public SslBundleKey getKey() { + return this.key; + } + + @Override + public SslOptions getOptions() { + return this.options; + } + + @Override + public String getProtocol() { + return this.protocol; + } + + @Override + public SslManagerBundle getManagers() { + return this.managers; + } + + /** + * Get the {@link SslBundle} that should be used for the given {@link Ssl} instance. + * @param ssl the source ssl instance + * @return a {@link SslBundle} instance + * @throws NoSuchSslBundleException if a bundle lookup fails + */ + public static SslBundle get(Ssl ssl) throws NoSuchSslBundleException { + return get(ssl, null, null); + } + + /** + * Get the {@link SslBundle} that should be used for the given {@link Ssl} instance. + * @param ssl the source ssl instance + * @param sslBundles the bundles that should be used when {@link Ssl#getBundle()} is + * set + * @return a {@link SslBundle} instance + * @throws NoSuchSslBundleException if a bundle lookup fails + */ + public static SslBundle get(Ssl ssl, SslBundles sslBundles) throws NoSuchSslBundleException { + return get(ssl, sslBundles, null); + } + + /** + * Get the {@link SslBundle} that should be used for the given {@link Ssl} and + * {@link SslStoreProvider} instances. + * @param ssl the source {@link Ssl} instance + * @param sslBundles the bundles that should be used when {@link Ssl#getBundle()} is + * set + * @param sslStoreProvider the {@link SslStoreProvider} to use or {@code null} + * @return a {@link SslBundle} instance + * @throws NoSuchSslBundleException if a bundle lookup fails + * @deprecated since 3.1.0 for removal in 3.3.0 along with {@link SslStoreProvider} + */ + @Deprecated(since = "3.1.0", forRemoval = true) + @SuppressWarnings("removal") + public static SslBundle get(Ssl ssl, SslBundles sslBundles, SslStoreProvider sslStoreProvider) { + Assert.state(Ssl.isEnabled(ssl), "SSL is not enabled"); + String keyPassword = (sslStoreProvider != null) ? sslStoreProvider.getKeyPassword() : null; + keyPassword = (keyPassword != null) ? keyPassword : ssl.getKeyPassword(); + if (sslStoreProvider != null) { + SslStoreBundle stores = new SslStoreProviderBundleAdapter(sslStoreProvider); + return new WebServerSslBundle(stores, keyPassword, ssl); + } + String bundleName = ssl.getBundle(); + if (StringUtils.hasText(bundleName)) { + Assert.state(sslBundles != null, + () -> "SSL bundle '%s' was requested but no SslBundles instance was provided" + .formatted(bundleName)); + return sslBundles.getBundle(bundleName); + } + SslStoreBundle stores = createStoreBundle(ssl); + return new WebServerSslBundle(stores, keyPassword, ssl); + } + + private static SslStoreBundle createStoreBundle(Ssl ssl) { + if (hasCertificateProperties(ssl)) { + return createPemStoreBundle(ssl); + } + if (hasJavaKeyStoreProperties(ssl)) { + return createJksStoreBundle(ssl); + } + throw new IllegalStateException("SSL is enabled but no trust material is configured"); + } + + static SslBundle createCertificateFileSslStoreProviderDelegate(Ssl ssl) { + if (!hasCertificateProperties(ssl)) { + return null; + } + SslStoreBundle stores = createPemStoreBundle(ssl); + return new WebServerSslBundle(stores, ssl.getKeyPassword(), ssl); + } + + private static boolean hasCertificateProperties(Ssl ssl) { + return Ssl.isEnabled(ssl) && ssl.getCertificate() != null && ssl.getCertificatePrivateKey() != null; + } + + private static boolean hasJavaKeyStoreProperties(Ssl ssl) { + return Ssl.isEnabled(ssl) && ssl.getKeyStore() != null + || (ssl.getKeyStoreType() != null && ssl.getKeyStoreType().equals("PKCS11")); + } + + /** + * Class to adapt a {@link SslStoreProvider} into a {@link SslStoreBundle}. + */ + @SuppressWarnings("removal") + private static class SslStoreProviderBundleAdapter implements SslStoreBundle { + + private final SslStoreProvider sslStoreProvider; + + SslStoreProviderBundleAdapter(SslStoreProvider sslStoreProvider) { + this.sslStoreProvider = sslStoreProvider; + } + + @Override + public KeyStore getKeyStore() { + return ThrowingSupplier.of(this.sslStoreProvider::getKeyStore).get(); + } + + @Override + public String getKeyStorePassword() { + return null; + } + + @Override + public KeyStore getTrustStore() { + return ThrowingSupplier.of(this.sslStoreProvider::getTrustStore).get(); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactoryTests.java index dfdf965fff..feecbe4925 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactoryTests.java @@ -44,6 +44,13 @@ import reactor.test.StepVerifier; import org.springframework.boot.rsocket.server.RSocketServer; import org.springframework.boot.rsocket.server.RSocketServer.Transport; import org.springframework.boot.rsocket.server.RSocketServerCustomizer; +import org.springframework.boot.ssl.DefaultSslBundleRegistry; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundleKey; +import org.springframework.boot.ssl.jks.JksSslStoreBundle; +import org.springframework.boot.ssl.jks.JksSslStoreDetails; +import org.springframework.boot.ssl.pem.PemSslStoreBundle; +import org.springframework.boot.ssl.pem.PemSslStoreDetails; import org.springframework.boot.web.server.Ssl; import org.springframework.core.codec.CharSequenceEncoder; import org.springframework.core.codec.StringDecoder; @@ -191,6 +198,50 @@ class NettyRSocketServerFactoryTests { "src/test/resources/test-cert.pem", Transport.WEBSOCKET); } + @Test + void tcpTransportBasicSslFromClassPathWithBundle() { + testBasicSslWithKeyStoreFromBundle("classpath:test.jks", "password", Transport.TCP); + } + + @Test + void tcpTransportBasicSslFromFileSystemWithBundle() { + testBasicSslWithKeyStoreFromBundle("src/test/resources/test.jks", "password", Transport.TCP); + } + + @Test + void websocketTransportBasicSslFromClassPathWithBundle() { + testBasicSslWithKeyStoreFromBundle("classpath:test.jks", "password", Transport.WEBSOCKET); + } + + @Test + void websocketTransportBasicSslFromFileSystemWithBundle() { + testBasicSslWithKeyStoreFromBundle("src/test/resources/test.jks", "password", Transport.WEBSOCKET); + } + + @Test + void tcpTransportBasicSslCertificateFromClassPathWithBundle() { + testBasicSslWithPemCertificateFromBundle("classpath:test-cert.pem", "classpath:test-key.pem", + "classpath:test-cert.pem", Transport.TCP); + } + + @Test + void tcpTransportBasicSslCertificateFromFileSystemWithBundle() { + testBasicSslWithPemCertificateFromBundle("src/test/resources/test-cert.pem", "src/test/resources/test-key.pem", + "src/test/resources/test-cert.pem", Transport.TCP); + } + + @Test + void websocketTransportBasicSslCertificateFromClassPathWithBundle() { + testBasicSslWithPemCertificateFromBundle("classpath:test-cert.pem", "classpath:test-key.pem", + "classpath:test-cert.pem", Transport.WEBSOCKET); + } + + @Test + void websocketTransportBasicSslCertificateFromFileSystemWithBundle() { + testBasicSslWithPemCertificateFromBundle("src/test/resources/test-cert.pem", "src/test/resources/test-key.pem", + "src/test/resources/test-cert.pem", Transport.WEBSOCKET); + } + private void checkEchoRequest() { String payload = "test payload"; Mono response = this.requester.route("test").data(payload).retrieveMono(String.class); @@ -228,6 +279,39 @@ class NettyRSocketServerFactoryTests { checkEchoRequest(); } + private void testBasicSslWithKeyStoreFromBundle(String keyStore, String keyPassword, Transport transport) { + NettyRSocketServerFactory factory = getFactory(); + factory.setTransport(transport); + JksSslStoreDetails keyStoreDetails = JksSslStoreDetails.forLocation(keyStore); + JksSslStoreDetails trustStoreDetails = null; + SslBundle sslBundle = SslBundle.of(new JksSslStoreBundle(keyStoreDetails, trustStoreDetails), + SslBundleKey.of(keyPassword)); + factory.setSsl(Ssl.forBundle("test")); + factory.setSslBundles(new DefaultSslBundleRegistry("test", sslBundle)); + this.server = factory.create(new EchoRequestResponseAcceptor()); + this.server.start(); + this.requester = (transport == Transport.TCP) ? createSecureRSocketTcpClient() + : createSecureRSocketWebSocketClient(); + checkEchoRequest(); + } + + private void testBasicSslWithPemCertificateFromBundle(String certificate, String certificatePrivateKey, + String trustCertificate, Transport transport) { + NettyRSocketServerFactory factory = getFactory(); + factory.setTransport(transport); + PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate(certificate) + .withPrivateKey(certificatePrivateKey); + PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate(trustCertificate); + SslBundle sslBundle = SslBundle.of(new PemSslStoreBundle(keyStoreDetails, trustStoreDetails)); + factory.setSsl(Ssl.forBundle("test")); + factory.setSslBundles(new DefaultSslBundleRegistry("test", sslBundle)); + this.server = factory.create(new EchoRequestResponseAcceptor()); + this.server.start(); + this.requester = (transport == Transport.TCP) ? createSecureRSocketTcpClient() + : createSecureRSocketWebSocketClient(); + checkEchoRequest(); + } + @Test void tcpTransportSslRejectsInsecureClient() { NettyRSocketServerFactory factory = getFactory(); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/AliasKeyManagerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/AliasKeyManagerFactoryTests.java new file mode 100644 index 0000000000..f858ae3245 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/AliasKeyManagerFactoryTests.java @@ -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"); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/DefaultSslBundleRegistryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/DefaultSslBundleRegistryTests.java new file mode 100644 index 0000000000..d8cf034eef --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/DefaultSslBundleRegistryTests.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/DefaultSslManagerBundleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/DefaultSslManagerBundleTests.java new file mode 100644 index 0000000000..ffe6986b3b --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/DefaultSslManagerBundleTests.java @@ -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; + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/NoSuchSslBundleExceptionTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/NoSuchSslBundleExceptionTests.java new file mode 100644 index 0000000000..d8b5dbb2d5 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/NoSuchSslBundleExceptionTests.java @@ -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"); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/SslBundleKeyTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/SslBundleKeyTests.java new file mode 100644 index 0000000000..db3d3a42d5 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/SslBundleKeyTests.java @@ -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'"); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/SslBundleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/SslBundleTests.java new file mode 100644 index 0000000000..090d0cc5bc --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/SslBundleTests.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/SslManagerBundleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/SslManagerBundleTests.java new file mode 100644 index 0000000000..b58eb2e64e --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/SslManagerBundleTests.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/SslOptionsTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/SslOptionsTests.java new file mode 100644 index 0000000000..018ab2ecc4 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/SslOptionsTests.java @@ -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 ciphers = Set.of("a", "b", "c"); + Set 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 ciphers = null; + Set enabledProtocols = null; + SslOptions options = SslOptions.of(ciphers, enabledProtocols); + assertThat(options.getCiphers()).isEmpty(); + assertThat(options.getEnabledProtocols()).isEmpty(); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/SslStoreBundleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/SslStoreBundleTests.java new file mode 100644 index 0000000000..76b99cf73d --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/SslStoreBundleTests.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/jks/JksSslStoreBundleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/jks/JksSslStoreBundleTests.java new file mode 100644 index 0000000000..87b0f972e2 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/jks/JksSslStoreBundleTests.java @@ -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 storeContainingCertAndKey(String keyAlias, String keyPassword) { + return storeContainingCertAndKey(KeyStore.getDefaultType(), keyAlias, keyPassword); + } + + private Consumer 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(); + }); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/CertificateParserTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemCertificateParserTests.java similarity index 63% rename from spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/CertificateParserTests.java rename to spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemCertificateParserTests.java index 1455b4db88..20ceee1b9a 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/CertificateParserTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemCertificateParserTests.java @@ -14,44 +14,44 @@ * limitations under the License. */ -package org.springframework.boot.web.server; +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; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** - * Tests for {@link CertificateParser}. + * Tests for {@link PemCertificateParser}. * * @author Scott Frederick */ -class CertificateParserTests { +class PemCertificateParserTests { @Test - void parseCertificate() { - X509Certificate[] certificates = CertificateParser.parse("classpath:test-cert.pem"); + 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() { - X509Certificate[] certificates = CertificateParser.parse("classpath:test-cert-chain.pem"); + 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"); } - @Test - void parseWithInvalidPathWillThrowException() { - String path = "file:///bad/path/cert.pem"; - assertThatIllegalStateException().isThrownBy(() -> CertificateParser.parse("file:///bad/path/cert.pem")) - .withMessageContaining(path); + private String read(String path) throws IOException { + return new ClassPathResource(path).getContentAsString(StandardCharsets.UTF_8); } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java new file mode 100644 index 0000000000..649d66f699 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/PrivateKeyParserTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java similarity index 60% rename from spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/PrivateKeyParserTests.java rename to spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java index 390fb1b2b6..e38c0b47f8 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/PrivateKeyParserTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java @@ -14,49 +14,49 @@ * limitations under the License. */ -package org.springframework.boot.web.server; +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 PrivateKeyParser}. + * Tests for {@link PemPrivateKeyParser}. * * @author Scott Frederick */ -class PrivateKeyParserTests { +class PemPrivateKeyParserTests { @Test - void parsePkcs8KeyFile() { - PrivateKey privateKey = PrivateKeyParser.parse("classpath:test-key.pem"); + 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() { - PrivateKey privateKey = PrivateKeyParser.parse("classpath:test-ec-key.pem"); + 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 parseWithNonKeyFileWillThrowException() { - String path = "classpath:test-banner.txt"; - assertThatIllegalStateException().isThrownBy(() -> PrivateKeyParser.parse("file://" + path)) - .withMessageContaining(path); + void parseWithNonKeyTextWillThrowException() { + assertThatIllegalStateException().isThrownBy(() -> PemPrivateKeyParser.parse(read("test-banner.txt"))); } - @Test - void parseWithInvalidPathWillThrowException() { - String path = "file:///bad/path/key.pem"; - assertThatIllegalStateException().isThrownBy(() -> PrivateKeyParser.parse(path)).withMessageContaining(path); + private String read(String path) throws IOException { + return new ClassPathResource(path).getContentAsString(StandardCharsets.UTF_8); } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java new file mode 100644 index 0000000000..fd092261b1 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java @@ -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 storeContainingCert(String keyAlias) { + return storeContainingCert(KeyStore.getDefaultType(), keyAlias); + } + + private Consumer 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 storeContainingCertAndKey(String keyAlias) { + return storeContainingCertAndKey(KeyStore.getDefaultType(), keyAlias); + } + + private Consumer 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(); + }); + } + +} 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 6847396a9b..1be7c83447 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,7 +35,7 @@ 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.SslStoreProviderFactory; +import org.springframework.boot.web.server.WebServerSslBundle; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -91,10 +91,10 @@ class SslServerCustomizerTests { @Test void configureSslWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsException() { Ssl ssl = new Ssl(); - SslServerCustomizer customizer = new SslServerCustomizer(null, ssl, null, null); - assertThatIllegalStateException().isThrownBy( - () -> customizer.configureSsl(new SslContextFactory.Server(), ssl, SslStoreProviderFactory.from(ssl))) - .withMessageContaining("KeyStore location must not be empty or null"); + assertThatIllegalStateException().isThrownBy(() -> { + SslServerCustomizer customizer = new SslServerCustomizer(null, null, null, WebServerSslBundle.get(ssl)); + customizer.configureSsl(new SslContextFactory.Server(), ssl.getClientAuth()); + }).withMessageContaining("SSL is enabled but no trust material is configured"); } @Test @@ -104,10 +104,10 @@ class SslServerCustomizerTests { ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME); 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, SslStoreProviderFactory.from(ssl))) - .withMessageContaining("must be empty or null for PKCS11 key stores"); + assertThatIllegalStateException().isThrownBy(() -> { + SslServerCustomizer customizer = new SslServerCustomizer(null, null, null, WebServerSslBundle.get(ssl)); + customizer.configureSsl(new SslContextFactory.Server(), ssl.getClientAuth()); + }).withMessageContaining("must be empty or null for PKCS11 hardware key stores"); } @Test @@ -116,8 +116,10 @@ class SslServerCustomizerTests { ssl.setKeyStoreType("PKCS11"); ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME); ssl.setKeyStorePassword("1234"); - SslServerCustomizer customizer = new SslServerCustomizer(null, ssl, null, null); - assertThatNoException().isThrownBy(() -> customizer.configureSsl(new SslContextFactory.Server(), ssl, null)); + assertThatNoException().isThrownBy(() -> { + SslServerCustomizer customizer = new SslServerCustomizer(null, null, null, WebServerSslBundle.get(ssl)); + customizer.configureSsl(new SslContextFactory.Server(), ssl.getClientAuth()); + }); } private Server createCustomizedServer() { @@ -132,7 +134,8 @@ class SslServerCustomizerTests { private Server createCustomizedServer(Ssl ssl, Http2 http2) { Server server = new Server(); - new SslServerCustomizer(new InetSocketAddress(0), ssl, null, http2).customize(server); + new SslServerCustomizer(http2, new InetSocketAddress(0), ssl.getClientAuth(), WebServerSslBundle.get(ssl)) + .customize(server); return server; } 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 deleted file mode 100644 index 0fc9b4942d..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/SslServerCustomizerTests.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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.embedded.netty; - -import java.security.NoSuchProviderException; - -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.SslStoreProviderFactory; - -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.assertj.core.api.Assertions.assertThatNoException; - -/** - * Tests for {@link SslServerCustomizer}. - * - * @author Andy Wilkinson - * @author Raheela Aslam - * @author Cyril Dangerville - * @author Scott Frederick - */ -@SuppressWarnings("deprecation") -@MockPkcs11Security -class SslServerCustomizerTests { - - @Test - void keyStoreProviderIsUsedWhenCreatingKeyStore() { - Ssl ssl = new Ssl(); - ssl.setKeyPassword("password"); - ssl.setKeyStore("src/test/resources/test.jks"); - ssl.setKeyStoreProvider("com.example.KeyStoreProvider"); - SslServerCustomizer customizer = new SslServerCustomizer(ssl, null, null); - assertThatIllegalStateException() - .isThrownBy(() -> customizer.getKeyManagerFactory(ssl, SslStoreProviderFactory.from(ssl))) - .withCauseInstanceOf(NoSuchProviderException.class) - .withMessageContaining("com.example.KeyStoreProvider"); - } - - @Test - void trustStoreProviderIsUsedWhenCreatingTrustStore() { - Ssl ssl = new Ssl(); - ssl.setTrustStorePassword("password"); - ssl.setTrustStore("src/test/resources/test.jks"); - ssl.setTrustStoreProvider("com.example.TrustStoreProvider"); - SslServerCustomizer customizer = new SslServerCustomizer(ssl, null, null); - assertThatIllegalStateException() - .isThrownBy(() -> customizer.getTrustManagerFactory(SslStoreProviderFactory.from(ssl))) - .withCauseInstanceOf(NoSuchProviderException.class) - .withMessageContaining("com.example.TrustStoreProvider"); - } - - @Test - void getKeyManagerFactoryWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsException() { - Ssl ssl = new Ssl(); - SslServerCustomizer customizer = new SslServerCustomizer(ssl, null, null); - assertThatIllegalStateException() - .isThrownBy(() -> customizer.getKeyManagerFactory(ssl, SslStoreProviderFactory.from(ssl))) - .withCauseInstanceOf(IllegalStateException.class) - .withMessageContaining("KeyStore location must not be empty or null"); - } - - @Test - void getKeyManagerFactoryWhenSslIsEnabledWithPkcs11AndKeyStoreThrowsException() { - Ssl ssl = new Ssl(); - ssl.setKeyStoreType("PKCS11"); - ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME); - ssl.setKeyStore("src/test/resources/test.jks"); - ssl.setKeyPassword("password"); - SslServerCustomizer customizer = new SslServerCustomizer(ssl, null, null); - assertThatIllegalStateException() - .isThrownBy(() -> customizer.getKeyManagerFactory(ssl, SslStoreProviderFactory.from(ssl))) - .withCauseInstanceOf(IllegalStateException.class) - .withMessageContaining("must be empty or null for PKCS11 key stores"); - } - - @Test - void getKeyManagerFactoryWhenSslIsEnabledWithPkcs11AndKeyStoreProvider() { - Ssl ssl = new Ssl(); - ssl.setKeyStoreType("PKCS11"); - ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME); - ssl.setKeyStorePassword("1234"); - SslServerCustomizer customizer = new SslServerCustomizer(ssl, null, 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 be311fb9f0..058d2dd6e5 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,6 +41,7 @@ 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.WebServerSslBundle; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; @@ -58,6 +59,7 @@ import static org.mockito.Mockito.mock; * @author Scott Frederick * @author Cyril Dangerville */ +@SuppressWarnings("removal") @ExtendWith(OutputCaptureExtension.class) @DirtiesUrlFactories @MockPkcs11Security @@ -84,10 +86,11 @@ class SslConnectorCustomizerTests { @Test void sslCiphersConfiguration() throws Exception { Ssl ssl = new Ssl(); - ssl.setKeyStore("test.jks"); + ssl.setKeyStore("classpath:test.jks"); ssl.setKeyStorePassword("secret"); ssl.setCiphers(new String[] { "ALPHA", "BRAVO", "CHARLIE" }); - SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl, null); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(), + WebServerSslBundle.get(ssl)); Connector connector = this.tomcat.getConnector(); customizer.customize(connector); this.tomcat.start(); @@ -102,7 +105,8 @@ class SslConnectorCustomizerTests { ssl.setKeyStore("src/test/resources/test.jks"); ssl.setEnabledProtocols(new String[] { "TLSv1.1", "TLSv1.2" }); ssl.setCiphers(new String[] { "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "BRAVO" }); - SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl, null); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(), + WebServerSslBundle.get(ssl)); Connector connector = this.tomcat.getConnector(); customizer.customize(connector); this.tomcat.start(); @@ -118,7 +122,8 @@ class SslConnectorCustomizerTests { ssl.setKeyStore("src/test/resources/test.jks"); ssl.setEnabledProtocols(new String[] { "TLSv1.2" }); ssl.setCiphers(new String[] { "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "BRAVO" }); - SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl, null); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(), + WebServerSslBundle.get(ssl)); Connector connector = this.tomcat.getConnector(); customizer.customize(connector); this.tomcat.start(); @@ -128,6 +133,7 @@ class SslConnectorCustomizerTests { } @Test + @Deprecated(since = "3.1.0", forRemoval = true) void customizeWhenSslStoreProviderProvidesOnlyKeyStoreShouldUseDefaultTruststore() throws Exception { Ssl ssl = new Ssl(); ssl.setKeyPassword("password"); @@ -135,7 +141,8 @@ class SslConnectorCustomizerTests { SslStoreProvider sslStoreProvider = mock(SslStoreProvider.class); KeyStore keyStore = loadStore(); given(sslStoreProvider.getKeyStore()).willReturn(keyStore); - SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl, sslStoreProvider); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(), + WebServerSslBundle.get(ssl, null, sslStoreProvider)); Connector connector = this.tomcat.getConnector(); customizer.customize(connector); this.tomcat.start(); @@ -148,6 +155,7 @@ class SslConnectorCustomizerTests { } @Test + @Deprecated(since = "3.1.0", forRemoval = true) void customizeWhenSslStoreProviderProvidesOnlyTrustStoreShouldUseDefaultKeystore() throws Exception { Ssl ssl = new Ssl(); ssl.setKeyPassword("password"); @@ -155,7 +163,8 @@ class SslConnectorCustomizerTests { SslStoreProvider sslStoreProvider = mock(SslStoreProvider.class); KeyStore trustStore = loadStore(); given(sslStoreProvider.getTrustStore()).willReturn(trustStore); - SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl, sslStoreProvider); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(), + WebServerSslBundle.get(ssl, null, sslStoreProvider)); Connector connector = this.tomcat.getConnector(); customizer.customize(connector); this.tomcat.start(); @@ -164,6 +173,7 @@ class SslConnectorCustomizerTests { } @Test + @Deprecated(since = "3.1.0", forRemoval = true) void customizeWhenSslStoreProviderPresentShouldIgnorePasswordFromSsl(CapturedOutput output) throws Exception { System.setProperty("javax.net.ssl.trustStorePassword", "trustStoreSecret"); Ssl ssl = new Ssl(); @@ -172,7 +182,8 @@ class SslConnectorCustomizerTests { SslStoreProvider sslStoreProvider = mock(SslStoreProvider.class); given(sslStoreProvider.getTrustStore()).willReturn(loadStore()); given(sslStoreProvider.getKeyStore()).willReturn(loadStore()); - SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl, sslStoreProvider); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(), + WebServerSslBundle.get(ssl, null, sslStoreProvider)); Connector connector = this.tomcat.getConnector(); customizer.customize(connector); this.tomcat.start(); @@ -182,9 +193,11 @@ class SslConnectorCustomizerTests { @Test void customizeWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsException() { - assertThatIllegalStateException() - .isThrownBy(() -> new SslConnectorCustomizer(new Ssl()).customize(this.tomcat.getConnector())) - .withMessageContaining("KeyStore location must not be empty or null"); + assertThatIllegalStateException().isThrownBy(() -> { + SslConnectorCustomizer customizer = new SslConnectorCustomizer(Ssl.ClientAuth.NONE, + WebServerSslBundle.get(new Ssl())); + customizer.customize(this.tomcat.getConnector()); + }).withMessageContaining("SSL is enabled but no trust material is configured"); } @Test @@ -194,9 +207,10 @@ class SslConnectorCustomizerTests { ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME); ssl.setKeyStore("src/test/resources/test.jks"); ssl.setKeyPassword("password"); - SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(), + WebServerSslBundle.get(ssl)); assertThatIllegalStateException().isThrownBy(() -> customizer.customize(this.tomcat.getConnector())) - .withMessageContaining("must be empty or null for PKCS11 key stores"); + .withMessageContaining("must be empty or null for PKCS11 hardware key stores"); } @Test @@ -205,7 +219,8 @@ class SslConnectorCustomizerTests { ssl.setKeyStoreType("PKCS11"); ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME); ssl.setKeyStorePassword("1234"); - SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl, null); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(), + WebServerSslBundle.get(ssl)); assertThatNoException().isThrownBy(() -> customizer.customize(this.tomcat.getConnector())); } 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 deleted file mode 100644 index 312c56e0f6..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizerTests.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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.embedded.undertow; - -import java.net.InetAddress; -import java.security.NoSuchProviderException; - -import javax.net.ssl.KeyManager; - -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.SslStoreProviderFactory; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.assertj.core.api.Assertions.assertThatNoException; - -/** - * Tests for {@link SslBuilderCustomizer} - * - * @author Brian Clozel - * @author Raheela Aslam - * @author Cyril Dangerville - */ -@MockPkcs11Security -class SslBuilderCustomizerTests { - - @Test - void getKeyManagersWhenAliasIsNullShouldNotDecorate() throws Exception { - Ssl ssl = new Ssl(); - ssl.setKeyPassword("password"); - ssl.setKeyStore("src/test/resources/test.jks"); - SslBuilderCustomizer customizer = new SslBuilderCustomizer(8080, InetAddress.getLocalHost(), 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); - } - - @Test - void keyStoreProviderIsUsedWhenCreatingKeyStore() throws Exception { - Ssl ssl = new Ssl(); - ssl.setKeyPassword("password"); - ssl.setKeyStore("src/test/resources/test.jks"); - ssl.setKeyStoreProvider("com.example.KeyStoreProvider"); - SslBuilderCustomizer customizer = new SslBuilderCustomizer(8080, InetAddress.getLocalHost(), ssl, null); - assertThatIllegalStateException() - .isThrownBy(() -> customizer.getKeyManagers(ssl, SslStoreProviderFactory.from(ssl))) - .withCauseInstanceOf(NoSuchProviderException.class) - .withMessageContaining("com.example.KeyStoreProvider"); - } - - @Test - void trustStoreProviderIsUsedWhenCreatingTrustStore() throws Exception { - Ssl ssl = new Ssl(); - ssl.setTrustStorePassword("password"); - ssl.setTrustStore("src/test/resources/test.jks"); - ssl.setTrustStoreProvider("com.example.TrustStoreProvider"); - SslBuilderCustomizer customizer = new SslBuilderCustomizer(8080, InetAddress.getLocalHost(), ssl, null); - assertThatIllegalStateException() - .isThrownBy(() -> customizer.getTrustManagers(SslStoreProviderFactory.from(ssl))) - .withMessageContaining("com.example.TrustStoreProvider"); - } - - @Test - void getKeyManagersWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsException() throws Exception { - Ssl ssl = new Ssl(); - SslBuilderCustomizer customizer = new SslBuilderCustomizer(8080, InetAddress.getLocalHost(), ssl, null); - assertThatIllegalStateException() - .isThrownBy(() -> customizer.getKeyManagers(ssl, SslStoreProviderFactory.from(ssl))) - .withCauseInstanceOf(IllegalStateException.class) - .withMessageContaining("KeyStore location must not be empty or null"); - } - - @Test - void configureSslWhenSslIsEnabledWithPkcs11AndKeyStoreThrowsException() throws Exception { - Ssl ssl = new Ssl(); - ssl.setKeyStoreType("PKCS11"); - ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME); - ssl.setKeyStore("src/test/resources/test.jks"); - ssl.setKeyPassword("password"); - SslBuilderCustomizer customizer = new SslBuilderCustomizer(8080, InetAddress.getLocalHost(), ssl, null); - assertThatIllegalStateException() - .isThrownBy(() -> customizer.getKeyManagers(ssl, SslStoreProviderFactory.from(ssl))) - .withCauseInstanceOf(IllegalStateException.class) - .withMessageContaining("must be empty or null for PKCS11 key stores"); - } - - @Test - void customizeWhenSslIsEnabledWithPkcs11AndKeyStoreProvider() throws Exception { - Ssl ssl = new Ssl(); - ssl.setKeyStoreType("PKCS11"); - ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME); - ssl.setKeyStorePassword("1234"); - SslBuilderCustomizer customizer = new SslBuilderCustomizer(8080, InetAddress.getLocalHost(), 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 6751947151..89527678e4 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 @@ -212,7 +212,7 @@ public abstract class AbstractReactiveWebServerFactoryTests { } protected void assertThatSslWithInvalidAliasCallFails(ThrowingCallable call) { - assertThatThrownBy(call).hasStackTraceContaining("Keystore does not contain specified alias 'test-alias-404'"); + assertThatThrownBy(call).hasStackTraceContaining("Keystore does not contain alias 'test-alias-404'"); } protected ReactorClientHttpConnector buildTrustAllSslConnector() { @@ -402,7 +402,7 @@ public abstract class AbstractReactiveWebServerFactoryTests { @Test void whenSslIsEnabledAndNoKeyStoreIsConfiguredThenServerFailsToStart() { assertThatIllegalStateException().isThrownBy(() -> testBasicSslWithKeyStore(null, null)) - .withMessageContaining("KeyStore location must not be empty or null"); + .withMessageContaining("SSL is enabled but no trust material is configured"); } @Test diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/CertificateFileSslStoreProviderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/CertificateFileSslStoreProviderTests.java deleted file mode 100644 index 8f0fcdd662..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/CertificateFileSslStoreProviderTests.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2012-2022 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.UnrecoverableKeyException; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link CertificateFileSslStoreProvider}. - * - * @author Scott Frederick - */ -class CertificateFileSslStoreProviderTests { - - @Test - void fromSslWhenNullReturnsNull() { - assertThat(CertificateFileSslStoreProvider.from(null)).isNull(); - } - - @Test - void fromSslWhenDisabledReturnsNull() { - assertThat(CertificateFileSslStoreProvider.from(new Ssl())).isNull(); - } - - @Test - void fromSslWithCertAndKeyReturnsStoreProvider() throws Exception { - Ssl ssl = new Ssl(); - ssl.setEnabled(true); - ssl.setCertificate("classpath:test-cert.pem"); - ssl.setCertificatePrivateKey("classpath:test-key.pem"); - SslStoreProvider storeProvider = CertificateFileSslStoreProvider.from(ssl); - assertThat(storeProvider).isNotNull(); - assertStoreContainsCertAndKey(storeProvider.getKeyStore(), KeyStore.getDefaultType(), "spring-boot-web"); - assertThat(storeProvider.getTrustStore()).isNull(); - } - - @Test - void fromSslWithCertAndKeyAndTrustCertReturnsStoreProvider() throws Exception { - Ssl ssl = new Ssl(); - ssl.setEnabled(true); - ssl.setCertificate("classpath:test-cert.pem"); - ssl.setCertificatePrivateKey("classpath:test-key.pem"); - ssl.setTrustCertificate("classpath:test-cert.pem"); - SslStoreProvider storeProvider = CertificateFileSslStoreProvider.from(ssl); - assertThat(storeProvider).isNotNull(); - assertStoreContainsCertAndKey(storeProvider.getKeyStore(), KeyStore.getDefaultType(), "spring-boot-web"); - assertStoreContainsCert(storeProvider.getTrustStore(), KeyStore.getDefaultType(), "spring-boot-web-0"); - } - - @Test - void fromSslWithCertAndKeyAndTrustCertAndTrustKeyReturnsStoreProvider() throws Exception { - Ssl ssl = new Ssl(); - ssl.setEnabled(true); - ssl.setCertificate("classpath:test-cert.pem"); - ssl.setCertificatePrivateKey("classpath:test-key.pem"); - ssl.setTrustCertificate("classpath:test-cert.pem"); - ssl.setTrustCertificatePrivateKey("classpath:test-key.pem"); - SslStoreProvider storeProvider = CertificateFileSslStoreProvider.from(ssl); - assertThat(storeProvider).isNotNull(); - assertStoreContainsCertAndKey(storeProvider.getKeyStore(), KeyStore.getDefaultType(), "spring-boot-web"); - assertStoreContainsCertAndKey(storeProvider.getTrustStore(), KeyStore.getDefaultType(), "spring-boot-web"); - } - - @Test - void fromSslWithKeyAliasReturnsStoreProvider() throws Exception { - Ssl ssl = new Ssl(); - ssl.setEnabled(true); - ssl.setKeyAlias("test-alias"); - ssl.setCertificate("classpath:test-cert.pem"); - ssl.setCertificatePrivateKey("classpath:test-key.pem"); - ssl.setTrustCertificate("classpath:test-cert.pem"); - ssl.setTrustCertificatePrivateKey("classpath:test-key.pem"); - SslStoreProvider storeProvider = CertificateFileSslStoreProvider.from(ssl); - assertThat(storeProvider).isNotNull(); - assertStoreContainsCertAndKey(storeProvider.getKeyStore(), KeyStore.getDefaultType(), "test-alias"); - assertStoreContainsCertAndKey(storeProvider.getTrustStore(), KeyStore.getDefaultType(), "test-alias"); - } - - @Test - void fromSslWithStoreTypeReturnsStoreProvider() throws Exception { - Ssl ssl = new Ssl(); - ssl.setEnabled(true); - ssl.setKeyStoreType("PKCS12"); - ssl.setTrustStoreType("PKCS12"); - ssl.setCertificate("classpath:test-cert.pem"); - ssl.setCertificatePrivateKey("classpath:test-key.pem"); - ssl.setTrustCertificate("classpath:test-cert.pem"); - ssl.setTrustCertificatePrivateKey("classpath:test-key.pem"); - SslStoreProvider storeProvider = CertificateFileSslStoreProvider.from(ssl); - assertThat(storeProvider).isNotNull(); - assertStoreContainsCertAndKey(storeProvider.getKeyStore(), "PKCS12", "spring-boot-web"); - assertStoreContainsCertAndKey(storeProvider.getTrustStore(), "PKCS12", "spring-boot-web"); - } - - private void assertStoreContainsCertAndKey(KeyStore keyStore, String keyStoreType, String keyAlias) - 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, new char[] {})).isNotNull(); - } - - private void assertStoreContainsCert(KeyStore keyStore, String keyStoreType, String keyAlias) - 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, new char[] {})).isNull(); - } - -} 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 deleted file mode 100644 index d9fea419d8..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/JavaKeyStoreSslStoreProviderTests.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * 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(); - } - -} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/SslConfigurationValidatorTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/SslConfigurationValidatorTests.java index 95672ce2d9..b87a7cff82 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/SslConfigurationValidatorTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/SslConfigurationValidatorTests.java @@ -31,7 +31,8 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; * * @author Chris Bono */ - +@SuppressWarnings("removal") +@Deprecated(since = "3.1.0", forRemoval = true) class SslConfigurationValidatorTests { private static final String VALID_ALIAS = "test-alias"; @@ -67,7 +68,7 @@ class SslConfigurationValidatorTests { void validateKeyAliasWhenAliasNotFoundShouldThrowException() { assertThatThrownBy(() -> SslConfigurationValidator.validateKeyAlias(this.keyStore, INVALID_ALIAS)) .isInstanceOf(IllegalStateException.class) - .hasMessage("Keystore does not contain specified alias '" + INVALID_ALIAS + "'"); + .hasMessage("Keystore does not contain alias '" + INVALID_ALIAS + "'"); } @Test diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/WebServerSslBundleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/WebServerSslBundleTests.java new file mode 100644 index 0000000000..9d6c0a1d6f --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/WebServerSslBundleTests.java @@ -0,0 +1,167 @@ +/* + * 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.security.KeyStore; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundleKey; +import org.springframework.boot.ssl.SslOptions; +import org.springframework.boot.ssl.SslStoreBundle; +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.assertThatIllegalStateException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link WebServerSslBundle}. + * + * @author Scott Frederick + * @author Phillip Webb + */ +class WebServerSslBundleTests { + + @Test + void whenSslDisabledThrowsException() { + Ssl ssl = new Ssl(); + ssl.setEnabled(false); + assertThatIllegalStateException().isThrownBy(() -> WebServerSslBundle.get(ssl)) + .withMessage("SSL is not enabled"); + } + + @Test + void whenFromJksProperties() { + Ssl ssl = new Ssl(); + ssl.setKeyStore("classpath:test.p12"); + ssl.setKeyStorePassword("secret"); + ssl.setKeyStoreType("PKCS12"); + ssl.setTrustStore("classpath:test.p12"); + ssl.setTrustStorePassword("secret"); + ssl.setTrustStoreType("PKCS12"); + ssl.setKeyPassword("password"); + ssl.setKeyAlias("alias"); + ssl.setClientAuth(Ssl.ClientAuth.NONE); + ssl.setCiphers(new String[] { "ONE", "TWO", "THREE" }); + ssl.setEnabledProtocols(new String[] { "TLSv1.1", "TLSv1.2" }); + ssl.setProtocol("TestProtocol"); + SslBundle bundle = WebServerSslBundle.get(ssl); + assertThat(bundle).isNotNull(); + assertThat(bundle.getProtocol()).isEqualTo("TestProtocol"); + SslBundleKey key = bundle.getKey(); + assertThat(key.getPassword()).isEqualTo("password"); + assertThat(key.getAlias()).isEqualTo("alias"); + SslStoreBundle stores = bundle.getStores(); + assertThat(stores.getKeyStorePassword()).isEqualTo("secret"); + assertThat(stores.getKeyStore()).isNotNull(); + assertThat(stores.getTrustStore()).isNotNull(); + SslOptions options = bundle.getOptions(); + assertThat(options.getCiphers()).containsExactly("ONE", "TWO", "THREE"); + assertThat(options.getEnabledProtocols()).containsExactly("TLSv1.1", "TLSv1.2"); + } + + @Test + void whenFromJksPropertiesWithPkcs11StoreType() { + Ssl ssl = new Ssl(); + ssl.setKeyStorePassword("secret"); + ssl.setKeyStoreType("PKCS11"); + ssl.setKeyPassword("password"); + ssl.setClientAuth(Ssl.ClientAuth.NONE); + SslBundle bundle = WebServerSslBundle.get(ssl); + assertThat(bundle).isNotNull(); + assertThat(bundle.getStores().getKeyStorePassword()).isEqualTo("secret"); + assertThat(bundle.getKey().getPassword()).isEqualTo("password"); + } + + @Test + void whenFromPemProperties() { + Ssl ssl = new Ssl(); + ssl.setCertificate("classpath:test-cert.pem"); + ssl.setCertificatePrivateKey("classpath:test-key.pem"); + ssl.setTrustCertificate("classpath:test-cert-chain.pem"); + ssl.setKeyStoreType("PKCS12"); + ssl.setTrustStoreType("PKCS12"); + ssl.setKeyPassword("password"); + ssl.setClientAuth(Ssl.ClientAuth.NONE); + ssl.setCiphers(new String[] { "ONE", "TWO", "THREE" }); + ssl.setEnabledProtocols(new String[] { "TLSv1.1", "TLSv1.2" }); + ssl.setProtocol("TLSv1.1"); + SslBundle bundle = WebServerSslBundle.get(ssl); + assertThat(bundle).isNotNull(); + SslBundleKey key = bundle.getKey(); + assertThat(key.getAlias()).isNull(); + assertThat(key.getPassword()).isEqualTo("password"); + SslStoreBundle stores = bundle.getStores(); + assertThat(stores.getKeyStorePassword()).isNull(); + assertThat(stores.getKeyStore()).isNotNull(); + assertThat(stores.getTrustStore()).isNotNull(); + SslOptions options = bundle.getOptions(); + assertThat(options.getCiphers()).containsExactly("ONE", "TWO", "THREE"); + assertThat(options.getEnabledProtocols()).containsExactly("TLSv1.1", "TLSv1.2"); + } + + @Test + @Deprecated(since = "3.1.0", forRemoval = true) + @SuppressWarnings("removal") + void whenFromCustomSslStoreProvider() throws Exception { + SslStoreProvider sslStoreProvider = mock(SslStoreProvider.class); + KeyStore keyStore = loadStore(); + given(sslStoreProvider.getKeyStore()).willReturn(keyStore); + given(sslStoreProvider.getTrustStore()).willReturn(keyStore); + Ssl ssl = new Ssl(); + ssl.setKeyStoreType("PKCS12"); + ssl.setTrustStoreType("PKCS12"); + ssl.setKeyPassword("password"); + ssl.setClientAuth(Ssl.ClientAuth.NONE); + ssl.setCiphers(new String[] { "ONE", "TWO", "THREE" }); + ssl.setEnabledProtocols(new String[] { "TLSv1.1", "TLSv1.2" }); + ssl.setProtocol("TLSv1.1"); + SslBundle bundle = WebServerSslBundle.get(ssl, null, sslStoreProvider); + assertThat(bundle).isNotNull(); + SslBundleKey key = bundle.getKey(); + assertThat(key.getPassword()).isEqualTo("password"); + assertThat(key.getAlias()).isNull(); + SslStoreBundle stores = bundle.getStores(); + assertThat(stores.getKeyStore()).isNotNull(); + assertThat(stores.getTrustStore()).isNotNull(); + SslOptions options = bundle.getOptions(); + assertThat(options.getCiphers()).containsExactly("ONE", "TWO", "THREE"); + assertThat(options.getEnabledProtocols()).containsExactly("TLSv1.1", "TLSv1.2"); + } + + @Test + void whenMissingPropertiesThrowsException() { + Ssl ssl = new Ssl(); + assertThatIllegalStateException().isThrownBy(() -> WebServerSslBundle.get(ssl)) + .withMessageContaining("SSL is enabled but no trust material is configured"); + } + + private KeyStore loadStore() throws Exception { + Resource resource = new ClassPathResource("test.p12"); + try (InputStream stream = resource.getInputStream()) { + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(stream, "secret".toCharArray()); + return keyStore; + } + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java index 82140ebe1d..f0d79128fa 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java @@ -111,6 +111,13 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.mockito.InOrder; +import org.springframework.boot.ssl.DefaultSslBundleRegistry; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslStoreBundle; +import org.springframework.boot.ssl.jks.JksSslStoreBundle; +import org.springframework.boot.ssl.jks.JksSslStoreDetails; +import org.springframework.boot.ssl.pem.PemSslStoreBundle; +import org.springframework.boot.ssl.pem.PemSslStoreDetails; import org.springframework.boot.system.ApplicationHome; import org.springframework.boot.system.ApplicationTemp; import org.springframework.boot.testsupport.system.CapturedOutput; @@ -170,6 +177,7 @@ import static org.mockito.Mockito.mock; * @author Raja Kolli * @author Scott Frederick */ +@SuppressWarnings("removal") @ExtendWith(OutputCaptureExtension.class) @DirtiesUrlFactories public abstract class AbstractServletWebServerFactoryTests { @@ -473,7 +481,7 @@ public abstract class AbstractServletWebServerFactoryTests { } protected void assertThatSslWithInvalidAliasCallFails(ThrowingCallable call) { - assertThatThrownBy(call).hasStackTraceContaining("Keystore does not contain specified alias 'test-alias-404'"); + assertThatThrownBy(call).hasStackTraceContaining("Keystore does not contain alias 'test-alias-404'"); } @Test @@ -557,6 +565,44 @@ public abstract class AbstractServletWebServerFactoryTests { assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory)).isEqualTo("test"); } + @Test + void pkcs12KeyStoreAndTrustStoreFromBundle() throws Exception { + AbstractServletWebServerFactory factory = getFactory(); + addTestTxtFile(factory); + factory.setSsl(Ssl.forBundle("test")); + factory.setSslBundles( + new DefaultSslBundleRegistry("test", createJksSslBundle("classpath:test.p12", "classpath:test.p12"))); + this.webServer = factory.getWebServer(); + this.webServer.start(); + KeyStore keyStore = KeyStore.getInstance("pkcs12"); + loadStore(keyStore, new FileSystemResource("src/test/resources/test.p12")); + SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( + new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy()) + .loadKeyMaterial(keyStore, "secret".toCharArray()) + .build()); + HttpComponentsClientHttpRequestFactory requestFactory = createHttpComponentsRequestFactory(socketFactory); + assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory)).isEqualTo("test"); + } + + @Test + void pemKeyStoreAndTrustStoreFromBundle() throws Exception { + AbstractServletWebServerFactory factory = getFactory(); + addTestTxtFile(factory); + factory.setSsl(Ssl.forBundle("test")); + factory.setSslBundles(new DefaultSslBundleRegistry("test", + createPemSslBundle("classpath:test-cert.pem", "classpath:test-key.pem"))); + this.webServer = factory.getWebServer(); + this.webServer.start(); + KeyStore keyStore = KeyStore.getInstance("pkcs12"); + loadStore(keyStore, new FileSystemResource("src/test/resources/test.p12")); + SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( + new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy()) + .loadKeyMaterial(keyStore, "secret".toCharArray()) + .build()); + HttpComponentsClientHttpRequestFactory requestFactory = createHttpComponentsRequestFactory(socketFactory); + assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory)).isEqualTo("test"); + } + @Test void sslNeedsClientAuthenticationSucceedsWithClientCertificate() throws Exception { AbstractServletWebServerFactory factory = getFactory(); @@ -621,6 +667,7 @@ public abstract class AbstractServletWebServerFactoryTests { } @Test + @Deprecated(since = "3.1.0", forRemoval = true) void sslWithCustomSslStoreProvider() throws Exception { AbstractServletWebServerFactory factory = getFactory(); addTestTxtFile(factory); @@ -715,6 +762,24 @@ public abstract class AbstractServletWebServerFactoryTests { return ssl; } + private SslBundle createJksSslBundle(String keyStore, String trustStore) { + JksSslStoreDetails keyStoreDetails = getJksStoreDetails(keyStore); + JksSslStoreDetails trustStoreDetails = getJksStoreDetails(trustStore); + SslStoreBundle stores = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails); + return SslBundle.of(stores); + } + + private JksSslStoreDetails getJksStoreDetails(String location) { + return new JksSslStoreDetails(getStoreType(location), null, location, "secret"); + } + + private SslBundle createPemSslBundle(String cert, String privateKey) { + PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate(cert).withPrivateKey(privateKey); + PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate(cert); + SslStoreBundle stores = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails); + return SslBundle.of(stores); + } + protected void testRestrictedSSLProtocolsAndCipherSuites(String[] protocols, String[] ciphers) throws Exception { AbstractServletWebServerFactory factory = getFactory(); factory.setSsl(getSsl(null, "password", "src/test/resources/restricted.jks", null, protocols, ciphers));