From 66db13b962ab7a9be4cc0b45bf38d58f1d397c79 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Thu, 20 Apr 2023 20:12:56 -0700 Subject: [PATCH] Refactor web server support to use SslBundles Update Tomcat, Jetty, Undertow and Netty servers so that an SslBundle is used to apply SSL configuration. Existing `Ssl` properties are internally adapted to an `SslBundle` using the `WebServerSslBundle` class. Additionally, if `Ssl.getBundle()` returns a non-null value the the `SslBundles` bean will be used to find a registered bundle by name. See gh-34814 --- ...itional-spring-configuration-metadata.json | 4 + .../RSocketServerAutoConfiguration.java | 5 +- ...tiveWebServerFactoryAutoConfiguration.java | 8 +- .../ReactiveWebServerFactoryCustomizer.java | 22 +- ...vletWebServerFactoryAutoConfiguration.java | 9 +- .../ServletWebServerFactoryCustomizer.java | 23 +- ...itional-spring-configuration-metadata.json | 4 + .../RSocketServerAutoConfigurationTests.java | 32 ++- ...ebServerFactoryAutoConfigurationTests.java | 17 +- ...activeWebServerFactoryCustomizerTests.java | 10 +- .../netty/NettyRSocketServerFactory.java | 39 +-- .../ConfigurableRSocketServerFactory.java | 15 +- .../jetty/JettyReactiveWebServerFactory.java | 7 +- .../jetty/JettyServletWebServerFactory.java | 5 +- .../embedded/jetty/SslServerCustomizer.java | 136 +++++----- .../netty/NettyReactiveWebServerFactory.java | 10 +- .../embedded/netty/SslServerCustomizer.java | 210 ++------------ .../tomcat/SslConnectorCustomizer.java | 91 +++---- .../TomcatReactiveWebServerFactory.java | 5 +- .../tomcat/TomcatServletWebServerFactory.java | 5 +- .../undertow/SslBuilderCustomizer.java | 166 ++---------- .../UndertowReactiveWebServerFactory.java | 4 +- .../UndertowServletWebServerFactory.java | 2 +- .../UndertowWebServerFactoryDelegate.java | 10 +- .../AbstractConfigurableWebServerFactory.java | 26 +- .../CertificateFileSslStoreProvider.java | 88 ++---- .../boot/web/server/CertificateParser.java | 109 -------- .../server/ConfigurableWebServerFactory.java | 16 +- .../server/JavaKeyStoreSslStoreProvider.java | 97 ------- .../boot/web/server/PrivateKeyParser.java | 256 ------------------ .../springframework/boot/web/server/Ssl.java | 64 ++++- .../web/server/SslConfigurationValidator.java | 18 +- .../boot/web/server/SslStoreProvider.java | 7 +- .../web/server/SslStoreProviderFactory.java | 41 --- .../boot/web/server/WebServerSslBundle.java | 214 +++++++++++++++ .../netty/NettyRSocketServerFactoryTests.java | 84 ++++++ .../jetty/SslServerCustomizerTests.java | 27 +- .../netty/SslServerCustomizerTests.java | 104 ------- .../tomcat/SslConnectorCustomizerTests.java | 41 ++- .../undertow/SslBuilderCustomizerTests.java | 116 -------- ...AbstractReactiveWebServerFactoryTests.java | 4 +- .../CertificateFileSslStoreProviderTests.java | 133 --------- .../web/server/CertificateParserTests.java | 57 ---- .../JavaKeyStoreSslStoreProviderTests.java | 142 ---------- .../web/server/PrivateKeyParserTests.java | 62 ----- .../SslConfigurationValidatorTests.java | 5 +- .../web/server/WebServerSslBundleTests.java | 167 ++++++++++++ .../AbstractServletWebServerFactoryTests.java | 67 ++++- 48 files changed, 1025 insertions(+), 1759 deletions(-) delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/CertificateParser.java delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/JavaKeyStoreSslStoreProvider.java delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/PrivateKeyParser.java delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslStoreProviderFactory.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/SslServerCustomizerTests.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizerTests.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/CertificateFileSslStoreProviderTests.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/CertificateParserTests.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/JavaKeyStoreSslStoreProviderTests.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/PrivateKeyParserTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/WebServerSslBundleTests.java 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/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 ad8a87a6f4..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 @@ -2639,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/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/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/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/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 Http2 http2; - private final Ssl ssl; + private final InetSocketAddress address; - private final SslStoreProvider sslStoreProvider; + private final ClientAuth clientAuth; - private final Http2 http2; + private final SslBundle sslBundle; - SslServerCustomizer(InetSocketAddress address, Ssl ssl, SslStoreProvider sslStoreProvider, Http2 http2) { + 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()); } - } - - 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); + catch (Exception ex) { + throw new IllegalStateException("Unable to set SSL store: " + ex.getMessage(), ex); } } - 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 SslContextFactory sslContextFactory; + private final SslBundleKey key; - private final String keyAlias; + private final SslContextFactory sslContextFactory; - 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()); - } - if (this.ssl.getCiphers() != null) { - builder.ciphers(Arrays.asList(this.ssl.getCiphers())); + builder.trustManager(this.sslBundle.getManagers().getTrustManagerFactory()); + SslOptions options = this.sslBundle.getOptions(); + if (!CollectionUtils.isEmpty(options.getEnabledProtocols())) { + builder.protocols(options.getEnabledProtocols()); } - 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 static final String DEFAULT_KEY_ALIAS = "spring-boot-web"; - - private final Ssl ssl; + private final SslBundle delegate; - 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/CertificateParser.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/CertificateParser.java deleted file mode 100644 index 23f30baa12..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/CertificateParser.java +++ /dev/null @@ -1,109 +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.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; -import java.util.ArrayList; -import java.util.Base64; -import java.util.List; -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 { - - private static final String HEADER = "-+BEGIN\\s+.*CERTIFICATE[^-]*-+(?:\\s|\\r|\\n)+"; - - private static final String BASE64_TEXT = "([a-z0-9+/=\\r\\n]+)"; - - private static final String FOOTER = "-+END\\s+.*CERTIFICATE[^-]*-+"; - - private static final Pattern PATTERN = Pattern.compile(HEADER + BASE64_TEXT + FOOTER, Pattern.CASE_INSENSITIVE); - - private CertificateParser() { - } - - /** - * Load certificates from the specified resource. - * @param path the certificate to parse - * @return the parsed certificates - */ - static X509Certificate[] parse(String path) { - CertificateFactory factory = getCertificateFactory(); - List certificates = new ArrayList<>(); - readCertificates(path, factory, certificates::add); - return certificates.toArray(new X509Certificate[0]); - } - - private static CertificateFactory getCertificateFactory() { - try { - return CertificateFactory.getInstance("X.509"); - } - catch (CertificateException ex) { - throw new IllegalStateException("Unable to get X.509 certificate factory", ex); - } - } - - private static void readCertificates(String resource, CertificateFactory factory, - Consumer consumer) { - try { - String text = readText(resource); - Matcher matcher = PATTERN.matcher(text); - while (matcher.find()) { - String encodedText = matcher.group(1); - byte[] decodedBytes = decodeBase64(encodedText); - ByteArrayInputStream inputStream = new ByteArrayInputStream(decodedBytes); - while (inputStream.available() > 0) { - consumer.accept((X509Certificate) factory.generateCertificate(inputStream)); - } - } - } - 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); - } - } - - private static byte[] decodeBase64(String content) { - byte[] bytes = content.replaceAll("\r", "").replaceAll("\n", "").getBytes(); - return Base64.getDecoder().decode(bytes); - } - -} 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/PrivateKeyParser.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/PrivateKeyParser.java deleted file mode 100644 index 45635bae38..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/PrivateKeyParser.java +++ /dev/null @@ -1,256 +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.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; -import java.security.spec.PKCS8EncodedKeySpec; -import java.util.ArrayList; -import java.util.Base64; -import java.util.Collections; -import java.util.List; -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 { - - private static final String PKCS1_HEADER = "-+BEGIN\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+"; - - private static final String PKCS1_FOOTER = "-+END\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+"; - - private static final String PKCS8_HEADER = "-+BEGIN\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+"; - - private static final String PKCS8_FOOTER = "-+END\\s+PRIVATE\\s+KEY[^-]*-+"; - - private static final String EC_HEADER = "-+BEGIN\\s+EC\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+"; - - private static final String EC_FOOTER = "-+END\\s+EC\\s+PRIVATE\\s+KEY[^-]*-+"; - - private static final String BASE64_TEXT = "([a-z0-9+/=\\r\\n]+)"; - - 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(PKCS8_HEADER, PKCS8_FOOTER, "RSA", PKCS8EncodedKeySpec::new)); - PEM_PARSERS = Collections.unmodifiableList(parsers); - } - - /** - * ASN.1 encoded object identifier {@literal 1.2.840.113549.1.1.1}. - */ - private static final int[] RSA_ALGORITHM = { 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01 }; - - /** - * ASN.1 encoded object identifier {@literal 1.2.840.10045.2.1}. - */ - private static final int[] EC_ALGORITHM = { 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01 }; - - /** - * ASN.1 encoded object identifier {@literal 1.3.132.0.34}. - */ - private static final int[] EC_PARAMETERS = { 0x2b, 0x81, 0x04, 0x00, 0x22 }; - - private PrivateKeyParser() { - } - - private static PKCS8EncodedKeySpec createKeySpecForPkcs1(byte[] bytes) { - return createKeySpecForAlgorithm(bytes, RSA_ALGORITHM, null); - } - - private static PKCS8EncodedKeySpec createKeySpecForEc(byte[] bytes) { - return createKeySpecForAlgorithm(bytes, EC_ALGORITHM, EC_PARAMETERS); - } - - private static PKCS8EncodedKeySpec createKeySpecForAlgorithm(byte[] bytes, int[] algorithm, int[] parameters) { - try { - DerEncoder encoder = new DerEncoder(); - encoder.integer(0x00); // Version 0 - DerEncoder algorithmIdentifier = new DerEncoder(); - algorithmIdentifier.objectIdentifier(algorithm); - algorithmIdentifier.objectIdentifier(parameters); - byte[] byteArray = algorithmIdentifier.toByteArray(); - encoder.sequence(byteArray); - encoder.octetString(bytes); - return new PKCS8EncodedKeySpec(encoder.toSequence()); - } - catch (IOException ex) { - throw new IllegalStateException(ex); - } - } - - /** - * Load a private key from the specified resource. - * @param resource the private key to parse - * @return the parsed private key - */ - static PrivateKey parse(String resource) { - try { - String text = readText(resource); - for (PemParser pemParser : PEM_PARSERS) { - PrivateKey privateKey = pemParser.parse(text); - if (privateKey != null) { - return privateKey; - } - } - 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); - } - } - - /** - * Parser for a specific PEM format. - */ - private static class PemParser { - - private final Pattern pattern; - - private final String algorithm; - - private final Function keySpecFactory; - - PemParser(String header, String footer, String algorithm, - Function keySpecFactory) { - this.pattern = Pattern.compile(header + BASE64_TEXT + footer, Pattern.CASE_INSENSITIVE); - this.algorithm = algorithm; - this.keySpecFactory = keySpecFactory; - } - - PrivateKey parse(String text) { - Matcher matcher = this.pattern.matcher(text); - return (!matcher.find()) ? null : parse(decodeBase64(matcher.group(1))); - } - - private static byte[] decodeBase64(String content) { - byte[] contentBytes = content.replaceAll("\r", "").replaceAll("\n", "").getBytes(); - return Base64.getDecoder().decode(contentBytes); - } - - private PrivateKey parse(byte[] bytes) { - try { - PKCS8EncodedKeySpec keySpec = this.keySpecFactory.apply(bytes); - KeyFactory keyFactory = KeyFactory.getInstance(this.algorithm); - return keyFactory.generatePrivate(keySpec); - } - catch (GeneralSecurityException ex) { - throw new IllegalArgumentException("Unexpected key format", ex); - } - } - - } - - /** - * Simple ASN.1 DER encoder. - */ - static class DerEncoder { - - private final ByteArrayOutputStream stream = new ByteArrayOutputStream(); - - void objectIdentifier(int... encodedObjectIdentifier) throws IOException { - int code = (encodedObjectIdentifier != null) ? 0x06 : 0x05; - codeLengthBytes(code, bytes(encodedObjectIdentifier)); - } - - void integer(int... encodedInteger) throws IOException { - codeLengthBytes(0x02, bytes(encodedInteger)); - } - - void octetString(byte[] bytes) throws IOException { - codeLengthBytes(0x04, bytes); - } - - void sequence(int... elements) throws IOException { - sequence(bytes(elements)); - } - - void sequence(byte[] bytes) throws IOException { - codeLengthBytes(0x30, bytes); - } - - void codeLengthBytes(int code, byte[] bytes) throws IOException { - this.stream.write(code); - int length = (bytes != null) ? bytes.length : 0; - if (length <= 127) { - this.stream.write(length & 0xFF); - } - else { - ByteArrayOutputStream lengthStream = new ByteArrayOutputStream(); - while (length != 0) { - lengthStream.write(length & 0xFF); - length = length >> 8; - } - byte[] lengthBytes = lengthStream.toByteArray(); - this.stream.write(0x80 | lengthBytes.length); - for (int i = lengthBytes.length - 1; i >= 0; i--) { - this.stream.write(lengthBytes[i]); - } - } - if (bytes != null) { - this.stream.write(bytes); - } - } - - private static byte[] bytes(int... elements) { - if (elements == null) { - return null; - } - byte[] result = new byte[elements.length]; - for (int i = 0; i < elements.length; i++) { - result[i] = (byte) elements[i]; - } - return result; - } - - byte[] toSequence() throws IOException { - DerEncoder sequenceEncoder = new DerEncoder(); - sequenceEncoder.sequence(toByteArray()); - return sequenceEncoder.toByteArray(); - } - - byte[] toByteArray() { - return this.stream.toByteArray(); - } - - } - -} 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/SslStoreProviderFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslStoreProviderFactory.java deleted file mode 100644 index d9b9f0d1e2..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslStoreProviderFactory.java +++ /dev/null @@ -1,41 +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; - -/** - * Creates an {@link SslStoreProvider} based on SSL configuration properties. - * - * @author Scott Frederick - * @since 3.1.0 - */ -public final class SslStoreProviderFactory { - - private SslStoreProviderFactory() { - } - - /** - * Create an {@link SslStoreProvider} if the appropriate SSL properties are - * configured. - * @param ssl the SSL properties - * @return an {@code SslStoreProvider} or {@code null} - */ - public static SslStoreProvider from(Ssl ssl) { - SslStoreProvider sslStoreProvider = CertificateFileSslStoreProvider.from(ssl); - return ((sslStoreProvider != null) ? sslStoreProvider : JavaKeyStoreSslStoreProvider.from(ssl)); - } - -} diff --git a/spring-boot-project/spring-boot/src/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/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/CertificateParserTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/CertificateParserTests.java deleted file mode 100644 index 1455b4db88..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/CertificateParserTests.java +++ /dev/null @@ -1,57 +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.cert.X509Certificate; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; - -/** - * Tests for {@link CertificateParser}. - * - * @author Scott Frederick - */ -class CertificateParserTests { - - @Test - void parseCertificate() { - X509Certificate[] certificates = CertificateParser.parse("classpath: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"); - 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); - } - -} 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/PrivateKeyParserTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/PrivateKeyParserTests.java deleted file mode 100644 index 390fb1b2b6..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/PrivateKeyParserTests.java +++ /dev/null @@ -1,62 +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.PrivateKey; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; - -/** - * Tests for {@link PrivateKeyParser}. - * - * @author Scott Frederick - */ -class PrivateKeyParserTests { - - @Test - void parsePkcs8KeyFile() { - PrivateKey privateKey = PrivateKeyParser.parse("classpath: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"); - 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); - } - - @Test - void parseWithInvalidPathWillThrowException() { - String path = "file:///bad/path/key.pem"; - assertThatIllegalStateException().isThrownBy(() -> PrivateKeyParser.parse(path)).withMessageContaining(path); - } - -} 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));