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
pull/35107/head
Scott Frederick 2 years ago committed by Phillip Webb
parent 8e1f24f98f
commit 66db13b962

@ -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."

@ -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<RSocketServerCustomizer> customizers) {
ObjectProvider<RSocketServerCustomizer> customizers, ObjectProvider<SslBundles> 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;
}

@ -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> sslBundles) {
return new ReactiveWebServerFactoryCustomizer(serverProperties, sslBundles.getIfAvailable());
}
@Bean

@ -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);
}
}

@ -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<WebListenerRegistrar> webListenerRegistrars,
ObjectProvider<CookieSameSiteSupplier> cookieSameSiteSuppliers) {
ObjectProvider<CookieSameSiteSupplier> cookieSameSiteSuppliers, ObjectProvider<SslBundles> sslBundles) {
return new ServletWebServerFactoryCustomizer(serverProperties, webListenerRegistrars.orderedStream().toList(),
cookieSameSiteSuppliers.orderedStream().toList());
cookieSameSiteSuppliers.orderedStream().toList(), sslBundles.getIfAvailable());
}
@Bean

@ -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<CookieSameSiteSupplier> cookieSameSiteSuppliers;
private final SslBundles sslBundles;
public ServletWebServerFactoryCustomizer(ServerProperties serverProperties) {
this(serverProperties, Collections.emptyList());
}
public ServletWebServerFactoryCustomizer(ServerProperties serverProperties,
List<WebListenerRegistrar> webListenerRegistrars) {
this(serverProperties, webListenerRegistrars, null);
this(serverProperties, webListenerRegistrars, null, null);
}
ServletWebServerFactoryCustomizer(ServerProperties serverProperties,
List<WebListenerRegistrar> webListenerRegistrars, List<CookieSameSiteSupplier> cookieSameSiteSuppliers) {
List<WebListenerRegistrar> webListenerRegistrars, List<CookieSameSiteSupplier> 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));
}
}

@ -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."

@ -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)

@ -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(

@ -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

@ -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<CloseableChannel> 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) {

@ -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);
}

@ -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);
}
}

@ -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);
}
/**

@ -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());
}
}

@ -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() {

@ -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);
}
}
}

@ -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) {

@ -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

@ -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);
}
/**

@ -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);
}
}
}

@ -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<HttpHandlerFactory> httpHandlerFactories = this.delegate.createHttpHandlerFactories(this,
(next) -> new UndertowHttpHandlerAdapter(httpHandler));
return new UndertowWebServer(builder, httpHandlerFactories, getPort() >= 0);

@ -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());
}

@ -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<SslBundle> 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 {

@ -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);
}
/**

@ -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;
}
}

@ -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<X509Certificate> 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<X509Certificate> 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);
}
}

@ -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

@ -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;
}
}

@ -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<PemParser> PEM_PARSERS;
static {
List<PemParser> 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<byte[], PKCS8EncodedKeySpec> keySpecFactory;
PemParser(String header, String footer, String algorithm,
Function<byte[], PKCS8EncodedKeySpec> 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();
}
}
}

@ -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 <R> 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> 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;
};
}
}

@ -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);
}
}

@ -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 {
/**

@ -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));
}
}

@ -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();
}
}
}

@ -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<String> 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();

@ -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;
}

@ -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)));
}
}

@ -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()));
}

@ -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)));
}
}

@ -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

@ -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();
}
}

@ -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);
}
}

@ -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();
}
}

@ -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);
}
}

@ -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

@ -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;
}
}
}

@ -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));

Loading…
Cancel
Save