Provide centralized configuration of SSL properties
Closes gh-34814
This commit is contained in:
@@ -2064,6 +2064,10 @@
|
||||
"level": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "management.server.ssl.bundle",
|
||||
"description": "The name of a configured SSL bundle."
|
||||
},
|
||||
{
|
||||
"name": "management.server.ssl.certificate",
|
||||
"description": "Path to a PEM-encoded SSL certificate file."
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.springframework.boot.rsocket.context.RSocketServerBootstrap;
|
||||
import org.springframework.boot.rsocket.netty.NettyRSocketServerFactory;
|
||||
import org.springframework.boot.rsocket.server.RSocketServerCustomizer;
|
||||
import org.springframework.boot.rsocket.server.RSocketServerFactory;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -54,6 +55,7 @@ import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHa
|
||||
* server port is configured, a new standalone RSocket server is created.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Scott Frederick
|
||||
* @since 2.2.0
|
||||
*/
|
||||
@AutoConfiguration(after = RSocketStrategiesAutoConfiguration.class)
|
||||
@@ -85,7 +87,7 @@ public class RSocketServerAutoConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
RSocketServerFactory rSocketServerFactory(RSocketProperties properties, ReactorResourceFactory resourceFactory,
|
||||
ObjectProvider<RSocketServerCustomizer> customizers) {
|
||||
ObjectProvider<RSocketServerCustomizer> customizers, ObjectProvider<SslBundles> sslBundles) {
|
||||
NettyRSocketServerFactory factory = new NettyRSocketServerFactory();
|
||||
factory.setResourceFactory(resourceFactory);
|
||||
factory.setTransport(properties.getServer().getTransport());
|
||||
@@ -94,6 +96,7 @@ public class RSocketServerAutoConfiguration {
|
||||
map.from(properties.getServer().getPort()).to(factory::setPort);
|
||||
map.from(properties.getServer().getFragmentSize()).to(factory::setFragmentSize);
|
||||
map.from(properties.getServer().getSsl()).to(factory::setSsl);
|
||||
factory.setSslBundles(sslBundles.getIfAvailable());
|
||||
factory.setRSocketServerCustomizers(customizers.orderedStream().toList());
|
||||
return factory;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure.web.reactive;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
@@ -31,6 +32,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.autoconfigure.web.ServerProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
@@ -45,6 +47,7 @@ import org.springframework.web.server.adapter.ForwardedHeaderTransformer;
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for a reactive web server.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Scott Frederick
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
|
||||
@@ -60,8 +63,9 @@ import org.springframework.web.server.adapter.ForwardedHeaderTransformer;
|
||||
public class ReactiveWebServerFactoryAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public ReactiveWebServerFactoryCustomizer reactiveWebServerFactoryCustomizer(ServerProperties serverProperties) {
|
||||
return new ReactiveWebServerFactoryCustomizer(serverProperties);
|
||||
public ReactiveWebServerFactoryCustomizer reactiveWebServerFactoryCustomizer(ServerProperties serverProperties,
|
||||
ObjectProvider<SslBundles> sslBundles) {
|
||||
return new ReactiveWebServerFactoryCustomizer(serverProperties, sslBundles.getIfAvailable());
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.boot.autoconfigure.web.reactive;
|
||||
|
||||
import org.springframework.boot.autoconfigure.web.ServerProperties;
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.web.reactive.server.ConfigurableReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||
import org.springframework.core.Ordered;
|
||||
@@ -28,6 +29,7 @@ import org.springframework.core.Ordered;
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Yunkun Huang
|
||||
* @author Scott Frederick
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class ReactiveWebServerFactoryCustomizer
|
||||
@@ -35,8 +37,25 @@ public class ReactiveWebServerFactoryCustomizer
|
||||
|
||||
private final ServerProperties serverProperties;
|
||||
|
||||
private final SslBundles sslBundles;
|
||||
|
||||
/**
|
||||
* Create a new {@link ReactiveWebServerFactoryCustomizer} instance.
|
||||
* @param serverProperties the server properties
|
||||
*/
|
||||
public ReactiveWebServerFactoryCustomizer(ServerProperties serverProperties) {
|
||||
this(serverProperties, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ReactiveWebServerFactoryCustomizer} instance.
|
||||
* @param serverProperties the server properties
|
||||
* @param sslBundles the SSL bundles
|
||||
* @since 3.1.0
|
||||
*/
|
||||
public ReactiveWebServerFactoryCustomizer(ServerProperties serverProperties, SslBundles sslBundles) {
|
||||
this.serverProperties = serverProperties;
|
||||
this.sslBundles = sslBundles;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -53,6 +72,7 @@ public class ReactiveWebServerFactoryCustomizer
|
||||
map.from(this.serverProperties::getCompression).to(factory::setCompression);
|
||||
map.from(this.serverProperties::getHttp2).to(factory::setHttp2);
|
||||
map.from(this.serverProperties.getShutdown()).to(factory::setShutdown);
|
||||
map.from(() -> this.sslBundles).to(factory::setSslBundles);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -33,8 +33,10 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
|
||||
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.ServerProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.web.server.ErrorPageRegistrarBeanPostProcessor;
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
@@ -57,9 +59,10 @@ import org.springframework.web.filter.ForwardedHeaderFilter;
|
||||
* @author Ivan Sopov
|
||||
* @author Brian Clozel
|
||||
* @author Stephane Nicoll
|
||||
* @author Scott Frederick
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@AutoConfiguration(after = SslAutoConfiguration.class)
|
||||
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
|
||||
@ConditionalOnClass(ServletRequest.class)
|
||||
@ConditionalOnWebApplication(type = Type.SERVLET)
|
||||
@@ -73,9 +76,9 @@ public class ServletWebServerFactoryAutoConfiguration {
|
||||
@Bean
|
||||
public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties,
|
||||
ObjectProvider<WebListenerRegistrar> webListenerRegistrars,
|
||||
ObjectProvider<CookieSameSiteSupplier> cookieSameSiteSuppliers) {
|
||||
ObjectProvider<CookieSameSiteSupplier> cookieSameSiteSuppliers, ObjectProvider<SslBundles> sslBundles) {
|
||||
return new ServletWebServerFactoryCustomizer(serverProperties, webListenerRegistrars.orderedStream().toList(),
|
||||
cookieSameSiteSuppliers.orderedStream().toList());
|
||||
cookieSameSiteSuppliers.orderedStream().toList(), sslBundles.getIfAvailable());
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -21,6 +21,7 @@ import java.util.List;
|
||||
|
||||
import org.springframework.boot.autoconfigure.web.ServerProperties;
|
||||
import org.springframework.boot.context.properties.PropertyMapper;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||
import org.springframework.boot.web.servlet.WebListenerRegistrar;
|
||||
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
|
||||
@@ -36,6 +37,7 @@ import org.springframework.util.CollectionUtils;
|
||||
* @author Stephane Nicoll
|
||||
* @author Olivier Lamy
|
||||
* @author Yunkun Huang
|
||||
* @author Scott Frederick
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class ServletWebServerFactoryCustomizer
|
||||
@@ -47,20 +49,24 @@ public class ServletWebServerFactoryCustomizer
|
||||
|
||||
private final List<CookieSameSiteSupplier> cookieSameSiteSuppliers;
|
||||
|
||||
private final SslBundles sslBundles;
|
||||
|
||||
public ServletWebServerFactoryCustomizer(ServerProperties serverProperties) {
|
||||
this(serverProperties, Collections.emptyList());
|
||||
}
|
||||
|
||||
public ServletWebServerFactoryCustomizer(ServerProperties serverProperties,
|
||||
List<WebListenerRegistrar> webListenerRegistrars) {
|
||||
this(serverProperties, webListenerRegistrars, null);
|
||||
this(serverProperties, webListenerRegistrars, null, null);
|
||||
}
|
||||
|
||||
ServletWebServerFactoryCustomizer(ServerProperties serverProperties,
|
||||
List<WebListenerRegistrar> webListenerRegistrars, List<CookieSameSiteSupplier> cookieSameSiteSuppliers) {
|
||||
List<WebListenerRegistrar> webListenerRegistrars, List<CookieSameSiteSupplier> cookieSameSiteSuppliers,
|
||||
SslBundles sslBundles) {
|
||||
this.serverProperties = serverProperties;
|
||||
this.webListenerRegistrars = webListenerRegistrars;
|
||||
this.cookieSameSiteSuppliers = cookieSameSiteSuppliers;
|
||||
this.sslBundles = sslBundles;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -84,12 +90,11 @@ public class ServletWebServerFactoryCustomizer
|
||||
map.from(this.serverProperties::getServerHeader).to(factory::setServerHeader);
|
||||
map.from(this.serverProperties.getServlet()::getContextParameters).to(factory::setInitParameters);
|
||||
map.from(this.serverProperties.getShutdown()).to(factory::setShutdown);
|
||||
for (WebListenerRegistrar registrar : this.webListenerRegistrars) {
|
||||
registrar.register(factory);
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(this.cookieSameSiteSuppliers)) {
|
||||
factory.setCookieSameSiteSuppliers(this.cookieSameSiteSuppliers);
|
||||
}
|
||||
map.from(() -> this.sslBundles).to(factory::setSslBundles);
|
||||
map.from(() -> this.cookieSameSiteSuppliers)
|
||||
.whenNot(CollectionUtils::isEmpty)
|
||||
.to(factory::setCookieSameSiteSuppliers);
|
||||
this.webListenerRegistrars.forEach((registrar) -> registrar.register(factory));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,6 +26,8 @@ import org.junit.jupiter.api.Test;
|
||||
import reactor.netty.http.server.HttpServer;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
|
||||
import org.springframework.boot.ssl.NoSuchSslBundleException;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||
import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories;
|
||||
@@ -64,13 +66,15 @@ import static org.mockito.Mockito.mock;
|
||||
* @author Brian Clozel
|
||||
* @author Raheela Aslam
|
||||
* @author Madhura Bhave
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
@DirtiesUrlFactories
|
||||
class ReactiveWebServerFactoryAutoConfigurationTests {
|
||||
|
||||
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner(
|
||||
AnnotationConfigReactiveWebServerApplicationContext::new)
|
||||
.withConfiguration(AutoConfigurations.of(ReactiveWebServerFactoryAutoConfiguration.class));
|
||||
.withConfiguration(
|
||||
AutoConfigurations.of(ReactiveWebServerFactoryAutoConfiguration.class, SslAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void createFromConfigClass() {
|
||||
@@ -118,6 +122,17 @@ class ReactiveWebServerFactoryAutoConfigurationTests {
|
||||
.isInstanceOf(TomcatReactiveWebServerFactory.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void webServerFailsWithInvalidSslBundle() {
|
||||
this.contextRunner.withUserConfiguration(HttpHandlerConfiguration.class)
|
||||
.withPropertyValues("server.port=0", "server.ssl.bundle=test-bundle")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasFailed();
|
||||
assertThat(context.getStartupFailure().getCause()).isInstanceOf(NoSuchSslBundleException.class)
|
||||
.withFailMessage("test");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void tomcatConnectorCustomizerBeanIsAddedToFactory() {
|
||||
ReactiveWebApplicationContextRunner runner = new ReactiveWebApplicationContextRunner(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -23,6 +23,8 @@ import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.boot.autoconfigure.web.ServerProperties;
|
||||
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.web.reactive.server.ConfigurableReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.Shutdown;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
@@ -36,16 +38,19 @@ import static org.mockito.Mockito.mock;
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Yunkun Huang
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class ReactiveWebServerFactoryCustomizerTests {
|
||||
|
||||
private final ServerProperties properties = new ServerProperties();
|
||||
|
||||
private final SslBundles sslBundles = new DefaultSslBundleRegistry();
|
||||
|
||||
private ReactiveWebServerFactoryCustomizer customizer;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.customizer = new ReactiveWebServerFactoryCustomizer(this.properties);
|
||||
this.customizer = new ReactiveWebServerFactoryCustomizer(this.properties, this.sslBundles);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -72,6 +77,7 @@ class ReactiveWebServerFactoryCustomizerTests {
|
||||
this.properties.setSsl(ssl);
|
||||
this.customizer.customize(factory);
|
||||
then(factory).should().setSsl(ssl);
|
||||
then(factory).should().setSslBundles(this.sslBundles);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -30,4 +30,6 @@ include::features/developing-auto-configuration.adoc[]
|
||||
|
||||
include::features/kotlin.adoc[]
|
||||
|
||||
include::features/ssl.adoc[]
|
||||
|
||||
include::features/whats-next.adoc[]
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
[[features.ssl]]
|
||||
== SSL
|
||||
Spring Boot provides the ability to configure SSL trust material that can be applied to several types of connections in order to support secure communications.
|
||||
Configuration properties with the prefix `spring.ssl.bundle` can be used to specify named sets of trust material and associated information.
|
||||
|
||||
|
||||
|
||||
[[features.ssl.jks]]
|
||||
=== Configuring SSL With Java KeyStore Files
|
||||
Configuration properties with the prefix `spring.ssl.bundle.jks` can be used to configure bundles of trust material created with the Java `keytool` utility and stored in Java KeyStore files in the JKS or PKCS12 format.
|
||||
Each bundle has a user-provided name that can be used to reference the bundle.
|
||||
|
||||
When used to secure an embedded web server, a `keystore` is typically configured with a Java KeyStore containing a certificate and private key as shown in this example:
|
||||
|
||||
[source,yaml,indent=0,subs="verbatim",configblocks]
|
||||
----
|
||||
spring:
|
||||
ssl:
|
||||
bundle:
|
||||
jks:
|
||||
mybundle:
|
||||
key:
|
||||
alias: "application"
|
||||
keystore:
|
||||
location: "classpath:application.p12"
|
||||
password: "secret"
|
||||
type: "PKCS12"
|
||||
----
|
||||
|
||||
When used to secure a client-side connection, a `truststore` is typically configured with a Java KeyStore containing the server certificate as shown in this example:
|
||||
|
||||
[source,yaml,indent=0,subs="verbatim",configblocks]
|
||||
----
|
||||
spring:
|
||||
ssl:
|
||||
bundle:
|
||||
jks:
|
||||
mybundle:
|
||||
truststore:
|
||||
location: "classpath:server.p12"
|
||||
password: "secret"
|
||||
----
|
||||
|
||||
See {spring-boot-autoconfigure-module-code}/ssl/JksSslBundleProperties.java[JksSslBundleProperties] for the full set of supported properties.
|
||||
|
||||
|
||||
|
||||
[[features.ssl.pem]]
|
||||
=== Configuring SSL With PEM-encoded Certificates
|
||||
Configuration properties with the prefix `spring.ssl.bundle.pem` can be used to configure bundles of trust material in the form of PEM-encoded text.
|
||||
Each bundle has a user-provided name that can be used to reference the bundle.
|
||||
|
||||
When used to secure an embedded web server, a `keystore` is typically configured with a certificate and private key as shown in this example:
|
||||
|
||||
[source,yaml,indent=0,subs="verbatim",configblocks]
|
||||
----
|
||||
spring:
|
||||
ssl:
|
||||
bundle:
|
||||
pem:
|
||||
mybundle:
|
||||
keystore:
|
||||
certificate: "classpath:application.crt"
|
||||
private-key: "classpath:application.key"
|
||||
----
|
||||
|
||||
When used to secure an embedded web server, a `truststore` is typically configured with the server certificate as shown in this example:
|
||||
|
||||
[source,yaml,indent=0,subs="verbatim",configblocks]
|
||||
----
|
||||
spring:
|
||||
ssl:
|
||||
bundle:
|
||||
pem:
|
||||
mybundle:
|
||||
truststore:
|
||||
certificate: "classpath:server.crt"
|
||||
----
|
||||
|
||||
See {spring-boot-autoconfigure-module-code}/ssl/PemSslBundleProperties.java[PemSslBundleProperties] for the full set of supported properties.
|
||||
|
||||
|
||||
|
||||
[[features.ssl.applying]]
|
||||
=== Applying SSL Bundles
|
||||
Once configured using properties, SSL bundles can be referred to by name in configuration properties for various types of connections that are auto-configured by Spring Boot.
|
||||
See the sections on <<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[]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -39,9 +39,12 @@ import org.springframework.boot.rsocket.server.ConfigurableRSocketServerFactory;
|
||||
import org.springframework.boot.rsocket.server.RSocketServer;
|
||||
import org.springframework.boot.rsocket.server.RSocketServerCustomizer;
|
||||
import org.springframework.boot.rsocket.server.RSocketServerFactory;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.web.embedded.netty.SslServerCustomizer;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.SslStoreProvider;
|
||||
import org.springframework.boot.web.server.SslStoreProviderFactory;
|
||||
import org.springframework.boot.web.server.WebServerSslBundle;
|
||||
import org.springframework.http.client.reactive.ReactorResourceFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
@@ -55,6 +58,7 @@ import org.springframework.util.unit.DataSize;
|
||||
* @author Scott Frederick
|
||||
* @since 2.2.0
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
public class NettyRSocketServerFactory implements RSocketServerFactory, ConfigurableRSocketServerFactory {
|
||||
|
||||
private int port = 9898;
|
||||
@@ -75,6 +79,8 @@ public class NettyRSocketServerFactory implements RSocketServerFactory, Configur
|
||||
|
||||
private SslStoreProvider sslStoreProvider;
|
||||
|
||||
private SslBundles sslBundles;
|
||||
|
||||
@Override
|
||||
public void setPort(int port) {
|
||||
this.port = port;
|
||||
@@ -105,6 +111,11 @@ public class NettyRSocketServerFactory implements RSocketServerFactory, Configur
|
||||
this.sslStoreProvider = sslStoreProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSslBundles(SslBundles sslBundles) {
|
||||
this.sslBundles = sslBundles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link ReactorResourceFactory} to get the shared resources from.
|
||||
* @param resourceFactory the server resources
|
||||
@@ -172,17 +183,14 @@ public class NettyRSocketServerFactory implements RSocketServerFactory, Configur
|
||||
if (this.resourceFactory != null) {
|
||||
httpServer = httpServer.runOn(this.resourceFactory.getLoopResources());
|
||||
}
|
||||
if (this.ssl != null && this.ssl.isEnabled()) {
|
||||
if (Ssl.isEnabled(this.ssl)) {
|
||||
httpServer = customizeSslConfiguration(httpServer);
|
||||
}
|
||||
return WebsocketServerTransport.create(httpServer.bindAddress(this::getListenAddress));
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private HttpServer customizeSslConfiguration(HttpServer httpServer) {
|
||||
org.springframework.boot.web.embedded.netty.SslServerCustomizer sslServerCustomizer = new org.springframework.boot.web.embedded.netty.SslServerCustomizer(
|
||||
this.ssl, null, getOrCreateSslStoreProvider());
|
||||
return sslServerCustomizer.apply(httpServer);
|
||||
return new SslServerCustomizer(null, this.ssl.getClientAuth(), getSslBundle()).apply(httpServer);
|
||||
}
|
||||
|
||||
private ServerTransport<CloseableChannel> createTcpTransport() {
|
||||
@@ -190,19 +198,15 @@ public class NettyRSocketServerFactory implements RSocketServerFactory, Configur
|
||||
if (this.resourceFactory != null) {
|
||||
tcpServer = tcpServer.runOn(this.resourceFactory.getLoopResources());
|
||||
}
|
||||
if (this.ssl != null && this.ssl.isEnabled()) {
|
||||
TcpSslServerCustomizer sslServerCustomizer = new TcpSslServerCustomizer(this.ssl,
|
||||
getOrCreateSslStoreProvider());
|
||||
tcpServer = sslServerCustomizer.apply(tcpServer);
|
||||
if (Ssl.isEnabled(this.ssl)) {
|
||||
tcpServer = new TcpSslServerCustomizer(this.ssl.getClientAuth(), getSslBundle()).apply(tcpServer);
|
||||
}
|
||||
return TcpServerTransport.create(tcpServer.bindAddress(this::getListenAddress));
|
||||
}
|
||||
|
||||
private SslStoreProvider getOrCreateSslStoreProvider() {
|
||||
if (this.sslStoreProvider != null) {
|
||||
return this.sslStoreProvider;
|
||||
}
|
||||
return SslStoreProviderFactory.from(this.ssl);
|
||||
@SuppressWarnings("deprecation")
|
||||
private SslBundle getSslBundle() {
|
||||
return WebServerSslBundle.get(this.ssl, this.sslBundles, this.sslStoreProvider);
|
||||
}
|
||||
|
||||
private InetSocketAddress getListenAddress() {
|
||||
@@ -212,12 +216,11 @@ public class NettyRSocketServerFactory implements RSocketServerFactory, Configur
|
||||
return new InetSocketAddress(this.port);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private static final class TcpSslServerCustomizer
|
||||
extends org.springframework.boot.web.embedded.netty.SslServerCustomizer {
|
||||
|
||||
private TcpSslServerCustomizer(Ssl ssl, SslStoreProvider sslStoreProvider) {
|
||||
super(ssl, null, sslStoreProvider);
|
||||
private TcpSslServerCustomizer(Ssl.ClientAuth clientAuth, SslBundle sslBundle) {
|
||||
super(null, clientAuth, sslBundle);
|
||||
}
|
||||
|
||||
private TcpServer apply(TcpServer server) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.boot.rsocket.server;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.SslStoreProvider;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
@@ -26,6 +27,7 @@ import org.springframework.util.unit.DataSize;
|
||||
* A configurable {@link RSocketServerFactory}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Scott Frederick
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public interface ConfigurableRSocketServerFactory {
|
||||
@@ -66,7 +68,18 @@ public interface ConfigurableRSocketServerFactory {
|
||||
/**
|
||||
* Sets a provider that will be used to obtain SSL stores.
|
||||
* @param sslStoreProvider the SSL store provider
|
||||
* @deprecated since 3.1.0 for removal in 3.3.0 in favor of
|
||||
* {@link #setSslBundles(SslBundles)}
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
@Deprecated(since = "3.1.0", forRemoval = true)
|
||||
void setSslStoreProvider(SslStoreProvider sslStoreProvider);
|
||||
|
||||
/**
|
||||
* Sets an SSL bundle that can be used to get SSL configuration.
|
||||
* @param sslBundles the SSL bundles
|
||||
* @since 3.1.0
|
||||
*/
|
||||
void setSslBundles(SslBundles sslBundles);
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -43,6 +43,7 @@ import org.eclipse.jetty.util.thread.ThreadPool;
|
||||
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.Shutdown;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.http.client.reactive.JettyResourceFactory;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
@@ -179,7 +180,7 @@ public class JettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
|
||||
contextHandler.addServlet(servletHolder, "/");
|
||||
server.setHandler(addHandlerWrappers(contextHandler));
|
||||
JettyReactiveWebServerFactory.logger.info("Server initialized with port: " + port);
|
||||
if (getSsl() != null && getSsl().isEnabled()) {
|
||||
if (Ssl.isEnabled(getSsl())) {
|
||||
customizeSsl(server, address);
|
||||
}
|
||||
for (JettyServerCustomizer customizer : getServerCustomizers()) {
|
||||
@@ -236,7 +237,7 @@ public class JettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
|
||||
}
|
||||
|
||||
private void customizeSsl(Server server, InetSocketAddress address) {
|
||||
new SslServerCustomizer(address, getSsl(), getOrCreateSslStoreProvider(), getHttp2()).customize(server);
|
||||
new SslServerCustomizer(getHttp2(), address, getSsl().getClientAuth(), getSslBundle()).customize(server);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ import org.springframework.boot.web.server.Cookie.SameSite;
|
||||
import org.springframework.boot.web.server.ErrorPage;
|
||||
import org.springframework.boot.web.server.MimeMappings;
|
||||
import org.springframework.boot.web.server.Shutdown;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.boot.web.servlet.ServletContextInitializer;
|
||||
import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory;
|
||||
@@ -162,7 +163,7 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor
|
||||
configureWebAppContext(context, initializers);
|
||||
server.setHandler(addHandlerWrappers(context));
|
||||
this.logger.info("Server initialized with port: " + port);
|
||||
if (getSsl() != null && getSsl().isEnabled()) {
|
||||
if (Ssl.isEnabled(getSsl())) {
|
||||
customizeSsl(server, address);
|
||||
}
|
||||
for (JettyServerCustomizer customizer : getServerCustomizers()) {
|
||||
@@ -220,7 +221,7 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor
|
||||
}
|
||||
|
||||
private void customizeSsl(Server server, InetSocketAddress address) {
|
||||
new SslServerCustomizer(address, getSsl(), getOrCreateSslStoreProvider(), getHttp2()).customize(server);
|
||||
new SslServerCustomizer(getHttp2(), address, getSsl().getClientAuth(), getSslBundle()).customize(server);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,12 +32,15 @@ import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.server.SslConnectionFactory;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundleKey;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
import org.springframework.boot.ssl.SslStoreBundle;
|
||||
import org.springframework.boot.web.server.Http2;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.SslConfigurationValidator;
|
||||
import org.springframework.boot.web.server.SslStoreProvider;
|
||||
import org.springframework.boot.web.server.Ssl.ClientAuth;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
@@ -51,18 +54,18 @@ import org.springframework.util.ObjectUtils;
|
||||
*/
|
||||
class SslServerCustomizer implements JettyServerCustomizer {
|
||||
|
||||
private final InetSocketAddress address;
|
||||
|
||||
private final 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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -32,6 +32,7 @@ import reactor.netty.resources.LoopResources;
|
||||
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.Shutdown;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.http.client.reactive.ReactorResourceFactory;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
@@ -166,7 +167,7 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
|
||||
else {
|
||||
server = server.bindAddress(this::getListenAddress);
|
||||
}
|
||||
if (getSsl() != null && getSsl().isEnabled()) {
|
||||
if (Ssl.isEnabled(getSsl())) {
|
||||
server = customizeSslConfiguration(server);
|
||||
}
|
||||
if (getCompression() != null && getCompression().getEnabled()) {
|
||||
@@ -177,11 +178,8 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
|
||||
return applyCustomizers(server);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private HttpServer customizeSslConfiguration(HttpServer httpServer) {
|
||||
SslServerCustomizer sslServerCustomizer = new SslServerCustomizer(getSsl(), getHttp2(),
|
||||
getOrCreateSslStoreProvider());
|
||||
return sslServerCustomizer.apply(httpServer);
|
||||
return new SslServerCustomizer(getHttp2(), getSsl().getClientAuth(), getSslBundle()).apply(httpServer);
|
||||
}
|
||||
|
||||
private HttpProtocol[] listProtocols() {
|
||||
|
||||
@@ -16,35 +16,17 @@
|
||||
|
||||
package org.springframework.boot.web.embedded.netty;
|
||||
|
||||
import java.net.Socket;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Principal;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.net.ssl.KeyManager;
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.KeyManagerFactorySpi;
|
||||
import javax.net.ssl.ManagerFactoryParameters;
|
||||
import javax.net.ssl.SSLEngine;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509ExtendedKeyManager;
|
||||
|
||||
import io.netty.handler.ssl.ClientAuth;
|
||||
import reactor.netty.http.Http11SslContextSpec;
|
||||
import reactor.netty.http.Http2SslContextSpec;
|
||||
import reactor.netty.http.server.HttpServer;
|
||||
import reactor.netty.tcp.AbstractProtocolSslContextSpec;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
import org.springframework.boot.web.server.Http2;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.SslConfigurationValidator;
|
||||
import org.springframework.boot.web.server.SslStoreProvider;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* {@link NettyServerCustomizer} that configures SSL for the given Reactor Netty server
|
||||
@@ -56,21 +38,19 @@ import org.springframework.boot.web.server.SslStoreProvider;
|
||||
* @author Cyril Dangerville
|
||||
* @author Scott Frederick
|
||||
* @since 2.0.0
|
||||
* @deprecated this class is meant for Spring Boot internal use only.
|
||||
*/
|
||||
@Deprecated(since = "2.0.0", forRemoval = false)
|
||||
public class SslServerCustomizer implements NettyServerCustomizer {
|
||||
|
||||
private final Ssl ssl;
|
||||
|
||||
private final Http2 http2;
|
||||
|
||||
private final SslStoreProvider sslStoreProvider;
|
||||
private final Ssl.ClientAuth clientAuth;
|
||||
|
||||
public SslServerCustomizer(Ssl ssl, Http2 http2, SslStoreProvider sslStoreProvider) {
|
||||
this.ssl = ssl;
|
||||
private final SslBundle sslBundle;
|
||||
|
||||
public SslServerCustomizer(Http2 http2, Ssl.ClientAuth clientAuth, SslBundle sslBundle) {
|
||||
this.http2 = http2;
|
||||
this.sslStoreProvider = sslStoreProvider;
|
||||
this.clientAuth = clientAuth;
|
||||
this.sslBundle = sslBundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -80,172 +60,22 @@ public class SslServerCustomizer implements NettyServerCustomizer {
|
||||
}
|
||||
|
||||
protected AbstractProtocolSslContextSpec<?> createSslContextSpec() {
|
||||
AbstractProtocolSslContextSpec<?> sslContextSpec;
|
||||
if (this.http2 != null && this.http2.isEnabled()) {
|
||||
sslContextSpec = Http2SslContextSpec.forServer(getKeyManagerFactory(this.ssl, this.sslStoreProvider));
|
||||
}
|
||||
else {
|
||||
sslContextSpec = Http11SslContextSpec.forServer(getKeyManagerFactory(this.ssl, this.sslStoreProvider));
|
||||
}
|
||||
AbstractProtocolSslContextSpec<?> sslContextSpec = (this.http2 != null && this.http2.isEnabled())
|
||||
? Http2SslContextSpec.forServer(this.sslBundle.getManagers().getKeyManagerFactory())
|
||||
: Http11SslContextSpec.forServer(this.sslBundle.getManagers().getKeyManagerFactory());
|
||||
sslContextSpec.configure((builder) -> {
|
||||
builder.trustManager(getTrustManagerFactory(this.sslStoreProvider));
|
||||
if (this.ssl.getEnabledProtocols() != null) {
|
||||
builder.protocols(this.ssl.getEnabledProtocols());
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,10 +24,13 @@ import org.apache.tomcat.util.net.SSLHostConfig;
|
||||
import org.apache.tomcat.util.net.SSLHostConfigCertificate;
|
||||
import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type;
|
||||
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.SslStoreProvider;
|
||||
import org.springframework.boot.web.server.SslStoreProviderFactory;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundleKey;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
import org.springframework.boot.ssl.SslStoreBundle;
|
||||
import org.springframework.boot.web.server.Ssl.ClientAuth;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -40,18 +43,13 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
class SslConnectorCustomizer implements TomcatConnectorCustomizer {
|
||||
|
||||
private final Ssl ssl;
|
||||
private final ClientAuth clientAuth;
|
||||
|
||||
private final SslStoreProvider sslStoreProvider;
|
||||
private final SslBundle sslBundle;
|
||||
|
||||
SslConnectorCustomizer(Ssl ssl) {
|
||||
this(ssl, SslStoreProviderFactory.from(ssl));
|
||||
}
|
||||
|
||||
SslConnectorCustomizer(Ssl ssl, SslStoreProvider sslStoreProvider) {
|
||||
Assert.notNull(ssl, "Ssl configuration should not be null");
|
||||
this.ssl = ssl;
|
||||
this.sslStoreProvider = sslStoreProvider;
|
||||
SslConnectorCustomizer(ClientAuth clientAuth, SslBundle sslBundle) {
|
||||
this.clientAuth = clientAuth;
|
||||
this.sslBundle = sslBundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -59,7 +57,7 @@ class SslConnectorCustomizer implements TomcatConnectorCustomizer {
|
||||
ProtocolHandler handler = connector.getProtocolHandler();
|
||||
Assert.state(handler instanceof AbstractHttp11JsseProtocol,
|
||||
"To use SSL, the connector's protocol handler must be an AbstractHttp11JsseProtocol subclass");
|
||||
configureSsl((AbstractHttp11JsseProtocol<?>) handler, this.ssl, this.sslStoreProvider);
|
||||
configureSsl((AbstractHttp11JsseProtocol<?>) handler);
|
||||
connector.setScheme("https");
|
||||
connector.setSecure(true);
|
||||
}
|
||||
@@ -67,68 +65,59 @@ class SslConnectorCustomizer implements TomcatConnectorCustomizer {
|
||||
/**
|
||||
* Configure Tomcat's {@link AbstractHttp11JsseProtocol} for SSL.
|
||||
* @param protocol the protocol
|
||||
* @param ssl the ssl details
|
||||
* @param sslStoreProvider the ssl store provider
|
||||
*/
|
||||
protected void configureSsl(AbstractHttp11JsseProtocol<?> protocol, Ssl ssl, SslStoreProvider sslStoreProvider) {
|
||||
void configureSsl(AbstractHttp11JsseProtocol<?> protocol) {
|
||||
SslBundleKey key = this.sslBundle.getKey();
|
||||
SslStoreBundle stores = this.sslBundle.getStores();
|
||||
SslOptions options = this.sslBundle.getOptions();
|
||||
protocol.setSSLEnabled(true);
|
||||
SSLHostConfig sslHostConfig = new SSLHostConfig();
|
||||
sslHostConfig.setHostName(protocol.getDefaultSSLHostConfigName());
|
||||
sslHostConfig.setSslProtocol(ssl.getProtocol());
|
||||
sslHostConfig.setSslProtocol(this.sslBundle.getProtocol());
|
||||
protocol.addSslHostConfig(sslHostConfig);
|
||||
configureSslClientAuth(sslHostConfig, ssl);
|
||||
configureSslClientAuth(sslHostConfig);
|
||||
SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, Type.UNDEFINED);
|
||||
if (ssl.getKeyStorePassword() != null) {
|
||||
certificate.setCertificateKeystorePassword(ssl.getKeyStorePassword());
|
||||
String keystorePassword = (stores.getKeyStorePassword() != null) ? stores.getKeyStorePassword() : "";
|
||||
certificate.setCertificateKeystorePassword(keystorePassword);
|
||||
if (key.getPassword() != null) {
|
||||
certificate.setCertificateKeyPassword(key.getPassword());
|
||||
}
|
||||
if (ssl.getKeyPassword() != null) {
|
||||
certificate.setCertificateKeyPassword(ssl.getKeyPassword());
|
||||
}
|
||||
if (ssl.getKeyAlias() != null) {
|
||||
certificate.setCertificateKeyAlias(ssl.getKeyAlias());
|
||||
if (key.getAlias() != null) {
|
||||
certificate.setCertificateKeyAlias(key.getAlias());
|
||||
}
|
||||
sslHostConfig.addCertificate(certificate);
|
||||
String ciphers = StringUtils.arrayToCommaDelimitedString(ssl.getCiphers());
|
||||
if (StringUtils.hasText(ciphers)) {
|
||||
if (!CollectionUtils.isEmpty(options.getCiphers())) {
|
||||
String ciphers = StringUtils.collectionToCommaDelimitedString(options.getCiphers());
|
||||
sslHostConfig.setCiphers(ciphers);
|
||||
}
|
||||
configureEnabledProtocols(protocol, ssl);
|
||||
if (sslStoreProvider != null) {
|
||||
configureSslStoreProvider(protocol, sslHostConfig, certificate, sslStoreProvider);
|
||||
String keyPassword = sslStoreProvider.getKeyPassword();
|
||||
if (keyPassword != null) {
|
||||
certificate.setCertificateKeyPassword(keyPassword);
|
||||
}
|
||||
}
|
||||
configureEnabledProtocols(protocol);
|
||||
configureSslStoreProvider(protocol, sslHostConfig, certificate);
|
||||
}
|
||||
|
||||
private void configureEnabledProtocols(AbstractHttp11JsseProtocol<?> protocol, Ssl ssl) {
|
||||
if (ssl.getEnabledProtocols() != null) {
|
||||
private void configureEnabledProtocols(AbstractHttp11JsseProtocol<?> protocol) {
|
||||
SslOptions options = this.sslBundle.getOptions();
|
||||
if (!CollectionUtils.isEmpty(options.getEnabledProtocols())) {
|
||||
for (SSLHostConfig sslHostConfig : protocol.findSslHostConfigs()) {
|
||||
sslHostConfig.setProtocols(StringUtils.arrayToCommaDelimitedString(ssl.getEnabledProtocols()));
|
||||
sslHostConfig.setProtocols(StringUtils.collectionToCommaDelimitedString(options.getEnabledProtocols()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void configureSslClientAuth(SSLHostConfig config, Ssl ssl) {
|
||||
if (ssl.getClientAuth() == Ssl.ClientAuth.NEED) {
|
||||
config.setCertificateVerification("required");
|
||||
}
|
||||
else if (ssl.getClientAuth() == Ssl.ClientAuth.WANT) {
|
||||
config.setCertificateVerification("optional");
|
||||
}
|
||||
private void configureSslClientAuth(SSLHostConfig config) {
|
||||
config.setCertificateVerification(ClientAuth.map(this.clientAuth, "none", "optional", "required"));
|
||||
}
|
||||
|
||||
protected void configureSslStoreProvider(AbstractHttp11JsseProtocol<?> protocol, SSLHostConfig sslHostConfig,
|
||||
SSLHostConfigCertificate certificate, SslStoreProvider sslStoreProvider) {
|
||||
SSLHostConfigCertificate certificate) {
|
||||
Assert.isInstanceOf(Http11NioProtocol.class, protocol,
|
||||
"SslStoreProvider can only be used with Http11NioProtocol");
|
||||
try {
|
||||
if (sslStoreProvider.getKeyStore() != null) {
|
||||
certificate.setCertificateKeystore(sslStoreProvider.getKeyStore());
|
||||
SslStoreBundle stores = this.sslBundle.getStores();
|
||||
if (stores.getKeyStore() != null) {
|
||||
certificate.setCertificateKeystore(stores.getKeyStore());
|
||||
}
|
||||
if (sslStoreProvider.getTrustStore() != null) {
|
||||
sslHostConfig.setTrustStore(sslStoreProvider.getTrustStore());
|
||||
if (stores.getTrustStore() != null) {
|
||||
sslHostConfig.setTrustStore(stores.getTrustStore());
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.apache.tomcat.util.scan.StandardJarScanFilter;
|
||||
import org.springframework.boot.util.LambdaSafe;
|
||||
import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.http.server.reactive.TomcatHttpHandlerAdapter;
|
||||
@@ -199,7 +200,7 @@ public class TomcatReactiveWebServerFactory extends AbstractReactiveWebServerFac
|
||||
if (getHttp2() != null && getHttp2().isEnabled()) {
|
||||
connector.addUpgradeProtocol(new Http2Protocol());
|
||||
}
|
||||
if (getSsl() != null && getSsl().isEnabled()) {
|
||||
if (Ssl.isEnabled(getSsl())) {
|
||||
customizeSsl(connector);
|
||||
}
|
||||
TomcatConnectorCustomizer compression = new CompressionConnectorCustomizer(getCompression());
|
||||
@@ -223,7 +224,7 @@ public class TomcatReactiveWebServerFactory extends AbstractReactiveWebServerFac
|
||||
}
|
||||
|
||||
private void customizeSsl(Connector connector) {
|
||||
new SslConnectorCustomizer(getSsl(), getOrCreateSslStoreProvider()).customize(connector);
|
||||
new SslConnectorCustomizer(getSsl().getClientAuth(), getSslBundle()).customize(connector);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -71,6 +71,7 @@ import org.springframework.boot.util.LambdaSafe;
|
||||
import org.springframework.boot.web.server.Cookie.SameSite;
|
||||
import org.springframework.boot.web.server.ErrorPage;
|
||||
import org.springframework.boot.web.server.MimeMappings;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.boot.web.servlet.ServletContextInitializer;
|
||||
import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory;
|
||||
@@ -339,7 +340,7 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto
|
||||
if (getHttp2() != null && getHttp2().isEnabled()) {
|
||||
connector.addUpgradeProtocol(new Http2Protocol());
|
||||
}
|
||||
if (getSsl() != null && getSsl().isEnabled()) {
|
||||
if (Ssl.isEnabled(getSsl())) {
|
||||
customizeSsl(connector);
|
||||
}
|
||||
TomcatConnectorCustomizer compression = new CompressionConnectorCustomizer(getCompression());
|
||||
@@ -363,7 +364,7 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto
|
||||
}
|
||||
|
||||
private void customizeSsl(Connector connector) {
|
||||
new SslConnectorCustomizer(getSsl(), getOrCreateSslStoreProvider()).customize(connector);
|
||||
new SslConnectorCustomizer(getSsl().getClientAuth(), getSslBundle()).customize(connector);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,30 +17,18 @@
|
||||
package org.springframework.boot.web.embedded.undertow;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Principal;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import javax.net.ssl.KeyManager;
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLEngine;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509ExtendedKeyManager;
|
||||
|
||||
import io.undertow.Undertow;
|
||||
import org.xnio.Options;
|
||||
import org.xnio.Sequence;
|
||||
import org.xnio.SslClientAuthMode;
|
||||
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.SslConfigurationValidator;
|
||||
import org.springframework.boot.web.server.SslStoreProvider;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
import org.springframework.boot.web.server.Ssl.ClientAuth;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* {@link UndertowBuilderCustomizer} that configures SSL on the given builder instance.
|
||||
@@ -56,34 +44,29 @@ class SslBuilderCustomizer implements UndertowBuilderCustomizer {
|
||||
|
||||
private final InetAddress address;
|
||||
|
||||
private final Ssl ssl;
|
||||
private final ClientAuth clientAuth;
|
||||
|
||||
private final SslStoreProvider sslStoreProvider;
|
||||
private final SslBundle sslBundle;
|
||||
|
||||
SslBuilderCustomizer(int port, InetAddress address, Ssl ssl, SslStoreProvider sslStoreProvider) {
|
||||
SslBuilderCustomizer(int port, InetAddress address, ClientAuth clientAuth, SslBundle sslBundle) {
|
||||
this.port = port;
|
||||
this.address = address;
|
||||
this.ssl = ssl;
|
||||
this.sslStoreProvider = sslStoreProvider;
|
||||
this.clientAuth = clientAuth;
|
||||
this.sslBundle = sslBundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(Undertow.Builder builder) {
|
||||
try {
|
||||
SSLContext sslContext = SSLContext.getInstance(this.ssl.getProtocol());
|
||||
sslContext.init(getKeyManagers(this.ssl, this.sslStoreProvider), getTrustManagers(this.sslStoreProvider),
|
||||
null);
|
||||
builder.addHttpsListener(this.port, getListenAddress(), sslContext);
|
||||
builder.setSocketOption(Options.SSL_CLIENT_AUTH_MODE, getSslClientAuthMode(this.ssl));
|
||||
if (this.ssl.getEnabledProtocols() != null) {
|
||||
builder.setSocketOption(Options.SSL_ENABLED_PROTOCOLS, Sequence.of(this.ssl.getEnabledProtocols()));
|
||||
}
|
||||
if (this.ssl.getCiphers() != null) {
|
||||
builder.setSocketOption(Options.SSL_ENABLED_CIPHER_SUITES, Sequence.of(this.ssl.getCiphers()));
|
||||
}
|
||||
SslOptions options = this.sslBundle.getOptions();
|
||||
SSLContext sslContext = this.sslBundle.createSslContext();
|
||||
builder.addHttpsListener(this.port, getListenAddress(), sslContext);
|
||||
builder.setSocketOption(Options.SSL_CLIENT_AUTH_MODE, ClientAuth.map(this.clientAuth,
|
||||
SslClientAuthMode.NOT_REQUESTED, SslClientAuthMode.REQUESTED, SslClientAuthMode.REQUIRED));
|
||||
if (!CollectionUtils.isEmpty(options.getEnabledProtocols())) {
|
||||
builder.setSocketOption(Options.SSL_ENABLED_PROTOCOLS, Sequence.of(options.getEnabledProtocols()));
|
||||
}
|
||||
catch (NoSuchAlgorithmException | KeyManagementException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
if (!CollectionUtils.isEmpty(options.getCiphers())) {
|
||||
builder.setSocketOption(Options.SSL_ENABLED_CIPHER_SUITES, Sequence.of(options.getCiphers()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,117 +77,4 @@ class SslBuilderCustomizer implements UndertowBuilderCustomizer {
|
||||
return this.address.getHostAddress();
|
||||
}
|
||||
|
||||
private SslClientAuthMode getSslClientAuthMode(Ssl ssl) {
|
||||
if (ssl.getClientAuth() == Ssl.ClientAuth.NEED) {
|
||||
return SslClientAuthMode.REQUIRED;
|
||||
}
|
||||
if (ssl.getClientAuth() == Ssl.ClientAuth.WANT) {
|
||||
return SslClientAuthMode.REQUESTED;
|
||||
}
|
||||
return SslClientAuthMode.NOT_REQUESTED;
|
||||
}
|
||||
|
||||
KeyManager[] getKeyManagers(Ssl ssl, SslStoreProvider sslStoreProvider) {
|
||||
try {
|
||||
KeyStore keyStore = sslStoreProvider.getKeyStore();
|
||||
SslConfigurationValidator.validateKeyAlias(keyStore, ssl.getKeyAlias());
|
||||
KeyManagerFactory keyManagerFactory = KeyManagerFactory
|
||||
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||
String keyPassword = sslStoreProvider.getKeyPassword();
|
||||
if (keyPassword == null) {
|
||||
keyPassword = (ssl.getKeyPassword() != null) ? ssl.getKeyPassword() : ssl.getKeyStorePassword();
|
||||
}
|
||||
keyManagerFactory.init(keyStore, (keyPassword != null) ? keyPassword.toCharArray() : null);
|
||||
if (ssl.getKeyAlias() != null) {
|
||||
return getConfigurableAliasKeyManagers(ssl, keyManagerFactory.getKeyManagers());
|
||||
}
|
||||
return keyManagerFactory.getKeyManagers();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Could not load key managers: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private KeyManager[] getConfigurableAliasKeyManagers(Ssl ssl, KeyManager[] keyManagers) {
|
||||
for (int i = 0; i < keyManagers.length; i++) {
|
||||
if (keyManagers[i] instanceof X509ExtendedKeyManager) {
|
||||
keyManagers[i] = new ConfigurableAliasKeyManager((X509ExtendedKeyManager) keyManagers[i],
|
||||
ssl.getKeyAlias());
|
||||
}
|
||||
}
|
||||
return keyManagers;
|
||||
}
|
||||
|
||||
TrustManager[] getTrustManagers(SslStoreProvider sslStoreProvider) {
|
||||
try {
|
||||
KeyStore store = sslStoreProvider.getTrustStore();
|
||||
TrustManagerFactory trustManagerFactory = TrustManagerFactory
|
||||
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
trustManagerFactory.init(store);
|
||||
return trustManagerFactory.getTrustManagers();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Could not load trust managers: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link X509ExtendedKeyManager} that supports custom alias configuration.
|
||||
*/
|
||||
private static class ConfigurableAliasKeyManager extends X509ExtendedKeyManager {
|
||||
|
||||
private final X509ExtendedKeyManager keyManager;
|
||||
|
||||
private final String alias;
|
||||
|
||||
ConfigurableAliasKeyManager(X509ExtendedKeyManager keyManager, String alias) {
|
||||
this.keyManager = keyManager;
|
||||
this.alias = alias;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chooseEngineClientAlias(String[] strings, Principal[] principals, SSLEngine sslEngine) {
|
||||
return this.keyManager.chooseEngineClientAlias(strings, principals, sslEngine);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chooseEngineServerAlias(String s, Principal[] principals, SSLEngine sslEngine) {
|
||||
if (this.alias == null) {
|
||||
return this.keyManager.chooseEngineServerAlias(s, principals, sslEngine);
|
||||
}
|
||||
return this.alias;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
|
||||
return this.keyManager.chooseClientAlias(keyType, issuers, socket);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
|
||||
return this.keyManager.chooseServerAlias(keyType, issuers, socket);
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getCertificateChain(String alias) {
|
||||
return this.keyManager.getCertificateChain(alias);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getClientAliases(String keyType, Principal[] issuers) {
|
||||
return this.keyManager.getClientAliases(keyType, issuers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrivateKey getPrivateKey(String alias) {
|
||||
return this.keyManager.getPrivateKey(alias);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getServerAliases(String keyType, Principal[] issuers) {
|
||||
return this.keyManager.getServerAliases(keyType, issuers);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -137,7 +137,7 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF
|
||||
|
||||
@Override
|
||||
public WebServer getWebServer(org.springframework.http.server.reactive.HttpHandler httpHandler) {
|
||||
Undertow.Builder builder = this.delegate.createBuilder(this);
|
||||
Undertow.Builder builder = this.delegate.createBuilder(this, this::getSslBundle);
|
||||
List<HttpHandlerFactory> httpHandlerFactories = this.delegate.createHttpHandlerFactories(this,
|
||||
(next) -> new UndertowHttpHandlerAdapter(httpHandler));
|
||||
return new UndertowWebServer(builder, httpHandlerFactories, getPort() >= 0);
|
||||
|
||||
@@ -294,7 +294,7 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac
|
||||
|
||||
@Override
|
||||
public WebServer getWebServer(ServletContextInitializer... initializers) {
|
||||
Builder builder = this.delegate.createBuilder(this);
|
||||
Builder builder = this.delegate.createBuilder(this, this::getSslBundle);
|
||||
DeploymentManager manager = createManager(initializers);
|
||||
return getUndertowWebServer(builder, manager, getPort());
|
||||
}
|
||||
|
||||
@@ -24,12 +24,14 @@ import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import io.undertow.Handlers;
|
||||
import io.undertow.Undertow;
|
||||
import io.undertow.Undertow.Builder;
|
||||
import io.undertow.UndertowOptions;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.web.server.AbstractConfigurableWebServerFactory;
|
||||
import org.springframework.boot.web.server.Compression;
|
||||
import org.springframework.boot.web.server.Http2;
|
||||
@@ -141,8 +143,7 @@ class UndertowWebServerFactoryDelegate {
|
||||
return this.useForwardHeaders;
|
||||
}
|
||||
|
||||
Builder createBuilder(AbstractConfigurableWebServerFactory factory) {
|
||||
Ssl ssl = factory.getSsl();
|
||||
Builder createBuilder(AbstractConfigurableWebServerFactory factory, Supplier<SslBundle> sslBundleSupplier) {
|
||||
InetAddress address = factory.getAddress();
|
||||
int port = factory.getPort();
|
||||
Builder builder = Undertow.builder();
|
||||
@@ -162,8 +163,9 @@ class UndertowWebServerFactoryDelegate {
|
||||
if (http2 != null) {
|
||||
builder.setServerOption(UndertowOptions.ENABLE_HTTP2, http2.isEnabled());
|
||||
}
|
||||
if (ssl != null && ssl.isEnabled()) {
|
||||
new SslBuilderCustomizer(factory.getPort(), address, ssl, factory.getOrCreateSslStoreProvider())
|
||||
Ssl ssl = factory.getSsl();
|
||||
if (Ssl.isEnabled(ssl)) {
|
||||
new SslBuilderCustomizer(factory.getPort(), address, ssl.getClientAuth(), sslBundleSupplier.get())
|
||||
.customize(builder);
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -24,6 +24,8 @@ import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -49,8 +51,11 @@ public abstract class AbstractConfigurableWebServerFactory implements Configurab
|
||||
|
||||
private Ssl ssl;
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
private SslStoreProvider sslStoreProvider;
|
||||
|
||||
private SslBundles sslBundles;
|
||||
|
||||
private Http2 http2;
|
||||
|
||||
private Compression compression;
|
||||
@@ -130,15 +135,22 @@ public abstract class AbstractConfigurableWebServerFactory implements Configurab
|
||||
this.ssl = ssl;
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
public SslStoreProvider getSslStoreProvider() {
|
||||
return this.sslStoreProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("removal")
|
||||
public void setSslStoreProvider(SslStoreProvider sslStoreProvider) {
|
||||
this.sslStoreProvider = sslStoreProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSslBundles(SslBundles sslBundles) {
|
||||
this.sslBundles = sslBundles;
|
||||
}
|
||||
|
||||
public Http2 getHttp2() {
|
||||
return this.http2;
|
||||
}
|
||||
@@ -184,12 +196,24 @@ public abstract class AbstractConfigurableWebServerFactory implements Configurab
|
||||
* Return the provided {@link SslStoreProvider} or create one using {@link Ssl}
|
||||
* properties.
|
||||
* @return the {@code SslStoreProvider}
|
||||
* @deprecated since 3.1.0 for removal in 3.3.0 in favor of {@link #getSslBundle()}
|
||||
*/
|
||||
@Deprecated(since = "3.1.0", forRemoval = true)
|
||||
@SuppressWarnings("removal")
|
||||
public final SslStoreProvider getOrCreateSslStoreProvider() {
|
||||
if (this.sslStoreProvider != null) {
|
||||
return this.sslStoreProvider;
|
||||
}
|
||||
return SslStoreProviderFactory.from(this.ssl);
|
||||
return CertificateFileSslStoreProvider.from(this.ssl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link SslBundle} that should be used with this server.
|
||||
* @return the SSL bundle
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
protected final SslBundle getSslBundle() {
|
||||
return WebServerSslBundle.get(this.ssl, this.sslBundles, this.sslStoreProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,12 +16,10 @@
|
||||
|
||||
package org.springframework.boot.web.server;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.pem.PemSslStoreBundle;
|
||||
|
||||
/**
|
||||
* An {@link SslStoreProvider} that creates key and trust stores from certificate and
|
||||
@@ -29,82 +27,32 @@ import java.security.cert.X509Certificate;
|
||||
*
|
||||
* @author Scott Frederick
|
||||
* @since 2.7.0
|
||||
* @deprecated since 3.1.0 for removal in 3.3.0 in favor of registering a
|
||||
* {@link SslBundle} backed by a {@link PemSslStoreBundle}.
|
||||
*/
|
||||
@Deprecated(since = "3.1.0", forRemoval = true)
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
public final class CertificateFileSslStoreProvider implements SslStoreProvider {
|
||||
|
||||
/**
|
||||
* The password of the private key entry in the {@link #getKeyStore provided
|
||||
* KeyStore}.
|
||||
*/
|
||||
private static final String KEY_PASSWORD = "";
|
||||
private 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,11 +19,14 @@ package org.springframework.boot.web.server;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
|
||||
/**
|
||||
* A configurable {@link WebServerFactory}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Brian Clozel
|
||||
* @author Scott Frederick
|
||||
* @since 2.0.0
|
||||
* @see ErrorPageRegistry
|
||||
*/
|
||||
@@ -58,9 +61,20 @@ public interface ConfigurableWebServerFactory extends WebServerFactory, ErrorPag
|
||||
/**
|
||||
* Sets a provider that will be used to obtain SSL stores.
|
||||
* @param sslStoreProvider the SSL store provider
|
||||
* @deprecated since 3.1.0 for removal in 3.3.0, in favor of
|
||||
* {@link #setSslBundles(SslBundles)}
|
||||
*/
|
||||
@Deprecated(since = "3.1.0", forRemoval = true)
|
||||
@SuppressWarnings("removal")
|
||||
void setSslStoreProvider(SslStoreProvider sslStoreProvider);
|
||||
|
||||
/**
|
||||
* Sets the SSL bundles that can be used to configure SSL connections.
|
||||
* @param sslBundles the SSL bundles
|
||||
* @since 3.1.0
|
||||
*/
|
||||
void setSslBundles(SslBundles sslBundles);
|
||||
|
||||
/**
|
||||
* Sets the HTTP/2 configuration that will be applied to the server.
|
||||
* @param http2 the HTTP/2 configuration
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.security.KeyStore;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* An {@link SslStoreProvider} that creates key and trust stores from Java keystore files.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
final class JavaKeyStoreSslStoreProvider implements SslStoreProvider {
|
||||
|
||||
private final Ssl ssl;
|
||||
|
||||
private JavaKeyStoreSslStoreProvider(Ssl ssl) {
|
||||
this.ssl = ssl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyStore getKeyStore() throws Exception {
|
||||
return createKeyStore(this.ssl.getKeyStoreType(), this.ssl.getKeyStoreProvider(), this.ssl.getKeyStore(),
|
||||
this.ssl.getKeyStorePassword());
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyStore getTrustStore() throws Exception {
|
||||
if (this.ssl.getTrustStore() == null) {
|
||||
return null;
|
||||
}
|
||||
return createKeyStore(this.ssl.getTrustStoreType(), this.ssl.getTrustStoreProvider(), this.ssl.getTrustStore(),
|
||||
this.ssl.getTrustStorePassword());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeyPassword() {
|
||||
return this.ssl.getKeyPassword();
|
||||
}
|
||||
|
||||
private KeyStore createKeyStore(String type, String provider, String location, String password) throws Exception {
|
||||
type = (type != null) ? type : "JKS";
|
||||
char[] passwordChars = (password != null) ? password.toCharArray() : null;
|
||||
KeyStore store = (provider != null) ? KeyStore.getInstance(type, provider) : KeyStore.getInstance(type);
|
||||
if (type.equalsIgnoreCase("PKCS11")) {
|
||||
Assert.state(!StringUtils.hasText(location),
|
||||
() -> "KeyStore location is '" + location + "', but must be empty or null for PKCS11 key stores");
|
||||
store.load(null, passwordChars);
|
||||
}
|
||||
else {
|
||||
Assert.state(StringUtils.hasText(location), () -> "KeyStore location must not be empty or null");
|
||||
try {
|
||||
URL url = ResourceUtils.getURL(location);
|
||||
try (InputStream stream = url.openStream()) {
|
||||
store.load(stream, passwordChars);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Could not load key store '" + location + "'", ex);
|
||||
}
|
||||
}
|
||||
return store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an {@link SslStoreProvider} if the appropriate SSL properties are
|
||||
* configured.
|
||||
* @param ssl the SSL properties
|
||||
* @return an {@code SslStoreProvider} or {@code null}
|
||||
*/
|
||||
static SslStoreProvider from(Ssl ssl) {
|
||||
if (ssl != null && ssl.isEnabled()) {
|
||||
return new JavaKeyStoreSslStoreProvider(ssl);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -29,6 +29,8 @@ public class Ssl {
|
||||
|
||||
private boolean enabled = true;
|
||||
|
||||
private String bundle;
|
||||
|
||||
private ClientAuth clientAuth;
|
||||
|
||||
private String[] ciphers;
|
||||
@@ -77,6 +79,24 @@ public class Ssl {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the name of the SSL bundle to use.
|
||||
* @return the SSL bundle name
|
||||
* @since 3.1.0
|
||||
*/
|
||||
public String getBundle() {
|
||||
return this.bundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the name of the SSL bundle to use.
|
||||
* @param bundle the SSL bundle name
|
||||
* @since 3.1.0
|
||||
*/
|
||||
public void setBundle(String bundle) {
|
||||
this.bundle = bundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return Whether client authentication is not wanted ("none"), wanted ("want") or
|
||||
* needed ("need"). Requires a trust store.
|
||||
@@ -295,6 +315,28 @@ public class Ssl {
|
||||
this.protocol = protocol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if SSL is enabled for the given instance.
|
||||
* @param ssl the {@link Ssl SSL} instance or {@code null}
|
||||
* @return {@code true} is SSL is enabled
|
||||
* @since 3.1.0
|
||||
*/
|
||||
public static boolean isEnabled(Ssl ssl) {
|
||||
return (ssl != null) && ssl.isEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create an {@link Ssl} instance for a specific bundle name.
|
||||
* @param bundle the name of the bundle
|
||||
* @return a new {@link Ssl} instance with the bundle set
|
||||
* @since 3.1.0
|
||||
*/
|
||||
public static Ssl forBundle(String bundle) {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setBundle(bundle);
|
||||
return ssl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client authentication types.
|
||||
*/
|
||||
@@ -313,7 +355,25 @@ public class Ssl {
|
||||
/**
|
||||
* Client authentication is needed and mandatory.
|
||||
*/
|
||||
NEED
|
||||
NEED;
|
||||
|
||||
/**
|
||||
* Map an optional {@link ClientAuth} value to a different type.
|
||||
* @param <R> the result type
|
||||
* @param clientAuth the client auth to map (may be {@code null})
|
||||
* @param none the value for {@link ClientAuth#NONE} or {@code null}
|
||||
* @param want the value for {@link ClientAuth#WANT}
|
||||
* @param need the value for {@link ClientAuth#NEED}
|
||||
* @return the mapped value
|
||||
* @since 3.1.0
|
||||
*/
|
||||
public static <R> R map(ClientAuth clientAuth, R none, R want, R need) {
|
||||
return switch ((clientAuth != null) ? clientAuth : NONE) {
|
||||
case NONE -> none;
|
||||
case WANT -> want;
|
||||
case NEED -> need;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -17,33 +17,25 @@
|
||||
package org.springframework.boot.web.server;
|
||||
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.boot.ssl.SslBundleKey;
|
||||
|
||||
/**
|
||||
* Provides utilities around SSL.
|
||||
*
|
||||
* @author Chris Bono
|
||||
* @since 2.1.13
|
||||
* @deprecated since 3.1.0 for removal in 3.3.0 in favor of
|
||||
* {@link SslBundleKey#assertContainsAlias(KeyStore)}
|
||||
*/
|
||||
@Deprecated(since = "3.1.0", forRemoval = true)
|
||||
public final class SslConfigurationValidator {
|
||||
|
||||
private SslConfigurationValidator() {
|
||||
}
|
||||
|
||||
public static void validateKeyAlias(KeyStore keyStore, String keyAlias) {
|
||||
if (StringUtils.hasLength(keyAlias)) {
|
||||
try {
|
||||
Assert.state(keyStore.containsAlias(keyAlias),
|
||||
() -> String.format("Keystore does not contain specified alias '%s'", keyAlias));
|
||||
}
|
||||
catch (KeyStoreException ex) {
|
||||
throw new IllegalStateException(
|
||||
String.format("Could not determine if keystore contains alias '%s'", keyAlias), ex);
|
||||
}
|
||||
}
|
||||
SslBundleKey.of(null, keyAlias).assertContainsAlias(keyStore);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,13 +18,18 @@ package org.springframework.boot.web.server;
|
||||
|
||||
import java.security.KeyStore;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
|
||||
/**
|
||||
* Interface to provide SSL key stores for an {@link WebServer} to use. Can be used when
|
||||
* file based key stores cannot be used.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.0.0
|
||||
* @deprecated since 3.1.0 for removal in 3.3.0 in favor of registering an
|
||||
* {@link SslBundle}.
|
||||
*/
|
||||
@Deprecated(since = "3.1.0", forRemoval = true)
|
||||
public interface SslStoreProvider {
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server;
|
||||
|
||||
import java.security.KeyStore;
|
||||
|
||||
import org.springframework.boot.ssl.NoSuchSslBundleException;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundleKey;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.ssl.SslManagerBundle;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
import org.springframework.boot.ssl.SslStoreBundle;
|
||||
import org.springframework.boot.ssl.jks.JksSslStoreBundle;
|
||||
import org.springframework.boot.ssl.jks.JksSslStoreDetails;
|
||||
import org.springframework.boot.ssl.pem.PemSslStoreBundle;
|
||||
import org.springframework.boot.ssl.pem.PemSslStoreDetails;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.function.ThrowingSupplier;
|
||||
|
||||
/**
|
||||
* {@link SslBundle} backed by {@link Ssl} or an {@link SslStoreProvider}.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
* @author Phillip Webb
|
||||
* @since 3.1.0
|
||||
*/
|
||||
public final class WebServerSslBundle implements SslBundle {
|
||||
|
||||
private final SslStoreBundle stores;
|
||||
|
||||
private final SslBundleKey key;
|
||||
|
||||
private final SslOptions options;
|
||||
|
||||
private final String protocol;
|
||||
|
||||
private final SslManagerBundle managers;
|
||||
|
||||
private WebServerSslBundle(SslStoreBundle stores, String keyPassword, Ssl ssl) {
|
||||
this.stores = stores;
|
||||
this.key = SslBundleKey.of(keyPassword, ssl.getKeyAlias());
|
||||
this.protocol = ssl.getProtocol();
|
||||
this.options = SslOptions.of(ssl.getCiphers(), ssl.getEnabledProtocols());
|
||||
this.managers = SslManagerBundle.from(this.stores, this.key);
|
||||
}
|
||||
|
||||
private static SslStoreBundle createPemStoreBundle(Ssl ssl) {
|
||||
PemSslStoreDetails keyStoreDetails = new PemSslStoreDetails(ssl.getKeyStoreType(), ssl.getCertificate(),
|
||||
ssl.getCertificatePrivateKey());
|
||||
PemSslStoreDetails trustStoreDetails = new PemSslStoreDetails(ssl.getTrustStoreType(),
|
||||
ssl.getTrustCertificate(), ssl.getTrustCertificatePrivateKey());
|
||||
return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, ssl.getKeyAlias());
|
||||
}
|
||||
|
||||
private static SslStoreBundle createJksStoreBundle(Ssl ssl) {
|
||||
JksSslStoreDetails keyStoreDetails = new JksSslStoreDetails(ssl.getKeyStoreType(), ssl.getKeyStoreProvider(),
|
||||
ssl.getKeyStore(), ssl.getKeyStorePassword());
|
||||
JksSslStoreDetails trustStoreDetails = new JksSslStoreDetails(ssl.getTrustStoreType(),
|
||||
ssl.getTrustStoreProvider(), ssl.getTrustStore(), ssl.getTrustStorePassword());
|
||||
return new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SslStoreBundle getStores() {
|
||||
return this.stores;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SslBundleKey getKey() {
|
||||
return this.key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SslOptions getOptions() {
|
||||
return this.options;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProtocol() {
|
||||
return this.protocol;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SslManagerBundle getManagers() {
|
||||
return this.managers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link SslBundle} that should be used for the given {@link Ssl} instance.
|
||||
* @param ssl the source ssl instance
|
||||
* @return a {@link SslBundle} instance
|
||||
* @throws NoSuchSslBundleException if a bundle lookup fails
|
||||
*/
|
||||
public static SslBundle get(Ssl ssl) throws NoSuchSslBundleException {
|
||||
return get(ssl, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link SslBundle} that should be used for the given {@link Ssl} instance.
|
||||
* @param ssl the source ssl instance
|
||||
* @param sslBundles the bundles that should be used when {@link Ssl#getBundle()} is
|
||||
* set
|
||||
* @return a {@link SslBundle} instance
|
||||
* @throws NoSuchSslBundleException if a bundle lookup fails
|
||||
*/
|
||||
public static SslBundle get(Ssl ssl, SslBundles sslBundles) throws NoSuchSslBundleException {
|
||||
return get(ssl, sslBundles, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link SslBundle} that should be used for the given {@link Ssl} and
|
||||
* {@link SslStoreProvider} instances.
|
||||
* @param ssl the source {@link Ssl} instance
|
||||
* @param sslBundles the bundles that should be used when {@link Ssl#getBundle()} is
|
||||
* set
|
||||
* @param sslStoreProvider the {@link SslStoreProvider} to use or {@code null}
|
||||
* @return a {@link SslBundle} instance
|
||||
* @throws NoSuchSslBundleException if a bundle lookup fails
|
||||
* @deprecated since 3.1.0 for removal in 3.3.0 along with {@link SslStoreProvider}
|
||||
*/
|
||||
@Deprecated(since = "3.1.0", forRemoval = true)
|
||||
@SuppressWarnings("removal")
|
||||
public static SslBundle get(Ssl ssl, SslBundles sslBundles, SslStoreProvider sslStoreProvider) {
|
||||
Assert.state(Ssl.isEnabled(ssl), "SSL is not enabled");
|
||||
String keyPassword = (sslStoreProvider != null) ? sslStoreProvider.getKeyPassword() : null;
|
||||
keyPassword = (keyPassword != null) ? keyPassword : ssl.getKeyPassword();
|
||||
if (sslStoreProvider != null) {
|
||||
SslStoreBundle stores = new SslStoreProviderBundleAdapter(sslStoreProvider);
|
||||
return new WebServerSslBundle(stores, keyPassword, ssl);
|
||||
}
|
||||
String bundleName = ssl.getBundle();
|
||||
if (StringUtils.hasText(bundleName)) {
|
||||
Assert.state(sslBundles != null,
|
||||
() -> "SSL bundle '%s' was requested but no SslBundles instance was provided"
|
||||
.formatted(bundleName));
|
||||
return sslBundles.getBundle(bundleName);
|
||||
}
|
||||
SslStoreBundle stores = createStoreBundle(ssl);
|
||||
return new WebServerSslBundle(stores, keyPassword, ssl);
|
||||
}
|
||||
|
||||
private static SslStoreBundle createStoreBundle(Ssl ssl) {
|
||||
if (hasCertificateProperties(ssl)) {
|
||||
return createPemStoreBundle(ssl);
|
||||
}
|
||||
if (hasJavaKeyStoreProperties(ssl)) {
|
||||
return createJksStoreBundle(ssl);
|
||||
}
|
||||
throw new IllegalStateException("SSL is enabled but no trust material is configured");
|
||||
}
|
||||
|
||||
static SslBundle createCertificateFileSslStoreProviderDelegate(Ssl ssl) {
|
||||
if (!hasCertificateProperties(ssl)) {
|
||||
return null;
|
||||
}
|
||||
SslStoreBundle stores = createPemStoreBundle(ssl);
|
||||
return new WebServerSslBundle(stores, ssl.getKeyPassword(), ssl);
|
||||
}
|
||||
|
||||
private static boolean hasCertificateProperties(Ssl ssl) {
|
||||
return Ssl.isEnabled(ssl) && ssl.getCertificate() != null && ssl.getCertificatePrivateKey() != null;
|
||||
}
|
||||
|
||||
private static boolean hasJavaKeyStoreProperties(Ssl ssl) {
|
||||
return Ssl.isEnabled(ssl) && ssl.getKeyStore() != null
|
||||
|| (ssl.getKeyStoreType() != null && ssl.getKeyStoreType().equals("PKCS11"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to adapt a {@link SslStoreProvider} into a {@link SslStoreBundle}.
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
private static class SslStoreProviderBundleAdapter implements SslStoreBundle {
|
||||
|
||||
private final SslStoreProvider sslStoreProvider;
|
||||
|
||||
SslStoreProviderBundleAdapter(SslStoreProvider sslStoreProvider) {
|
||||
this.sslStoreProvider = sslStoreProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyStore getKeyStore() {
|
||||
return ThrowingSupplier.of(this.sslStoreProvider::getKeyStore).get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeyStorePassword() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyStore getTrustStore() {
|
||||
return ThrowingSupplier.of(this.sslStoreProvider::getTrustStore).get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -44,6 +44,13 @@ import reactor.test.StepVerifier;
|
||||
import org.springframework.boot.rsocket.server.RSocketServer;
|
||||
import org.springframework.boot.rsocket.server.RSocketServer.Transport;
|
||||
import org.springframework.boot.rsocket.server.RSocketServerCustomizer;
|
||||
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundleKey;
|
||||
import org.springframework.boot.ssl.jks.JksSslStoreBundle;
|
||||
import org.springframework.boot.ssl.jks.JksSslStoreDetails;
|
||||
import org.springframework.boot.ssl.pem.PemSslStoreBundle;
|
||||
import org.springframework.boot.ssl.pem.PemSslStoreDetails;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.core.codec.CharSequenceEncoder;
|
||||
import org.springframework.core.codec.StringDecoder;
|
||||
@@ -191,6 +198,50 @@ class NettyRSocketServerFactoryTests {
|
||||
"src/test/resources/test-cert.pem", Transport.WEBSOCKET);
|
||||
}
|
||||
|
||||
@Test
|
||||
void tcpTransportBasicSslFromClassPathWithBundle() {
|
||||
testBasicSslWithKeyStoreFromBundle("classpath:test.jks", "password", Transport.TCP);
|
||||
}
|
||||
|
||||
@Test
|
||||
void tcpTransportBasicSslFromFileSystemWithBundle() {
|
||||
testBasicSslWithKeyStoreFromBundle("src/test/resources/test.jks", "password", Transport.TCP);
|
||||
}
|
||||
|
||||
@Test
|
||||
void websocketTransportBasicSslFromClassPathWithBundle() {
|
||||
testBasicSslWithKeyStoreFromBundle("classpath:test.jks", "password", Transport.WEBSOCKET);
|
||||
}
|
||||
|
||||
@Test
|
||||
void websocketTransportBasicSslFromFileSystemWithBundle() {
|
||||
testBasicSslWithKeyStoreFromBundle("src/test/resources/test.jks", "password", Transport.WEBSOCKET);
|
||||
}
|
||||
|
||||
@Test
|
||||
void tcpTransportBasicSslCertificateFromClassPathWithBundle() {
|
||||
testBasicSslWithPemCertificateFromBundle("classpath:test-cert.pem", "classpath:test-key.pem",
|
||||
"classpath:test-cert.pem", Transport.TCP);
|
||||
}
|
||||
|
||||
@Test
|
||||
void tcpTransportBasicSslCertificateFromFileSystemWithBundle() {
|
||||
testBasicSslWithPemCertificateFromBundle("src/test/resources/test-cert.pem", "src/test/resources/test-key.pem",
|
||||
"src/test/resources/test-cert.pem", Transport.TCP);
|
||||
}
|
||||
|
||||
@Test
|
||||
void websocketTransportBasicSslCertificateFromClassPathWithBundle() {
|
||||
testBasicSslWithPemCertificateFromBundle("classpath:test-cert.pem", "classpath:test-key.pem",
|
||||
"classpath:test-cert.pem", Transport.WEBSOCKET);
|
||||
}
|
||||
|
||||
@Test
|
||||
void websocketTransportBasicSslCertificateFromFileSystemWithBundle() {
|
||||
testBasicSslWithPemCertificateFromBundle("src/test/resources/test-cert.pem", "src/test/resources/test-key.pem",
|
||||
"src/test/resources/test-cert.pem", Transport.WEBSOCKET);
|
||||
}
|
||||
|
||||
private void checkEchoRequest() {
|
||||
String payload = "test payload";
|
||||
Mono<String> response = this.requester.route("test").data(payload).retrieveMono(String.class);
|
||||
@@ -228,6 +279,39 @@ class NettyRSocketServerFactoryTests {
|
||||
checkEchoRequest();
|
||||
}
|
||||
|
||||
private void testBasicSslWithKeyStoreFromBundle(String keyStore, String keyPassword, Transport transport) {
|
||||
NettyRSocketServerFactory factory = getFactory();
|
||||
factory.setTransport(transport);
|
||||
JksSslStoreDetails keyStoreDetails = JksSslStoreDetails.forLocation(keyStore);
|
||||
JksSslStoreDetails trustStoreDetails = null;
|
||||
SslBundle sslBundle = SslBundle.of(new JksSslStoreBundle(keyStoreDetails, trustStoreDetails),
|
||||
SslBundleKey.of(keyPassword));
|
||||
factory.setSsl(Ssl.forBundle("test"));
|
||||
factory.setSslBundles(new DefaultSslBundleRegistry("test", sslBundle));
|
||||
this.server = factory.create(new EchoRequestResponseAcceptor());
|
||||
this.server.start();
|
||||
this.requester = (transport == Transport.TCP) ? createSecureRSocketTcpClient()
|
||||
: createSecureRSocketWebSocketClient();
|
||||
checkEchoRequest();
|
||||
}
|
||||
|
||||
private void testBasicSslWithPemCertificateFromBundle(String certificate, String certificatePrivateKey,
|
||||
String trustCertificate, Transport transport) {
|
||||
NettyRSocketServerFactory factory = getFactory();
|
||||
factory.setTransport(transport);
|
||||
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate(certificate)
|
||||
.withPrivateKey(certificatePrivateKey);
|
||||
PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate(trustCertificate);
|
||||
SslBundle sslBundle = SslBundle.of(new PemSslStoreBundle(keyStoreDetails, trustStoreDetails));
|
||||
factory.setSsl(Ssl.forBundle("test"));
|
||||
factory.setSslBundles(new DefaultSslBundleRegistry("test", sslBundle));
|
||||
this.server = factory.create(new EchoRequestResponseAcceptor());
|
||||
this.server.start();
|
||||
this.requester = (transport == Transport.TCP) ? createSecureRSocketTcpClient()
|
||||
: createSecureRSocketWebSocketClient();
|
||||
checkEchoRequest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void tcpTransportSslRejectsInsecureClient() {
|
||||
NettyRSocketServerFactory factory = getFactory();
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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'");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -35,7 +35,7 @@ import org.springframework.boot.web.embedded.test.MockPkcs11Security;
|
||||
import org.springframework.boot.web.embedded.test.MockPkcs11SecurityProvider;
|
||||
import org.springframework.boot.web.server.Http2;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.SslStoreProviderFactory;
|
||||
import org.springframework.boot.web.server.WebServerSslBundle;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
@@ -91,10 +91,10 @@ class SslServerCustomizerTests {
|
||||
@Test
|
||||
void configureSslWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsException() {
|
||||
Ssl ssl = new Ssl();
|
||||
SslServerCustomizer customizer = new SslServerCustomizer(null, ssl, null, null);
|
||||
assertThatIllegalStateException().isThrownBy(
|
||||
() -> customizer.configureSsl(new SslContextFactory.Server(), ssl, SslStoreProviderFactory.from(ssl)))
|
||||
.withMessageContaining("KeyStore location must not be empty or null");
|
||||
assertThatIllegalStateException().isThrownBy(() -> {
|
||||
SslServerCustomizer customizer = new SslServerCustomizer(null, null, null, WebServerSslBundle.get(ssl));
|
||||
customizer.configureSsl(new SslContextFactory.Server(), ssl.getClientAuth());
|
||||
}).withMessageContaining("SSL is enabled but no trust material is configured");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -104,10 +104,10 @@ class SslServerCustomizerTests {
|
||||
ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME);
|
||||
ssl.setKeyStore("src/test/resources/test.jks");
|
||||
ssl.setKeyPassword("password");
|
||||
SslServerCustomizer customizer = new SslServerCustomizer(null, ssl, null, null);
|
||||
assertThatIllegalStateException().isThrownBy(
|
||||
() -> customizer.configureSsl(new SslContextFactory.Server(), ssl, SslStoreProviderFactory.from(ssl)))
|
||||
.withMessageContaining("must be empty or null for PKCS11 key stores");
|
||||
assertThatIllegalStateException().isThrownBy(() -> {
|
||||
SslServerCustomizer customizer = new SslServerCustomizer(null, null, null, WebServerSslBundle.get(ssl));
|
||||
customizer.configureSsl(new SslContextFactory.Server(), ssl.getClientAuth());
|
||||
}).withMessageContaining("must be empty or null for PKCS11 hardware key stores");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -116,8 +116,10 @@ class SslServerCustomizerTests {
|
||||
ssl.setKeyStoreType("PKCS11");
|
||||
ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME);
|
||||
ssl.setKeyStorePassword("1234");
|
||||
SslServerCustomizer customizer = new SslServerCustomizer(null, ssl, null, null);
|
||||
assertThatNoException().isThrownBy(() -> customizer.configureSsl(new SslContextFactory.Server(), ssl, null));
|
||||
assertThatNoException().isThrownBy(() -> {
|
||||
SslServerCustomizer customizer = new SslServerCustomizer(null, null, null, WebServerSslBundle.get(ssl));
|
||||
customizer.configureSsl(new SslContextFactory.Server(), ssl.getClientAuth());
|
||||
});
|
||||
}
|
||||
|
||||
private Server createCustomizedServer() {
|
||||
@@ -132,7 +134,8 @@ class SslServerCustomizerTests {
|
||||
|
||||
private Server createCustomizedServer(Ssl ssl, Http2 http2) {
|
||||
Server server = new Server();
|
||||
new SslServerCustomizer(new InetSocketAddress(0), ssl, null, http2).customize(server);
|
||||
new SslServerCustomizer(http2, new InetSocketAddress(0), ssl.getClientAuth(), WebServerSslBundle.get(ssl))
|
||||
.customize(server);
|
||||
return server;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.embedded.netty;
|
||||
|
||||
import java.security.NoSuchProviderException;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.web.embedded.test.MockPkcs11Security;
|
||||
import org.springframework.boot.web.embedded.test.MockPkcs11SecurityProvider;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.SslStoreProviderFactory;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
/**
|
||||
* Tests for {@link SslServerCustomizer}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Raheela Aslam
|
||||
* @author Cyril Dangerville
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@MockPkcs11Security
|
||||
class SslServerCustomizerTests {
|
||||
|
||||
@Test
|
||||
void keyStoreProviderIsUsedWhenCreatingKeyStore() {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyPassword("password");
|
||||
ssl.setKeyStore("src/test/resources/test.jks");
|
||||
ssl.setKeyStoreProvider("com.example.KeyStoreProvider");
|
||||
SslServerCustomizer customizer = new SslServerCustomizer(ssl, null, null);
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> customizer.getKeyManagerFactory(ssl, SslStoreProviderFactory.from(ssl)))
|
||||
.withCauseInstanceOf(NoSuchProviderException.class)
|
||||
.withMessageContaining("com.example.KeyStoreProvider");
|
||||
}
|
||||
|
||||
@Test
|
||||
void trustStoreProviderIsUsedWhenCreatingTrustStore() {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setTrustStorePassword("password");
|
||||
ssl.setTrustStore("src/test/resources/test.jks");
|
||||
ssl.setTrustStoreProvider("com.example.TrustStoreProvider");
|
||||
SslServerCustomizer customizer = new SslServerCustomizer(ssl, null, null);
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> customizer.getTrustManagerFactory(SslStoreProviderFactory.from(ssl)))
|
||||
.withCauseInstanceOf(NoSuchProviderException.class)
|
||||
.withMessageContaining("com.example.TrustStoreProvider");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getKeyManagerFactoryWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsException() {
|
||||
Ssl ssl = new Ssl();
|
||||
SslServerCustomizer customizer = new SslServerCustomizer(ssl, null, null);
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> customizer.getKeyManagerFactory(ssl, SslStoreProviderFactory.from(ssl)))
|
||||
.withCauseInstanceOf(IllegalStateException.class)
|
||||
.withMessageContaining("KeyStore location must not be empty or null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getKeyManagerFactoryWhenSslIsEnabledWithPkcs11AndKeyStoreThrowsException() {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyStoreType("PKCS11");
|
||||
ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME);
|
||||
ssl.setKeyStore("src/test/resources/test.jks");
|
||||
ssl.setKeyPassword("password");
|
||||
SslServerCustomizer customizer = new SslServerCustomizer(ssl, null, null);
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> customizer.getKeyManagerFactory(ssl, SslStoreProviderFactory.from(ssl)))
|
||||
.withCauseInstanceOf(IllegalStateException.class)
|
||||
.withMessageContaining("must be empty or null for PKCS11 key stores");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getKeyManagerFactoryWhenSslIsEnabledWithPkcs11AndKeyStoreProvider() {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyStoreType("PKCS11");
|
||||
ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME);
|
||||
ssl.setKeyStorePassword("1234");
|
||||
SslServerCustomizer customizer = new SslServerCustomizer(ssl, null, null);
|
||||
assertThatNoException()
|
||||
.isThrownBy(() -> customizer.getKeyManagerFactory(ssl, SslStoreProviderFactory.from(ssl)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -41,6 +41,7 @@ import org.springframework.boot.web.embedded.test.MockPkcs11Security;
|
||||
import org.springframework.boot.web.embedded.test.MockPkcs11SecurityProvider;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.SslStoreProvider;
|
||||
import org.springframework.boot.web.server.WebServerSslBundle;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
@@ -58,6 +59,7 @@ import static org.mockito.Mockito.mock;
|
||||
* @author Scott Frederick
|
||||
* @author Cyril Dangerville
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
@DirtiesUrlFactories
|
||||
@MockPkcs11Security
|
||||
@@ -84,10 +86,11 @@ class SslConnectorCustomizerTests {
|
||||
@Test
|
||||
void sslCiphersConfiguration() throws Exception {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyStore("test.jks");
|
||||
ssl.setKeyStore("classpath:test.jks");
|
||||
ssl.setKeyStorePassword("secret");
|
||||
ssl.setCiphers(new String[] { "ALPHA", "BRAVO", "CHARLIE" });
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl, null);
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(),
|
||||
WebServerSslBundle.get(ssl));
|
||||
Connector connector = this.tomcat.getConnector();
|
||||
customizer.customize(connector);
|
||||
this.tomcat.start();
|
||||
@@ -102,7 +105,8 @@ class SslConnectorCustomizerTests {
|
||||
ssl.setKeyStore("src/test/resources/test.jks");
|
||||
ssl.setEnabledProtocols(new String[] { "TLSv1.1", "TLSv1.2" });
|
||||
ssl.setCiphers(new String[] { "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "BRAVO" });
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl, null);
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(),
|
||||
WebServerSslBundle.get(ssl));
|
||||
Connector connector = this.tomcat.getConnector();
|
||||
customizer.customize(connector);
|
||||
this.tomcat.start();
|
||||
@@ -118,7 +122,8 @@ class SslConnectorCustomizerTests {
|
||||
ssl.setKeyStore("src/test/resources/test.jks");
|
||||
ssl.setEnabledProtocols(new String[] { "TLSv1.2" });
|
||||
ssl.setCiphers(new String[] { "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "BRAVO" });
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl, null);
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(),
|
||||
WebServerSslBundle.get(ssl));
|
||||
Connector connector = this.tomcat.getConnector();
|
||||
customizer.customize(connector);
|
||||
this.tomcat.start();
|
||||
@@ -128,6 +133,7 @@ class SslConnectorCustomizerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Deprecated(since = "3.1.0", forRemoval = true)
|
||||
void customizeWhenSslStoreProviderProvidesOnlyKeyStoreShouldUseDefaultTruststore() throws Exception {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyPassword("password");
|
||||
@@ -135,7 +141,8 @@ class SslConnectorCustomizerTests {
|
||||
SslStoreProvider sslStoreProvider = mock(SslStoreProvider.class);
|
||||
KeyStore keyStore = loadStore();
|
||||
given(sslStoreProvider.getKeyStore()).willReturn(keyStore);
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl, sslStoreProvider);
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(),
|
||||
WebServerSslBundle.get(ssl, null, sslStoreProvider));
|
||||
Connector connector = this.tomcat.getConnector();
|
||||
customizer.customize(connector);
|
||||
this.tomcat.start();
|
||||
@@ -148,6 +155,7 @@ class SslConnectorCustomizerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Deprecated(since = "3.1.0", forRemoval = true)
|
||||
void customizeWhenSslStoreProviderProvidesOnlyTrustStoreShouldUseDefaultKeystore() throws Exception {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyPassword("password");
|
||||
@@ -155,7 +163,8 @@ class SslConnectorCustomizerTests {
|
||||
SslStoreProvider sslStoreProvider = mock(SslStoreProvider.class);
|
||||
KeyStore trustStore = loadStore();
|
||||
given(sslStoreProvider.getTrustStore()).willReturn(trustStore);
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl, sslStoreProvider);
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(),
|
||||
WebServerSslBundle.get(ssl, null, sslStoreProvider));
|
||||
Connector connector = this.tomcat.getConnector();
|
||||
customizer.customize(connector);
|
||||
this.tomcat.start();
|
||||
@@ -164,6 +173,7 @@ class SslConnectorCustomizerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Deprecated(since = "3.1.0", forRemoval = true)
|
||||
void customizeWhenSslStoreProviderPresentShouldIgnorePasswordFromSsl(CapturedOutput output) throws Exception {
|
||||
System.setProperty("javax.net.ssl.trustStorePassword", "trustStoreSecret");
|
||||
Ssl ssl = new Ssl();
|
||||
@@ -172,7 +182,8 @@ class SslConnectorCustomizerTests {
|
||||
SslStoreProvider sslStoreProvider = mock(SslStoreProvider.class);
|
||||
given(sslStoreProvider.getTrustStore()).willReturn(loadStore());
|
||||
given(sslStoreProvider.getKeyStore()).willReturn(loadStore());
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl, sslStoreProvider);
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(),
|
||||
WebServerSslBundle.get(ssl, null, sslStoreProvider));
|
||||
Connector connector = this.tomcat.getConnector();
|
||||
customizer.customize(connector);
|
||||
this.tomcat.start();
|
||||
@@ -182,9 +193,11 @@ class SslConnectorCustomizerTests {
|
||||
|
||||
@Test
|
||||
void customizeWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsException() {
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> new SslConnectorCustomizer(new Ssl()).customize(this.tomcat.getConnector()))
|
||||
.withMessageContaining("KeyStore location must not be empty or null");
|
||||
assertThatIllegalStateException().isThrownBy(() -> {
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(Ssl.ClientAuth.NONE,
|
||||
WebServerSslBundle.get(new Ssl()));
|
||||
customizer.customize(this.tomcat.getConnector());
|
||||
}).withMessageContaining("SSL is enabled but no trust material is configured");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -194,9 +207,10 @@ class SslConnectorCustomizerTests {
|
||||
ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME);
|
||||
ssl.setKeyStore("src/test/resources/test.jks");
|
||||
ssl.setKeyPassword("password");
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl);
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(),
|
||||
WebServerSslBundle.get(ssl));
|
||||
assertThatIllegalStateException().isThrownBy(() -> customizer.customize(this.tomcat.getConnector()))
|
||||
.withMessageContaining("must be empty or null for PKCS11 key stores");
|
||||
.withMessageContaining("must be empty or null for PKCS11 hardware key stores");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -205,7 +219,8 @@ class SslConnectorCustomizerTests {
|
||||
ssl.setKeyStoreType("PKCS11");
|
||||
ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME);
|
||||
ssl.setKeyStorePassword("1234");
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl, null);
|
||||
SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(),
|
||||
WebServerSslBundle.get(ssl));
|
||||
assertThatNoException().isThrownBy(() -> customizer.customize(this.tomcat.getConnector()));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.embedded.undertow;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.security.NoSuchProviderException;
|
||||
|
||||
import javax.net.ssl.KeyManager;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.web.embedded.test.MockPkcs11Security;
|
||||
import org.springframework.boot.web.embedded.test.MockPkcs11SecurityProvider;
|
||||
import org.springframework.boot.web.server.Ssl;
|
||||
import org.springframework.boot.web.server.SslStoreProviderFactory;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
/**
|
||||
* Tests for {@link SslBuilderCustomizer}
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Raheela Aslam
|
||||
* @author Cyril Dangerville
|
||||
*/
|
||||
@MockPkcs11Security
|
||||
class SslBuilderCustomizerTests {
|
||||
|
||||
@Test
|
||||
void getKeyManagersWhenAliasIsNullShouldNotDecorate() throws Exception {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyPassword("password");
|
||||
ssl.setKeyStore("src/test/resources/test.jks");
|
||||
SslBuilderCustomizer customizer = new SslBuilderCustomizer(8080, InetAddress.getLocalHost(), ssl, null);
|
||||
KeyManager[] keyManagers = customizer.getKeyManagers(ssl, SslStoreProviderFactory.from(ssl));
|
||||
Class<?> name = Class
|
||||
.forName("org.springframework.boot.web.embedded.undertow.SslBuilderCustomizer$ConfigurableAliasKeyManager");
|
||||
assertThat(keyManagers[0]).isNotInstanceOf(name);
|
||||
}
|
||||
|
||||
@Test
|
||||
void keyStoreProviderIsUsedWhenCreatingKeyStore() throws Exception {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyPassword("password");
|
||||
ssl.setKeyStore("src/test/resources/test.jks");
|
||||
ssl.setKeyStoreProvider("com.example.KeyStoreProvider");
|
||||
SslBuilderCustomizer customizer = new SslBuilderCustomizer(8080, InetAddress.getLocalHost(), ssl, null);
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> customizer.getKeyManagers(ssl, SslStoreProviderFactory.from(ssl)))
|
||||
.withCauseInstanceOf(NoSuchProviderException.class)
|
||||
.withMessageContaining("com.example.KeyStoreProvider");
|
||||
}
|
||||
|
||||
@Test
|
||||
void trustStoreProviderIsUsedWhenCreatingTrustStore() throws Exception {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setTrustStorePassword("password");
|
||||
ssl.setTrustStore("src/test/resources/test.jks");
|
||||
ssl.setTrustStoreProvider("com.example.TrustStoreProvider");
|
||||
SslBuilderCustomizer customizer = new SslBuilderCustomizer(8080, InetAddress.getLocalHost(), ssl, null);
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> customizer.getTrustManagers(SslStoreProviderFactory.from(ssl)))
|
||||
.withMessageContaining("com.example.TrustStoreProvider");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getKeyManagersWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsException() throws Exception {
|
||||
Ssl ssl = new Ssl();
|
||||
SslBuilderCustomizer customizer = new SslBuilderCustomizer(8080, InetAddress.getLocalHost(), ssl, null);
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> customizer.getKeyManagers(ssl, SslStoreProviderFactory.from(ssl)))
|
||||
.withCauseInstanceOf(IllegalStateException.class)
|
||||
.withMessageContaining("KeyStore location must not be empty or null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void configureSslWhenSslIsEnabledWithPkcs11AndKeyStoreThrowsException() throws Exception {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyStoreType("PKCS11");
|
||||
ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME);
|
||||
ssl.setKeyStore("src/test/resources/test.jks");
|
||||
ssl.setKeyPassword("password");
|
||||
SslBuilderCustomizer customizer = new SslBuilderCustomizer(8080, InetAddress.getLocalHost(), ssl, null);
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> customizer.getKeyManagers(ssl, SslStoreProviderFactory.from(ssl)))
|
||||
.withCauseInstanceOf(IllegalStateException.class)
|
||||
.withMessageContaining("must be empty or null for PKCS11 key stores");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeWhenSslIsEnabledWithPkcs11AndKeyStoreProvider() throws Exception {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyStoreType("PKCS11");
|
||||
ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME);
|
||||
ssl.setKeyStorePassword("1234");
|
||||
SslBuilderCustomizer customizer = new SslBuilderCustomizer(8080, InetAddress.getLocalHost(), ssl, null);
|
||||
assertThatNoException().isThrownBy(() -> customizer.getKeyManagers(ssl, SslStoreProviderFactory.from(ssl)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -212,7 +212,7 @@ public abstract class AbstractReactiveWebServerFactoryTests {
|
||||
}
|
||||
|
||||
protected void assertThatSslWithInvalidAliasCallFails(ThrowingCallable call) {
|
||||
assertThatThrownBy(call).hasStackTraceContaining("Keystore does not contain specified alias 'test-alias-404'");
|
||||
assertThatThrownBy(call).hasStackTraceContaining("Keystore does not contain alias 'test-alias-404'");
|
||||
}
|
||||
|
||||
protected ReactorClientHttpConnector buildTrustAllSslConnector() {
|
||||
@@ -402,7 +402,7 @@ public abstract class AbstractReactiveWebServerFactoryTests {
|
||||
@Test
|
||||
void whenSslIsEnabledAndNoKeyStoreIsConfiguredThenServerFailsToStart() {
|
||||
assertThatIllegalStateException().isThrownBy(() -> testBasicSslWithKeyStore(null, null))
|
||||
.withMessageContaining("KeyStore location must not be empty or null");
|
||||
.withMessageContaining("SSL is enabled but no trust material is configured");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server;
|
||||
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link CertificateFileSslStoreProvider}.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class CertificateFileSslStoreProviderTests {
|
||||
|
||||
@Test
|
||||
void fromSslWhenNullReturnsNull() {
|
||||
assertThat(CertificateFileSslStoreProvider.from(null)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromSslWhenDisabledReturnsNull() {
|
||||
assertThat(CertificateFileSslStoreProvider.from(new Ssl())).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromSslWithCertAndKeyReturnsStoreProvider() throws Exception {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setEnabled(true);
|
||||
ssl.setCertificate("classpath:test-cert.pem");
|
||||
ssl.setCertificatePrivateKey("classpath:test-key.pem");
|
||||
SslStoreProvider storeProvider = CertificateFileSslStoreProvider.from(ssl);
|
||||
assertThat(storeProvider).isNotNull();
|
||||
assertStoreContainsCertAndKey(storeProvider.getKeyStore(), KeyStore.getDefaultType(), "spring-boot-web");
|
||||
assertThat(storeProvider.getTrustStore()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromSslWithCertAndKeyAndTrustCertReturnsStoreProvider() throws Exception {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setEnabled(true);
|
||||
ssl.setCertificate("classpath:test-cert.pem");
|
||||
ssl.setCertificatePrivateKey("classpath:test-key.pem");
|
||||
ssl.setTrustCertificate("classpath:test-cert.pem");
|
||||
SslStoreProvider storeProvider = CertificateFileSslStoreProvider.from(ssl);
|
||||
assertThat(storeProvider).isNotNull();
|
||||
assertStoreContainsCertAndKey(storeProvider.getKeyStore(), KeyStore.getDefaultType(), "spring-boot-web");
|
||||
assertStoreContainsCert(storeProvider.getTrustStore(), KeyStore.getDefaultType(), "spring-boot-web-0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromSslWithCertAndKeyAndTrustCertAndTrustKeyReturnsStoreProvider() throws Exception {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setEnabled(true);
|
||||
ssl.setCertificate("classpath:test-cert.pem");
|
||||
ssl.setCertificatePrivateKey("classpath:test-key.pem");
|
||||
ssl.setTrustCertificate("classpath:test-cert.pem");
|
||||
ssl.setTrustCertificatePrivateKey("classpath:test-key.pem");
|
||||
SslStoreProvider storeProvider = CertificateFileSslStoreProvider.from(ssl);
|
||||
assertThat(storeProvider).isNotNull();
|
||||
assertStoreContainsCertAndKey(storeProvider.getKeyStore(), KeyStore.getDefaultType(), "spring-boot-web");
|
||||
assertStoreContainsCertAndKey(storeProvider.getTrustStore(), KeyStore.getDefaultType(), "spring-boot-web");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromSslWithKeyAliasReturnsStoreProvider() throws Exception {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setEnabled(true);
|
||||
ssl.setKeyAlias("test-alias");
|
||||
ssl.setCertificate("classpath:test-cert.pem");
|
||||
ssl.setCertificatePrivateKey("classpath:test-key.pem");
|
||||
ssl.setTrustCertificate("classpath:test-cert.pem");
|
||||
ssl.setTrustCertificatePrivateKey("classpath:test-key.pem");
|
||||
SslStoreProvider storeProvider = CertificateFileSslStoreProvider.from(ssl);
|
||||
assertThat(storeProvider).isNotNull();
|
||||
assertStoreContainsCertAndKey(storeProvider.getKeyStore(), KeyStore.getDefaultType(), "test-alias");
|
||||
assertStoreContainsCertAndKey(storeProvider.getTrustStore(), KeyStore.getDefaultType(), "test-alias");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromSslWithStoreTypeReturnsStoreProvider() throws Exception {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setEnabled(true);
|
||||
ssl.setKeyStoreType("PKCS12");
|
||||
ssl.setTrustStoreType("PKCS12");
|
||||
ssl.setCertificate("classpath:test-cert.pem");
|
||||
ssl.setCertificatePrivateKey("classpath:test-key.pem");
|
||||
ssl.setTrustCertificate("classpath:test-cert.pem");
|
||||
ssl.setTrustCertificatePrivateKey("classpath:test-key.pem");
|
||||
SslStoreProvider storeProvider = CertificateFileSslStoreProvider.from(ssl);
|
||||
assertThat(storeProvider).isNotNull();
|
||||
assertStoreContainsCertAndKey(storeProvider.getKeyStore(), "PKCS12", "spring-boot-web");
|
||||
assertStoreContainsCertAndKey(storeProvider.getTrustStore(), "PKCS12", "spring-boot-web");
|
||||
}
|
||||
|
||||
private void assertStoreContainsCertAndKey(KeyStore keyStore, String keyStoreType, String keyAlias)
|
||||
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
|
||||
assertThat(keyStore).isNotNull();
|
||||
assertThat(keyStore.getType()).isEqualTo(keyStoreType);
|
||||
assertThat(keyStore.containsAlias(keyAlias)).isTrue();
|
||||
assertThat(keyStore.getCertificate(keyAlias)).isNotNull();
|
||||
assertThat(keyStore.getKey(keyAlias, new char[] {})).isNotNull();
|
||||
}
|
||||
|
||||
private void assertStoreContainsCert(KeyStore keyStore, String keyStoreType, String keyAlias)
|
||||
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
|
||||
assertThat(keyStore).isNotNull();
|
||||
assertThat(keyStore.getType()).isEqualTo(keyStoreType);
|
||||
assertThat(keyStore.containsAlias(keyAlias)).isTrue();
|
||||
assertThat(keyStore.getCertificate(keyAlias)).isNotNull();
|
||||
assertThat(keyStore.getKey(keyAlias, new char[] {})).isNull();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,7 +31,8 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
*
|
||||
* @author Chris Bono
|
||||
*/
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
@Deprecated(since = "3.1.0", forRemoval = true)
|
||||
class SslConfigurationValidatorTests {
|
||||
|
||||
private static final String VALID_ALIAS = "test-alias";
|
||||
@@ -67,7 +68,7 @@ class SslConfigurationValidatorTests {
|
||||
void validateKeyAliasWhenAliasNotFoundShouldThrowException() {
|
||||
assertThatThrownBy(() -> SslConfigurationValidator.validateKeyAlias(this.keyStore, INVALID_ALIAS))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("Keystore does not contain specified alias '" + INVALID_ALIAS + "'");
|
||||
.hasMessage("Keystore does not contain alias '" + INVALID_ALIAS + "'");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyStore;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundleKey;
|
||||
import org.springframework.boot.ssl.SslOptions;
|
||||
import org.springframework.boot.ssl.SslStoreBundle;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link WebServerSslBundle}.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class WebServerSslBundleTests {
|
||||
|
||||
@Test
|
||||
void whenSslDisabledThrowsException() {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setEnabled(false);
|
||||
assertThatIllegalStateException().isThrownBy(() -> WebServerSslBundle.get(ssl))
|
||||
.withMessage("SSL is not enabled");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFromJksProperties() {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyStore("classpath:test.p12");
|
||||
ssl.setKeyStorePassword("secret");
|
||||
ssl.setKeyStoreType("PKCS12");
|
||||
ssl.setTrustStore("classpath:test.p12");
|
||||
ssl.setTrustStorePassword("secret");
|
||||
ssl.setTrustStoreType("PKCS12");
|
||||
ssl.setKeyPassword("password");
|
||||
ssl.setKeyAlias("alias");
|
||||
ssl.setClientAuth(Ssl.ClientAuth.NONE);
|
||||
ssl.setCiphers(new String[] { "ONE", "TWO", "THREE" });
|
||||
ssl.setEnabledProtocols(new String[] { "TLSv1.1", "TLSv1.2" });
|
||||
ssl.setProtocol("TestProtocol");
|
||||
SslBundle bundle = WebServerSslBundle.get(ssl);
|
||||
assertThat(bundle).isNotNull();
|
||||
assertThat(bundle.getProtocol()).isEqualTo("TestProtocol");
|
||||
SslBundleKey key = bundle.getKey();
|
||||
assertThat(key.getPassword()).isEqualTo("password");
|
||||
assertThat(key.getAlias()).isEqualTo("alias");
|
||||
SslStoreBundle stores = bundle.getStores();
|
||||
assertThat(stores.getKeyStorePassword()).isEqualTo("secret");
|
||||
assertThat(stores.getKeyStore()).isNotNull();
|
||||
assertThat(stores.getTrustStore()).isNotNull();
|
||||
SslOptions options = bundle.getOptions();
|
||||
assertThat(options.getCiphers()).containsExactly("ONE", "TWO", "THREE");
|
||||
assertThat(options.getEnabledProtocols()).containsExactly("TLSv1.1", "TLSv1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFromJksPropertiesWithPkcs11StoreType() {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyStorePassword("secret");
|
||||
ssl.setKeyStoreType("PKCS11");
|
||||
ssl.setKeyPassword("password");
|
||||
ssl.setClientAuth(Ssl.ClientAuth.NONE);
|
||||
SslBundle bundle = WebServerSslBundle.get(ssl);
|
||||
assertThat(bundle).isNotNull();
|
||||
assertThat(bundle.getStores().getKeyStorePassword()).isEqualTo("secret");
|
||||
assertThat(bundle.getKey().getPassword()).isEqualTo("password");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenFromPemProperties() {
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setCertificate("classpath:test-cert.pem");
|
||||
ssl.setCertificatePrivateKey("classpath:test-key.pem");
|
||||
ssl.setTrustCertificate("classpath:test-cert-chain.pem");
|
||||
ssl.setKeyStoreType("PKCS12");
|
||||
ssl.setTrustStoreType("PKCS12");
|
||||
ssl.setKeyPassword("password");
|
||||
ssl.setClientAuth(Ssl.ClientAuth.NONE);
|
||||
ssl.setCiphers(new String[] { "ONE", "TWO", "THREE" });
|
||||
ssl.setEnabledProtocols(new String[] { "TLSv1.1", "TLSv1.2" });
|
||||
ssl.setProtocol("TLSv1.1");
|
||||
SslBundle bundle = WebServerSslBundle.get(ssl);
|
||||
assertThat(bundle).isNotNull();
|
||||
SslBundleKey key = bundle.getKey();
|
||||
assertThat(key.getAlias()).isNull();
|
||||
assertThat(key.getPassword()).isEqualTo("password");
|
||||
SslStoreBundle stores = bundle.getStores();
|
||||
assertThat(stores.getKeyStorePassword()).isNull();
|
||||
assertThat(stores.getKeyStore()).isNotNull();
|
||||
assertThat(stores.getTrustStore()).isNotNull();
|
||||
SslOptions options = bundle.getOptions();
|
||||
assertThat(options.getCiphers()).containsExactly("ONE", "TWO", "THREE");
|
||||
assertThat(options.getEnabledProtocols()).containsExactly("TLSv1.1", "TLSv1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Deprecated(since = "3.1.0", forRemoval = true)
|
||||
@SuppressWarnings("removal")
|
||||
void whenFromCustomSslStoreProvider() throws Exception {
|
||||
SslStoreProvider sslStoreProvider = mock(SslStoreProvider.class);
|
||||
KeyStore keyStore = loadStore();
|
||||
given(sslStoreProvider.getKeyStore()).willReturn(keyStore);
|
||||
given(sslStoreProvider.getTrustStore()).willReturn(keyStore);
|
||||
Ssl ssl = new Ssl();
|
||||
ssl.setKeyStoreType("PKCS12");
|
||||
ssl.setTrustStoreType("PKCS12");
|
||||
ssl.setKeyPassword("password");
|
||||
ssl.setClientAuth(Ssl.ClientAuth.NONE);
|
||||
ssl.setCiphers(new String[] { "ONE", "TWO", "THREE" });
|
||||
ssl.setEnabledProtocols(new String[] { "TLSv1.1", "TLSv1.2" });
|
||||
ssl.setProtocol("TLSv1.1");
|
||||
SslBundle bundle = WebServerSslBundle.get(ssl, null, sslStoreProvider);
|
||||
assertThat(bundle).isNotNull();
|
||||
SslBundleKey key = bundle.getKey();
|
||||
assertThat(key.getPassword()).isEqualTo("password");
|
||||
assertThat(key.getAlias()).isNull();
|
||||
SslStoreBundle stores = bundle.getStores();
|
||||
assertThat(stores.getKeyStore()).isNotNull();
|
||||
assertThat(stores.getTrustStore()).isNotNull();
|
||||
SslOptions options = bundle.getOptions();
|
||||
assertThat(options.getCiphers()).containsExactly("ONE", "TWO", "THREE");
|
||||
assertThat(options.getEnabledProtocols()).containsExactly("TLSv1.1", "TLSv1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMissingPropertiesThrowsException() {
|
||||
Ssl ssl = new Ssl();
|
||||
assertThatIllegalStateException().isThrownBy(() -> WebServerSslBundle.get(ssl))
|
||||
.withMessageContaining("SSL is enabled but no trust material is configured");
|
||||
}
|
||||
|
||||
private KeyStore loadStore() throws Exception {
|
||||
Resource resource = new ClassPathResource("test.p12");
|
||||
try (InputStream stream = resource.getInputStream()) {
|
||||
KeyStore keyStore = KeyStore.getInstance("PKCS12");
|
||||
keyStore.load(stream, "secret".toCharArray());
|
||||
return keyStore;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -111,6 +111,13 @@ import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
import org.mockito.InOrder;
|
||||
|
||||
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslStoreBundle;
|
||||
import org.springframework.boot.ssl.jks.JksSslStoreBundle;
|
||||
import org.springframework.boot.ssl.jks.JksSslStoreDetails;
|
||||
import org.springframework.boot.ssl.pem.PemSslStoreBundle;
|
||||
import org.springframework.boot.ssl.pem.PemSslStoreDetails;
|
||||
import org.springframework.boot.system.ApplicationHome;
|
||||
import org.springframework.boot.system.ApplicationTemp;
|
||||
import org.springframework.boot.testsupport.system.CapturedOutput;
|
||||
@@ -170,6 +177,7 @@ import static org.mockito.Mockito.mock;
|
||||
* @author Raja Kolli
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
@DirtiesUrlFactories
|
||||
public abstract class AbstractServletWebServerFactoryTests {
|
||||
@@ -473,7 +481,7 @@ public abstract class AbstractServletWebServerFactoryTests {
|
||||
}
|
||||
|
||||
protected void assertThatSslWithInvalidAliasCallFails(ThrowingCallable call) {
|
||||
assertThatThrownBy(call).hasStackTraceContaining("Keystore does not contain specified alias 'test-alias-404'");
|
||||
assertThatThrownBy(call).hasStackTraceContaining("Keystore does not contain alias 'test-alias-404'");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -557,6 +565,44 @@ public abstract class AbstractServletWebServerFactoryTests {
|
||||
assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory)).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void pkcs12KeyStoreAndTrustStoreFromBundle() throws Exception {
|
||||
AbstractServletWebServerFactory factory = getFactory();
|
||||
addTestTxtFile(factory);
|
||||
factory.setSsl(Ssl.forBundle("test"));
|
||||
factory.setSslBundles(
|
||||
new DefaultSslBundleRegistry("test", createJksSslBundle("classpath:test.p12", "classpath:test.p12")));
|
||||
this.webServer = factory.getWebServer();
|
||||
this.webServer.start();
|
||||
KeyStore keyStore = KeyStore.getInstance("pkcs12");
|
||||
loadStore(keyStore, new FileSystemResource("src/test/resources/test.p12"));
|
||||
SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
|
||||
new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy())
|
||||
.loadKeyMaterial(keyStore, "secret".toCharArray())
|
||||
.build());
|
||||
HttpComponentsClientHttpRequestFactory requestFactory = createHttpComponentsRequestFactory(socketFactory);
|
||||
assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory)).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void pemKeyStoreAndTrustStoreFromBundle() throws Exception {
|
||||
AbstractServletWebServerFactory factory = getFactory();
|
||||
addTestTxtFile(factory);
|
||||
factory.setSsl(Ssl.forBundle("test"));
|
||||
factory.setSslBundles(new DefaultSslBundleRegistry("test",
|
||||
createPemSslBundle("classpath:test-cert.pem", "classpath:test-key.pem")));
|
||||
this.webServer = factory.getWebServer();
|
||||
this.webServer.start();
|
||||
KeyStore keyStore = KeyStore.getInstance("pkcs12");
|
||||
loadStore(keyStore, new FileSystemResource("src/test/resources/test.p12"));
|
||||
SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
|
||||
new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy())
|
||||
.loadKeyMaterial(keyStore, "secret".toCharArray())
|
||||
.build());
|
||||
HttpComponentsClientHttpRequestFactory requestFactory = createHttpComponentsRequestFactory(socketFactory);
|
||||
assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory)).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sslNeedsClientAuthenticationSucceedsWithClientCertificate() throws Exception {
|
||||
AbstractServletWebServerFactory factory = getFactory();
|
||||
@@ -621,6 +667,7 @@ public abstract class AbstractServletWebServerFactoryTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Deprecated(since = "3.1.0", forRemoval = true)
|
||||
void sslWithCustomSslStoreProvider() throws Exception {
|
||||
AbstractServletWebServerFactory factory = getFactory();
|
||||
addTestTxtFile(factory);
|
||||
@@ -715,6 +762,24 @@ public abstract class AbstractServletWebServerFactoryTests {
|
||||
return ssl;
|
||||
}
|
||||
|
||||
private SslBundle createJksSslBundle(String keyStore, String trustStore) {
|
||||
JksSslStoreDetails keyStoreDetails = getJksStoreDetails(keyStore);
|
||||
JksSslStoreDetails trustStoreDetails = getJksStoreDetails(trustStore);
|
||||
SslStoreBundle stores = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
|
||||
return SslBundle.of(stores);
|
||||
}
|
||||
|
||||
private JksSslStoreDetails getJksStoreDetails(String location) {
|
||||
return new JksSslStoreDetails(getStoreType(location), null, location, "secret");
|
||||
}
|
||||
|
||||
private SslBundle createPemSslBundle(String cert, String privateKey) {
|
||||
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate(cert).withPrivateKey(privateKey);
|
||||
PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate(cert);
|
||||
SslStoreBundle stores = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails);
|
||||
return SslBundle.of(stores);
|
||||
}
|
||||
|
||||
protected void testRestrictedSSLProtocolsAndCipherSuites(String[] protocols, String[] ciphers) throws Exception {
|
||||
AbstractServletWebServerFactory factory = getFactory();
|
||||
factory.setSsl(getSsl(null, "password", "src/test/resources/restricted.jks", null, protocols, ciphers));
|
||||
|
||||
Reference in New Issue
Block a user