Provide centralized configuration of SSL properties

Closes gh-34814
This commit is contained in:
Scott Frederick
2023-04-21 01:04:37 -07:00
committed by Phillip Webb
93 changed files with 4559 additions and 1332 deletions

View File

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

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

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.ssl;
import org.springframework.boot.ssl.jks.JksSslStoreBundle;
/**
* {@link SslBundleProperties} for Java keystores.
*
* @author Scott Frederick
* @author Phillip Webb
* @since 3.1.0
* @see JksSslStoreBundle
*/
public class JksSslBundleProperties extends SslBundleProperties {
/**
* Keystore properties.
*/
private final Store keystore = new Store();
/**
* Truststore properties.
*/
private final Store truststore = new Store();
public Store getKeystore() {
return this.keystore;
}
public Store getTruststore() {
return this.truststore;
}
/**
* Store properties.
*/
public static class Store {
/**
* Type of the store to create, e.g. JKS.
*/
private String type;
/**
* Provider for the store.
*/
private String provider;
/**
* Location of the resource containing the store content.
*/
private String location;
/**
* Password used to access the store.
*/
private String password;
public String getType() {
return this.type;
}
public void setType(String type) {
this.type = type;
}
public String getProvider() {
return this.provider;
}
public void setProvider(String provider) {
this.provider = provider;
}
public String getLocation() {
return this.location;
}
public void setLocation(String location) {
this.location = location;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.ssl;
import org.springframework.boot.ssl.pem.PemSslStoreBundle;
/**
* {@link SslBundleProperties} for PEM-encoded certificates and private keys.
*
* @author Scott Frederick
* @author Phillip Webb
* @since 3.1.0
* @see PemSslStoreBundle
*/
public class PemSslBundleProperties extends SslBundleProperties {
/**
* Keystore properties.
*/
private Store keystore = new Store();
/**
* Truststore properties.
*/
private Store truststore = new Store();
public Store getKeystore() {
return this.keystore;
}
public Store getTruststore() {
return this.truststore;
}
/**
* Store properties.
*/
public static class Store {
/**
* Type of the store to create, e.g. JKS.
*/
String type;
/**
* Location or content of the certificate in PEM format.
*/
String certificate;
/**
* Location or content of the private key in PEM format.
*/
String privateKey;
public String getType() {
return this.type;
}
public void setType(String type) {
this.type = type;
}
public String getCertificate() {
return this.certificate;
}
public void setCertificate(String certificate) {
this.certificate = certificate;
}
public String getPrivateKey() {
return this.privateKey;
}
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
}
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.ssl;
import org.springframework.boot.autoconfigure.ssl.SslBundleProperties.Key;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundleKey;
import org.springframework.boot.ssl.SslManagerBundle;
import org.springframework.boot.ssl.SslOptions;
import org.springframework.boot.ssl.SslStoreBundle;
import org.springframework.boot.ssl.jks.JksSslStoreBundle;
import org.springframework.boot.ssl.jks.JksSslStoreDetails;
import org.springframework.boot.ssl.pem.PemSslStoreBundle;
import org.springframework.boot.ssl.pem.PemSslStoreDetails;
/**
* {@link SslBundle} backed by {@link JksSslBundleProperties} or
* {@link PemSslBundleProperties}.
*
* @author Scott Frederick
* @author Phillip Webb
* @since 3.1.0
*/
public final class PropertiesSslBundle implements SslBundle {
private final SslStoreBundle stores;
private final SslBundleKey key;
private final SslOptions options;
private final String protocol;
private final SslManagerBundle managers;
private PropertiesSslBundle(SslStoreBundle stores, SslBundleProperties properties) {
this.stores = stores;
this.key = asSslKeyReference(properties.getKey());
this.options = asSslOptions(properties.getOptions());
this.protocol = properties.getProtocol();
this.managers = SslManagerBundle.from(this.stores, this.key);
}
private static SslBundleKey asSslKeyReference(Key key) {
return (key != null) ? SslBundleKey.of(key.getPassword(), key.getAlias()) : SslBundleKey.NONE;
}
private static SslOptions asSslOptions(SslBundleProperties.Options properties) {
return (properties != null) ? SslOptions.of(properties.getCiphers(), properties.getEnabledProtocols())
: SslOptions.NONE;
}
@Override
public SslStoreBundle getStores() {
return this.stores;
}
@Override
public SslBundleKey getKey() {
return this.key;
}
@Override
public SslOptions getOptions() {
return this.options;
}
@Override
public String getProtocol() {
return this.protocol;
}
@Override
public SslManagerBundle getManagers() {
return this.managers;
}
/**
* Get an {@link SslBundle} for the given {@link PemSslBundleProperties}.
* @param properties the source properties
* @return an {@link SslBundle} instance
*/
public static SslBundle get(PemSslBundleProperties properties) {
return new PropertiesSslBundle(asSslStoreBundle(properties), properties);
}
/**
* Get an {@link SslBundle} for the given {@link JksSslBundleProperties}.
* @param properties the source properties
* @return an {@link SslBundle} instance
*/
public static SslBundle get(JksSslBundleProperties properties) {
return new PropertiesSslBundle(asSslStoreBundle(properties), properties);
}
private static SslStoreBundle asSslStoreBundle(PemSslBundleProperties properties) {
PemSslStoreDetails keyStoreDetails = asStoreDetails(properties.getKeystore());
PemSslStoreDetails trustStoreDetails = asStoreDetails(properties.getTruststore());
return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, properties.getKey().getAlias());
}
private static PemSslStoreDetails asStoreDetails(PemSslBundleProperties.Store properties) {
return new PemSslStoreDetails(properties.getType(), properties.getCertificate(), properties.getPrivateKey());
}
private static SslStoreBundle asSslStoreBundle(JksSslBundleProperties properties) {
JksSslStoreDetails keyStoreDetails = asStoreDetails(properties.getKeystore());
JksSslStoreDetails trustStoreDetails = asStoreDetails(properties.getTruststore());
return new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
}
private static JksSslStoreDetails asStoreDetails(JksSslBundleProperties.Store properties) {
return new JksSslStoreDetails(properties.getType(), properties.getProvider(), properties.getLocation(),
properties.getPassword());
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.ssl;
import java.util.List;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
import org.springframework.boot.ssl.SslBundleRegistry;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.context.annotation.Bean;
/**
* {@link EnableAutoConfiguration Auto-configuration} for SSL.
*
* @author Scott Frederick
* @since 3.1.0
*/
@AutoConfiguration
@EnableConfigurationProperties(SslProperties.class)
public class SslAutoConfiguration {
SslAutoConfiguration() {
}
@Bean
public SslPropertiesBundleRegistrar sslPropertiesSslBundleRegistrar(SslProperties sslProperties) {
return new SslPropertiesBundleRegistrar(sslProperties);
}
@Bean
@ConditionalOnMissingBean({ SslBundleRegistry.class, SslBundles.class })
public DefaultSslBundleRegistry sslBundleRegistry(List<SslBundleRegistrar> sslBundleRegistrars) {
DefaultSslBundleRegistry registry = new DefaultSslBundleRegistry();
sslBundleRegistrars.forEach((registrar) -> registrar.registerBundles(registry));
return registry;
}
}

View File

@@ -0,0 +1,124 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.ssl;
import java.util.Set;
import org.springframework.boot.ssl.SslBundle;
/**
* Base class for SSL Bundle properties.
*
* @author Scott Frederick
* @author Phillip Webb
* @since 3.1.0
* @see SslBundle
*/
public abstract class SslBundleProperties {
/**
* Key details for the bundle.
*/
private final Key key = new Key();
/**
* Options for the SLL connection.
*/
private final Options options = new Options();
/**
* SSL Protocol to use.
*/
private String protocol = SslBundle.DEFAULT_PROTOCOL;
public Key getKey() {
return this.key;
}
public Options getOptions() {
return this.options;
}
public String getProtocol() {
return this.protocol;
}
public void setProtocol(String protocol) {
this.protocol = protocol;
}
public static class Options {
/**
* Supported SSL ciphers.
*/
private Set<String> ciphers;
/**
* Enabled SSL protocols.
*/
private Set<String> enabledProtocols;
public Set<String> getCiphers() {
return this.ciphers;
}
public void setCiphers(Set<String> ciphers) {
this.ciphers = ciphers;
}
public Set<String> getEnabledProtocols() {
return this.enabledProtocols;
}
public void setEnabledProtocols(Set<String> enabledProtocols) {
this.enabledProtocols = enabledProtocols;
}
}
public static class Key {
/**
* The password used to access the key in the key store.
*/
private String password;
/**
* The alias that identifies the key in the key store.
*/
private String alias;
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
public String getAlias() {
return this.alias;
}
public void setAlias(String alias) {
this.alias = alias;
}
}
}

View File

@@ -14,28 +14,26 @@
* limitations under the License.
*/
package org.springframework.boot.web.server;
package org.springframework.boot.autoconfigure.ssl;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundleRegistry;
/**
* Creates an {@link SslStoreProvider} based on SSL configuration properties.
* Interface to be implemented by types that register {@link SslBundle} instances with an
* {@link SslBundleRegistry}.
*
* @author Scott Frederick
* @since 3.1.0
*/
public final class SslStoreProviderFactory {
private SslStoreProviderFactory() {
}
@FunctionalInterface
public interface SslBundleRegistrar {
/**
* Create an {@link SslStoreProvider} if the appropriate SSL properties are
* configured.
* @param ssl the SSL properties
* @return an {@code SslStoreProvider} or {@code null}
* Callback method for registering {@link SslBundle}s with an
* {@link SslBundleRegistry}.
* @param registry the registry that accepts {@code SslBundle}s
*/
public static SslStoreProvider from(Ssl ssl) {
SslStoreProvider sslStoreProvider = CertificateFileSslStoreProvider.from(ssl);
return ((sslStoreProvider != null) ? sslStoreProvider : JavaKeyStoreSslStoreProvider.from(ssl));
}
void registerBundles(SslBundleRegistry registry);
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.ssl;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
/**
* Properties for centralized SSL trust material configuration.
*
* @author Scott Frederick
* @since 3.1.0
*/
@ConfigurationProperties(prefix = "spring.ssl")
public class SslProperties {
/**
* SSL bundles.
*/
private final Bundles bundle = new Bundles();
public Bundles getBundle() {
return this.bundle;
}
/**
* Properties to define SSL Bundles.
*/
public static class Bundles {
/**
* PEM-encoded SSL trust material.
*/
@NestedConfigurationProperty
private final Map<String, PemSslBundleProperties> pem = new LinkedHashMap<>();
/**
* Java keystore SSL trust material.
*/
@NestedConfigurationProperty
private final Map<String, JksSslBundleProperties> jks = new LinkedHashMap<>();
public Map<String, PemSslBundleProperties> getPem() {
return this.pem;
}
public Map<String, JksSslBundleProperties> getJks() {
return this.jks;
}
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.ssl;
import java.util.Map;
import java.util.function.Function;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundleRegistry;
/**
* A {@link SslBundleRegistrar} that registers SSL bundles based
* {@link SslProperties#getBundle() configuration properties}.
*
* @author Scott Frederick
* @author Phillip Webb
*/
class SslPropertiesBundleRegistrar implements SslBundleRegistrar {
private final SslProperties.Bundles properties;
SslPropertiesBundleRegistrar(SslProperties properties) {
this.properties = properties.getBundle();
}
@Override
public void registerBundles(SslBundleRegistry registry) {
registerBundles(registry, this.properties.getPem(), PropertiesSslBundle::get);
registerBundles(registry, this.properties.getJks(), PropertiesSslBundle::get);
}
private <P extends SslBundleProperties> void registerBundles(SslBundleRegistry registry, Map<String, P> properties,
Function<P, SslBundle> bundleFactory) {
properties.forEach((bundleName, bundleProperties) -> registry.registerBundle(bundleName,
bundleFactory.apply(bundleProperties)));
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Auto-configuration for SSL bundles.
*/
package org.springframework.boot.autoconfigure.ssl;

View File

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

View File

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

View File

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

View File

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

View File

@@ -262,6 +262,10 @@
"name": "server.shutdown",
"defaultValue": "immediate"
},
{
"name": "server.ssl.bundle",
"description": "The name of a configured SSL bundle."
},
{
"name": "server.ssl.certificate",
"description": "Path to a PEM-encoded SSL certificate file."
@@ -2635,6 +2639,10 @@
"level": "error"
}
},
{
"name": "spring.rsocket.server.ssl.bundle",
"description": "The name of a configured SSL bundle."
},
{
"name": "spring.rsocket.server.ssl.certificate",
"description": "Path to a PEM-encoded SSL certificate file."

View File

@@ -116,6 +116,7 @@ org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.Reactiv
org.springframework.boot.autoconfigure.security.oauth2.server.servlet.OAuth2AuthorizationServerAutoConfiguration
org.springframework.boot.autoconfigure.security.oauth2.server.servlet.OAuth2AuthorizationServerJwtAutoConfiguration
org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration
org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration
org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration
org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration

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

View File

@@ -0,0 +1,145 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.ssl;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundleRegistry;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SslAutoConfiguration}.
*
* @author Scott Frederick
* @author Phillip Webb
*/
class SslAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(SslAutoConfiguration.class));
@Test
void sslBundlesCreatedWithNoConfiguration() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(SslBundleRegistry.class));
}
@Test
void sslBundlesCreatedWithCertificates() {
List<String> propertyValues = new ArrayList<>();
propertyValues.add("spring.ssl.bundle.pem.first.key.alias=alias1");
propertyValues.add("spring.ssl.bundle.pem.first.key.password=secret1");
propertyValues.add("spring.ssl.bundle.pem.first.keystore.certificate=cert1.pem");
propertyValues.add("spring.ssl.bundle.pem.first.keystore.private-key=key1.pem");
propertyValues.add("spring.ssl.bundle.pem.first.keystore.type=JKS");
propertyValues.add("spring.ssl.bundle.pem.first.truststore.type=PKCS12");
propertyValues.add("spring.ssl.bundle.pem.second.key.alias=alias2");
propertyValues.add("spring.ssl.bundle.pem.second.key.password=secret2");
propertyValues.add("spring.ssl.bundle.pem.second.keystore.certificate=cert2.pem");
propertyValues.add("spring.ssl.bundle.pem.second.keystore.private-key=key2.pem");
propertyValues.add("spring.ssl.bundle.pem.second.keystore.type=PKCS12");
propertyValues.add("spring.ssl.bundle.pem.second.truststore.certificate=ca.pem");
propertyValues.add("spring.ssl.bundle.pem.second.truststore.private-key=ca-key.pem");
propertyValues.add("spring.ssl.bundle.pem.second.truststore.type=JKS");
this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> {
assertThat(context).hasSingleBean(SslBundles.class);
SslBundles bundles = context.getBean(SslBundles.class);
SslBundle first = bundles.getBundle("first");
assertThat(first).isNotNull();
assertThat(first.getStores()).isNotNull();
assertThat(first.getManagers()).isNotNull();
assertThat(first.getKey().getAlias()).isEqualTo("alias1");
assertThat(first.getKey().getPassword()).isEqualTo("secret1");
assertThat(first.getStores()).extracting("keyStoreDetails").extracting("type").isEqualTo("JKS");
assertThat(first.getStores()).extracting("trustStoreDetails").extracting("type").isEqualTo("PKCS12");
SslBundle second = bundles.getBundle("second");
assertThat(second).isNotNull();
assertThat(second.getStores()).isNotNull();
assertThat(second.getManagers()).isNotNull();
assertThat(second.getKey().getAlias()).isEqualTo("alias2");
assertThat(second.getKey().getPassword()).isEqualTo("secret2");
assertThat(second.getStores()).extracting("keyStoreDetails").extracting("type").isEqualTo("PKCS12");
assertThat(second.getStores()).extracting("trustStoreDetails").extracting("type").isEqualTo("JKS");
});
}
@Test
void sslBundlesCreatedWithCustomSslBundle() {
List<String> propertyValues = new ArrayList<>();
propertyValues.add("custom.ssl.key.alias=alias1");
propertyValues.add("custom.ssl.key.password=secret1");
propertyValues.add("custom.ssl.keystore.type=JKS");
propertyValues.add("custom.ssl.truststore.type=PKCS12");
this.contextRunner.withUserConfiguration(CustomSslBundleConfiguration.class)
.withPropertyValues(propertyValues.toArray(String[]::new))
.run((context) -> {
assertThat(context).hasSingleBean(SslBundles.class);
SslBundles bundles = context.getBean(SslBundles.class);
SslBundle first = bundles.getBundle("custom");
assertThat(first).isNotNull();
assertThat(first.getStores()).isNotNull();
assertThat(first.getManagers()).isNotNull();
assertThat(first.getKey().getAlias()).isEqualTo("alias1");
assertThat(first.getKey().getPassword()).isEqualTo("secret1");
assertThat(first.getStores()).extracting("keyStoreDetails").extracting("type").isEqualTo("JKS");
assertThat(first.getStores()).extracting("trustStoreDetails").extracting("type").isEqualTo("PKCS12");
});
}
@Configuration
@EnableConfigurationProperties(CustomSslProperties.class)
public static class CustomSslBundleConfiguration {
@Bean
public SslBundleRegistrar customSslBundlesRegistrar(CustomSslProperties properties) {
return new CustomSslBundlesRegistrar(properties);
}
}
@ConfigurationProperties("custom.ssl")
static class CustomSslProperties extends PemSslBundleProperties {
}
static class CustomSslBundlesRegistrar implements SslBundleRegistrar {
private final CustomSslProperties properties;
CustomSslBundlesRegistrar(CustomSslProperties properties) {
this.properties = properties;
}
@Override
public void registerBundles(SslBundleRegistry registry) {
registry.registerBundle("custom", PropertiesSslBundle.get(this.properties));
}
}
}

View File

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

View File

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

View File

@@ -30,4 +30,6 @@ include::features/developing-auto-configuration.adoc[]
include::features/kotlin.adoc[]
include::features/ssl.adoc[]
include::features/whats-next.adoc[]

View File

@@ -0,0 +1,99 @@
[[features.ssl]]
== SSL
Spring Boot provides the ability to configure SSL trust material that can be applied to several types of connections in order to support secure communications.
Configuration properties with the prefix `spring.ssl.bundle` can be used to specify named sets of trust material and associated information.
[[features.ssl.jks]]
=== Configuring SSL With Java KeyStore Files
Configuration properties with the prefix `spring.ssl.bundle.jks` can be used to configure bundles of trust material created with the Java `keytool` utility and stored in Java KeyStore files in the JKS or PKCS12 format.
Each bundle has a user-provided name that can be used to reference the bundle.
When used to secure an embedded web server, a `keystore` is typically configured with a Java KeyStore containing a certificate and private key as shown in this example:
[source,yaml,indent=0,subs="verbatim",configblocks]
----
spring:
ssl:
bundle:
jks:
mybundle:
key:
alias: "application"
keystore:
location: "classpath:application.p12"
password: "secret"
type: "PKCS12"
----
When used to secure a client-side connection, a `truststore` is typically configured with a Java KeyStore containing the server certificate as shown in this example:
[source,yaml,indent=0,subs="verbatim",configblocks]
----
spring:
ssl:
bundle:
jks:
mybundle:
truststore:
location: "classpath:server.p12"
password: "secret"
----
See {spring-boot-autoconfigure-module-code}/ssl/JksSslBundleProperties.java[JksSslBundleProperties] for the full set of supported properties.
[[features.ssl.pem]]
=== Configuring SSL With PEM-encoded Certificates
Configuration properties with the prefix `spring.ssl.bundle.pem` can be used to configure bundles of trust material in the form of PEM-encoded text.
Each bundle has a user-provided name that can be used to reference the bundle.
When used to secure an embedded web server, a `keystore` is typically configured with a certificate and private key as shown in this example:
[source,yaml,indent=0,subs="verbatim",configblocks]
----
spring:
ssl:
bundle:
pem:
mybundle:
keystore:
certificate: "classpath:application.crt"
private-key: "classpath:application.key"
----
When used to secure an embedded web server, a `truststore` is typically configured with the server certificate as shown in this example:
[source,yaml,indent=0,subs="verbatim",configblocks]
----
spring:
ssl:
bundle:
pem:
mybundle:
truststore:
certificate: "classpath:server.crt"
----
See {spring-boot-autoconfigure-module-code}/ssl/PemSslBundleProperties.java[PemSslBundleProperties] for the full set of supported properties.
[[features.ssl.applying]]
=== Applying SSL Bundles
Once configured using properties, SSL bundles can be referred to by name in configuration properties for various types of connections that are auto-configured by Spring Boot.
See the sections on <<howto#howto.webserver.configure-ssl,embedded web servers>> and <<data#data,data technologies>> for further information.
[[features.ssl.bundles]]
=== Using SSL Bundles
Spring Boot auto-configures a bean of type `SslBundles` that provides access to each of the named bundles configured using the `spring.ssl.bundle` properties.
An `SslBundle` can be retrieved from the auto-configured `SslBundles` bean and used to create a `javax.net.ssl.SSLContext` or objects of other types from the `java.net.ssl` package that are typically used to configure SSL connectivity in other APIs.
The following example shows retrieving an `SslBundle` and using it to create an `SSLContext`:
include::code:MyComponent[]

View File

@@ -207,6 +207,16 @@ The following example shows setting SSL properties using PEM-encoded certificate
trust-certificate: "classpath:ca-cert.crt"
----
Alternatively, the SSL trust material can be configured in an <<features#features.ssl,SSL bundle>> and applied to the web server as shown in this example:
[source,yaml,indent=0,subs="verbatim",configprops,configblocks]
----
server:
port: 8443
ssl:
bundle: "example"
----
See {spring-boot-module-code}/web/server/Ssl.java[`Ssl`] for details of all of the supported properties.
Using configuration such as the preceding example means the application no longer supports a plain HTTP connector at port 8080.

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docs.features.ssl.bundles;
import javax.net.ssl.SSLContext;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.stereotype.Component;
@Component
public class MyComponent {
@SuppressWarnings("unused")
public MyComponent(SslBundles sslBundles) {
SslBundle sslBundle = sslBundles.getBundle("mybundle");
SSLContext sslContext = sslBundle.createSslContext();
// do something with the created sslContext
}
}

View File

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

View File

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

View File

@@ -0,0 +1,149 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl;
import java.net.Socket;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.KeyManagerFactorySpi;
import javax.net.ssl.ManagerFactoryParameters;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.X509ExtendedKeyManager;
/**
* {@link KeyManagerFactory} that allows a configurable key alias to be used. Due to the
* fact that the actual calls to retrieve the key by alias are done at request time the
* approach is to wrap the actual key managers with a {@link AliasX509ExtendedKeyManager}.
* The actual SPI has to be wrapped as well due to the fact that
* {@link KeyManagerFactory#getKeyManagers()} is final.
*
* @author Scott Frederick
*/
final class AliasKeyManagerFactory extends KeyManagerFactory {
AliasKeyManagerFactory(KeyManagerFactory delegate, String alias, String algorithm) {
super(new AliasKeyManagerFactorySpi(delegate, alias), delegate.getProvider(), algorithm);
}
/**
* {@link KeyManagerFactorySpi} that allows a configurable key alias to be used.
*/
private static final class AliasKeyManagerFactorySpi extends KeyManagerFactorySpi {
private final KeyManagerFactory delegate;
private final String alias;
private AliasKeyManagerFactorySpi(KeyManagerFactory delegate, String alias) {
this.delegate = delegate;
this.alias = alias;
}
@Override
protected void engineInit(KeyStore keyStore, char[] chars)
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
this.delegate.init(keyStore, chars);
}
@Override
protected void engineInit(ManagerFactoryParameters managerFactoryParameters)
throws InvalidAlgorithmParameterException {
throw new InvalidAlgorithmParameterException("Unsupported ManagerFactoryParameters");
}
@Override
protected KeyManager[] engineGetKeyManagers() {
return Arrays.stream(this.delegate.getKeyManagers())
.filter(X509ExtendedKeyManager.class::isInstance)
.map(X509ExtendedKeyManager.class::cast)
.map(this::wrap)
.toArray(KeyManager[]::new);
}
private AliasKeyManagerFactory.AliasX509ExtendedKeyManager wrap(X509ExtendedKeyManager keyManager) {
return new AliasX509ExtendedKeyManager(keyManager, this.alias);
}
}
/**
* {@link X509ExtendedKeyManager} that allows a configurable key alias to be used.
*/
static final class AliasX509ExtendedKeyManager extends X509ExtendedKeyManager {
private final X509ExtendedKeyManager delegate;
private final String alias;
private AliasX509ExtendedKeyManager(X509ExtendedKeyManager keyManager, String alias) {
this.delegate = keyManager;
this.alias = alias;
}
@Override
public String chooseEngineClientAlias(String[] strings, Principal[] principals, SSLEngine sslEngine) {
return this.delegate.chooseEngineClientAlias(strings, principals, sslEngine);
}
@Override
public String chooseEngineServerAlias(String s, Principal[] principals, SSLEngine sslEngine) {
return this.alias;
}
@Override
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
return this.delegate.chooseClientAlias(keyType, issuers, socket);
}
@Override
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
return this.delegate.chooseServerAlias(keyType, issuers, socket);
}
@Override
public X509Certificate[] getCertificateChain(String alias) {
return this.delegate.getCertificateChain(alias);
}
@Override
public String[] getClientAliases(String keyType, Principal[] issuers) {
return this.delegate.getClientAliases(keyType, issuers);
}
@Override
public PrivateKey getPrivateKey(String alias) {
return this.delegate.getPrivateKey(alias);
}
@Override
public String[] getServerAliases(String keyType, Principal[] issuers) {
return this.delegate.getServerAliases(keyType, issuers);
}
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.util.Assert;
/**
* Default {@link SslBundleRegistry} implementation.
*
* @author Scott Frederick
* @since 3.1.0
*/
public class DefaultSslBundleRegistry implements SslBundleRegistry, SslBundles {
private final Map<String, SslBundle> bundles = new ConcurrentHashMap<>();
public DefaultSslBundleRegistry() {
}
public DefaultSslBundleRegistry(String name, SslBundle bundle) {
registerBundle(name, bundle);
}
@Override
public void registerBundle(String name, SslBundle bundle) {
Assert.notNull(name, "Name must not be null");
Assert.notNull(bundle, "Bundle must not be null");
SslBundle previous = this.bundles.putIfAbsent(name, bundle);
Assert.state(previous == null, () -> "Cannot replace existing SSL bundle '%s'".formatted(name));
}
@Override
public SslBundle getBundle(String name) {
Assert.notNull(name, "Name must not be null");
SslBundle bundle = this.bundles.get(name);
if (bundle == null) {
throw new NoSuchSslBundleException(name, "SSL bundle name '%s' cannot be found".formatted(name));
}
return bundle;
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
/**
* Default implementation of {@link SslManagerBundle}.
*
* @author Scott Frederick
* @see SslManagerBundle#from(SslStoreBundle, SslBundleKey)
*/
class DefaultSslManagerBundle implements SslManagerBundle {
private final SslStoreBundle storeBundle;
private final SslBundleKey key;
DefaultSslManagerBundle(SslStoreBundle storeBundle, SslBundleKey key) {
this.storeBundle = (storeBundle != null) ? storeBundle : SslStoreBundle.NONE;
this.key = (key != null) ? key : SslBundleKey.NONE;
}
@Override
public KeyManagerFactory getKeyManagerFactory() {
try {
KeyStore store = this.storeBundle.getKeyStore();
this.key.assertContainsAlias(store);
String alias = this.key.getAlias();
String algorithm = KeyManagerFactory.getDefaultAlgorithm();
KeyManagerFactory factory = getKeyManagerFactoryInstance(algorithm);
factory = (alias != null) ? new AliasKeyManagerFactory(factory, alias, algorithm) : factory;
String password = this.key.getPassword();
password = (password != null) ? password : this.storeBundle.getKeyStorePassword();
factory.init(store, (password != null) ? password.toCharArray() : null);
return factory;
}
catch (RuntimeException ex) {
throw ex;
}
catch (Exception ex) {
throw new IllegalStateException("Could not load key manager factory: " + ex.getMessage(), ex);
}
}
@Override
public TrustManagerFactory getTrustManagerFactory() {
try {
KeyStore store = this.storeBundle.getTrustStore();
String algorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory factory = getTrustManagerFactoryInstance(algorithm);
factory.init(store);
return factory;
}
catch (Exception ex) {
throw new IllegalStateException("Could not load trust manager factory: " + ex.getMessage(), ex);
}
}
protected KeyManagerFactory getKeyManagerFactoryInstance(String algorithm) throws NoSuchAlgorithmException {
return KeyManagerFactory.getInstance(algorithm);
}
protected TrustManagerFactory getTrustManagerFactoryInstance(String algorithm) throws NoSuchAlgorithmException {
return TrustManagerFactory.getInstance(algorithm);
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl;
/**
* Exception indicating that an {@link SslBundle} was referenced with a name that does not
* match any registered bundle.
*
* @author Scott Frederick
* @since 3.1.0
*/
public class NoSuchSslBundleException extends RuntimeException {
private final String bundleName;
/**
* Create a new {@code SslBundleNotFoundException} instance.
* @param bundleName the name of the bundle that could not be found
* @param message the exception message
*/
public NoSuchSslBundleException(String bundleName, String message) {
this(bundleName, message, null);
}
/**
* Create a new {@code SslBundleNotFoundException} instance.
* @param bundleName the name of the bundle that could not be found
* @param message the exception message
* @param cause the exception cause
*/
public NoSuchSslBundleException(String bundleName, String message, Throwable cause) {
super(message, cause);
this.bundleName = bundleName;
}
/**
* Return the name of the bundle that was not found.
* @return the bundle name
*/
public String getBundleName() {
return this.bundleName;
}
}

View File

@@ -0,0 +1,166 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import org.springframework.util.StringUtils;
/**
* A bundle of trust material that can be used to establish an SSL connection.
*
* @author Scott Frederick
* @since 3.1.0
*/
public interface SslBundle {
/**
* The default protocol to use.
*/
String DEFAULT_PROTOCOL = "TLS";
/**
* Return the {@link SslStoreBundle} that can be used to access this bundle's key and
* trust stores.
* @return the {@code SslStoreBundle} instance for this bundle
*/
SslStoreBundle getStores();
/**
* Return a reference to the key that should be used for this bundle or
* {@link SslBundleKey#NONE}.
* @return a reference to the SSL key that should be used
*/
SslBundleKey getKey();
/**
* Return {@link SslOptions} that should be applied when establishing the SSL
* connection.
* @return the options that should be applied
*/
SslOptions getOptions();
/**
* Return the protocol to use when establishing the connection. Values should be
* supported by {@link SSLContext#getInstance(String)}.
* @return the SSL protocol
* @see SSLContext#getInstance(String)
*/
String getProtocol();
/**
* Return the {@link SslManagerBundle} that can be used to access this bundle's
* {@link KeyManager key} and {@link TrustManager trust} managers.
* @return the {@code SslManagerBundle} instance for this bundle
*/
SslManagerBundle getManagers();
/**
* Factory method to create a new {@link SSLContext} for this bundle.
* @return a new {@link SSLContext} instance
*/
default SSLContext createSslContext() {
return getManagers().createSslContext(getProtocol());
}
/**
* Factory method to create a new {@link SslBundle} instance.
* @param stores the stores or {@code null}
* @return a new {@link SslBundle} instance
*/
static SslBundle of(SslStoreBundle stores) {
return of(stores, null, null);
}
/**
* Factory method to create a new {@link SslBundle} instance.
* @param stores the stores or {@code null}
* @param key the key or {@code null}
* @return a new {@link SslBundle} instance
*/
static SslBundle of(SslStoreBundle stores, SslBundleKey key) {
return of(stores, key, null);
}
/**
* Factory method to create a new {@link SslBundle} instance.
* @param stores the stores or {@code null}
* @param key the key or {@code null}
* @param options the options or {@code null}
* @return a new {@link SslBundle} instance
*/
static SslBundle of(SslStoreBundle stores, SslBundleKey key, SslOptions options) {
return of(stores, key, options, null);
}
/**
* Factory method to create a new {@link SslBundle} instance.
* @param stores the stores or {@code null}
* @param key the key or {@code null}
* @param options the options or {@code null}
* @param protocol the protocol or {@code null}
* @return a new {@link SslBundle} instance
*/
static SslBundle of(SslStoreBundle stores, SslBundleKey key, SslOptions options, String protocol) {
return of(stores, key, options, protocol, null);
}
/**
* Factory method to create a new {@link SslBundle} instance.
* @param stores the stores or {@code null}
* @param key the key or {@code null}
* @param options the options or {@code null}
* @param protocol the protocol or {@code null}
* @param managers the managers or {@code null}
* @return a new {@link SslBundle} instance
*/
static SslBundle of(SslStoreBundle stores, SslBundleKey key, SslOptions options, String protocol,
SslManagerBundle managers) {
SslManagerBundle managersToUse = (managers != null) ? managers : SslManagerBundle.from(stores, key);
return new SslBundle() {
@Override
public SslStoreBundle getStores() {
return (stores != null) ? stores : SslStoreBundle.NONE;
}
@Override
public SslBundleKey getKey() {
return (key != null) ? key : SslBundleKey.NONE;
}
@Override
public SslOptions getOptions() {
return (options != null) ? options : SslOptions.NONE;
}
@Override
public String getProtocol() {
return (!StringUtils.hasText(protocol)) ? DEFAULT_PROTOCOL : protocol;
}
@Override
public SslManagerBundle getManagers() {
return managersToUse;
}
};
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl;
import java.security.KeyStore;
import java.security.KeyStoreException;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* A reference to a single key obtained via {@link SslBundle}.
*
* @author Phillip Webb
* @since 3.1.0
*/
public interface SslBundleKey {
/**
* {@link SslBundleKey} that returns no values.
*/
SslBundleKey NONE = of(null, null);
/**
* Return the password that should be used to access the key or {@code null} if no
* password is required.
* @return the key password
*/
String getPassword();
/**
* Return the alias of the key or {@code null} if the key has no alias.
* @return the key alias
*/
String getAlias();
/**
* Assert that the alias is contained in the given keystore.
* @param keyStore the keystore to check
*/
default void assertContainsAlias(KeyStore keyStore) {
String alias = getAlias();
if (StringUtils.hasLength(alias) && keyStore != null) {
try {
Assert.state(keyStore.containsAlias(alias),
() -> String.format("Keystore does not contain alias '%s'", alias));
}
catch (KeyStoreException ex) {
throw new IllegalStateException(
String.format("Could not determine if keystore contains alias '%s'", alias), ex);
}
}
}
/**
* Factory method to create a new {@link SslBundleKey} instance.
* @param password the password used to access the key
* @return a new {@link SslBundleKey} instance
*/
static SslBundleKey of(String password) {
return of(password, null);
}
/**
* Factory method to create a new {@link SslBundleKey} instance.
* @param password the password used to access the key
* @param alias the alias of the key
* @return a new {@link SslBundleKey} instance
*/
static SslBundleKey of(String password, String alias) {
return new SslBundleKey() {
@Override
public String getPassword() {
return password;
}
@Override
public String getAlias() {
return alias;
}
};
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl;
/**
* Interface that can be used to register an {@link SslBundle} for a given name.
*
* @author Scott Frederick
* @since 3.1.0
*/
public interface SslBundleRegistry {
/**
* Register a named {@link SslBundle}.
* @param name the bundle name
* @param bundle the bundle
*/
void registerBundle(String name, SslBundle bundle);
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl;
/**
* A managed set of {@link SslBundle} instances that can be retrieved by name.
*
* @author Scott Frederick
* @since 3.1.0
*/
public interface SslBundles {
/**
* Return an {@link SslBundle} with the provided name.
* @param bundleName the bundle name
* @return the bundle
* @throws NoSuchSslBundleException if a bundle with the provided name does not exist
*/
SslBundle getBundle(String bundleName) throws NoSuchSslBundleException;
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import org.springframework.util.Assert;
/**
* A bundle of key and trust managers that can be used to establish an SSL connection.
* Instances are usually created {@link #from(SslStoreBundle, SslBundleKey) from} an
* {@link SslStoreBundle}.
*
* @author Scott Frederick
* @since 3.1.0
* @see SslStoreBundle
* @see SslBundle#getManagers()
*/
public interface SslManagerBundle {
/**
* Return the {@code KeyManager} instances used to establish identity.
* @return the key managers
*/
default KeyManager[] getKeyManagers() {
return getKeyManagerFactory().getKeyManagers();
}
/**
* Return the {@code KeyManagerFactory} used to establish identity.
* @return the key manager factory
*/
KeyManagerFactory getKeyManagerFactory();
/**
* Return the {@link TrustManager} instances used to establish trust.
* @return the trust managers
*/
default TrustManager[] getTrustManagers() {
return getTrustManagerFactory().getTrustManagers();
}
/**
* Return the {@link TrustManagerFactory} used to establish trust.
* @return the trust manager factory
*/
TrustManagerFactory getTrustManagerFactory();
/**
* Factory method to create a new {@link SSLContext} for the {@link #getKeyManagers()
* key managers} and {@link #getTrustManagers() trust managers} managed by this
* instance.
* @param protocol the standard name of the SSL protocol. See
* {@link SSLContext#getInstance(String)}
* @return a new {@link SSLContext} instance
*/
default SSLContext createSslContext(String protocol) {
try {
SSLContext sslContext = SSLContext.getInstance(protocol);
sslContext.init(getKeyManagers(), getTrustManagers(), null);
return sslContext;
}
catch (Exception ex) {
throw new IllegalStateException("Could not load SSL context: " + ex.getMessage(), ex);
}
}
/**
* Factory method to create a new {@link SslManagerBundle} instance.
* @param keyManagerFactory the key manager factory
* @param trustManagerFactory the trust manager factory
* @return a new {@link SslManagerBundle} instance
*/
static SslManagerBundle of(KeyManagerFactory keyManagerFactory, TrustManagerFactory trustManagerFactory) {
Assert.notNull(keyManagerFactory, "KeyManagerFactory must not be null");
Assert.notNull(trustManagerFactory, "TrustManagerFactory must not be null");
return new SslManagerBundle() {
@Override
public KeyManagerFactory getKeyManagerFactory() {
return keyManagerFactory;
}
@Override
public TrustManagerFactory getTrustManagerFactory() {
return trustManagerFactory;
}
};
}
/**
* Factory method to create a new {@link SslManagerBundle} backed by the given
* {@link SslBundle} and {@link SslBundleKey}.
* @param storeBundle the SSL store bundle
* @param key the key reference
* @return a new {@link SslManagerBundle} instance
*/
static SslManagerBundle from(SslStoreBundle storeBundle, SslBundleKey key) {
return new DefaultSslManagerBundle(storeBundle, key);
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.net.ssl.SSLEngine;
/**
* Configuration options that should be applied when establishing an SSL connection.
*
* @author Scott Frederick
* @since 3.1.0
* @see SslBundle#getOptions()
*/
public interface SslOptions {
/**
* {@link SslOptions} that returns no values.
*/
SslOptions NONE = of(Collections.emptySet(), Collections.emptySet());
/**
* Return the ciphers that can be used or an empty set. The cipher names in this set
* should be compatible with those supported by
* {@link SSLEngine#getSupportedCipherSuites()}.
* @return the ciphers that can be used
*/
Set<String> getCiphers();
/**
* Return the protocols that should be enabled or an empty set. The protocols names in
* this set should be compatible with those supported by
* {@link SSLEngine#getSupportedProtocols()}.
* @return the protocols to enable
*/
Set<String> getEnabledProtocols();
/**
* Factory method to create a new {@link SslOptions} instance.
* @param ciphers the ciphers
* @param enabledProtocols the enabled protocols
* @return a new {@link SslOptions} instance
*/
static SslOptions of(String[] ciphers, String[] enabledProtocols) {
return of(asSet(ciphers), asSet(enabledProtocols));
}
/**
* Factory method to create a new {@link SslOptions} instance.
* @param ciphers the ciphers
* @param enabledProtocols the enabled protocols
* @return a new {@link SslOptions} instance
*/
static SslOptions of(Set<String> ciphers, Set<String> enabledProtocols) {
return new SslOptions() {
@Override
public Set<String> getCiphers() {
return (ciphers != null) ? ciphers : Collections.emptySet();
}
@Override
public Set<String> getEnabledProtocols() {
return (enabledProtocols != null) ? enabledProtocols : Collections.emptySet();
}
};
}
private static Set<String> asSet(String[] array) {
return (array != null) ? Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(array))) : null;
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl;
import java.security.KeyStore;
/**
* A bundle of key and trust stores that can be used to establish an SSL connection.
*
* @author Scott Frederick
* @since 3.1.0
* @see SslBundle#getStores()
*/
public interface SslStoreBundle {
/**
* {@link SslStoreBundle} that returns {@code null} for each method.
*/
SslStoreBundle NONE = of(null, null, null);
/**
* Return a key store generated from the trust material or {@code null}.
* @return the key store
*/
KeyStore getKeyStore();
/**
* Return the password for the key in the key store or {@code null}.
* @return the key password
*/
String getKeyStorePassword();
/**
* Return a trust store generated from the trust material or {@code null}.
* @return the trust store
*/
KeyStore getTrustStore();
/**
* Factory method to create a new {@link SslStoreBundle} instance.
* @param keyStore the key store or {@code null}
* @param keyStorePassword the key store password or {@code null}
* @param trustStore the trust store or {@code null}
* @return a new {@link SslStoreBundle} instance
*/
static SslStoreBundle of(KeyStore keyStore, String keyStorePassword, KeyStore trustStore) {
return new SslStoreBundle() {
@Override
public KeyStore getKeyStore() {
return keyStore;
}
@Override
public KeyStore getTrustStore() {
return trustStore;
}
@Override
public String getKeyStorePassword() {
return keyStorePassword;
}
};
}
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl.jks;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.CertificateException;
import org.springframework.boot.ssl.SslStoreBundle;
import org.springframework.util.Assert;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
/**
* {@link SslStoreBundle} backed by a Java keystore.
*
* @author Scott Frederick
* @author Phillip Webb
* @since 3.1.0
*/
public class JksSslStoreBundle implements SslStoreBundle {
private final JksSslStoreDetails keyStoreDetails;
private final JksSslStoreDetails trustStoreDetails;
/**
* Create a new {@link JksSslStoreBundle} instance.
* @param keyStoreDetails the key store details
* @param trustStoreDetails the trust store details
*/
public JksSslStoreBundle(JksSslStoreDetails keyStoreDetails, JksSslStoreDetails trustStoreDetails) {
this.keyStoreDetails = keyStoreDetails;
this.trustStoreDetails = trustStoreDetails;
}
@Override
public KeyStore getKeyStore() {
return createKeyStore("key", this.keyStoreDetails);
}
@Override
public String getKeyStorePassword() {
return (this.keyStoreDetails != null) ? this.keyStoreDetails.password() : null;
}
@Override
public KeyStore getTrustStore() {
return createKeyStore("trust", this.trustStoreDetails);
}
private KeyStore createKeyStore(String name, JksSslStoreDetails details) {
if (details == null || details.isEmpty()) {
return null;
}
try {
String type = (!StringUtils.hasText(details.type())) ? KeyStore.getDefaultType() : details.type();
char[] password = (details.password() != null) ? details.password().toCharArray() : null;
String location = details.location();
KeyStore store = getKeyStoreInstance(type, details.provider());
if (isHardwareKeystoreType(type)) {
loadHardwareKeyStore(store, location, password);
}
else {
loadKeyStore(store, location, password);
}
return store;
}
catch (Exception ex) {
throw new IllegalStateException("Unable to create %s store: %s".formatted(name, ex.getMessage()), ex);
}
}
private KeyStore getKeyStoreInstance(String type, String provider)
throws KeyStoreException, NoSuchProviderException {
return (!StringUtils.hasText(provider)) ? KeyStore.getInstance(type) : KeyStore.getInstance(type, provider);
}
private boolean isHardwareKeystoreType(String type) {
return type.equalsIgnoreCase("PKCS11");
}
private void loadHardwareKeyStore(KeyStore store, String location, char[] password)
throws IOException, NoSuchAlgorithmException, CertificateException {
Assert.state(!StringUtils.hasText(location),
() -> "Location is '%s', but must be empty or null for PKCS11 hardware key stores".formatted(location));
store.load(null, password);
}
private void loadKeyStore(KeyStore store, String location, char[] password) {
Assert.state(StringUtils.hasText(location), () -> "Location must not be empty or null");
try {
URL url = ResourceUtils.getURL(location);
try (InputStream stream = url.openStream()) {
store.load(stream, password);
}
}
catch (Exception ex) {
throw new IllegalStateException("Could not load store from '" + location + "'", ex);
}
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl.jks;
import java.security.KeyStore;
import org.springframework.util.StringUtils;
/**
* Details for an individual trust or key store in a {@link JksSslStoreBundle}.
*
* @param type the key store type, for example {@code JKS} or {@code PKCS11}. A
* {@code null} value will use {@link KeyStore#getDefaultType()}).
* @param provider the name of the key store provider
* @param location the location of the key store file or {@code null} if using a
* {@code PKCS11} hardware store
* @param password the password used to unlock the store or {@code null}
* @author Scott Frederick
* @author Phillip Webb
* @since 3.1.0
*/
public record JksSslStoreDetails(String type, String provider, String location, String password) {
/**
* Return a new {@link JksSslStoreDetails} instance with a new password.
* @param password the new password
* @return a new {@link JksSslStoreDetails} instance
*/
public JksSslStoreDetails withPassword(String password) {
return new JksSslStoreDetails(this.type, this.provider, this.location, password);
}
boolean isEmpty() {
return isEmpty(this.type) && isEmpty(this.provider) && isEmpty(this.location);
}
private boolean isEmpty(String value) {
return !StringUtils.hasText(value);
}
/**
* Factory method to create a new {@link JksSslStoreDetails} instance for the given
* location.
* @param location the location
* @return a new {@link JksSslStoreDetails} instance.
*/
public static JksSslStoreDetails forLocation(String location) {
return new JksSslStoreDetails(null, null, location, null);
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* SSL trust material provider for Java KeyStores.
*/
package org.springframework.boot.ssl.jks;

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Management of trust material that can be used to establish an SSL connection.
*/
package org.springframework.boot.ssl;

View File

@@ -14,13 +14,9 @@
* limitations under the License.
*/
package org.springframework.boot.web.server;
package org.springframework.boot.ssl.pem;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
@@ -31,16 +27,13 @@ import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.ResourceUtils;
/**
* Parser for X.509 certificates in PEM format.
*
* @author Scott Frederick
* @author Phillip Webb
*/
final class CertificateParser {
final class PemCertificateParser {
private static final String HEADER = "-+BEGIN\\s+.*CERTIFICATE[^-]*-+(?:\\s|\\r|\\n)+";
@@ -50,19 +43,22 @@ final class CertificateParser {
private static final Pattern PATTERN = Pattern.compile(HEADER + BASE64_TEXT + FOOTER, Pattern.CASE_INSENSITIVE);
private CertificateParser() {
private PemCertificateParser() {
}
/**
* Load certificates from the specified resource.
* @param path the certificate to parse
* Parse certificates from the specified string.
* @param certificates the certificates to parse
* @return the parsed certificates
*/
static X509Certificate[] parse(String path) {
static X509Certificate[] parse(String certificates) {
if (certificates == null) {
return null;
}
CertificateFactory factory = getCertificateFactory();
List<X509Certificate> certificates = new ArrayList<>();
readCertificates(path, factory, certificates::add);
return certificates.toArray(new X509Certificate[0]);
List<X509Certificate> certs = new ArrayList<>();
readCertificates(certificates, factory, certs::add);
return (!certs.isEmpty()) ? certs.toArray(X509Certificate[]::new) : null;
}
private static CertificateFactory getCertificateFactory() {
@@ -74,10 +70,8 @@ final class CertificateParser {
}
}
private static void readCertificates(String resource, CertificateFactory factory,
Consumer<X509Certificate> consumer) {
private static void readCertificates(String text, CertificateFactory factory, Consumer<X509Certificate> consumer) {
try {
String text = readText(resource);
Matcher matcher = PATTERN.matcher(text);
while (matcher.find()) {
String encodedText = matcher.group(1);
@@ -88,16 +82,8 @@ final class CertificateParser {
}
}
}
catch (CertificateException | IOException ex) {
throw new IllegalStateException("Error reading certificate from '" + resource + "' : " + ex.getMessage(),
ex);
}
}
private static String readText(String resource) throws IOException {
URL url = ResourceUtils.getURL(resource);
try (Reader reader = new InputStreamReader(url.openStream())) {
return FileCopyUtils.copyToString(reader);
catch (CertificateException ex) {
throw new IllegalStateException("Error reading certificate: " + ex.getMessage(), ex);
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl.pem;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.ResourceUtils;
/**
* Utility to load PEM content.
*
* @author Scott Frederick
* @author Phillip Webb
*/
final class PemContent {
private static final Pattern PEM_HEADER = Pattern.compile("-+BEGIN\\s+[^-]*-+", Pattern.CASE_INSENSITIVE);
private static final Pattern PEM_FOOTER = Pattern.compile("-+END\\s+[^-]*-+", Pattern.CASE_INSENSITIVE);
private PemContent() {
}
static String load(String content) {
if (content == null || isPemContent(content)) {
return content;
}
try {
URL url = ResourceUtils.getURL(content);
try (Reader reader = new InputStreamReader(url.openStream(), StandardCharsets.UTF_8)) {
return FileCopyUtils.copyToString(reader);
}
}
catch (IOException ex) {
throw new IllegalStateException(
"Error reading certificate or key from file '" + content + "':" + ex.getMessage(), ex);
}
}
private static boolean isPemContent(String content) {
return content != null && PEM_HEADER.matcher(content).find() && PEM_FOOTER.matcher(content).find();
}
}

View File

@@ -14,13 +14,10 @@
* limitations under the License.
*/
package org.springframework.boot.web.server;
package org.springframework.boot.ssl.pem;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.PrivateKey;
@@ -33,16 +30,13 @@ import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.ResourceUtils;
/**
* Parser for PKCS private key files in PEM format.
*
* @author Scott Frederick
* @author Phillip Webb
*/
final class PrivateKeyParser {
final class PemPrivateKeyParser {
private static final String PKCS1_HEADER = "-+BEGIN\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+";
@@ -61,8 +55,8 @@ final class PrivateKeyParser {
private static final List<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(PKCS1_HEADER, PKCS1_FOOTER, "RSA", PemPrivateKeyParser::createKeySpecForPkcs1));
parsers.add(new PemParser(EC_HEADER, EC_FOOTER, "EC", PemPrivateKeyParser::createKeySpecForEc));
parsers.add(new PemParser(PKCS8_HEADER, PKCS8_FOOTER, "RSA", PKCS8EncodedKeySpec::new));
PEM_PARSERS = Collections.unmodifiableList(parsers);
}
@@ -82,7 +76,7 @@ final class PrivateKeyParser {
*/
private static final int[] EC_PARAMETERS = { 0x2b, 0x81, 0x04, 0x00, 0x22 };
private PrivateKeyParser() {
private PemPrivateKeyParser() {
}
private static PKCS8EncodedKeySpec createKeySpecForPkcs1(byte[] bytes) {
@@ -111,15 +105,17 @@ final class PrivateKeyParser {
}
/**
* Load a private key from the specified resource.
* @param resource the private key to parse
* Parse a private key from the specified string.
* @param key the private key to parse
* @return the parsed private key
*/
static PrivateKey parse(String resource) {
static PrivateKey parse(String key) {
if (key == null) {
return null;
}
try {
String text = readText(resource);
for (PemParser pemParser : PEM_PARSERS) {
PrivateKey privateKey = pemParser.parse(text);
PrivateKey privateKey = pemParser.parse(key);
if (privateKey != null) {
return privateKey;
}
@@ -127,14 +123,7 @@ final class PrivateKeyParser {
throw new IllegalStateException("Unrecognized private key format");
}
catch (Exception ex) {
throw new IllegalStateException("Error loading private key file " + resource, ex);
}
}
private static String readText(String resource) throws IOException {
URL url = ResourceUtils.getURL(resource);
try (Reader reader = new InputStreamReader(url.openStream())) {
return FileCopyUtils.copyToString(reader);
throw new IllegalStateException("Error loading private key file: " + ex.getMessage(), ex);
}
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl.pem;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import org.springframework.boot.ssl.SslStoreBundle;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* {@link SslStoreBundle} backed by PEM-encoded certificates and private keys.
*
* @author Scott Frederick
* @author Phillip Webb
* @since 3.1.0
*/
public class PemSslStoreBundle implements SslStoreBundle {
private static final String DEFAULT_KEY_ALIAS = "ssl";
private final PemSslStoreDetails keyStoreDetails;
private final PemSslStoreDetails trustStoreDetails;
private final String keyAlias;
/**
* Create a new {@link PemSslStoreBundle} instance.
* @param keyStoreDetails the key store details
* @param trustStoreDetails the trust store details
*/
public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails) {
this(keyStoreDetails, trustStoreDetails, null);
}
/**
* Create a new {@link PemSslStoreBundle} instance.
* @param keyStoreDetails the key store details
* @param trustStoreDetails the trust store details
* @param keyAlias the key alias to use or {@code null} to use a default alias
*/
public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails,
String keyAlias) {
this.keyAlias = keyAlias;
this.keyStoreDetails = keyStoreDetails;
this.trustStoreDetails = trustStoreDetails;
}
@Override
public KeyStore getKeyStore() {
return createKeyStore("key", this.keyStoreDetails);
}
@Override
public String getKeyStorePassword() {
return null;
}
@Override
public KeyStore getTrustStore() {
return createKeyStore("trust", this.trustStoreDetails);
}
private KeyStore createKeyStore(String name, PemSslStoreDetails details) {
if (details == null || details.isEmpty()) {
return null;
}
try {
Assert.notNull(details.certificate(), "CertificateContent must not be null");
String type = (!StringUtils.hasText(details.type())) ? KeyStore.getDefaultType() : details.type();
KeyStore store = KeyStore.getInstance(type);
store.load(null);
String certificateContent = PemContent.load(details.certificate());
String privateKeyContent = PemContent.load(details.privateKey());
X509Certificate[] certificates = PemCertificateParser.parse(certificateContent);
PrivateKey privateKey = PemPrivateKeyParser.parse(privateKeyContent);
addCertificates(store, certificates, privateKey);
return store;
}
catch (Exception ex) {
throw new IllegalStateException("Unable to create %s store: %s".formatted(name, ex.getMessage()), ex);
}
}
private void addCertificates(KeyStore keyStore, X509Certificate[] certificates, PrivateKey privateKey)
throws KeyStoreException {
String alias = (this.keyAlias != null) ? this.keyAlias : DEFAULT_KEY_ALIAS;
if (privateKey != null) {
keyStore.setKeyEntry(alias, privateKey, null, certificates);
}
else {
for (int index = 0; index < certificates.length; index++) {
keyStore.setCertificateEntry(alias + "-" + index, certificates[index]);
}
}
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl.pem;
import java.security.KeyStore;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
/**
* Details for an individual trust or key store in a {@link PemSslStoreBundle}.
*
* @param type the key store type, for example {@code JKS} or {@code PKCS11}. A
* {@code null} value will use {@link KeyStore#getDefaultType()}).
* @param certificate the certificate content (either the PEM content itself or something
* that can be loaded by {@link ResourceUtils#getURL})
* @param privateKey the private key content (either the PEM content itself or something
* that can be loaded by {@link ResourceUtils#getURL})
* @author Scott Frederick
* @author Phillip Webb
* @since 3.1.0
*/
public record PemSslStoreDetails(String type, String certificate, String privateKey) {
/**
* Return a new {@link PemSslStoreDetails} instance with a new private key.
* @param privateKey the new private key
* @return a new {@link PemSslStoreDetails} instance
*/
public PemSslStoreDetails withPrivateKey(String privateKey) {
return new PemSslStoreDetails(this.type, this.certificate, privateKey);
}
boolean isEmpty() {
return isEmpty(this.type) && isEmpty(this.certificate) && isEmpty(this.privateKey);
}
private boolean isEmpty(String value) {
return !StringUtils.hasText(value);
}
/**
* Factory method to create a new {@link PemSslStoreDetails} instance for the given
* certificate.
* @param certificate the certificate
* @return a new {@link PemSslStoreDetails} instance.
*/
public static PemSslStoreDetails forCertificate(String certificate) {
return new PemSslStoreDetails(null, certificate, null);
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* SSL trust material provider for PEM-encoded certificates.
*/
package org.springframework.boot.ssl.pem;

View File

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

View File

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

View File

@@ -32,12 +32,15 @@ import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundleKey;
import org.springframework.boot.ssl.SslOptions;
import org.springframework.boot.ssl.SslStoreBundle;
import org.springframework.boot.web.server.Http2;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.SslConfigurationValidator;
import org.springframework.boot.web.server.SslStoreProvider;
import org.springframework.boot.web.server.Ssl.ClientAuth;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
/**
@@ -51,18 +54,18 @@ import org.springframework.util.ObjectUtils;
*/
class SslServerCustomizer implements JettyServerCustomizer {
private final InetSocketAddress address;
private final Ssl ssl;
private final SslStoreProvider sslStoreProvider;
private final Http2 http2;
SslServerCustomizer(InetSocketAddress address, Ssl ssl, SslStoreProvider sslStoreProvider, Http2 http2) {
private final InetSocketAddress address;
private final ClientAuth clientAuth;
private final SslBundle sslBundle;
SslServerCustomizer(Http2 http2, InetSocketAddress address, ClientAuth clientAuth, SslBundle sslBundle) {
this.address = address;
this.ssl = ssl;
this.sslStoreProvider = sslStoreProvider;
this.clientAuth = clientAuth;
this.sslBundle = sslBundle;
this.http2 = http2;
}
@@ -70,41 +73,42 @@ class SslServerCustomizer implements JettyServerCustomizer {
public void customize(Server server) {
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setEndpointIdentificationAlgorithm(null);
configureSsl(sslContextFactory, this.ssl, this.sslStoreProvider);
ServerConnector connector = createConnector(server, sslContextFactory, this.address);
configureSsl(sslContextFactory, this.clientAuth);
ServerConnector connector = createConnector(server, sslContextFactory);
server.setConnectors(new Connector[] { connector });
}
private ServerConnector createConnector(Server server, SslContextFactory.Server sslContextFactory,
InetSocketAddress address) {
private ServerConnector createConnector(Server server, SslContextFactory.Server sslContextFactory) {
HttpConfiguration config = new HttpConfiguration();
config.setSendServerVersion(false);
config.setSecureScheme("https");
config.setSecurePort(address.getPort());
config.setSecurePort(this.address.getPort());
config.addCustomizer(new SecureRequestCustomizer());
ServerConnector connector = createServerConnector(server, sslContextFactory, config);
connector.setPort(address.getPort());
connector.setHost(address.getHostString());
connector.setPort(this.address.getPort());
connector.setHost(this.address.getHostString());
return connector;
}
private ServerConnector createServerConnector(Server server, SslContextFactory.Server sslContextFactory,
HttpConfiguration config) {
if (this.http2 == null || !this.http2.isEnabled()) {
return createHttp11ServerConnector(server, config, sslContextFactory);
return createHttp11ServerConnector(config, sslContextFactory, server);
}
Assert.state(isJettyAlpnPresent(),
() -> "An 'org.eclipse.jetty:jetty-alpn-*-server' dependency is required for HTTP/2 support.");
Assert.state(isJettyHttp2Present(),
() -> "The 'org.eclipse.jetty.http2:http2-server' dependency is required for HTTP/2 support.");
return createHttp2ServerConnector(server, config, sslContextFactory);
return createHttp2ServerConnector(config, sslContextFactory, server);
}
private ServerConnector createHttp11ServerConnector(Server server, HttpConfiguration config,
SslContextFactory.Server sslContextFactory) {
private ServerConnector createHttp11ServerConnector(HttpConfiguration config,
SslContextFactory.Server sslContextFactory, Server server) {
SslConnectionFactory sslConnectionFactory = createSslConnectionFactory(sslContextFactory,
HttpVersion.HTTP_1_1.asString());
HttpConnectionFactory connectionFactory = new HttpConnectionFactory(config);
return new SslValidatingServerConnector(server, sslContextFactory, this.ssl.getKeyAlias(),
createSslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()), connectionFactory);
return new SslValidatingServerConnector(this.sslBundle.getKey(), sslContextFactory, server,
sslConnectionFactory, connectionFactory);
}
private SslConnectionFactory createSslConnectionFactory(SslContextFactory.Server sslContextFactory,
@@ -132,8 +136,8 @@ class SslServerCustomizer implements JettyServerCustomizer {
return ClassUtils.isPresent("org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory", null);
}
private ServerConnector createHttp2ServerConnector(Server server, HttpConfiguration config,
SslContextFactory.Server sslContextFactory) {
private ServerConnector createHttp2ServerConnector(HttpConfiguration config,
SslContextFactory.Server sslContextFactory, Server server) {
HttpConnectionFactory http = new HttpConnectionFactory(config);
HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(config);
ALPNServerConnectionFactory alpn = createAlpnServerConnectionFactory();
@@ -141,8 +145,9 @@ class SslServerCustomizer implements JettyServerCustomizer {
if (isConscryptPresent()) {
sslContextFactory.setProvider("Conscrypt");
}
SslConnectionFactory ssl = createSslConnectionFactory(sslContextFactory, alpn.getProtocol());
return new SslValidatingServerConnector(server, sslContextFactory, this.ssl.getKeyAlias(), ssl, alpn, h2, http);
SslConnectionFactory sslConnectionFactory = createSslConnectionFactory(sslContextFactory, alpn.getProtocol());
return new SslValidatingServerConnector(this.sslBundle.getKey(), sslContextFactory, server,
sslConnectionFactory, alpn, h2, http);
}
private ALPNServerConnectionFactory createAlpnServerConnectionFactory() {
@@ -163,53 +168,40 @@ class SslServerCustomizer implements JettyServerCustomizer {
/**
* Configure the SSL connection.
* @param factory the Jetty {@link Server SslContextFactory.Server}.
* @param ssl the ssl details.
* @param sslStoreProvider the ssl store provider
* @param clientAuth the client authentication mode
*/
protected void configureSsl(SslContextFactory.Server factory, Ssl ssl, SslStoreProvider sslStoreProvider) {
factory.setProtocol(ssl.getProtocol());
configureSslClientAuth(factory, ssl);
configureSslPasswords(factory, ssl);
factory.setCertAlias(ssl.getKeyAlias());
if (!ObjectUtils.isEmpty(ssl.getCiphers())) {
factory.setIncludeCipherSuites(ssl.getCiphers());
protected void configureSsl(SslContextFactory.Server factory, ClientAuth clientAuth) {
SslBundleKey key = this.sslBundle.getKey();
SslOptions options = this.sslBundle.getOptions();
SslStoreBundle stores = this.sslBundle.getStores();
factory.setProtocol(this.sslBundle.getProtocol());
configureSslClientAuth(factory, clientAuth);
if (stores.getKeyStorePassword() != null) {
factory.setKeyStorePassword(stores.getKeyStorePassword());
}
factory.setCertAlias(key.getAlias());
if (!ObjectUtils.isEmpty(options.getCiphers())) {
factory.setIncludeCipherSuites(options.getCiphers().toArray(String[]::new));
factory.setExcludeCipherSuites();
}
if (ssl.getEnabledProtocols() != null) {
factory.setIncludeProtocols(ssl.getEnabledProtocols());
if (!CollectionUtils.isEmpty(options.getEnabledProtocols())) {
factory.setIncludeProtocols(options.getEnabledProtocols().toArray(String[]::new));
}
if (sslStoreProvider != null) {
try {
String keyPassword = sslStoreProvider.getKeyPassword();
if (keyPassword != null) {
factory.setKeyManagerPassword(keyPassword);
}
factory.setKeyStore(sslStoreProvider.getKeyStore());
factory.setTrustStore(sslStoreProvider.getTrustStore());
}
catch (Exception ex) {
throw new IllegalStateException("Unable to set SSL store: " + ex.getMessage(), ex);
try {
if (key.getPassword() != null) {
factory.setKeyManagerPassword(key.getPassword());
}
factory.setKeyStore(stores.getKeyStore());
factory.setTrustStore(stores.getTrustStore());
}
catch (Exception ex) {
throw new IllegalStateException("Unable to set SSL store: " + ex.getMessage(), ex);
}
}
private void configureSslClientAuth(SslContextFactory.Server factory, Ssl ssl) {
if (ssl.getClientAuth() == Ssl.ClientAuth.NEED) {
factory.setNeedClientAuth(true);
factory.setWantClientAuth(true);
}
else if (ssl.getClientAuth() == Ssl.ClientAuth.WANT) {
factory.setWantClientAuth(true);
}
}
private void configureSslPasswords(SslContextFactory.Server factory, Ssl ssl) {
if (ssl.getKeyStorePassword() != null) {
factory.setKeyStorePassword(ssl.getKeyStorePassword());
}
if (ssl.getKeyPassword() != null) {
factory.setKeyManagerPassword(ssl.getKeyPassword());
}
private void configureSslClientAuth(SslContextFactory.Server factory, ClientAuth clientAuth) {
factory.setWantClientAuth(clientAuth == ClientAuth.WANT || clientAuth == ClientAuth.NEED);
factory.setNeedClientAuth(clientAuth == ClientAuth.NEED);
}
/**
@@ -217,28 +209,28 @@ class SslServerCustomizer implements JettyServerCustomizer {
*/
static class SslValidatingServerConnector extends ServerConnector {
private final SslBundleKey key;
private final SslContextFactory sslContextFactory;
private final String keyAlias;
SslValidatingServerConnector(Server server, SslContextFactory sslContextFactory, String keyAlias,
SslValidatingServerConnector(SslBundleKey key, SslContextFactory sslContextFactory, Server server,
SslConnectionFactory sslConnectionFactory, HttpConnectionFactory connectionFactory) {
super(server, sslConnectionFactory, connectionFactory);
this.key = key;
this.sslContextFactory = sslContextFactory;
this.keyAlias = keyAlias;
}
SslValidatingServerConnector(Server server, SslContextFactory sslContextFactory, String keyAlias,
SslValidatingServerConnector(SslBundleKey keyAlias, SslContextFactory sslContextFactory, Server server,
ConnectionFactory... factories) {
super(server, factories);
this.key = keyAlias;
this.sslContextFactory = sslContextFactory;
this.keyAlias = keyAlias;
}
@Override
protected void doStart() throws Exception {
super.doStart();
SslConfigurationValidator.validateKeyAlias(this.sslContextFactory.getKeyStore(), this.keyAlias);
this.key.assertContainsAlias(this.sslContextFactory.getKeyStore());
}
}

View File

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

View File

@@ -16,35 +16,17 @@
package org.springframework.boot.web.embedded.netty;
import java.net.Socket;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.KeyManagerFactorySpi;
import javax.net.ssl.ManagerFactoryParameters;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509ExtendedKeyManager;
import io.netty.handler.ssl.ClientAuth;
import reactor.netty.http.Http11SslContextSpec;
import reactor.netty.http.Http2SslContextSpec;
import reactor.netty.http.server.HttpServer;
import reactor.netty.tcp.AbstractProtocolSslContextSpec;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslOptions;
import org.springframework.boot.web.server.Http2;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.SslConfigurationValidator;
import org.springframework.boot.web.server.SslStoreProvider;
import org.springframework.util.CollectionUtils;
/**
* {@link NettyServerCustomizer} that configures SSL for the given Reactor Netty server
@@ -56,21 +38,19 @@ import org.springframework.boot.web.server.SslStoreProvider;
* @author Cyril Dangerville
* @author Scott Frederick
* @since 2.0.0
* @deprecated this class is meant for Spring Boot internal use only.
*/
@Deprecated(since = "2.0.0", forRemoval = false)
public class SslServerCustomizer implements NettyServerCustomizer {
private final Ssl ssl;
private final Http2 http2;
private final SslStoreProvider sslStoreProvider;
private final Ssl.ClientAuth clientAuth;
public SslServerCustomizer(Ssl ssl, Http2 http2, SslStoreProvider sslStoreProvider) {
this.ssl = ssl;
private final SslBundle sslBundle;
public SslServerCustomizer(Http2 http2, Ssl.ClientAuth clientAuth, SslBundle sslBundle) {
this.http2 = http2;
this.sslStoreProvider = sslStoreProvider;
this.clientAuth = clientAuth;
this.sslBundle = sslBundle;
}
@Override
@@ -80,172 +60,22 @@ public class SslServerCustomizer implements NettyServerCustomizer {
}
protected AbstractProtocolSslContextSpec<?> createSslContextSpec() {
AbstractProtocolSslContextSpec<?> sslContextSpec;
if (this.http2 != null && this.http2.isEnabled()) {
sslContextSpec = Http2SslContextSpec.forServer(getKeyManagerFactory(this.ssl, this.sslStoreProvider));
}
else {
sslContextSpec = Http11SslContextSpec.forServer(getKeyManagerFactory(this.ssl, this.sslStoreProvider));
}
AbstractProtocolSslContextSpec<?> sslContextSpec = (this.http2 != null && this.http2.isEnabled())
? Http2SslContextSpec.forServer(this.sslBundle.getManagers().getKeyManagerFactory())
: Http11SslContextSpec.forServer(this.sslBundle.getManagers().getKeyManagerFactory());
sslContextSpec.configure((builder) -> {
builder.trustManager(getTrustManagerFactory(this.sslStoreProvider));
if (this.ssl.getEnabledProtocols() != null) {
builder.protocols(this.ssl.getEnabledProtocols());
builder.trustManager(this.sslBundle.getManagers().getTrustManagerFactory());
SslOptions options = this.sslBundle.getOptions();
if (!CollectionUtils.isEmpty(options.getEnabledProtocols())) {
builder.protocols(options.getEnabledProtocols());
}
if (this.ssl.getCiphers() != null) {
builder.ciphers(Arrays.asList(this.ssl.getCiphers()));
}
if (this.ssl.getClientAuth() == Ssl.ClientAuth.NEED) {
builder.clientAuth(ClientAuth.REQUIRE);
}
else if (this.ssl.getClientAuth() == Ssl.ClientAuth.WANT) {
builder.clientAuth(ClientAuth.OPTIONAL);
if (!CollectionUtils.isEmpty(options.getCiphers())) {
builder.ciphers(options.getCiphers());
}
builder.clientAuth(org.springframework.boot.web.server.Ssl.ClientAuth.map(this.clientAuth, ClientAuth.NONE,
ClientAuth.OPTIONAL, ClientAuth.REQUIRE));
});
return sslContextSpec;
}
KeyManagerFactory getKeyManagerFactory(Ssl ssl, SslStoreProvider sslStoreProvider) {
try {
KeyStore keyStore = sslStoreProvider.getKeyStore();
SslConfigurationValidator.validateKeyAlias(keyStore, ssl.getKeyAlias());
KeyManagerFactory keyManagerFactory = (ssl.getKeyAlias() == null)
? KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
: new ConfigurableAliasKeyManagerFactory(ssl.getKeyAlias(),
KeyManagerFactory.getDefaultAlgorithm());
String keyPassword = sslStoreProvider.getKeyPassword();
if (keyPassword == null) {
keyPassword = (ssl.getKeyPassword() != null) ? ssl.getKeyPassword() : ssl.getKeyStorePassword();
}
keyManagerFactory.init(keyStore, (keyPassword != null) ? keyPassword.toCharArray() : null);
return keyManagerFactory;
}
catch (Exception ex) {
throw new IllegalStateException("Could not load key manager factory: " + ex.getMessage(), ex);
}
}
TrustManagerFactory getTrustManagerFactory(SslStoreProvider sslStoreProvider) {
try {
KeyStore store = sslStoreProvider.getTrustStore();
TrustManagerFactory trustManagerFactory = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(store);
return trustManagerFactory;
}
catch (Exception ex) {
throw new IllegalStateException("Could not load trust manager factory: " + ex.getMessage(), ex);
}
}
/**
* A {@link KeyManagerFactory} that allows a configurable key alias to be used. Due to
* the fact that the actual calls to retrieve the key by alias are done at request
* time the approach is to wrap the actual key managers with a
* {@link ConfigurableAliasKeyManager}. The actual SPI has to be wrapped as well due
* to the fact that {@link KeyManagerFactory#getKeyManagers()} is final.
*/
private static final class ConfigurableAliasKeyManagerFactory extends KeyManagerFactory {
private ConfigurableAliasKeyManagerFactory(String alias, String algorithm) throws NoSuchAlgorithmException {
this(KeyManagerFactory.getInstance(algorithm), alias, algorithm);
}
private ConfigurableAliasKeyManagerFactory(KeyManagerFactory delegate, String alias, String algorithm) {
super(new ConfigurableAliasKeyManagerFactorySpi(delegate, alias), delegate.getProvider(), algorithm);
}
}
private static final class ConfigurableAliasKeyManagerFactorySpi extends KeyManagerFactorySpi {
private final KeyManagerFactory delegate;
private final String alias;
private ConfigurableAliasKeyManagerFactorySpi(KeyManagerFactory delegate, String alias) {
this.delegate = delegate;
this.alias = alias;
}
@Override
protected void engineInit(KeyStore keyStore, char[] chars)
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
this.delegate.init(keyStore, chars);
}
@Override
protected void engineInit(ManagerFactoryParameters managerFactoryParameters)
throws InvalidAlgorithmParameterException {
throw new InvalidAlgorithmParameterException("Unsupported ManagerFactoryParameters");
}
@Override
protected KeyManager[] engineGetKeyManagers() {
return Arrays.stream(this.delegate.getKeyManagers())
.filter(X509ExtendedKeyManager.class::isInstance)
.map(X509ExtendedKeyManager.class::cast)
.map(this::wrap)
.toArray(KeyManager[]::new);
}
private ConfigurableAliasKeyManager wrap(X509ExtendedKeyManager keyManager) {
return new ConfigurableAliasKeyManager(keyManager, this.alias);
}
}
private static final class ConfigurableAliasKeyManager extends X509ExtendedKeyManager {
private final X509ExtendedKeyManager delegate;
private final String alias;
private ConfigurableAliasKeyManager(X509ExtendedKeyManager keyManager, String alias) {
this.delegate = keyManager;
this.alias = alias;
}
@Override
public String chooseEngineClientAlias(String[] strings, Principal[] principals, SSLEngine sslEngine) {
return this.delegate.chooseEngineClientAlias(strings, principals, sslEngine);
}
@Override
public String chooseEngineServerAlias(String s, Principal[] principals, SSLEngine sslEngine) {
return (this.alias != null) ? this.alias : this.delegate.chooseEngineServerAlias(s, principals, sslEngine);
}
@Override
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
return this.delegate.chooseClientAlias(keyType, issuers, socket);
}
@Override
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
return this.delegate.chooseServerAlias(keyType, issuers, socket);
}
@Override
public X509Certificate[] getCertificateChain(String alias) {
return this.delegate.getCertificateChain(alias);
}
@Override
public String[] getClientAliases(String keyType, Principal[] issuers) {
return this.delegate.getClientAliases(keyType, issuers);
}
@Override
public PrivateKey getPrivateKey(String alias) {
return this.delegate.getPrivateKey(alias);
}
@Override
public String[] getServerAliases(String keyType, Principal[] issuers) {
return this.delegate.getServerAliases(keyType, issuers);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 the original author or authors.
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,12 +16,10 @@
package org.springframework.boot.web.server;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.pem.PemSslStoreBundle;
/**
* An {@link SslStoreProvider} that creates key and trust stores from certificate and
@@ -29,82 +27,32 @@ import java.security.cert.X509Certificate;
*
* @author Scott Frederick
* @since 2.7.0
* @deprecated since 3.1.0 for removal in 3.3.0 in favor of registering a
* {@link SslBundle} backed by a {@link PemSslStoreBundle}.
*/
@Deprecated(since = "3.1.0", forRemoval = true)
@SuppressWarnings({ "deprecation", "removal" })
public final class CertificateFileSslStoreProvider implements SslStoreProvider {
/**
* The password of the private key entry in the {@link #getKeyStore provided
* KeyStore}.
*/
private static final String KEY_PASSWORD = "";
private final SslBundle delegate;
private static final String DEFAULT_KEY_ALIAS = "spring-boot-web";
private final Ssl ssl;
private CertificateFileSslStoreProvider(Ssl ssl) {
this.ssl = ssl;
private CertificateFileSslStoreProvider(SslBundle delegate) {
this.delegate = delegate;
}
@Override
public KeyStore getKeyStore() throws Exception {
return createKeyStore(this.ssl.getCertificate(), this.ssl.getCertificatePrivateKey(),
this.ssl.getKeyStoreType(), this.ssl.getKeyAlias());
return this.delegate.getStores().getKeyStore();
}
@Override
public KeyStore getTrustStore() throws Exception {
if (this.ssl.getTrustCertificate() == null) {
return null;
}
return createKeyStore(this.ssl.getTrustCertificate(), this.ssl.getTrustCertificatePrivateKey(),
this.ssl.getTrustStoreType(), this.ssl.getKeyAlias());
return this.delegate.getStores().getTrustStore();
}
@Override
public String getKeyPassword() {
return KEY_PASSWORD;
}
/**
* Create a new {@link KeyStore} populated with the certificate stored at the
* specified file path and an optional private key.
* @param certPath the path to the certificate authority file
* @param keyPath the path to the private file
* @param storeType the {@code KeyStore} type to create
* @param keyAlias the alias to use when adding keys to the {@code KeyStore}
* @return the {@code KeyStore}
*/
private KeyStore createKeyStore(String certPath, String keyPath, String storeType, String keyAlias) {
try {
KeyStore keyStore = KeyStore.getInstance((storeType != null) ? storeType : KeyStore.getDefaultType());
keyStore.load(null);
X509Certificate[] certificates = CertificateParser.parse(certPath);
PrivateKey privateKey = (keyPath != null) ? PrivateKeyParser.parse(keyPath) : null;
try {
addCertificates(keyStore, certificates, privateKey, keyAlias);
}
catch (KeyStoreException ex) {
throw new IllegalStateException("Error adding certificates to KeyStore: " + ex.getMessage(), ex);
}
return keyStore;
}
catch (GeneralSecurityException | IOException ex) {
throw new IllegalStateException("Error creating KeyStore: " + ex.getMessage(), ex);
}
}
private void addCertificates(KeyStore keyStore, X509Certificate[] certificates, PrivateKey privateKey,
String keyAlias) throws KeyStoreException {
String alias = (keyAlias != null) ? keyAlias : DEFAULT_KEY_ALIAS;
if (privateKey != null) {
keyStore.setKeyEntry(alias, privateKey, KEY_PASSWORD.toCharArray(), certificates);
}
else {
for (int index = 0; index < certificates.length; index++) {
keyStore.setCertificateEntry(alias + "-" + index, certificates[index]);
}
}
return this.delegate.getKey().getPassword();
}
/**
@@ -114,12 +62,8 @@ public final class CertificateFileSslStoreProvider implements SslStoreProvider {
* @return an {@code SslStoreProvider} or {@code null}
*/
public static SslStoreProvider from(Ssl ssl) {
if (ssl != null && ssl.isEnabled()) {
if (ssl.getCertificate() != null && ssl.getCertificatePrivateKey() != null) {
return new CertificateFileSslStoreProvider(ssl);
}
}
return null;
SslBundle delegate = WebServerSslBundle.createCertificateFileSslStoreProviderDelegate(ssl);
return (delegate != null) ? new CertificateFileSslStoreProvider(delegate) : null;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl;
import java.util.Arrays;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.X509ExtendedKeyManager;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link AliasKeyManagerFactory}.
*
* @author Phillip Webb
*/
class AliasKeyManagerFactoryTests {
@Test
void chooseEngineServerAliasReturnsAlias() throws Exception {
KeyManagerFactory delegate = mock(KeyManagerFactory.class);
given(delegate.getKeyManagers()).willReturn(new KeyManager[] { mock(X509ExtendedKeyManager.class) });
AliasKeyManagerFactory factory = new AliasKeyManagerFactory(delegate, "test-alias",
KeyManagerFactory.getDefaultAlgorithm());
factory.init(null, null);
KeyManager[] keyManagers = factory.getKeyManagers();
X509ExtendedKeyManager x509KeyManager = (X509ExtendedKeyManager) Arrays.stream(keyManagers)
.filter(X509ExtendedKeyManager.class::isInstance)
.findAny()
.get();
String chosenAlias = x509KeyManager.chooseEngineServerAlias(null, null, null);
assertThat(chosenAlias).isEqualTo("test-alias");
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link DefaultSslBundleRegistry}.
*
* @author Phillip Webb
*/
class DefaultSslBundleRegistryTests {
private SslBundle bundle1 = mock(SslBundle.class);
private SslBundle bundle2 = mock(SslBundle.class);
private DefaultSslBundleRegistry registry = new DefaultSslBundleRegistry();
@Test
void createWithNameAndBundleRegistersBundle() {
DefaultSslBundleRegistry registry = new DefaultSslBundleRegistry("test", this.bundle1);
assertThat(registry.getBundle("test")).isSameAs(this.bundle1);
}
@Test
void registerBundleWhenNameIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.registry.registerBundle(null, this.bundle1))
.withMessage("Name must not be null");
}
@Test
void registerBundleWhenBundleIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.registry.registerBundle("test", null))
.withMessage("Bundle must not be null");
}
@Test
void registerBundleWhenNameIsTakenThrowsException() {
this.registry.registerBundle("test", this.bundle1);
assertThatIllegalStateException().isThrownBy(() -> this.registry.registerBundle("test", this.bundle2))
.withMessage("Cannot replace existing SSL bundle 'test'");
}
@Test
void registerBundleRegistersBundle() {
this.registry.registerBundle("test", this.bundle1);
assertThat(this.registry.getBundle("test")).isSameAs(this.bundle1);
}
@Test
void getBundleWhenNameIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.registry.getBundle(null))
.withMessage("Name must not be null");
}
@Test
void getBundleWhenNoSuchBundleThrowsException() {
this.registry.registerBundle("test", this.bundle1);
assertThatExceptionOfType(NoSuchSslBundleException.class).isThrownBy(() -> this.registry.getBundle("missing"))
.satisfies((ex) -> assertThat(ex.getBundleName()).isEqualTo("missing"));
}
@Test
void getBundleReturnsBundle() {
this.registry.registerBundle("test1", this.bundle1);
this.registry.registerBundle("test2", this.bundle2);
assertThat(this.registry.getBundle("test1")).isSameAs(this.bundle1);
assertThat(this.registry.getBundle("test2")).isSameAs(this.bundle2);
}
}

View File

@@ -0,0 +1,156 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link DefaultSslManagerBundle}.
*
* @author Phillip Webb
*/
class DefaultSslManagerBundleTests {
private KeyManagerFactory keyManagerFactory = mock(KeyManagerFactory.class);
private TrustManagerFactory trustManagerFactory = mock(TrustManagerFactory.class);
@Test
void getKeyManagerFactoryWhenStoreBundleIsNull() throws Exception {
DefaultSslManagerBundle bundle = new TestDefaultSslManagerBundle(null, SslBundleKey.NONE);
KeyManagerFactory result = bundle.getKeyManagerFactory();
assertThat(result).isNotNull();
then(this.keyManagerFactory).should().init(null, null);
}
@Test
void getKeyManagerFactoryWhenKeyIsNull() throws Exception {
DefaultSslManagerBundle bundle = new TestDefaultSslManagerBundle(SslStoreBundle.NONE, null);
KeyManagerFactory result = bundle.getKeyManagerFactory();
assertThat(result).isSameAs(this.keyManagerFactory);
then(this.keyManagerFactory).should().init(null, null);
}
@Test
void getKeyManagerFactoryWhenHasKeyAliasReturnsWrapped() {
DefaultSslManagerBundle bundle = new TestDefaultSslManagerBundle(null, SslBundleKey.of("secret", "alias"));
KeyManagerFactory result = bundle.getKeyManagerFactory();
assertThat(result).isInstanceOf(AliasKeyManagerFactory.class);
}
@Test
void getKeyManagerFactoryWhenHasKeyPassword() throws Exception {
DefaultSslManagerBundle bundle = new TestDefaultSslManagerBundle(null, SslBundleKey.of("secret"));
KeyManagerFactory result = bundle.getKeyManagerFactory();
assertThat(result).isSameAs(this.keyManagerFactory);
then(this.keyManagerFactory).should().init(null, "secret".toCharArray());
}
@Test
void getKeyManagerFactoryWhenHasKeyStorePassword() throws Exception {
SslStoreBundle storeBundle = SslStoreBundle.of(null, "secret", null);
DefaultSslManagerBundle bundle = new TestDefaultSslManagerBundle(storeBundle, null);
KeyManagerFactory result = bundle.getKeyManagerFactory();
assertThat(result).isSameAs(this.keyManagerFactory);
then(this.keyManagerFactory).should().init(null, "secret".toCharArray());
}
@Test
void getKeyManagerFactoryWhenHasAliasNotInStoreThrowsException() throws Exception {
KeyStore keyStore = mock(KeyStore.class);
given(keyStore.containsAlias("alias")).willReturn(false);
SslStoreBundle storeBundle = SslStoreBundle.of(keyStore, null, null);
DefaultSslManagerBundle bundle = new TestDefaultSslManagerBundle(storeBundle,
SslBundleKey.of("secret", "alias"));
assertThatIllegalStateException().isThrownBy(() -> bundle.getKeyManagerFactory())
.withMessage("Keystore does not contain alias 'alias'");
}
@Test
void getKeyManagerFactoryWhenHasAliasNotDeterminedInStoreThrowsException() throws Exception {
KeyStore keyStore = mock(KeyStore.class);
given(keyStore.containsAlias("alias")).willThrow(KeyStoreException.class);
SslStoreBundle storeBundle = SslStoreBundle.of(keyStore, null, null);
DefaultSslManagerBundle bundle = new TestDefaultSslManagerBundle(storeBundle,
SslBundleKey.of("secret", "alias"));
assertThatIllegalStateException().isThrownBy(() -> bundle.getKeyManagerFactory())
.withMessage("Could not determine if keystore contains alias 'alias'");
}
@Test
void getKeyManagerFactoryWhenHasStore() throws Exception {
KeyStore keyStore = mock(KeyStore.class);
SslStoreBundle storeBundle = SslStoreBundle.of(keyStore, null, null);
DefaultSslManagerBundle bundle = new TestDefaultSslManagerBundle(storeBundle, null);
KeyManagerFactory result = bundle.getKeyManagerFactory();
assertThat(result).isSameAs(this.keyManagerFactory);
then(this.keyManagerFactory).should().init(keyStore, null);
}
@Test
void getTrustManagerFactoryWhenStoreBundleIsNull() throws Exception {
DefaultSslManagerBundle bundle = new TestDefaultSslManagerBundle(null, null);
TrustManagerFactory result = bundle.getTrustManagerFactory();
assertThat(result).isSameAs(this.trustManagerFactory);
then(this.trustManagerFactory).should().init((KeyStore) null);
}
@Test
void getTrustManagerFactoryWhenHasStore() throws Exception {
KeyStore trustStore = mock(KeyStore.class);
SslStoreBundle storeBundle = SslStoreBundle.of(null, null, trustStore);
DefaultSslManagerBundle bundle = new TestDefaultSslManagerBundle(storeBundle, null);
TrustManagerFactory result = bundle.getTrustManagerFactory();
assertThat(result).isSameAs(this.trustManagerFactory);
then(this.trustManagerFactory).should().init(trustStore);
}
/**
* Test version of {@link DefaultSslManagerBundle}.
*/
class TestDefaultSslManagerBundle extends DefaultSslManagerBundle {
TestDefaultSslManagerBundle(SslStoreBundle storeBundle, SslBundleKey key) {
super(storeBundle, key);
}
@Override
protected KeyManagerFactory getKeyManagerFactoryInstance(String algorithm) throws NoSuchAlgorithmException {
return DefaultSslManagerBundleTests.this.keyManagerFactory;
}
@Override
protected TrustManagerFactory getTrustManagerFactoryInstance(String algorithm) throws NoSuchAlgorithmException {
return DefaultSslManagerBundleTests.this.trustManagerFactory;
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link NoSuchSslBundleException}.
*
* @author Phillip Webb
*/
class NoSuchSslBundleExceptionTests {
@Test
void createCreatesException() {
Throwable cause = new RuntimeException();
NoSuchSslBundleException exception = new NoSuchSslBundleException("name", "badness", cause);
assertThat(exception).hasMessage("badness").hasCause(cause);
assertThat(exception.getBundleName()).isEqualTo("name");
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl;
import java.security.KeyStore;
import java.security.KeyStoreException;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link SslBundleKey}.
*
* @author Phillip Webb
*/
class SslBundleKeyTests {
@Test
void noneHasNoValues() {
SslBundleKey keyReference = SslBundleKey.NONE;
assertThat(keyReference.getPassword()).isNull();
assertThat(keyReference.getAlias()).isNull();
}
@Test
void ofCreatesWithPasswordSslKeyReference() {
SslBundleKey keyReference = SslBundleKey.of("password");
assertThat(keyReference.getPassword()).isEqualTo("password");
assertThat(keyReference.getAlias()).isNull();
}
@Test
void ofCreatesWithPasswordAndAliasSslKeyReference() {
SslBundleKey keyReference = SslBundleKey.of("password", "alias");
assertThat(keyReference.getPassword()).isEqualTo("password");
assertThat(keyReference.getAlias()).isEqualTo("alias");
}
@Test
void getKeyManagerFactoryWhenHasAliasNotInStoreThrowsException() throws Exception {
KeyStore keyStore = mock(KeyStore.class);
given(keyStore.containsAlias("alias")).willReturn(false);
SslBundleKey key = SslBundleKey.of("secret", "alias");
assertThatIllegalStateException().isThrownBy(() -> key.assertContainsAlias(keyStore))
.withMessage("Keystore does not contain alias 'alias'");
}
@Test
void getKeyManagerFactoryWhenHasAliasNotDeterminedInStoreThrowsException() throws Exception {
KeyStore keyStore = mock(KeyStore.class);
given(keyStore.containsAlias("alias")).willThrow(KeyStoreException.class);
SslBundleKey key = SslBundleKey.of("secret", "alias");
assertThatIllegalStateException().isThrownBy(() -> key.assertContainsAlias(keyStore))
.withMessage("Could not determine if keystore contains alias 'alias'");
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link SslBundle}.
*
* @author Phillip Webb
*/
class SslBundleTests {
@Test
void createSslContextDelegatesToManagers() {
SslManagerBundle managers = mock(SslManagerBundle.class);
SslBundle bundle = SslBundle.of(null, null, null, "testprotocol", managers);
bundle.createSslContext();
then(managers).should().createSslContext("testprotocol");
}
@Test
void ofCreatesSslBundle() {
SslStoreBundle stores = mock(SslStoreBundle.class);
SslBundleKey key = mock(SslBundleKey.class);
SslOptions options = mock(SslOptions.class);
String protocol = "test";
SslManagerBundle managers = mock(SslManagerBundle.class);
SslBundle bundle = SslBundle.of(stores, key, options, protocol, managers);
assertThat(bundle.getStores()).isSameAs(stores);
assertThat(bundle.getKey()).isSameAs(key);
assertThat(bundle.getOptions()).isSameAs(options);
assertThat(bundle.getProtocol()).isSameAs(protocol);
assertThat(bundle.getManagers()).isSameAs(managers);
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link SslManagerBundle}.
*
* @author Phillip Webb
*/
class SslManagerBundleTests {
private KeyManagerFactory keyManagerFactory = mock(KeyManagerFactory.class);
private TrustManagerFactory trustManagerFactory = mock(TrustManagerFactory.class);
@Test
void getKeyManagersDelegatesToFactory() {
SslManagerBundle bundle = SslManagerBundle.of(this.keyManagerFactory, this.trustManagerFactory);
bundle.getKeyManagers();
then(this.keyManagerFactory).should().getKeyManagers();
}
@Test
void getTrustManagersDelegatesToFactory() {
SslManagerBundle bundle = SslManagerBundle.of(this.keyManagerFactory, this.trustManagerFactory);
bundle.getTrustManagers();
then(this.trustManagerFactory).should().getTrustManagers();
}
@Test
void createSslContextCreatesInitializedSslContext() {
SslManagerBundle bundle = SslManagerBundle.of(this.keyManagerFactory, this.trustManagerFactory);
SSLContext sslContext = bundle.createSslContext("TLS");
assertThat(sslContext).isNotNull();
assertThat(sslContext.getProtocol()).isEqualTo("TLS");
}
@Test
void ofWhenKeyManagerFactoryIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> SslManagerBundle.of(null, this.trustManagerFactory))
.withMessage("KeyManagerFactory must not be null");
}
@Test
void ofWhenTrustManagerFactoryIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> SslManagerBundle.of(this.keyManagerFactory, null))
.withMessage("TrustManagerFactory must not be null");
}
@Test
void ofCreatesSslManagerBundle() {
SslManagerBundle bundle = SslManagerBundle.of(this.keyManagerFactory, this.trustManagerFactory);
assertThat(bundle.getKeyManagerFactory()).isSameAs(this.keyManagerFactory);
assertThat(bundle.getTrustManagerFactory()).isSameAs(this.trustManagerFactory);
}
@Test
void fromCreatesDefaultSslManagerBundle() {
SslManagerBundle bundle = SslManagerBundle.from(SslStoreBundle.NONE, SslBundleKey.NONE);
assertThat(bundle).isInstanceOf(DefaultSslManagerBundle.class);
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl;
import java.util.Set;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SslOptions}.
*
* @author Phillip Webb
*/
class SslOptionsTests {
@Test
void noneReturnsEmptyCollections() {
SslOptions options = SslOptions.NONE;
assertThat(options.getCiphers()).isEmpty();
assertThat(options.getEnabledProtocols()).isEmpty();
}
@Test
void ofWithArrayCreatesSslOptions() {
String[] ciphers = { "a", "b", "c" };
String[] enabledProtocols = { "d", "e", "f" };
SslOptions options = SslOptions.of(ciphers, enabledProtocols);
assertThat(options.getCiphers()).containsExactly(ciphers);
assertThat(options.getEnabledProtocols()).containsExactly(enabledProtocols);
}
@Test
void ofWithNullArraysCreatesSslOptions() {
String[] ciphers = null;
String[] enabledProtocols = null;
SslOptions options = SslOptions.of(ciphers, enabledProtocols);
assertThat(options.getCiphers()).isEmpty();
assertThat(options.getEnabledProtocols()).isEmpty();
}
@Test
void ofWithSetCreatesSslOptions() {
Set<String> ciphers = Set.of("a", "b", "c");
Set<String> enabledProtocols = Set.of("d", "e", "f");
SslOptions options = SslOptions.of(ciphers, enabledProtocols);
assertThat(options.getCiphers()).isEqualTo(ciphers);
assertThat(options.getEnabledProtocols()).isEqualTo(enabledProtocols);
}
@Test
void ofWithNullSetCreatesSslOptions() {
Set<String> ciphers = null;
Set<String> enabledProtocols = null;
SslOptions options = SslOptions.of(ciphers, enabledProtocols);
assertThat(options.getCiphers()).isEmpty();
assertThat(options.getEnabledProtocols()).isEmpty();
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl;
import java.security.KeyStore;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link SslStoreBundle}
*
* @author Phillip Webb
*/
class SslStoreBundleTests {
@Test
void noneReturnsEmptySslStoreBundle() {
SslStoreBundle bundle = SslStoreBundle.NONE;
assertThat(bundle.getKeyStore()).isNull();
assertThat(bundle.getKeyStorePassword()).isNull();
assertThat(bundle.getTrustStore()).isNull();
}
@Test
void ofCreatesStoreBundle() {
KeyStore keyStore = mock(KeyStore.class);
String keyStorePassword = "secret";
KeyStore trustStore = mock(KeyStore.class);
SslStoreBundle bundle = SslStoreBundle.of(keyStore, keyStorePassword, trustStore);
assertThat(bundle.getKeyStore()).isSameAs(keyStore);
assertThat(bundle.getKeyStorePassword()).isEqualTo(keyStorePassword);
assertThat(bundle.getTrustStore()).isSameAs(trustStore);
}
}

View File

@@ -0,0 +1,137 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl.jks;
import java.security.KeyStore;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.boot.web.embedded.test.MockPkcs11Security;
import org.springframework.util.function.ThrowingConsumer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link JksSslStoreBundle}.
*
* @author Scott Frederick
* @author Phillip Webb
*/
@MockPkcs11Security
class JksSslStoreBundleTests {
@Test
void whenNullStores() {
JksSslStoreDetails keyStoreDetails = null;
JksSslStoreDetails trustStoreDetails = null;
JksSslStoreBundle bundle = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getKeyStore()).isNull();
assertThat(bundle.getKeyStorePassword()).isNull();
assertThat(bundle.getTrustStore()).isNull();
}
@Test
void whenStoresHaveNoValues() {
JksSslStoreDetails keyStoreDetails = JksSslStoreDetails.forLocation(null);
JksSslStoreDetails trustStoreDetails = JksSslStoreDetails.forLocation(null);
JksSslStoreBundle bundle = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getKeyStore()).isNull();
assertThat(bundle.getKeyStorePassword()).isNull();
assertThat(bundle.getTrustStore()).isNull();
}
@Test
void whenTypePKCS11AndLocationThrowsException() {
JksSslStoreDetails keyStoreDetails = new JksSslStoreDetails("PKCS11", null, "test.jks", null);
JksSslStoreDetails trustStoreDetails = null;
JksSslStoreBundle bundle = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThatIllegalStateException().isThrownBy(bundle::getKeyStore)
.withMessageContaining(
"Unable to create key store: Location is 'test.jks', but must be empty or null for PKCS11 hardware key stores");
}
@Test
void whenHasKeyStoreLocation() {
JksSslStoreDetails keyStoreDetails = JksSslStoreDetails.forLocation("classpath:test.jks")
.withPassword("secret");
JksSslStoreDetails trustStoreDetails = null;
JksSslStoreBundle bundle = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("test-alias", "password"));
}
@Test
void getTrustStoreWithLocations() {
JksSslStoreDetails keyStoreDetails = null;
JksSslStoreDetails trustStoreDetails = JksSslStoreDetails.forLocation("classpath:test.jks")
.withPassword("secret");
JksSslStoreBundle bundle = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("test-alias", "password"));
}
@Test
void whenHasKeyStoreType() {
JksSslStoreDetails keyStoreDetails = new JksSslStoreDetails("jks", null, "classpath:test.jks", "secret");
JksSslStoreDetails trustStoreDetails = null;
JksSslStoreBundle bundle = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("jks", "test-alias", "password"));
}
@Test
void whenHasTrustStoreType() {
JksSslStoreDetails keyStoreDetails = null;
JksSslStoreDetails trustStoreDetails = new JksSslStoreDetails("jks", null, "classpath:test.jks", "secret");
JksSslStoreBundle bundle = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("jks", "test-alias", "password"));
}
@Test
void whenHasKeyStoreProvider() {
JksSslStoreDetails keyStoreDetails = new JksSslStoreDetails(null, "com.example.KeyStoreProvider",
"classpath:test.jks", "secret");
JksSslStoreDetails trustStoreDetails = null;
JksSslStoreBundle bundle = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThatIllegalStateException().isThrownBy(bundle::getKeyStore)
.withMessageContaining("com.example.KeyStoreProvider");
}
@Test
void whenHasTrustStoreProvider() {
JksSslStoreDetails keyStoreDetails = null;
JksSslStoreDetails trustStoreDetails = new JksSslStoreDetails(null, "com.example.KeyStoreProvider",
"classpath:test.jks", "secret");
JksSslStoreBundle bundle = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThatIllegalStateException().isThrownBy(bundle::getTrustStore)
.withMessageContaining("com.example.KeyStoreProvider");
}
private Consumer<KeyStore> storeContainingCertAndKey(String keyAlias, String keyPassword) {
return storeContainingCertAndKey(KeyStore.getDefaultType(), keyAlias, keyPassword);
}
private Consumer<KeyStore> storeContainingCertAndKey(String keyStoreType, String keyAlias, String keyPassword) {
return ThrowingConsumer.of((keyStore) -> {
assertThat(keyStore).isNotNull();
assertThat(keyStore.getType()).isEqualTo(keyStoreType);
assertThat(keyStore.containsAlias(keyAlias)).isTrue();
assertThat(keyStore.getCertificate(keyAlias)).isNotNull();
assertThat(keyStore.getKey(keyAlias, keyPassword.toCharArray())).isNotNull();
});
}
}

View File

@@ -14,44 +14,44 @@
* limitations under the License.
*/
package org.springframework.boot.web.server;
package org.springframework.boot.ssl.pem;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link CertificateParser}.
* Tests for {@link PemCertificateParser}.
*
* @author Scott Frederick
*/
class CertificateParserTests {
class PemCertificateParserTests {
@Test
void parseCertificate() {
X509Certificate[] certificates = CertificateParser.parse("classpath:test-cert.pem");
void parseCertificate() throws Exception {
X509Certificate[] certificates = PemCertificateParser.parse(read("test-cert.pem"));
assertThat(certificates).isNotNull();
assertThat(certificates).hasSize(1);
assertThat(certificates[0].getType()).isEqualTo("X.509");
}
@Test
void parseCertificateChain() {
X509Certificate[] certificates = CertificateParser.parse("classpath:test-cert-chain.pem");
void parseCertificateChain() throws Exception {
X509Certificate[] certificates = PemCertificateParser.parse(read("test-cert-chain.pem"));
assertThat(certificates).isNotNull();
assertThat(certificates).hasSize(2);
assertThat(certificates[0].getType()).isEqualTo("X.509");
assertThat(certificates[1].getType()).isEqualTo("X.509");
}
@Test
void parseWithInvalidPathWillThrowException() {
String path = "file:///bad/path/cert.pem";
assertThatIllegalStateException().isThrownBy(() -> CertificateParser.parse("file:///bad/path/cert.pem"))
.withMessageContaining(path);
private String read(String path) throws IOException {
return new ClassPathResource(path).getContentAsString(StandardCharsets.UTF_8);
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl.pem;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link PemContent}.
*
* @author Phillip Webb
*/
class PemContentTests {
@Test
void loadWhenContentIsNullReturnsNull() {
assertThat(PemContent.load(null)).isNull();
}
@Test
void loadWhenContentIsPemContentReturnsContent() {
String content = """
-----BEGIN CERTIFICATE-----
MIICpDCCAYwCCQCDOqHKPjAhCTANBgkqhkiG9w0BAQUFADAUMRIwEAYDVQQDDAls
b2NhbGhvc3QwHhcNMTQwOTEwMjE0MzA1WhcNMTQxMDEwMjE0MzA1WjAUMRIwEAYD
VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDR
0KfxUw7MF/8RB5/YXOM7yLnoHYb/M/6dyoulMbtEdKKhQhU28o5FiDkHcEG9PJQL
gqrRgAjl3VmCC9omtfZJQ2EpfkTttkJjnKOOroXhYE51/CYSckapBYCVh8GkjUEJ
uEfnp07cTfYZFqViIgIWPZyjkzl3w4girS7kCuzNdDntVJVx5F/EsFwMA8n3C0Qa
zHQoM5s00Fer6aTwd6AW0JD5QkADavpfzZ554e4HrVGwHlM28WKQQkFzzGu44FFX
yVuEF3HeyVPug8GRHAc8UU7ijVgJB5TmbvRGYowIErD5i4VvGLuOv9mgR3aVyN0S
dJ1N7aJnXpeSQjAgf03jAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAE4yvwhbPldg
Bpl7sBw/m2B3bfiNeSqa4tII1PQ7ysgWVb9HbFNKkriScwDWlqo6ljZfJ+SDFCoj
bQz4fOFdMAOzRnpTrG2NAKMoJLY0/g/p7XO00PiC8T3h3BOJ5SHuW3gUyfGXmAYs
DnJxJOrwPzj57xvNXjNSbDOJ3DRfCbB0CWBexOeGDiUokoEq3Gnz04Q4ZfHyAcpZ
3deMw8Od5p9WAoCh3oClpFyOSzXYKZd+3ppMMtfc4wnbfocnfSFxj0UCpOEJw4Ez
+lGuHKdhNOVW9CmqPD1y76o6c8PQKuF7KZEoY2jvy3GeIfddBvqXgZ4PbWvFz1jO
32C9XWHwRA4=
-----END CERTIFICATE-----""";
assertThat(PemContent.load(content)).isEqualTo(content);
}
@Test
void loadWhenClasspathLocationReturnsContent() throws IOException {
String actual = PemContent.load("classpath:test-cert.pem");
String expected = new ClassPathResource("test-cert.pem").getContentAsString(StandardCharsets.UTF_8);
assertThat(actual).isEqualTo(expected);
}
@Test
void loadWhenFileLocationReturnsContent() throws IOException {
String actual = PemContent.load("src/test/resources/test-cert.pem");
String expected = new ClassPathResource("test-cert.pem").getContentAsString(StandardCharsets.UTF_8);
assertThat(actual).isEqualTo(expected);
}
}

View File

@@ -14,49 +14,49 @@
* limitations under the License.
*/
package org.springframework.boot.web.server;
package org.springframework.boot.ssl.pem;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link PrivateKeyParser}.
* Tests for {@link PemPrivateKeyParser}.
*
* @author Scott Frederick
*/
class PrivateKeyParserTests {
class PemPrivateKeyParserTests {
@Test
void parsePkcs8KeyFile() {
PrivateKey privateKey = PrivateKeyParser.parse("classpath:test-key.pem");
void parsePkcs8KeyFile() throws Exception {
PrivateKey privateKey = PemPrivateKeyParser.parse(read("test-key.pem"));
assertThat(privateKey).isNotNull();
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
assertThat(privateKey.getAlgorithm()).isEqualTo("RSA");
}
@Test
void parsePkcs8KeyFileWithEcdsa() {
PrivateKey privateKey = PrivateKeyParser.parse("classpath:test-ec-key.pem");
void parsePkcs8KeyFileWithEcdsa() throws Exception {
PrivateKey privateKey = PemPrivateKeyParser.parse(read("test-ec-key.pem"));
assertThat(privateKey).isNotNull();
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
assertThat(privateKey.getAlgorithm()).isEqualTo("EC");
}
@Test
void parseWithNonKeyFileWillThrowException() {
String path = "classpath:test-banner.txt";
assertThatIllegalStateException().isThrownBy(() -> PrivateKeyParser.parse("file://" + path))
.withMessageContaining(path);
void parseWithNonKeyTextWillThrowException() {
assertThatIllegalStateException().isThrownBy(() -> PemPrivateKeyParser.parse(read("test-banner.txt")));
}
@Test
void parseWithInvalidPathWillThrowException() {
String path = "file:///bad/path/key.pem";
assertThatIllegalStateException().isThrownBy(() -> PrivateKeyParser.parse(path)).withMessageContaining(path);
private String read(String path) throws IOException {
return new ClassPathResource(path).getContentAsString(StandardCharsets.UTF_8);
}
}

View File

@@ -0,0 +1,137 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl.pem;
import java.security.KeyStore;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.util.function.ThrowingConsumer;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link PemSslStoreBundle}.
*
* @author Scott Frederick
* @author Phillip Webb
*/
class PemSslStoreBundleTests {
@Test
void whenNullStores() {
PemSslStoreDetails keyStoreDetails = null;
PemSslStoreDetails trustStoreDetails = null;
PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getKeyStore()).isNull();
assertThat(bundle.getKeyStorePassword()).isNull();
assertThat(bundle.getTrustStore()).isNull();
}
@Test
void whenStoresHaveNoValues() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate(null);
PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate(null);
PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getKeyStore()).isNull();
assertThat(bundle.getKeyStorePassword()).isNull();
assertThat(bundle.getTrustStore()).isNull();
}
@Test
void whenHasKeyStoreDetailsCertAndKey() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem")
.withPrivateKey("classpath:test-key.pem");
PemSslStoreDetails trustStoreDetails = null;
PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("ssl"));
assertThat(bundle.getTrustStore()).isNull();
}
@Test
void whenHasKeyStoreDetailsAndTrustStoreDetailsWithoutKey() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem")
.withPrivateKey("classpath:test-key.pem");
PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem");
PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("ssl"));
assertThat(bundle.getTrustStore()).satisfies(storeContainingCert("ssl-0"));
}
@Test
void whenHasKeyStoreDetailsAndTrustStoreDetails() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem")
.withPrivateKey("classpath:test-key.pem");
PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem")
.withPrivateKey("classpath:test-key.pem");
PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("ssl"));
assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("ssl"));
}
@Test
void whenHasKeyStoreDetailsAndTrustStoreDetailsAndAlias() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem")
.withPrivateKey("classpath:test-key.pem");
PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem")
.withPrivateKey("classpath:test-key.pem");
PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, "test-alias");
assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("test-alias"));
assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("test-alias"));
}
@Test
void whenHasStoreType() {
PemSslStoreDetails keyStoreDetails = new PemSslStoreDetails("PKCS12", "classpath:test-cert.pem",
"classpath:test-key.pem");
PemSslStoreDetails trustStoreDetails = new PemSslStoreDetails("PKCS12", "classpath:test-cert.pem",
"classpath:test-key.pem");
PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("PKCS12", "ssl"));
assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("PKCS12", "ssl"));
}
private Consumer<KeyStore> storeContainingCert(String keyAlias) {
return storeContainingCert(KeyStore.getDefaultType(), keyAlias);
}
private Consumer<KeyStore> storeContainingCert(String keyStoreType, String keyAlias) {
return ThrowingConsumer.of((keyStore) -> {
assertThat(keyStore).isNotNull();
assertThat(keyStore.getType()).isEqualTo(keyStoreType);
assertThat(keyStore.containsAlias(keyAlias)).isTrue();
assertThat(keyStore.getCertificate(keyAlias)).isNotNull();
assertThat(keyStore.getKey(keyAlias, new char[] {})).isNull();
});
}
private Consumer<KeyStore> storeContainingCertAndKey(String keyAlias) {
return storeContainingCertAndKey(KeyStore.getDefaultType(), keyAlias);
}
private Consumer<KeyStore> storeContainingCertAndKey(String keyStoreType, String keyAlias) {
return ThrowingConsumer.of((keyStore) -> {
assertThat(keyStore).isNotNull();
assertThat(keyStore.getType()).isEqualTo(keyStoreType);
assertThat(keyStore.containsAlias(keyAlias)).isTrue();
assertThat(keyStore.getCertificate(keyAlias)).isNotNull();
assertThat(keyStore.getKey(keyAlias, new char[] {})).isNotNull();
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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