diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java index 20a7d7899abc..18882c278233 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java @@ -20,6 +20,7 @@ import java.io.InputStream; import java.security.GeneralSecurityException; import java.security.KeyStore; +import java.util.Set; import javax.net.ssl.TrustManagerFactory; @@ -53,6 +54,7 @@ import org.springframework.boot.io.ApplicationResourceLoader; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.ssl.pem.PemCertificate; import org.springframework.boot.ssl.pem.PemSslStore; import org.springframework.boot.ssl.pem.PemSslStoreDetails; import org.springframework.context.annotation.Bean; @@ -110,7 +112,7 @@ public Authenticator couchbaseAuthenticator(CouchbaseConnectionDetails connectio } Pem pem = this.properties.getAuthentication().getPem(); if (pem.getCertificates() != null) { - PemSslStoreDetails details = new PemSslStoreDetails(null, pem.getCertificates(), pem.getPrivateKey()); + PemSslStoreDetails details = new PemSslStoreDetails(null, Set.of(new PemCertificate(pem.getCertificates())), pem.getPrivateKey()); PemSslStore store = PemSslStore.load(details); return CertificateAuthenticator.fromKey(store.privateKey(), pem.getPrivateKeyPassword(), store.certificates()); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/BundleContentProperty.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/BundleContentProperty.java index 15b803bef642..e848b06823be 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/BundleContentProperty.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/BundleContentProperty.java @@ -21,7 +21,6 @@ import org.springframework.boot.io.ApplicationResourceLoader; import org.springframework.boot.ssl.pem.PemContent; import org.springframework.core.io.Resource; -import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -33,9 +32,12 @@ * @author Phillip Webb * @author Moritz Halbritter */ -record BundleContentProperty(String name, String value) { +record BundleContentProperty(String name, String value, boolean optional) { - private static final String OPTIONAL_URL_PREFIX = "optional:"; + BundleContentProperty(String name, String value) + { + this(name, value,false); + } /** * Return if the property value is PEM content. @@ -53,24 +55,16 @@ boolean hasValue() { return StringUtils.hasText(this.value); } - boolean isOptional() { - return this.value.startsWith(OPTIONAL_URL_PREFIX); - } - - String getRawValue() { - if (isOptional()) { - return this.value.substring(OPTIONAL_URL_PREFIX.length()); - } - return this.value; - } - WatchablePath toWatchPath() { try { - Resource resource = getResource(getRawValue()); + if (isPemContent()) { + return null; + } + Resource resource = getResource(); if (!resource.isFile()) { throw new BundleContentNotWatchableException(this); } - return new WatchablePath(Path.of(resource.getFile().getAbsolutePath()), isOptional()); + return new WatchablePath(this.optional, Path.of(resource.getFile().getAbsolutePath())); } catch (Exception ex) { if (ex instanceof BundleContentNotWatchableException bundleContentNotWatchableException) { @@ -81,9 +75,8 @@ WatchablePath toWatchPath() { } } - private Resource getResource(String value) { - Assert.state(!isPemContent(), "Value contains PEM content"); - return new ApplicationResourceLoader().getResource(value); + private Resource getResource() { + return new ApplicationResourceLoader().getResource(this.value); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/FileWatcher.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/FileWatcher.java index 3b8a6b34638c..bbe32d8d9700 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/FileWatcher.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/FileWatcher.java @@ -1,3 +1,4 @@ + /* * Copyright 2012-2023 the original author or authors. * @@ -217,8 +218,7 @@ public void close() throws IOException { private record Registration(Set paths, Runnable action) { Registration { - paths = paths.stream().map(watchablePath -> - new WatchablePath(watchablePath.path().toAbsolutePath(), watchablePath.optional())) + paths = paths.stream().map(watchablePath -> new WatchablePath(watchablePath.optional(), watchablePath.path().toAbsolutePath())) .collect(Collectors.toSet()); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemCertificateParser.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemCertificateParser.java new file mode 100644 index 000000000000..6826cb2ccc04 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemCertificateParser.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2024 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.PemCertificate; + +class PemCertificateParser { + + public static final String OPTIONAL_PREFIX = "optional:"; + + public PemCertificate parse(String source) { + boolean optional = source.startsWith(OPTIONAL_PREFIX); + String location = optional ? source.substring(OPTIONAL_PREFIX.length()) : source; + return new PemCertificate(location, optional); + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemSslBundleProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemSslBundleProperties.java index beb58d87dc91..b1f641d9e43c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemSslBundleProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemSslBundleProperties.java @@ -16,6 +16,9 @@ package org.springframework.boot.autoconfigure.ssl; +import java.util.HashSet; +import java.util.Set; + import org.springframework.boot.ssl.pem.PemSslStoreBundle; /** @@ -60,8 +63,14 @@ public static class Store { /** * Location or content of the certificate or certificate chain in PEM format. */ + @Deprecated private String certificate; + /** + * Set with location or content of the certificate or certificate chain in PEM format. + */ + private Set certificates = new HashSet<>(); + /** * Location or content of the private key in PEM format. */ @@ -85,14 +94,29 @@ public void setType(String type) { this.type = type; } + @Deprecated public String getCertificate() { return this.certificate; } + @Deprecated public void setCertificate(String certificate) { this.certificate = certificate; } + public Set getCertificates() { + if (this.certificate != null) { + Set allCertificates = new HashSet<>(this.certificates); + allCertificates.add(this.certificate); + return allCertificates; + } + return this.certificates; + } + + public void setCertificates(Set certificates) { + this.certificates = certificates; + } + public String getPrivateKey() { return this.privateKey; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java index 1e7efc7095d0..7891722b871b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java @@ -16,6 +16,9 @@ package org.springframework.boot.autoconfigure.ssl; +import java.util.Set; +import java.util.stream.Collectors; + import org.springframework.boot.autoconfigure.ssl.SslBundleProperties.Key; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundleKey; @@ -24,12 +27,12 @@ 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.PemCertificate; import org.springframework.boot.ssl.pem.PemSslStore; import org.springframework.boot.ssl.pem.PemSslStoreBundle; import org.springframework.boot.ssl.pem.PemSslStoreDetails; import org.springframework.core.style.ToStringCreator; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; /** * {@link SslBundle} backed by {@link JksSslBundleProperties} or @@ -41,8 +44,6 @@ */ public final class PropertiesSslBundle implements SslBundle { - private static final String OPTIONAL_URL_PREFIX = "optional:"; - private final SslStoreBundle stores; private final SslBundleKey key; @@ -121,19 +122,12 @@ private static PemSslStore getPemSslStore(String propertyName, PemSslBundlePrope } private static PemSslStoreDetails asPemSslStoreDetails(PemSslBundleProperties.Store properties) { - return new PemSslStoreDetails(properties.getType(), getRawCertificate(properties.getCertificate()), properties.getPrivateKey(), - properties.getPrivateKeyPassword(), isCertificateOptional(properties.getCertificate())); - } - - private static boolean isCertificateOptional(String certificate) { - return StringUtils.hasText(certificate) && certificate.startsWith(OPTIONAL_URL_PREFIX); - } - - private static String getRawCertificate(String certificate) { - if (isCertificateOptional(certificate)) { - return certificate.substring(OPTIONAL_URL_PREFIX.length()); - } - return certificate; + PemCertificateParser converter = new PemCertificateParser(); + Set pemCertificates = properties.getCertificates().stream() + .map(converter::parse) + .collect(Collectors.toSet()); + return new PemSslStoreDetails(properties.getType(), pemCertificates, properties.getPrivateKey(), + properties.getPrivateKeyPassword()); } /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java index fa9042f057c0..2c522fe37627 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java @@ -20,10 +20,13 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; +import org.springframework.boot.ssl.pem.PemCertificate; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundleRegistry; @@ -90,21 +93,33 @@ private Set watchedJksPaths(Bundle bundle private Set watchedPemPaths(Bundle bundle) { List watched = new ArrayList<>(); + BiFunction contentKeyStoreCertificateProperty = locationToBundleContentProperty(); watched .add(new BundleContentProperty("keystore.private-key", bundle.properties().getKeystore().getPrivateKey())); - watched - .add(new BundleContentProperty("keystore.certificate", bundle.properties().getKeystore().getCertificate())); + bundle.properties().getKeystore().getCertificates().stream() + .map(location -> contentKeyStoreCertificateProperty.apply(location, "keystore.certificate")) + .forEach(watched::add); watched.add(new BundleContentProperty("truststore.private-key", bundle.properties().getTruststore().getPrivateKey())); - watched.add(new BundleContentProperty("truststore.certificate", - bundle.properties().getTruststore().getCertificate())); + bundle.properties().getTruststore().getCertificates().stream() + .map(location -> contentKeyStoreCertificateProperty.apply(location, "truststore.certificate")) + .forEach(watched::add); return watchedPaths(bundle.name(), watched); } + private BiFunction locationToBundleContentProperty() { + PemCertificateParser certificateParser = new PemCertificateParser(); + return (location, name) -> { + PemCertificate certificate = certificateParser.parse(location); + return new BundleContentProperty(name, certificate.location(), certificate.optional()); + }; + } + private Set watchedPaths(String bundleName, List properties) { try { return properties.stream() .filter(BundleContentProperty::hasValue) + .filter(Predicate.not(BundleContentProperty::isPemContent)) .map(BundleContentProperty::toWatchPath) .collect(Collectors.toSet()); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/WatchablePath.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/WatchablePath.java index 7d8a81ea54e4..e5fc99893d96 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/WatchablePath.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/WatchablePath.java @@ -18,5 +18,5 @@ import java.nio.file.Path; -record WatchablePath(Path path, Boolean optional) { -} +record WatchablePath(boolean optional, Path path) { +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/LoadedPemSslStore.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/LoadedPemSslStore.java index 1e0cb79e43db..6b4a5a6527a6 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/LoadedPemSslStore.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/LoadedPemSslStore.java @@ -20,6 +20,7 @@ import java.io.UncheckedIOException; import java.security.PrivateKey; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.List; import java.util.function.Supplier; @@ -58,11 +59,13 @@ private static UncheckedIOException asUncheckedIOException(String message, Excep } private static List loadCertificates(PemSslStoreDetails details) throws IOException { - PemContent pemContent = PemContent.load(details.certificates(), details.optional()); - if (pemContent == null) { - return null; + List certificates = new ArrayList<>(); + for (PemCertificate certificate : details.certificateSet()) { + PemContent pemContent = PemContent.load(certificate.location(), certificate.optional()); + if (pemContent != null) { + certificates.addAll(pemContent.getCertificates()); + } } - List certificates = pemContent.getCertificates(); Assert.state(!CollectionUtils.isEmpty(certificates), "Loaded certificates are empty"); return certificates; } @@ -72,11 +75,6 @@ private static PrivateKey loadPrivateKey(PemSslStoreDetails details) throws IOEx return (pemContent != null) ? pemContent.getPrivateKey(details.privateKeyPassword()) : null; } - @Override - public boolean optional() { - return this.details.optional(); - } - @Override public String type() { return this.details.type(); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemCertificate.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemCertificate.java new file mode 100644 index 000000000000..e420ab1981e9 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemCertificate.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2024 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; + +public record PemCertificate (String location, boolean optional) { + + public PemCertificate(String location) { + this(location, false); + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java index 515658fa2ed9..b2429ca2e08a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java @@ -16,6 +16,7 @@ package org.springframework.boot.ssl.pem; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; @@ -28,6 +29,7 @@ import java.util.List; import java.util.Objects; import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.springframework.boot.io.ApplicationResourceLoader; import org.springframework.core.io.Resource; @@ -108,25 +110,22 @@ public String toString() { * Load {@link PemContent} from the given content (either the PEM content itself or a * reference to the resource to load). * @param content the content to load - * @param isOptional the content to load may be optional * @return a new {@link PemContent} instance * @throws IOException on IO error */ - static PemContent load(String content, Boolean isOptional) throws IOException { - if (isOptional && !Files.exists(Path.of(content))) { - return null; - } - return load(content); + static PemContent load(String content) throws IOException { + return load(content, false); } /** * Load {@link PemContent} from the given content (either the PEM content itself or a * reference to the resource to load). * @param content the content to load + * @param optional if the content is optional * @return a new {@link PemContent} instance * @throws IOException on IO error */ - static PemContent load(String content) throws IOException { + static PemContent load(String content, boolean optional) throws IOException { if (content == null) { return null; } @@ -138,6 +137,9 @@ static PemContent load(String content) throws IOException { return load(resource.getInputStream()); } catch (IOException | UncheckedIOException ex) { + if (ex instanceof FileNotFoundException && optional) { + return null; + } throw new IOException("Error reading certificate or key from file '%s'".formatted(content), ex); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java index 482003831819..d58fc9a71bee 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java @@ -33,9 +33,6 @@ */ public interface PemSslStore { - - boolean optional(); - /** * The key store type, for example {@code JKS} or {@code PKCS11}. A {@code null} value * will use {@link KeyStore#getDefaultType()}). @@ -167,11 +164,6 @@ public PrivateKey privateKey() { return privateKey; } - @Override - public boolean optional() { - return false; //TODO - } - }; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java index dda374dd0fda..7412b904e230 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java @@ -82,7 +82,7 @@ public KeyStore getTrustStore() { } private static KeyStore createKeyStore(String name, PemSslStore pemSslStore) { - if (pemSslStore == null || pemSslStore.optional() && pemSslStore.certificates() == null) { + if (pemSslStore == null) { return null; } try { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java index b8a4a443b18a..31947f70a870 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java @@ -17,7 +17,10 @@ package org.springframework.boot.ssl.pem; import java.security.KeyStore; +import java.util.Collections; +import java.util.Set; +import org.springframework.boot.io.ApplicationResourceLoader; import org.springframework.util.StringUtils; /** @@ -29,20 +32,20 @@ * @param password the password used * {@link KeyStore#setKeyEntry(String, java.security.Key, char[], java.security.cert.Certificate[]) * setting key entries} in the {@link KeyStore} - * @param certificates the certificates content (either the PEM content itself or or a + * @param certificateSet the set of certificates contents (either the PEM content itself or a * reference to the resource to load). When a {@link #privateKey() private key} is present * this value is treated as a certificate chain, otherwise it is treated a list of * certificates that should all be registered. * @param privateKey the private key content (either the PEM content itself or a reference * to the resource to load) * @param privateKeyPassword a password used to decrypt an encrypted private key - * @param optional certificates/privateKey may be optional * @author Scott Frederick * @author Phillip Webb * @since 3.1.0 * @see PemSslStore#load(PemSslStoreDetails) */ -public record PemSslStoreDetails(String type, String alias, String password, String certificates, String privateKey, String privateKeyPassword, boolean optional) { +public record PemSslStoreDetails(String type, String alias, String password, Set certificateSet, String privateKey, + String privateKeyPassword) { /** * Create a new {@link PemSslStoreDetails} instance. @@ -52,7 +55,7 @@ public record PemSslStoreDetails(String type, String alias, String password, Str * @param password the password used * {@link KeyStore#setKeyEntry(String, java.security.Key, char[], java.security.cert.Certificate[]) * setting key entries} in the {@link KeyStore} - * @param certificates the certificate content (either the PEM content itself or a + * @param certificateSet the set of certificate content (either the PEM content itself or a * reference to the resource to load) * @param privateKey the private key content (either the PEM content itself or a * reference to the resource to load) @@ -62,6 +65,23 @@ public record PemSslStoreDetails(String type, String alias, String password, Str public PemSslStoreDetails { } + /** + * Create a new {@link PemSslStoreDetails} instance. + * @param type the key store type, for example {@code JKS} or {@code PKCS11}. A + * {@code null} value will use {@link KeyStore#getDefaultType()}). + * @param alias the alias used when setting entries in the {@link KeyStore} + * @param password the password used + * @param certificates the certificate content (either the PEM content itself or a + * reference to the resource to load) + * @param privateKey the private key content (either the PEM content itself or a + * reference to the resource to load) + * @param privateKeyPassword a password used to decrypt an encrypted private key + */ + @Deprecated + public PemSslStoreDetails(String type, String alias, String password, String certificates, String privateKey, String privateKeyPassword) { + this(type, alias, password, toPemCertificates(certificates), privateKey, privateKeyPassword); + } + /** * Create a new {@link PemSslStoreDetails} instance. * @param type the key store type, for example {@code JKS} or {@code PKCS11}. A @@ -71,10 +91,10 @@ public record PemSslStoreDetails(String type, String alias, String password, Str * @param privateKey the private key content (either the PEM content itself or a * reference to the resource to load) * @param privateKeyPassword a password used to decrypt an encrypted private key - * @param optional certificates/privateKey may be optional */ - public PemSslStoreDetails(String type, String certificate, String privateKey, String privateKeyPassword, boolean optional) { - this(type, null, null, certificate, privateKey, privateKeyPassword, optional); + @Deprecated + public PemSslStoreDetails(String type, String certificate, String privateKey, String privateKeyPassword) { + this(type, null, null, certificate, privateKey, privateKeyPassword); } /** @@ -86,8 +106,48 @@ public PemSslStoreDetails(String type, String certificate, String privateKey, St * @param privateKey the private key content (either the PEM content itself or a * reference to the resource to load) */ + @Deprecated public PemSslStoreDetails(String type, String certificate, String privateKey) { - this(type, certificate, privateKey, null, false); + this(type, certificate, privateKey, null); + } + + /** + * Create a new {@link PemSslStoreDetails} instance. + * @param type the key store type, for example {@code JKS} or {@code PKCS11}. A + * {@code null} value will use {@link KeyStore#getDefaultType()}). + * @param certificates the set of certificate contents (either the PEM content itself or a + * reference to the resource to load) + * @param privateKey the private key content (either the PEM content itself or a + * reference to the resource to load) + * @param privateKeyPassword a password used to decrypt an encrypted private key + */ + public PemSslStoreDetails(String type, Set certificates, String privateKey, String privateKeyPassword) { + this(type, null, null, certificates, privateKey, privateKeyPassword); + } + + /** + * Create a new {@link PemSslStoreDetails} instance. + * @param type the key store type, for example {@code JKS} or {@code PKCS11}. A + * {@code null} value will use {@link KeyStore#getDefaultType()}). + * @param certificates the set of certificate contents (either the PEM content itself or a + * reference to the resource to load) + * @param privateKey the private key content (either the PEM content itself or a + * reference to the resource to load) + */ + public PemSslStoreDetails(String type, Set certificates, String privateKey) { + this(type, certificates, privateKey, null); + } + + /** + * Return the certificate content. + * @return the certificate content + * @deprecated + */ + @Deprecated() + public String certificates() { + return this.certificateSet.stream() + .findAny().map(PemCertificate::location) + .orElse(null); } /** @@ -97,7 +157,8 @@ public PemSslStoreDetails(String type, String certificate, String privateKey) { * @since 3.2.0 */ public PemSslStoreDetails withAlias(String alias) { - return new PemSslStoreDetails(this.type, alias, this.password, this.certificates, this.privateKey, this.privateKeyPassword, this.optional); + return new PemSslStoreDetails(this.type, alias, this.password, this.certificateSet, this.privateKey, + this.privateKeyPassword); } /** @@ -107,7 +168,8 @@ public PemSslStoreDetails withAlias(String alias) { * @since 3.2.0 */ public PemSslStoreDetails withPassword(String password) { - return new PemSslStoreDetails(this.type, this.alias, password, this.certificates, this.privateKey, this.privateKeyPassword, this.optional); + return new PemSslStoreDetails(this.type, this.alias, password, this.certificateSet, this.privateKey, + this.privateKeyPassword); } /** @@ -116,7 +178,8 @@ public PemSslStoreDetails withPassword(String password) { * @return a new {@link PemSslStoreDetails} instance */ public PemSslStoreDetails withPrivateKey(String privateKey) { - return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificates, privateKey, this.privateKeyPassword, this.optional); + return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificateSet, privateKey, + this.privateKeyPassword); } /** @@ -125,17 +188,25 @@ public PemSslStoreDetails withPrivateKey(String privateKey) { * @return a new {@link PemSslStoreDetails} instance */ public PemSslStoreDetails withPrivateKeyPassword(String privateKeyPassword) { - return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificates, this.privateKey, privateKeyPassword, this.optional); + return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificateSet, this.privateKey, privateKeyPassword); } boolean isEmpty() { - return isEmpty(this.type) && isEmpty(this.certificates) && isEmpty(this.privateKey); + return isEmpty(this.type) && isCertificatesEmpty() && isEmpty(this.privateKey); } private boolean isEmpty(String value) { return !StringUtils.hasText(value); } + private boolean isContentEmpty(PemCertificate value) { + return value.optional() ? !new ApplicationResourceLoader().getResource(value.location()).exists() : isEmpty(value.location()); + } + + boolean isCertificatesEmpty() { + return this.certificateSet == null || this.certificateSet.isEmpty() || this.certificateSet.stream().allMatch(this::isContentEmpty); + } + /** * Factory method to create a new {@link PemSslStoreDetails} instance for the given * certificate. Note: This method doesn't actually check if the provided value @@ -161,4 +232,11 @@ public static PemSslStoreDetails forCertificates(String certificates) { return new PemSslStoreDetails(null, certificates, null); } -} + private static Set toPemCertificates(String certificates) { + if (certificates != null) { + return Set.of(new PemCertificate(certificates)); + } + return Collections.emptySet(); + } + +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java index 5134881e77b5..822a28daea0d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java @@ -17,6 +17,7 @@ package org.springframework.boot.web.server; import java.security.KeyStore; +import java.util.Set; import org.springframework.boot.ssl.NoSuchSslBundleException; import org.springframework.boot.ssl.SslBundle; @@ -27,6 +28,7 @@ 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.PemCertificate; import org.springframework.boot.ssl.pem.PemSslStoreBundle; import org.springframework.boot.ssl.pem.PemSslStoreDetails; import org.springframework.core.style.ToStringCreator; @@ -61,7 +63,7 @@ private WebServerSslBundle(SslStoreBundle stores, String keyPassword, Ssl ssl) { } private static SslStoreBundle createPemKeyStoreBundle(Ssl ssl) { - PemSslStoreDetails keyStoreDetails = new PemSslStoreDetails(ssl.getKeyStoreType(), ssl.getCertificate(), + PemSslStoreDetails keyStoreDetails = new PemSslStoreDetails(ssl.getKeyStoreType(), Set.of(new PemCertificate(ssl.getCertificate())), ssl.getCertificatePrivateKey()) .withAlias(ssl.getKeyAlias()); return new PemSslStoreBundle(keyStoreDetails, null); @@ -69,7 +71,7 @@ private static SslStoreBundle createPemKeyStoreBundle(Ssl ssl) { private static SslStoreBundle createPemTrustStoreBundle(Ssl ssl) { PemSslStoreDetails trustStoreDetails = new PemSslStoreDetails(ssl.getTrustStoreType(), - ssl.getTrustCertificate(), ssl.getTrustCertificatePrivateKey()) + Set.of(new PemCertificate(ssl.getTrustCertificate())), ssl.getTrustCertificatePrivateKey()) .withAlias(ssl.getKeyAlias()); return new PemSslStoreBundle(null, trustStoreDetails); } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/build.gradle new file mode 100644 index 000000000000..f663d79f79b6 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/build.gradle @@ -0,0 +1,14 @@ +plugins { + id "java" + id "org.springframework.boot.conventions" +} + +description = "Spring Boot Tomcat SSL with multiple/optional certificates smoke test" + +dependencies { + implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web")) + implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-actuator")) + + testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) + testImplementation("org.apache.httpcomponents.client5:httpclient5") +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/java/smoketest/tomcat/ssl/SampleTomcatSslApplication.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/java/smoketest/tomcat/ssl/SampleTomcatSslApplication.java new file mode 100644 index 000000000000..18f1b5828833 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/java/smoketest/tomcat/ssl/SampleTomcatSslApplication.java @@ -0,0 +1,38 @@ +/* + * 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 smoketest.tomcat.ssl; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.web.client.RestTemplate; + +@SpringBootApplication +public class SampleTomcatSslApplication { + + public static void main(String[] args) { + SpringApplication.run(SampleTomcatSslApplication.class, args); + } + + @Bean + public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder, SslBundles sslBundles) { + return restTemplateBuilder.setSslBundle(sslBundles.getBundle("rest")).build(); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/java/smoketest/tomcat/ssl/web/SampleController.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/java/smoketest/tomcat/ssl/web/SampleController.java new file mode 100644 index 000000000000..7b906d59dca9 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/java/smoketest/tomcat/ssl/web/SampleController.java @@ -0,0 +1,30 @@ +/* + * 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 smoketest.tomcat.ssl.web; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class SampleController { + + @GetMapping("/") + public String helloWorld() { + return "Hello, world"; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/resources/certs/ca.crt b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/resources/certs/ca.crt new file mode 100644 index 000000000000..6aed0cc59e72 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/resources/certs/ca.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDBzCCAe+gAwIBAgIUSGye6EtFNkG/v6jyTLmzZkFm81YwDQYJKoZIhvcNAQEL +BQAwEzERMA8GA1UEAwwITXlSb290Q0EwHhcNMjQwODEzMTcxODA5WhcNMzQwODEx +MTcxODA5WjATMREwDwYDVQQDDAhNeVJvb3RDQTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAMStCYIWTOZcg8Ri5I3BJ6ctl5N9kouEr5S6A47UHg7N32NJ +cQ6sanhO5Y/l4vxZvd0AFpqG3Y5bFrtePn/TjY9qA9nXS4pavpGG1bLF9T7lm3Sp +AFc5FzhqmzxmzD7Eu9fPKxYCOycqyEtxxiUBhrPPgt+u6PwpDmO8uUhmzWts3HGn +LjfUSwWkHKvflT7nyZBzAj4biE7Y41LMwljb8Ox02+DLlYuT/4PRkB8erag4SSrK +2k1HvqFmaKQyx8FrLdqkyCGm3xB/DYfb2PfRpi4JxpLyumppcgBUgY73vP6D//M2 +N+6LaBXZNmF87CG9TRa4QhNbSYtiHjkiFMHBWfsCAwEAAaNTMFEwHQYDVR0OBBYE +FIwxgudDLzOSPbM/nYNfUTlI5UF4MB8GA1UdIwQYMBaAFIwxgudDLzOSPbM/nYNf +UTlI5UF4MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAKxEzFHn +9bywfoMeCsVjyCgIgKHupgLYH/tF109faTXAO0MtwI+xcI5UjOvrhmoGYjp860Rf +TQWWtcl+/D8YCc4SmPHURZZrFWRQVsgUJHWvjjgIdXJXh9Jz6WqnYac2U3rAmWEs +CfGrizKnDgNTskCKbdm458GCTKg1/jIROCfbI7Wzjwr/AfY5hV0XFYumguWUL8rR +SHtR/sU4KLbGLSuUfB6hIz4kSuNgKIVRzW/igSZ25YYh9/9GeygjvdBbYUt6FHGl +V07PHt4GrYdRl6a0iFKS4Y3JfuQrWo9nU4nzcxYWCoqYzmjNlxN4pm3kBzAeLPG3 +9pcSDSuYRqCV3hM= +-----END CERTIFICATE----- diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/resources/certs/ca.key b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/resources/certs/ca.key new file mode 100644 index 000000000000..458cd18e18e2 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/resources/certs/ca.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDErQmCFkzmXIPE +YuSNwSenLZeTfZKLhK+UugOO1B4Ozd9jSXEOrGp4TuWP5eL8Wb3dABaaht2OWxa7 +Xj5/042PagPZ10uKWr6RhtWyxfU+5Zt0qQBXORc4aps8Zsw+xLvXzysWAjsnKshL +ccYlAYazz4Lfruj8KQ5jvLlIZs1rbNxxpy431EsFpByr35U+58mQcwI+G4hO2ONS +zMJY2/DsdNvgy5WLk/+D0ZAfHq2oOEkqytpNR76hZmikMsfBay3apMghpt8Qfw2H +29j30aYuCcaS8rpqaXIAVIGO97z+g//zNjfui2gV2TZhfOwhvU0WuEITW0mLYh45 +IhTBwVn7AgMBAAECggEAGIyy3aKT+cM9jWN8vPcJ0JPf0kC/7Jtk4U8wx4DRua5X +/og5zQeXiKnsfMBIy5AWI4Jxz9sax7y2AzBZ49HP30Fv9p6pprz6AadPgG+2U6IM +fAzmZnzRWbDw7KK2RvV+rwsEiUxA/vwXoVcz0QW2PzadUvd9zJABZFC33gI7DPgC +0pQhIXo+S4kzzUb/iiXVIkIxnjV3wIK/Yqs1SoEKqXM6MPcV1lR0sK0bCVim3MnA +STG18IvMgcF3PkfR0aptwa/dnc6+zMX2dAqyFtCna4rqQWd6sVCo2pdAlCuW10v4 +E+dLm6QSpAFwdUa7FjuQM+ew/8ksPWg4VbFgtI+/tQKBgQDIeDnSciYP6KcxvhBu +RDh4IXJbFzjRENNpv5LFdn3ZntdW50JvUghZ5O7oCOdbPKE/d2z5LNQPCSXxuP+A +a1pZHjghxJ1im0Ko9XeWh8Pw8iYpkH3afA+TiN8BDvqlNEdAZRrz70MimAUfROEz +J3P+IPf91itwJXd0V9XDNGx+lwKBgQD7J8pniQ1zJuR/c1i948AZNhGSOCQ7GZRh +w1aHdninhYJ+IYlP3TAckJMlz8iA7MiO56y0YlFy3f2+8x2ewZl+eH9Cn86WbmvX +z8UgM6oPisVHuUCwZuphDGIsMk/Vb02WnGlr/ipsgzljznIfH0lngtPQB5s3xzHx +j8FQaWhQPQKBgQCAeQwVYjIiX+dGaZf+Eppd4pF27xrqYO4cBzn4ckeU/8bhWrOo +w9m2QpEZAxvBzMlJ8y9TQPdl62b10qlrk2EDW+p9OZPjbbz6qtVJExjvgUATwxXk +vzz8P+sqsn7PAQHosuLjEaLkuKgPsgTg05fydQ55Dpgn9trnJKNJxn8BYQKBgQCC +0X8D3sc6q49pM1ON1QtCFn+ggc2dWv2GzpBLjtHZsBkASceT6codltCOaWQugycU +CGhUrMFv62E4DLno7z5cObdPpJ2ejXVuu7IZy89QuR949G1VdMWwNxsLmkkrCwaG +5IGk1oaSbud9rRKUU1+Qovxg5xVaQE8rW419rOnAoQKBgF+aBsPRU0mZkXVcHqzh +x0jbaEfeQXCzFDh3l79IH/rQovNkT2me79eISLMlTkDms3cc9HsRypAVKtBn/adh +tSKP7HFK84eT9t6vLK8xlmuwoCsREbCQCXfapOtQlhPsF0l5cwftQXMcD1pYQNoQ +7B1UpDj+bh5/TN9PgW+lSiO5 +-----END PRIVATE KEY----- diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/resources/certs/ca_cert.crt b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/resources/certs/ca_cert.crt new file mode 100644 index 000000000000..4161a1f01417 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/resources/certs/ca_cert.crt @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBtTCCAVugAwIBAgIUeFllWZx5HIrt1LgcFMND3XCc1NQwCgYIKoZIzj0EAwQw +GzEZMBcGA1UEAwwQUm9vdCBDQSBmb3IgVGVzdDAgFw0yNDA4MjYxMzQwMTdaGA8y +MTI0MDgwMjEzNDAxN1owGzEZMBcGA1UEAwwQUm9vdCBDQSBmb3IgVGVzdDBZMBMG +ByqGSM49AgEGCCqGSM49AwEHA0IABNTroEMKMGFp72qfzemTzpMU3Mo6GBXyXSGF +yRnmACyRKBo6keXKwtOD6jG6yF/11Ri11TvxJF2p8vF1ELDnVtCjezB5MA8GA1Ud +EwEB/wQFMAMBAf8wKQYDVR0OBCIEIEwqs84LwXPgYvkfUlt9fHTa/x8+uL4Z6QIZ +5tSgFT5WMCsGA1UdIwQkMCKAIEwqs84LwXPgYvkfUlt9fHTa/x8+uL4Z6QIZ5tSg +FT5WMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDBANIADBFAiEA5mHBVaR87e/+ +OhBNCmNNNYLQ5kSCoOiF2KMrqXfLxKMCIEj6fiKz9B2ULOFA72imwvFNR7pLiz7Z +QU+hkNen9eMI +-----END CERTIFICATE----- diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/resources/certs/client.crt b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/resources/certs/client.crt new file mode 100644 index 000000000000..d2c3514636c8 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/resources/certs/client.crt @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICqzCCAZMCFDLPXWKQ/Lb6t8VFdOdDiyjFYcy0MA0GCSqGSIb3DQEBCwUAMBMx +ETAPBgNVBAMMCE15Um9vdENBMB4XDTI0MDgxMzE3MjAwNloXDTM0MDgxMTE3MjAw +NlowETEPMA0GA1UEAwwGY2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAnBHo9hDs0m6WyaDB0NRQwtsUbBmqTFUG7D3JNqhGoOHTP9NG9SujMYOO +gnTEYKIJr9Tao4RdQqqNJ5RdUyy2wVANwZD3DG2BNVmjkmGo6fLWZin+S8xYHrG7 +a9M5DXbaPLgtpg4Dpg2T7xbg+oPMze81BakAGazzfzyCJW+vRbsXnJ5gnCUCgF97 +J2om9RQOHd2yxpG3z4B6+dr0YhGLLX/gE3O8dIv81BwhY/kJrgicUx3bW+tBQKMQ +TbaGYTpCKN2KDFuWPXNLvSMmd67pXOHtRITK7tYrpYIKyzWNbHj6CdFfpvk162+n +L2MLTmE5pETE/KqE6ybVn/9Hi6rJ0QIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQDC +psAvRyFDMGJUwT54nxhcrpPIsUZftSlwusyYGJYQNpGS6jo1U76J+L25iOzZEdnY +f8dC2HqVD5mgn2XAulajsjsfuIbIZ9m/in4/p3ugvR3ZBYEiB5WHRD0cZqIBxpv7 +n5RFPrvCNMUct6QN6w9S/nyb3JHg4BqBWnUSyaE/7aEbgpCgUIqrv+WgMvu1ttmB +vS8uc+XY/YuR7zXAfmXYkYdbAVynCwWi6kF0wbfHx6NUTLNWA2sK/SdRZEL4WL5f +/FQ071BOFpYWKuKdUKo6R8xVuCCXGEwvgXTNGIkAMBa2mt/HEkk3U6YXL+LlBi80 +CDuQrDhQTIK7f+4H0Bs7 +-----END CERTIFICATE----- diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/resources/certs/client.key b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/resources/certs/client.key new file mode 100644 index 000000000000..a2f5b653edc2 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/resources/certs/client.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCcEej2EOzSbpbJ +oMHQ1FDC2xRsGapMVQbsPck2qEag4dM/00b1K6Mxg46CdMRgogmv1NqjhF1Cqo0n +lF1TLLbBUA3BkPcMbYE1WaOSYajp8tZmKf5LzFgesbtr0zkNdto8uC2mDgOmDZPv +FuD6g8zN7zUFqQAZrPN/PIIlb69FuxecnmCcJQKAX3snaib1FA4d3bLGkbfPgHr5 +2vRiEYstf+ATc7x0i/zUHCFj+QmuCJxTHdtb60FAoxBNtoZhOkIo3YoMW5Y9c0u9 +IyZ3rulc4e1EhMru1iulggrLNY1sePoJ0V+m+TXrb6cvYwtOYTmkRMT8qoTrJtWf +/0eLqsnRAgMBAAECggEAB70snd0HfUTOFdfynGoWyh7Gc7jPFMNnmTnUGzF7dRlV +mhXeMCSWjks9exMStM78J3uoztButnJSFwsYmJoAQvQ3BmjrkzJv5IcaIRVWJKmt +v3moGjabDQSXrFhYPSZuWnHwk4og3LBSLFooVEvKUVDiAnKXpm5I0b+cnYIdARpa +xmtb5iVPcbjElY4geqts3byLqL0hnJwfDbO5zcnV9pNWLVcDc7bHB8yBwdoiwWHt +JVn09zOouuIFKgLPDSZNTLKLffxYg1qt0B2CXQOK2EAq+xxE3IR2RoyaLbvzqylk +Tr7dz4rYMVgC/n+PAMwkHsW3CSBQl53qDNhm4kE7ZQKBgQDYE8mwje4Trxm04vUv +qmVcNjqnPnTQDxBD5V5/ixEkPjy2Am+kkBTq1gGj7RVMTeQ1ROZv2Vk/nDEDfRb5 +ZWOabgmsE6xImDcncz4Aa7YvHL3GTjGVQRKO9CITL+55ahOfGpguPC3s2DZ8lkgh +L71Z4QExdPUtSahFdDa07T2W1QKBgQC459fLQJg2mVNMuyeWFFC2lEene/9+ll+o +02VQ0ZguqBrKsfBbWHaeJcOOoPvWJiErqCKjQDE8gP8iBdGCUB6y/qVLwxdJaku+ +68rW4DMcddNZkra0gQ2oT7QAo6Mifrzqlw9l96vlCQuRelHNFSfyhKGC4bODZVlU +JLyj0aQdDQKBgQCH0jBmTWDIeLlU7ZCnTJl4FBJcTDMLEVztAMGctGKrAIAS/IcG +zxaG4syXKRDJLPD01wFubxXdmSVqBvgo/iVUzjRAOQGDhEKvBo6DnzEefheADmi2 +Y/fxad39Z5SkNxxsV0AvV96aUPI28BQY4DRKydeBKf5vYCxos/srUTD0nQKBgCpE +bRK8KE9KyzzeB1WKPU0PJjYF5UiFjUZlVGKeFsCLktxEwqHO3gaWsVY4PHkebDSz +kX9p3BdtkWSwmczFDc9y4EwqQ3d3werZsZte0rAtyutN20/1tC6GUapXvaHUANFL +SKzRaczIPYm6wVo0/NW2NclaWJOvpjTS1QBJms89AoGBANd66yaWtLPD9hfQb2um +mvfVeBJ1DxpICOIhW0A12/EReTKjVmopwzU50PdoF96Zh/ZyhzFRS7UF4yakVBYT +XIWKKUEtMrPaEuA9VaXE+f0r6c7UPpW88zSl2OH1s0DLI0mvN/bLrctFc/23vDcL +mGVyArVYEK1Yi68Lpdc90uLx +-----END PRIVATE KEY----- diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/resources/certs/server.crt b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/resources/certs/server.crt new file mode 100644 index 000000000000..38fd6fb6a342 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/resources/certs/server.crt @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICrjCCAZYCFHFhRF432nj9MmqucJFrWBdk+2mmMA0GCSqGSIb3DQEBCwUAMBMx +ETAPBgNVBAMMCE15Um9vdENBMB4XDTI0MDgxMzE3MTkxM1oXDTM0MDgxMTE3MTkx +M1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAo3p6vtlcNkfab9LXVKtNEghSgQ9sRbpGnoQdxQByYsbI3a7tSVHd +odZ9IxJ2cTi7hZCESCyZsW0HEc4LQxl/BRyOVLm+jyOexsjk18QHi+xs3lF3T/BQ +itmZBEd+rf2n9oYv2koTs4V/xwNNCwc2GShor7m07+u1ga2JbVGLnn139HyK8Ubb +3xMQlZWod+/NfUCMLnUdELpLBVlCfzBeF6HIGdDYKgDAI0/ODVBlvOHXn/HZz8ti +2dQHP51Tzs9OLRtMAZcXbgD0UiDz4niUZNG69muigfCCLV+5d3lyOnTUG+IbuVjP +f4/avfLYbavasU6KXjh/XTNxYxskr/Pd2wIDAQABMA0GCSqGSIb3DQEBCwUAA4IB +AQC85jPmxXeJWPppTyYBiB/xM0nnDkzrDLXB+8XTdp6dIDDZ9C68P63TNmkwReq3 ++pi2YR1mWRlmW7kB5FIm8Tp+KOowPjH2DBUcEpVy59uZtRZALRoSi5BON1cVaXe0 +qqw0iGcCHPJfvXmGqNDgufh+v5Jf+zpAroGiJCE1LkzY+Q5J36L9nT61yg1g5b4S +l1ShRW6E4ryP7BzJ6FaXBq02lYLGPdpyAUKPILS9QJw/yjsK89QM7VCtuP6r52H9 +MlucemDDtM06C4NCEk/lWhUJZ9+v401NKuwTapbxB3KQVCC3vMJsZBVNMBM/hPI3 +3rbjIVwxznNO9gUNslyYV+hr +-----END CERTIFICATE----- diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/resources/certs/server.key b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/resources/certs/server.key new file mode 100644 index 000000000000..f4dade3f8b9f --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/main/resources/certs/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCjenq+2Vw2R9pv +0tdUq00SCFKBD2xFukaehB3FAHJixsjdru1JUd2h1n0jEnZxOLuFkIRILJmxbQcR +zgtDGX8FHI5Uub6PI57GyOTXxAeL7GzeUXdP8FCK2ZkER36t/af2hi/aShOzhX/H +A00LBzYZKGivubTv67WBrYltUYuefXf0fIrxRtvfExCVlah37819QIwudR0QuksF +WUJ/MF4XocgZ0NgqAMAjT84NUGW84def8dnPy2LZ1Ac/nVPOz04tG0wBlxduAPRS +IPPieJRk0br2a6KB8IItX7l3eXI6dNQb4hu5WM9/j9q98thtq9qxTopeOH9dM3Fj +GySv893bAgMBAAECggEABOmt3aVnlYQERv8FlJhPSR7x58oAXXoTHDlpMZ3pUhma +OLtEi0MID5CEEzU/VPi4/fMRXp/kgIX/w+O2x+3wuMUaa+ZnGSMfZubrpaZQ+b4B +qY62MLNOoFWYuR2y62SnkwuGTZ+TRv5YkDEDtDSjxg7GUp2Yl+sz+bEu45ejRACB ++cwcLzpCroCTmSFRUVXtM1zGSN49OSyfLRZ5lrbwDl2zZYnaolNBOrI18spQwK9G +/WFaV+aLL0aHbbtBS/yl7TwKqPpk/1AKSS1nHm0Y+bkFiwEYvxqu3K5GogAPPHUn +9nYQxLwee0mWpcqSm6hWKuTbQKjOlUlzw7eyZlRrEQKBgQDJoNU0e9Vdo/L4vrXZ +/t/lquSK/1UvZ13XgeojFL9dEAekk9lCcUNQNVmUGZNAkXyDNY3e2ANZlkYxik2q +1v32yrCwjaRnFSUqnhV6RDhxMIaiCG7NdKgYk2j6k7SlejGHdJ5O24R19VmPRNQa +6PwxPsCEKy1Gygf575PKp2KIiwKBgQDPkATtY6SWgORrEOwYvOGP6ooQcD4jKRa8 +NhMzzGEtDd9zPH6dLB0Y2Qrf8HR6w0ayPgWmL8DgRypv1VPyHdg927Gn/l86KpR6 +1z7YM9T63j7YMfM+9hcWsMH/QpDXV6Qlg3N5UFvDW4oTqVCaANOPSoRi8IzYxAnc +CRQKzktZ8QKBgDOu3VfhsjSZlOt7/yNM+NlnL8QNZSmMhnp6W6j4ZYEWXc8q8tLc +M5P4yOh0kdFIObFsZdxMZLdvFLkYKYZ0K486L4ZiGFUwD2HYOcsod4tUE/6uyLAz +ie8awhsRB4ovQ0jkdLvj+xU9eeKGkxP+yr5YxoJaivWNTfQcHDcjJte3AoGBAJef +Oxo2icqviSx1BiLkB1ncGNL9S0bgAw2l6s0R5YLF+Y7yiANEcFTwZ7NCsbPj5kba +a8IEbD7pfaSID3R0PLyjOdngRav14tUBW5UP9+ryYrIHewtpNWCL6osPE0NbcDs/ +FSFvhDjnK6xFKO324JRx+NdVpW3LdvBXaV6jaAPhAoGARFuptdv5KbqHlqLgIa/E +uH8GZfvL0oNGEldW/2Mz/mS1k63MwF563zhY1StD7NPgF7rSNZevAUg2iBaGJ7f+ +RSbmY2XZwR55ST2lPL14fivoTcH4U19lWVx3lAH7uxjbFv9tWEKV85Nuc3yVhFHB +xnioJi1sp+ODi956s7OnKy4= +-----END PRIVATE KEY----- diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/test/java/smoketest/tomcat/ssl/SampleTomcatSslWithOptionalMultipleCertsApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/test/java/smoketest/tomcat/ssl/SampleTomcatSslWithOptionalMultipleCertsApplicationTests.java new file mode 100644 index 000000000000..656b75ab65fe --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat-ssl-trust-optional/src/test/java/smoketest/tomcat/ssl/SampleTomcatSslWithOptionalMultipleCertsApplicationTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2012-2024 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 smoketest.tomcat.ssl; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.io.ApplicationResourceLoader; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { + "spring.ssl.bundle.pem.server.keystore.certificates=classpath:certs/server.crt", + "spring.ssl.bundle.pem.server.keystore.private-key=classpath:certs/server.key", + "spring.ssl.bundle.pem.server.keystore.private-key-password=123456", + "spring.ssl.bundle.pem.server.truststore.certificates[0]=optional:${user.dir}/build/resources/main/newca.crt", + "spring.ssl.bundle.pem.server.truststore.certificates[1]=optional:classpath:certs/ca_cert.crt", + "spring.ssl.bundle.pem.server.reload-on-update=true", + "spring.ssl.bundle.pem.rest.keystore.certificates=classpath:certs/client.crt", + "spring.ssl.bundle.pem.rest.keystore.private-key=classpath:certs/client.key", + "spring.ssl.bundle.pem.rest.truststore.certificates=classpath:certs/ca.crt", + "server.ssl.client-auth=need", + "server.port=8443", + "server.ssl.bundle=server", + "spring.ssl.bundle.watch.file.quiet-period=5ms"}) +@ContextConfiguration(initializers = { SampleTomcatSslWithOptionalMultipleCertsApplicationTests.Initializer.class}) +class SampleTomcatSslWithOptionalMultipleCertsApplicationTests { + + private static final String OPTIONAL_PREFIX = "optional:"; + + @LocalServerPort + int port; + + @Autowired + private RestTemplate restTemplate; + + @Value("${spring.ssl.bundle.pem.server.truststore.certificates[0]}") + private String targetFile; + + public static class Initializer implements ApplicationContextInitializer { + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + try { + Resource resource = new ApplicationResourceLoader().getResource("classpath:newca.crt"); + if (resource.exists()) { + resource.getFile().delete(); + } + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + @Test + void testHome() { + final String URL = "https://localhost:%s/".formatted(this.port); + + ResourceAccessException exception = assertThrows(ResourceAccessException.class, () -> this.restTemplate.getForEntity(URL, String.class)); + String expectedMessage = "Received fatal alert: bad_certificate"; + assertThat(exception.getCause().getMessage()).isEqualTo(expectedMessage); + + copyCaFileFromResourceToExpectTruststoreCertificateProperty(); + + Awaitility.await().atMost(Duration.ofMinutes(1)).ignoreExceptions().untilAsserted(() -> { + ResponseEntity entity = this.restTemplate.getForEntity(URL, String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).isEqualTo("Hello, world"); + }); + } + + private void copyCaFileFromResourceToExpectTruststoreCertificateProperty() { + Path sourcePath = Paths.get("src/main/resources/certs", "ca.crt"); + if (this.targetFile != null) { + Path pathFile = Paths.get(this.targetFile.replace(OPTIONAL_PREFIX, "")); + try { + try (InputStream inputStream = Files.newInputStream(sourcePath)) { + Files.copy(inputStream, pathFile); + } + } + catch (IOException e) { + e.printStackTrace(); + } + } + } + +}