Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit ae68967

Browse files
committedSep 10, 2024·
Add support for multiple/optional certificates
1 parent c3efeaf commit ae68967

File tree

26 files changed

+593
-85
lines changed

26 files changed

+593
-85
lines changed
 

‎spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.io.InputStream;
2121
import java.security.GeneralSecurityException;
2222
import java.security.KeyStore;
23+
import java.util.Set;
2324

2425
import javax.net.ssl.TrustManagerFactory;
2526

@@ -53,6 +54,7 @@
5354
import org.springframework.boot.io.ApplicationResourceLoader;
5455
import org.springframework.boot.ssl.SslBundle;
5556
import org.springframework.boot.ssl.SslBundles;
57+
import org.springframework.boot.ssl.pem.PemCertificate;
5658
import org.springframework.boot.ssl.pem.PemSslStore;
5759
import org.springframework.boot.ssl.pem.PemSslStoreDetails;
5860
import org.springframework.context.annotation.Bean;
@@ -110,7 +112,7 @@ public Authenticator couchbaseAuthenticator(CouchbaseConnectionDetails connectio
110112
}
111113
Pem pem = this.properties.getAuthentication().getPem();
112114
if (pem.getCertificates() != null) {
113-
PemSslStoreDetails details = new PemSslStoreDetails(null, pem.getCertificates(), pem.getPrivateKey());
115+
PemSslStoreDetails details = new PemSslStoreDetails(null, Set.of(new PemCertificate(pem.getCertificates())), pem.getPrivateKey());
114116
PemSslStore store = PemSslStore.load(details);
115117
return CertificateAuthenticator.fromKey(store.privateKey(), pem.getPrivateKeyPassword(),
116118
store.certificates());

‎spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/BundleContentProperty.java

+12-19
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import org.springframework.boot.io.ApplicationResourceLoader;
2222
import org.springframework.boot.ssl.pem.PemContent;
2323
import org.springframework.core.io.Resource;
24-
import org.springframework.util.Assert;
2524
import org.springframework.util.StringUtils;
2625

2726
/**
@@ -33,9 +32,12 @@
3332
* @author Phillip Webb
3433
* @author Moritz Halbritter
3534
*/
36-
record BundleContentProperty(String name, String value) {
35+
record BundleContentProperty(String name, String value, boolean optional) {
3736

38-
private static final String OPTIONAL_URL_PREFIX = "optional:";
37+
BundleContentProperty(String name, String value)
38+
{
39+
this(name, value,false);
40+
}
3941

4042
/**
4143
* Return if the property value is PEM content.
@@ -53,24 +55,16 @@ boolean hasValue() {
5355
return StringUtils.hasText(this.value);
5456
}
5557

56-
boolean isOptional() {
57-
return this.value.startsWith(OPTIONAL_URL_PREFIX);
58-
}
59-
60-
String getRawValue() {
61-
if (isOptional()) {
62-
return this.value.substring(OPTIONAL_URL_PREFIX.length());
63-
}
64-
return this.value;
65-
}
66-
6758
WatchablePath toWatchPath() {
6859
try {
69-
Resource resource = getResource(getRawValue());
60+
if (isPemContent()) {
61+
return null;
62+
}
63+
Resource resource = getResource();
7064
if (!resource.isFile()) {
7165
throw new BundleContentNotWatchableException(this);
7266
}
73-
return new WatchablePath(Path.of(resource.getFile().getAbsolutePath()), isOptional());
67+
return new WatchablePath(this.optional, Path.of(resource.getFile().getAbsolutePath()));
7468
}
7569
catch (Exception ex) {
7670
if (ex instanceof BundleContentNotWatchableException bundleContentNotWatchableException) {
@@ -81,9 +75,8 @@ WatchablePath toWatchPath() {
8175
}
8276
}
8377

84-
private Resource getResource(String value) {
85-
Assert.state(!isPemContent(), "Value contains PEM content");
86-
return new ApplicationResourceLoader().getResource(value);
78+
private Resource getResource() {
79+
return new ApplicationResourceLoader().getResource(this.value);
8780
}
8881

8982
}

‎spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/FileWatcher.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
/*
23
* Copyright 2012-2023 the original author or authors.
34
*
@@ -217,8 +218,7 @@ public void close() throws IOException {
217218
private record Registration(Set<WatchablePath> paths, Runnable action) {
218219

219220
Registration {
220-
paths = paths.stream().map(watchablePath ->
221-
new WatchablePath(watchablePath.path().toAbsolutePath(), watchablePath.optional()))
221+
paths = paths.stream().map(watchablePath -> new WatchablePath(watchablePath.optional(), watchablePath.path().toAbsolutePath()))
222222
.collect(Collectors.toSet());
223223
}
224224

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.ssl;
18+
19+
import org.springframework.boot.ssl.pem.PemCertificate;
20+
21+
class PemCertificateParser {
22+
23+
public static final String OPTIONAL_PREFIX = "optional:";
24+
25+
public PemCertificate parse(String source) {
26+
boolean optional = source.startsWith(OPTIONAL_PREFIX);
27+
String location = optional ? source.substring(OPTIONAL_PREFIX.length()) : source;
28+
return new PemCertificate(location, optional);
29+
}
30+
}

‎spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemSslBundleProperties.java

+24
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
package org.springframework.boot.autoconfigure.ssl;
1818

19+
import java.util.HashSet;
20+
import java.util.Set;
21+
1922
import org.springframework.boot.ssl.pem.PemSslStoreBundle;
2023

2124
/**
@@ -60,8 +63,14 @@ public static class Store {
6063
/**
6164
* Location or content of the certificate or certificate chain in PEM format.
6265
*/
66+
@Deprecated
6367
private String certificate;
6468

69+
/**
70+
* Set with location or content of the certificate or certificate chain in PEM format.
71+
*/
72+
private Set<String> certificates = new HashSet<>();
73+
6574
/**
6675
* Location or content of the private key in PEM format.
6776
*/
@@ -85,14 +94,29 @@ public void setType(String type) {
8594
this.type = type;
8695
}
8796

97+
@Deprecated
8898
public String getCertificate() {
8999
return this.certificate;
90100
}
91101

102+
@Deprecated
92103
public void setCertificate(String certificate) {
93104
this.certificate = certificate;
94105
}
95106

107+
public Set<String> getCertificates() {
108+
if (this.certificate != null) {
109+
Set<String> allCertificates = new HashSet<>(this.certificates);
110+
allCertificates.add(this.certificate);
111+
return allCertificates;
112+
}
113+
return this.certificates;
114+
}
115+
116+
public void setCertificates(Set<String> certificates) {
117+
this.certificates = certificates;
118+
}
119+
96120
public String getPrivateKey() {
97121
return this.privateKey;
98122
}

‎spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java

+10-16
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
package org.springframework.boot.autoconfigure.ssl;
1818

19+
import java.util.Set;
20+
import java.util.stream.Collectors;
21+
1922
import org.springframework.boot.autoconfigure.ssl.SslBundleProperties.Key;
2023
import org.springframework.boot.ssl.SslBundle;
2124
import org.springframework.boot.ssl.SslBundleKey;
@@ -24,12 +27,12 @@
2427
import org.springframework.boot.ssl.SslStoreBundle;
2528
import org.springframework.boot.ssl.jks.JksSslStoreBundle;
2629
import org.springframework.boot.ssl.jks.JksSslStoreDetails;
30+
import org.springframework.boot.ssl.pem.PemCertificate;
2731
import org.springframework.boot.ssl.pem.PemSslStore;
2832
import org.springframework.boot.ssl.pem.PemSslStoreBundle;
2933
import org.springframework.boot.ssl.pem.PemSslStoreDetails;
3034
import org.springframework.core.style.ToStringCreator;
3135
import org.springframework.util.Assert;
32-
import org.springframework.util.StringUtils;
3336

3437
/**
3538
* {@link SslBundle} backed by {@link JksSslBundleProperties} or
@@ -41,8 +44,6 @@
4144
*/
4245
public final class PropertiesSslBundle implements SslBundle {
4346

44-
private static final String OPTIONAL_URL_PREFIX = "optional:";
45-
4647
private final SslStoreBundle stores;
4748

4849
private final SslBundleKey key;
@@ -121,19 +122,12 @@ private static PemSslStore getPemSslStore(String propertyName, PemSslBundlePrope
121122
}
122123

123124
private static PemSslStoreDetails asPemSslStoreDetails(PemSslBundleProperties.Store properties) {
124-
return new PemSslStoreDetails(properties.getType(), getRawCertificate(properties.getCertificate()), properties.getPrivateKey(),
125-
properties.getPrivateKeyPassword(), isCertificateOptional(properties.getCertificate()));
126-
}
127-
128-
private static boolean isCertificateOptional(String certificate) {
129-
return StringUtils.hasText(certificate) && certificate.startsWith(OPTIONAL_URL_PREFIX);
130-
}
131-
132-
private static String getRawCertificate(String certificate) {
133-
if (isCertificateOptional(certificate)) {
134-
return certificate.substring(OPTIONAL_URL_PREFIX.length());
135-
}
136-
return certificate;
125+
PemCertificateParser converter = new PemCertificateParser();
126+
Set<PemCertificate> pemCertificates = properties.getCertificates().stream()
127+
.map(converter::parse)
128+
.collect(Collectors.toSet());
129+
return new PemSslStoreDetails(properties.getType(), pemCertificates, properties.getPrivateKey(),
130+
properties.getPrivateKeyPassword());
137131
}
138132

139133
/**

‎spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java

+19-4
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@
2020
import java.util.List;
2121
import java.util.Map;
2222
import java.util.Set;
23+
import java.util.function.BiFunction;
2324
import java.util.function.Function;
25+
import java.util.function.Predicate;
2426
import java.util.function.Supplier;
2527
import java.util.stream.Collectors;
2628

29+
import org.springframework.boot.ssl.pem.PemCertificate;
2730
import org.springframework.boot.ssl.SslBundle;
2831
import org.springframework.boot.ssl.SslBundleRegistry;
2932

@@ -90,21 +93,33 @@ private Set<WatchablePath> watchedJksPaths(Bundle<JksSslBundleProperties> bundle
9093

9194
private Set<WatchablePath> watchedPemPaths(Bundle<PemSslBundleProperties> bundle) {
9295
List<BundleContentProperty> watched = new ArrayList<>();
96+
BiFunction<String, String, BundleContentProperty> contentKeyStoreCertificateProperty = locationToBundleContentProperty();
9397
watched
9498
.add(new BundleContentProperty("keystore.private-key", bundle.properties().getKeystore().getPrivateKey()));
95-
watched
96-
.add(new BundleContentProperty("keystore.certificate", bundle.properties().getKeystore().getCertificate()));
99+
bundle.properties().getKeystore().getCertificates().stream()
100+
.map(location -> contentKeyStoreCertificateProperty.apply(location, "keystore.certificate"))
101+
.forEach(watched::add);
97102
watched.add(new BundleContentProperty("truststore.private-key",
98103
bundle.properties().getTruststore().getPrivateKey()));
99-
watched.add(new BundleContentProperty("truststore.certificate",
100-
bundle.properties().getTruststore().getCertificate()));
104+
bundle.properties().getTruststore().getCertificates().stream()
105+
.map(location -> contentKeyStoreCertificateProperty.apply(location, "truststore.certificate"))
106+
.forEach(watched::add);
101107
return watchedPaths(bundle.name(), watched);
102108
}
103109

110+
private BiFunction<String, String, BundleContentProperty> locationToBundleContentProperty() {
111+
PemCertificateParser certificateParser = new PemCertificateParser();
112+
return (location, name) -> {
113+
PemCertificate certificate = certificateParser.parse(location);
114+
return new BundleContentProperty(name, certificate.location(), certificate.optional());
115+
};
116+
}
117+
104118
private Set<WatchablePath> watchedPaths(String bundleName, List<BundleContentProperty> properties) {
105119
try {
106120
return properties.stream()
107121
.filter(BundleContentProperty::hasValue)
122+
.filter(Predicate.not(BundleContentProperty::isPemContent))
108123
.map(BundleContentProperty::toWatchPath)
109124
.collect(Collectors.toSet());
110125
}

‎spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/WatchablePath.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@
1818

1919
import java.nio.file.Path;
2020

21-
record WatchablePath(Path path, Boolean optional) {
22-
}
21+
record WatchablePath(boolean optional, Path path) {
22+
}

‎spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/LoadedPemSslStore.java

+7-9
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.io.UncheckedIOException;
2121
import java.security.PrivateKey;
2222
import java.security.cert.X509Certificate;
23+
import java.util.ArrayList;
2324
import java.util.List;
2425
import java.util.function.Supplier;
2526

@@ -58,11 +59,13 @@ private static UncheckedIOException asUncheckedIOException(String message, Excep
5859
}
5960

6061
private static List<X509Certificate> loadCertificates(PemSslStoreDetails details) throws IOException {
61-
PemContent pemContent = PemContent.load(details.certificates(), details.optional());
62-
if (pemContent == null) {
63-
return null;
62+
List<X509Certificate> certificates = new ArrayList<>();
63+
for (PemCertificate certificate : details.certificateSet()) {
64+
PemContent pemContent = PemContent.load(certificate.location(), certificate.optional());
65+
if (pemContent != null) {
66+
certificates.addAll(pemContent.getCertificates());
67+
}
6468
}
65-
List<X509Certificate> certificates = pemContent.getCertificates();
6669
Assert.state(!CollectionUtils.isEmpty(certificates), "Loaded certificates are empty");
6770
return certificates;
6871
}
@@ -72,11 +75,6 @@ private static PrivateKey loadPrivateKey(PemSslStoreDetails details) throws IOEx
7275
return (pemContent != null) ? pemContent.getPrivateKey(details.privateKeyPassword()) : null;
7376
}
7477

75-
@Override
76-
public boolean optional() {
77-
return this.details.optional();
78-
}
79-
8078
@Override
8179
public String type() {
8280
return this.details.type();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.ssl.pem;
18+
19+
public record PemCertificate (String location, boolean optional) {
20+
21+
public PemCertificate(String location) {
22+
this(location, false);
23+
}
24+
}

‎spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java

+9-7
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.boot.ssl.pem;
1818

19+
import java.io.FileNotFoundException;
1920
import java.io.IOException;
2021
import java.io.InputStream;
2122
import java.io.UncheckedIOException;
@@ -28,6 +29,7 @@
2829
import java.util.List;
2930
import java.util.Objects;
3031
import java.util.regex.Pattern;
32+
import java.util.stream.Collectors;
3133

3234
import org.springframework.boot.io.ApplicationResourceLoader;
3335
import org.springframework.core.io.Resource;
@@ -108,25 +110,22 @@ public String toString() {
108110
* Load {@link PemContent} from the given content (either the PEM content itself or a
109111
* reference to the resource to load).
110112
* @param content the content to load
111-
* @param isOptional the content to load may be optional
112113
* @return a new {@link PemContent} instance
113114
* @throws IOException on IO error
114115
*/
115-
static PemContent load(String content, Boolean isOptional) throws IOException {
116-
if (isOptional && !Files.exists(Path.of(content))) {
117-
return null;
118-
}
119-
return load(content);
116+
static PemContent load(String content) throws IOException {
117+
return load(content, false);
120118
}
121119

122120
/**
123121
* Load {@link PemContent} from the given content (either the PEM content itself or a
124122
* reference to the resource to load).
125123
* @param content the content to load
124+
* @param optional if the content is optional
126125
* @return a new {@link PemContent} instance
127126
* @throws IOException on IO error
128127
*/
129-
static PemContent load(String content) throws IOException {
128+
static PemContent load(String content, boolean optional) throws IOException {
130129
if (content == null) {
131130
return null;
132131
}
@@ -138,6 +137,9 @@ static PemContent load(String content) throws IOException {
138137
return load(resource.getInputStream());
139138
}
140139
catch (IOException | UncheckedIOException ex) {
140+
if (ex instanceof FileNotFoundException && optional) {
141+
return null;
142+
}
141143
throw new IOException("Error reading certificate or key from file '%s'".formatted(content), ex);
142144
}
143145
}

‎spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java

-8
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,6 @@
3333
*/
3434
public interface PemSslStore {
3535

36-
37-
boolean optional();
38-
3936
/**
4037
* The key store type, for example {@code JKS} or {@code PKCS11}. A {@code null} value
4138
* will use {@link KeyStore#getDefaultType()}).
@@ -167,11 +164,6 @@ public PrivateKey privateKey() {
167164
return privateKey;
168165
}
169166

170-
@Override
171-
public boolean optional() {
172-
return false; //TODO
173-
}
174-
175167
};
176168
}
177169

‎spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public KeyStore getTrustStore() {
8282
}
8383

8484
private static KeyStore createKeyStore(String name, PemSslStore pemSslStore) {
85-
if (pemSslStore == null || pemSslStore.optional() && pemSslStore.certificates() == null) {
85+
if (pemSslStore == null) {
8686
return null;
8787
}
8888
try {

‎spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java

+92-14
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
package org.springframework.boot.ssl.pem;
1818

1919
import java.security.KeyStore;
20+
import java.util.Collections;
21+
import java.util.Set;
2022

23+
import org.springframework.boot.io.ApplicationResourceLoader;
2124
import org.springframework.util.StringUtils;
2225

2326
/**
@@ -29,20 +32,20 @@
2932
* @param password the password used
3033
* {@link KeyStore#setKeyEntry(String, java.security.Key, char[], java.security.cert.Certificate[])
3134
* setting key entries} in the {@link KeyStore}
32-
* @param certificates the certificates content (either the PEM content itself or or a
35+
* @param certificateSet the set of certificates contents (either the PEM content itself or a
3336
* reference to the resource to load). When a {@link #privateKey() private key} is present
3437
* this value is treated as a certificate chain, otherwise it is treated a list of
3538
* certificates that should all be registered.
3639
* @param privateKey the private key content (either the PEM content itself or a reference
3740
* to the resource to load)
3841
* @param privateKeyPassword a password used to decrypt an encrypted private key
39-
* @param optional certificates/privateKey may be optional
4042
* @author Scott Frederick
4143
* @author Phillip Webb
4244
* @since 3.1.0
4345
* @see PemSslStore#load(PemSslStoreDetails)
4446
*/
45-
public record PemSslStoreDetails(String type, String alias, String password, String certificates, String privateKey, String privateKeyPassword, boolean optional) {
47+
public record PemSslStoreDetails(String type, String alias, String password, Set<PemCertificate> certificateSet, String privateKey,
48+
String privateKeyPassword) {
4649

4750
/**
4851
* Create a new {@link PemSslStoreDetails} instance.
@@ -52,7 +55,7 @@ public record PemSslStoreDetails(String type, String alias, String password, Str
5255
* @param password the password used
5356
* {@link KeyStore#setKeyEntry(String, java.security.Key, char[], java.security.cert.Certificate[])
5457
* setting key entries} in the {@link KeyStore}
55-
* @param certificates the certificate content (either the PEM content itself or a
58+
* @param certificateSet the set of certificate content (either the PEM content itself or a
5659
* reference to the resource to load)
5760
* @param privateKey the private key content (either the PEM content itself or a
5861
* reference to the resource to load)
@@ -62,6 +65,23 @@ public record PemSslStoreDetails(String type, String alias, String password, Str
6265
public PemSslStoreDetails {
6366
}
6467

68+
/**
69+
* Create a new {@link PemSslStoreDetails} instance.
70+
* @param type the key store type, for example {@code JKS} or {@code PKCS11}. A
71+
* {@code null} value will use {@link KeyStore#getDefaultType()}).
72+
* @param alias the alias used when setting entries in the {@link KeyStore}
73+
* @param password the password used
74+
* @param certificates the certificate content (either the PEM content itself or a
75+
* reference to the resource to load)
76+
* @param privateKey the private key content (either the PEM content itself or a
77+
* reference to the resource to load)
78+
* @param privateKeyPassword a password used to decrypt an encrypted private key
79+
*/
80+
@Deprecated
81+
public PemSslStoreDetails(String type, String alias, String password, String certificates, String privateKey, String privateKeyPassword) {
82+
this(type, alias, password, toPemCertificates(certificates), privateKey, privateKeyPassword);
83+
}
84+
6585
/**
6686
* Create a new {@link PemSslStoreDetails} instance.
6787
* @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
7191
* @param privateKey the private key content (either the PEM content itself or a
7292
* reference to the resource to load)
7393
* @param privateKeyPassword a password used to decrypt an encrypted private key
74-
* @param optional certificates/privateKey may be optional
7594
*/
76-
public PemSslStoreDetails(String type, String certificate, String privateKey, String privateKeyPassword, boolean optional) {
77-
this(type, null, null, certificate, privateKey, privateKeyPassword, optional);
95+
@Deprecated
96+
public PemSslStoreDetails(String type, String certificate, String privateKey, String privateKeyPassword) {
97+
this(type, null, null, certificate, privateKey, privateKeyPassword);
7898
}
7999

80100
/**
@@ -86,8 +106,48 @@ public PemSslStoreDetails(String type, String certificate, String privateKey, St
86106
* @param privateKey the private key content (either the PEM content itself or a
87107
* reference to the resource to load)
88108
*/
109+
@Deprecated
89110
public PemSslStoreDetails(String type, String certificate, String privateKey) {
90-
this(type, certificate, privateKey, null, false);
111+
this(type, certificate, privateKey, null);
112+
}
113+
114+
/**
115+
* Create a new {@link PemSslStoreDetails} instance.
116+
* @param type the key store type, for example {@code JKS} or {@code PKCS11}. A
117+
* {@code null} value will use {@link KeyStore#getDefaultType()}).
118+
* @param certificates the set of certificate contents (either the PEM content itself or a
119+
* reference to the resource to load)
120+
* @param privateKey the private key content (either the PEM content itself or a
121+
* reference to the resource to load)
122+
* @param privateKeyPassword a password used to decrypt an encrypted private key
123+
*/
124+
public PemSslStoreDetails(String type, Set<PemCertificate> certificates, String privateKey, String privateKeyPassword) {
125+
this(type, null, null, certificates, privateKey, privateKeyPassword);
126+
}
127+
128+
/**
129+
* Create a new {@link PemSslStoreDetails} instance.
130+
* @param type the key store type, for example {@code JKS} or {@code PKCS11}. A
131+
* {@code null} value will use {@link KeyStore#getDefaultType()}).
132+
* @param certificates the set of certificate contents (either the PEM content itself or a
133+
* reference to the resource to load)
134+
* @param privateKey the private key content (either the PEM content itself or a
135+
* reference to the resource to load)
136+
*/
137+
public PemSslStoreDetails(String type, Set<PemCertificate> certificates, String privateKey) {
138+
this(type, certificates, privateKey, null);
139+
}
140+
141+
/**
142+
* Return the certificate content.
143+
* @return the certificate content
144+
* @deprecated
145+
*/
146+
@Deprecated()
147+
public String certificates() {
148+
return this.certificateSet.stream()
149+
.findAny().map(PemCertificate::location)
150+
.orElse(null);
91151
}
92152

93153
/**
@@ -97,7 +157,8 @@ public PemSslStoreDetails(String type, String certificate, String privateKey) {
97157
* @since 3.2.0
98158
*/
99159
public PemSslStoreDetails withAlias(String alias) {
100-
return new PemSslStoreDetails(this.type, alias, this.password, this.certificates, this.privateKey, this.privateKeyPassword, this.optional);
160+
return new PemSslStoreDetails(this.type, alias, this.password, this.certificateSet, this.privateKey,
161+
this.privateKeyPassword);
101162
}
102163

103164
/**
@@ -107,7 +168,8 @@ public PemSslStoreDetails withAlias(String alias) {
107168
* @since 3.2.0
108169
*/
109170
public PemSslStoreDetails withPassword(String password) {
110-
return new PemSslStoreDetails(this.type, this.alias, password, this.certificates, this.privateKey, this.privateKeyPassword, this.optional);
171+
return new PemSslStoreDetails(this.type, this.alias, password, this.certificateSet, this.privateKey,
172+
this.privateKeyPassword);
111173
}
112174

113175
/**
@@ -116,7 +178,8 @@ public PemSslStoreDetails withPassword(String password) {
116178
* @return a new {@link PemSslStoreDetails} instance
117179
*/
118180
public PemSslStoreDetails withPrivateKey(String privateKey) {
119-
return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificates, privateKey, this.privateKeyPassword, this.optional);
181+
return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificateSet, privateKey,
182+
this.privateKeyPassword);
120183
}
121184

122185
/**
@@ -125,17 +188,25 @@ public PemSslStoreDetails withPrivateKey(String privateKey) {
125188
* @return a new {@link PemSslStoreDetails} instance
126189
*/
127190
public PemSslStoreDetails withPrivateKeyPassword(String privateKeyPassword) {
128-
return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificates, this.privateKey, privateKeyPassword, this.optional);
191+
return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificateSet, this.privateKey, privateKeyPassword);
129192
}
130193

131194
boolean isEmpty() {
132-
return isEmpty(this.type) && isEmpty(this.certificates) && isEmpty(this.privateKey);
195+
return isEmpty(this.type) && isCertificatesEmpty() && isEmpty(this.privateKey);
133196
}
134197

135198
private boolean isEmpty(String value) {
136199
return !StringUtils.hasText(value);
137200
}
138201

202+
private boolean isContentEmpty(PemCertificate value) {
203+
return value.optional() ? !new ApplicationResourceLoader().getResource(value.location()).exists() : isEmpty(value.location());
204+
}
205+
206+
boolean isCertificatesEmpty() {
207+
return this.certificateSet == null || this.certificateSet.isEmpty() || this.certificateSet.stream().allMatch(this::isContentEmpty);
208+
}
209+
139210
/**
140211
* Factory method to create a new {@link PemSslStoreDetails} instance for the given
141212
* certificate. <b>Note:</b> This method doesn't actually check if the provided value
@@ -161,4 +232,11 @@ public static PemSslStoreDetails forCertificates(String certificates) {
161232
return new PemSslStoreDetails(null, certificates, null);
162233
}
163234

164-
}
235+
private static Set<PemCertificate> toPemCertificates(String certificates) {
236+
if (certificates != null) {
237+
return Set.of(new PemCertificate(certificates));
238+
}
239+
return Collections.emptySet();
240+
}
241+
242+
}

‎spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.boot.web.server;
1818

1919
import java.security.KeyStore;
20+
import java.util.Set;
2021

2122
import org.springframework.boot.ssl.NoSuchSslBundleException;
2223
import org.springframework.boot.ssl.SslBundle;
@@ -27,6 +28,7 @@
2728
import org.springframework.boot.ssl.SslStoreBundle;
2829
import org.springframework.boot.ssl.jks.JksSslStoreBundle;
2930
import org.springframework.boot.ssl.jks.JksSslStoreDetails;
31+
import org.springframework.boot.ssl.pem.PemCertificate;
3032
import org.springframework.boot.ssl.pem.PemSslStoreBundle;
3133
import org.springframework.boot.ssl.pem.PemSslStoreDetails;
3234
import org.springframework.core.style.ToStringCreator;
@@ -61,15 +63,15 @@ private WebServerSslBundle(SslStoreBundle stores, String keyPassword, Ssl ssl) {
6163
}
6264

6365
private static SslStoreBundle createPemKeyStoreBundle(Ssl ssl) {
64-
PemSslStoreDetails keyStoreDetails = new PemSslStoreDetails(ssl.getKeyStoreType(), ssl.getCertificate(),
66+
PemSslStoreDetails keyStoreDetails = new PemSslStoreDetails(ssl.getKeyStoreType(), Set.of(new PemCertificate(ssl.getCertificate())),
6567
ssl.getCertificatePrivateKey())
6668
.withAlias(ssl.getKeyAlias());
6769
return new PemSslStoreBundle(keyStoreDetails, null);
6870
}
6971

7072
private static SslStoreBundle createPemTrustStoreBundle(Ssl ssl) {
7173
PemSslStoreDetails trustStoreDetails = new PemSslStoreDetails(ssl.getTrustStoreType(),
72-
ssl.getTrustCertificate(), ssl.getTrustCertificatePrivateKey())
74+
Set.of(new PemCertificate(ssl.getTrustCertificate())), ssl.getTrustCertificatePrivateKey())
7375
.withAlias(ssl.getKeyAlias());
7476
return new PemSslStoreBundle(null, trustStoreDetails);
7577
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
plugins {
2+
id "java"
3+
id "org.springframework.boot.conventions"
4+
}
5+
6+
description = "Spring Boot Tomcat SSL with multiple/optional certificates smoke test"
7+
8+
dependencies {
9+
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web"))
10+
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-actuator"))
11+
12+
testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test"))
13+
testImplementation("org.apache.httpcomponents.client5:httpclient5")
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2012-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package smoketest.tomcat.ssl;
18+
19+
import org.springframework.boot.SpringApplication;
20+
import org.springframework.boot.autoconfigure.SpringBootApplication;
21+
import org.springframework.boot.ssl.SslBundles;
22+
import org.springframework.boot.web.client.RestTemplateBuilder;
23+
import org.springframework.context.annotation.Bean;
24+
import org.springframework.web.client.RestTemplate;
25+
26+
@SpringBootApplication
27+
public class SampleTomcatSslApplication {
28+
29+
public static void main(String[] args) {
30+
SpringApplication.run(SampleTomcatSslApplication.class, args);
31+
}
32+
33+
@Bean
34+
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder, SslBundles sslBundles) {
35+
return restTemplateBuilder.setSslBundle(sslBundles.getBundle("rest")).build();
36+
}
37+
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2012-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package smoketest.tomcat.ssl.web;
18+
19+
import org.springframework.web.bind.annotation.GetMapping;
20+
import org.springframework.web.bind.annotation.RestController;
21+
22+
@RestController
23+
public class SampleController {
24+
25+
@GetMapping("/")
26+
public String helloWorld() {
27+
return "Hello, world";
28+
}
29+
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDBzCCAe+gAwIBAgIUSGye6EtFNkG/v6jyTLmzZkFm81YwDQYJKoZIhvcNAQEL
3+
BQAwEzERMA8GA1UEAwwITXlSb290Q0EwHhcNMjQwODEzMTcxODA5WhcNMzQwODEx
4+
MTcxODA5WjATMREwDwYDVQQDDAhNeVJvb3RDQTCCASIwDQYJKoZIhvcNAQEBBQAD
5+
ggEPADCCAQoCggEBAMStCYIWTOZcg8Ri5I3BJ6ctl5N9kouEr5S6A47UHg7N32NJ
6+
cQ6sanhO5Y/l4vxZvd0AFpqG3Y5bFrtePn/TjY9qA9nXS4pavpGG1bLF9T7lm3Sp
7+
AFc5FzhqmzxmzD7Eu9fPKxYCOycqyEtxxiUBhrPPgt+u6PwpDmO8uUhmzWts3HGn
8+
LjfUSwWkHKvflT7nyZBzAj4biE7Y41LMwljb8Ox02+DLlYuT/4PRkB8erag4SSrK
9+
2k1HvqFmaKQyx8FrLdqkyCGm3xB/DYfb2PfRpi4JxpLyumppcgBUgY73vP6D//M2
10+
N+6LaBXZNmF87CG9TRa4QhNbSYtiHjkiFMHBWfsCAwEAAaNTMFEwHQYDVR0OBBYE
11+
FIwxgudDLzOSPbM/nYNfUTlI5UF4MB8GA1UdIwQYMBaAFIwxgudDLzOSPbM/nYNf
12+
UTlI5UF4MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAKxEzFHn
13+
9bywfoMeCsVjyCgIgKHupgLYH/tF109faTXAO0MtwI+xcI5UjOvrhmoGYjp860Rf
14+
TQWWtcl+/D8YCc4SmPHURZZrFWRQVsgUJHWvjjgIdXJXh9Jz6WqnYac2U3rAmWEs
15+
CfGrizKnDgNTskCKbdm458GCTKg1/jIROCfbI7Wzjwr/AfY5hV0XFYumguWUL8rR
16+
SHtR/sU4KLbGLSuUfB6hIz4kSuNgKIVRzW/igSZ25YYh9/9GeygjvdBbYUt6FHGl
17+
V07PHt4GrYdRl6a0iFKS4Y3JfuQrWo9nU4nzcxYWCoqYzmjNlxN4pm3kBzAeLPG3
18+
9pcSDSuYRqCV3hM=
19+
-----END CERTIFICATE-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDErQmCFkzmXIPE
3+
YuSNwSenLZeTfZKLhK+UugOO1B4Ozd9jSXEOrGp4TuWP5eL8Wb3dABaaht2OWxa7
4+
Xj5/042PagPZ10uKWr6RhtWyxfU+5Zt0qQBXORc4aps8Zsw+xLvXzysWAjsnKshL
5+
ccYlAYazz4Lfruj8KQ5jvLlIZs1rbNxxpy431EsFpByr35U+58mQcwI+G4hO2ONS
6+
zMJY2/DsdNvgy5WLk/+D0ZAfHq2oOEkqytpNR76hZmikMsfBay3apMghpt8Qfw2H
7+
29j30aYuCcaS8rpqaXIAVIGO97z+g//zNjfui2gV2TZhfOwhvU0WuEITW0mLYh45
8+
IhTBwVn7AgMBAAECggEAGIyy3aKT+cM9jWN8vPcJ0JPf0kC/7Jtk4U8wx4DRua5X
9+
/og5zQeXiKnsfMBIy5AWI4Jxz9sax7y2AzBZ49HP30Fv9p6pprz6AadPgG+2U6IM
10+
fAzmZnzRWbDw7KK2RvV+rwsEiUxA/vwXoVcz0QW2PzadUvd9zJABZFC33gI7DPgC
11+
0pQhIXo+S4kzzUb/iiXVIkIxnjV3wIK/Yqs1SoEKqXM6MPcV1lR0sK0bCVim3MnA
12+
STG18IvMgcF3PkfR0aptwa/dnc6+zMX2dAqyFtCna4rqQWd6sVCo2pdAlCuW10v4
13+
E+dLm6QSpAFwdUa7FjuQM+ew/8ksPWg4VbFgtI+/tQKBgQDIeDnSciYP6KcxvhBu
14+
RDh4IXJbFzjRENNpv5LFdn3ZntdW50JvUghZ5O7oCOdbPKE/d2z5LNQPCSXxuP+A
15+
a1pZHjghxJ1im0Ko9XeWh8Pw8iYpkH3afA+TiN8BDvqlNEdAZRrz70MimAUfROEz
16+
J3P+IPf91itwJXd0V9XDNGx+lwKBgQD7J8pniQ1zJuR/c1i948AZNhGSOCQ7GZRh
17+
w1aHdninhYJ+IYlP3TAckJMlz8iA7MiO56y0YlFy3f2+8x2ewZl+eH9Cn86WbmvX
18+
z8UgM6oPisVHuUCwZuphDGIsMk/Vb02WnGlr/ipsgzljznIfH0lngtPQB5s3xzHx
19+
j8FQaWhQPQKBgQCAeQwVYjIiX+dGaZf+Eppd4pF27xrqYO4cBzn4ckeU/8bhWrOo
20+
w9m2QpEZAxvBzMlJ8y9TQPdl62b10qlrk2EDW+p9OZPjbbz6qtVJExjvgUATwxXk
21+
vzz8P+sqsn7PAQHosuLjEaLkuKgPsgTg05fydQ55Dpgn9trnJKNJxn8BYQKBgQCC
22+
0X8D3sc6q49pM1ON1QtCFn+ggc2dWv2GzpBLjtHZsBkASceT6codltCOaWQugycU
23+
CGhUrMFv62E4DLno7z5cObdPpJ2ejXVuu7IZy89QuR949G1VdMWwNxsLmkkrCwaG
24+
5IGk1oaSbud9rRKUU1+Qovxg5xVaQE8rW419rOnAoQKBgF+aBsPRU0mZkXVcHqzh
25+
x0jbaEfeQXCzFDh3l79IH/rQovNkT2me79eISLMlTkDms3cc9HsRypAVKtBn/adh
26+
tSKP7HFK84eT9t6vLK8xlmuwoCsREbCQCXfapOtQlhPsF0l5cwftQXMcD1pYQNoQ
27+
7B1UpDj+bh5/TN9PgW+lSiO5
28+
-----END PRIVATE KEY-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIBtTCCAVugAwIBAgIUeFllWZx5HIrt1LgcFMND3XCc1NQwCgYIKoZIzj0EAwQw
3+
GzEZMBcGA1UEAwwQUm9vdCBDQSBmb3IgVGVzdDAgFw0yNDA4MjYxMzQwMTdaGA8y
4+
MTI0MDgwMjEzNDAxN1owGzEZMBcGA1UEAwwQUm9vdCBDQSBmb3IgVGVzdDBZMBMG
5+
ByqGSM49AgEGCCqGSM49AwEHA0IABNTroEMKMGFp72qfzemTzpMU3Mo6GBXyXSGF
6+
yRnmACyRKBo6keXKwtOD6jG6yF/11Ri11TvxJF2p8vF1ELDnVtCjezB5MA8GA1Ud
7+
EwEB/wQFMAMBAf8wKQYDVR0OBCIEIEwqs84LwXPgYvkfUlt9fHTa/x8+uL4Z6QIZ
8+
5tSgFT5WMCsGA1UdIwQkMCKAIEwqs84LwXPgYvkfUlt9fHTa/x8+uL4Z6QIZ5tSg
9+
FT5WMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDBANIADBFAiEA5mHBVaR87e/+
10+
OhBNCmNNNYLQ5kSCoOiF2KMrqXfLxKMCIEj6fiKz9B2ULOFA72imwvFNR7pLiz7Z
11+
QU+hkNen9eMI
12+
-----END CERTIFICATE-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIICqzCCAZMCFDLPXWKQ/Lb6t8VFdOdDiyjFYcy0MA0GCSqGSIb3DQEBCwUAMBMx
3+
ETAPBgNVBAMMCE15Um9vdENBMB4XDTI0MDgxMzE3MjAwNloXDTM0MDgxMTE3MjAw
4+
NlowETEPMA0GA1UEAwwGY2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
5+
CgKCAQEAnBHo9hDs0m6WyaDB0NRQwtsUbBmqTFUG7D3JNqhGoOHTP9NG9SujMYOO
6+
gnTEYKIJr9Tao4RdQqqNJ5RdUyy2wVANwZD3DG2BNVmjkmGo6fLWZin+S8xYHrG7
7+
a9M5DXbaPLgtpg4Dpg2T7xbg+oPMze81BakAGazzfzyCJW+vRbsXnJ5gnCUCgF97
8+
J2om9RQOHd2yxpG3z4B6+dr0YhGLLX/gE3O8dIv81BwhY/kJrgicUx3bW+tBQKMQ
9+
TbaGYTpCKN2KDFuWPXNLvSMmd67pXOHtRITK7tYrpYIKyzWNbHj6CdFfpvk162+n
10+
L2MLTmE5pETE/KqE6ybVn/9Hi6rJ0QIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQDC
11+
psAvRyFDMGJUwT54nxhcrpPIsUZftSlwusyYGJYQNpGS6jo1U76J+L25iOzZEdnY
12+
f8dC2HqVD5mgn2XAulajsjsfuIbIZ9m/in4/p3ugvR3ZBYEiB5WHRD0cZqIBxpv7
13+
n5RFPrvCNMUct6QN6w9S/nyb3JHg4BqBWnUSyaE/7aEbgpCgUIqrv+WgMvu1ttmB
14+
vS8uc+XY/YuR7zXAfmXYkYdbAVynCwWi6kF0wbfHx6NUTLNWA2sK/SdRZEL4WL5f
15+
/FQ071BOFpYWKuKdUKo6R8xVuCCXGEwvgXTNGIkAMBa2mt/HEkk3U6YXL+LlBi80
16+
CDuQrDhQTIK7f+4H0Bs7
17+
-----END CERTIFICATE-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCcEej2EOzSbpbJ
3+
oMHQ1FDC2xRsGapMVQbsPck2qEag4dM/00b1K6Mxg46CdMRgogmv1NqjhF1Cqo0n
4+
lF1TLLbBUA3BkPcMbYE1WaOSYajp8tZmKf5LzFgesbtr0zkNdto8uC2mDgOmDZPv
5+
FuD6g8zN7zUFqQAZrPN/PIIlb69FuxecnmCcJQKAX3snaib1FA4d3bLGkbfPgHr5
6+
2vRiEYstf+ATc7x0i/zUHCFj+QmuCJxTHdtb60FAoxBNtoZhOkIo3YoMW5Y9c0u9
7+
IyZ3rulc4e1EhMru1iulggrLNY1sePoJ0V+m+TXrb6cvYwtOYTmkRMT8qoTrJtWf
8+
/0eLqsnRAgMBAAECggEAB70snd0HfUTOFdfynGoWyh7Gc7jPFMNnmTnUGzF7dRlV
9+
mhXeMCSWjks9exMStM78J3uoztButnJSFwsYmJoAQvQ3BmjrkzJv5IcaIRVWJKmt
10+
v3moGjabDQSXrFhYPSZuWnHwk4og3LBSLFooVEvKUVDiAnKXpm5I0b+cnYIdARpa
11+
xmtb5iVPcbjElY4geqts3byLqL0hnJwfDbO5zcnV9pNWLVcDc7bHB8yBwdoiwWHt
12+
JVn09zOouuIFKgLPDSZNTLKLffxYg1qt0B2CXQOK2EAq+xxE3IR2RoyaLbvzqylk
13+
Tr7dz4rYMVgC/n+PAMwkHsW3CSBQl53qDNhm4kE7ZQKBgQDYE8mwje4Trxm04vUv
14+
qmVcNjqnPnTQDxBD5V5/ixEkPjy2Am+kkBTq1gGj7RVMTeQ1ROZv2Vk/nDEDfRb5
15+
ZWOabgmsE6xImDcncz4Aa7YvHL3GTjGVQRKO9CITL+55ahOfGpguPC3s2DZ8lkgh
16+
L71Z4QExdPUtSahFdDa07T2W1QKBgQC459fLQJg2mVNMuyeWFFC2lEene/9+ll+o
17+
02VQ0ZguqBrKsfBbWHaeJcOOoPvWJiErqCKjQDE8gP8iBdGCUB6y/qVLwxdJaku+
18+
68rW4DMcddNZkra0gQ2oT7QAo6Mifrzqlw9l96vlCQuRelHNFSfyhKGC4bODZVlU
19+
JLyj0aQdDQKBgQCH0jBmTWDIeLlU7ZCnTJl4FBJcTDMLEVztAMGctGKrAIAS/IcG
20+
zxaG4syXKRDJLPD01wFubxXdmSVqBvgo/iVUzjRAOQGDhEKvBo6DnzEefheADmi2
21+
Y/fxad39Z5SkNxxsV0AvV96aUPI28BQY4DRKydeBKf5vYCxos/srUTD0nQKBgCpE
22+
bRK8KE9KyzzeB1WKPU0PJjYF5UiFjUZlVGKeFsCLktxEwqHO3gaWsVY4PHkebDSz
23+
kX9p3BdtkWSwmczFDc9y4EwqQ3d3werZsZte0rAtyutN20/1tC6GUapXvaHUANFL
24+
SKzRaczIPYm6wVo0/NW2NclaWJOvpjTS1QBJms89AoGBANd66yaWtLPD9hfQb2um
25+
mvfVeBJ1DxpICOIhW0A12/EReTKjVmopwzU50PdoF96Zh/ZyhzFRS7UF4yakVBYT
26+
XIWKKUEtMrPaEuA9VaXE+f0r6c7UPpW88zSl2OH1s0DLI0mvN/bLrctFc/23vDcL
27+
mGVyArVYEK1Yi68Lpdc90uLx
28+
-----END PRIVATE KEY-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIICrjCCAZYCFHFhRF432nj9MmqucJFrWBdk+2mmMA0GCSqGSIb3DQEBCwUAMBMx
3+
ETAPBgNVBAMMCE15Um9vdENBMB4XDTI0MDgxMzE3MTkxM1oXDTM0MDgxMTE3MTkx
4+
M1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
5+
MIIBCgKCAQEAo3p6vtlcNkfab9LXVKtNEghSgQ9sRbpGnoQdxQByYsbI3a7tSVHd
6+
odZ9IxJ2cTi7hZCESCyZsW0HEc4LQxl/BRyOVLm+jyOexsjk18QHi+xs3lF3T/BQ
7+
itmZBEd+rf2n9oYv2koTs4V/xwNNCwc2GShor7m07+u1ga2JbVGLnn139HyK8Ubb
8+
3xMQlZWod+/NfUCMLnUdELpLBVlCfzBeF6HIGdDYKgDAI0/ODVBlvOHXn/HZz8ti
9+
2dQHP51Tzs9OLRtMAZcXbgD0UiDz4niUZNG69muigfCCLV+5d3lyOnTUG+IbuVjP
10+
f4/avfLYbavasU6KXjh/XTNxYxskr/Pd2wIDAQABMA0GCSqGSIb3DQEBCwUAA4IB
11+
AQC85jPmxXeJWPppTyYBiB/xM0nnDkzrDLXB+8XTdp6dIDDZ9C68P63TNmkwReq3
12+
+pi2YR1mWRlmW7kB5FIm8Tp+KOowPjH2DBUcEpVy59uZtRZALRoSi5BON1cVaXe0
13+
qqw0iGcCHPJfvXmGqNDgufh+v5Jf+zpAroGiJCE1LkzY+Q5J36L9nT61yg1g5b4S
14+
l1ShRW6E4ryP7BzJ6FaXBq02lYLGPdpyAUKPILS9QJw/yjsK89QM7VCtuP6r52H9
15+
MlucemDDtM06C4NCEk/lWhUJZ9+v401NKuwTapbxB3KQVCC3vMJsZBVNMBM/hPI3
16+
3rbjIVwxznNO9gUNslyYV+hr
17+
-----END CERTIFICATE-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCjenq+2Vw2R9pv
3+
0tdUq00SCFKBD2xFukaehB3FAHJixsjdru1JUd2h1n0jEnZxOLuFkIRILJmxbQcR
4+
zgtDGX8FHI5Uub6PI57GyOTXxAeL7GzeUXdP8FCK2ZkER36t/af2hi/aShOzhX/H
5+
A00LBzYZKGivubTv67WBrYltUYuefXf0fIrxRtvfExCVlah37819QIwudR0QuksF
6+
WUJ/MF4XocgZ0NgqAMAjT84NUGW84def8dnPy2LZ1Ac/nVPOz04tG0wBlxduAPRS
7+
IPPieJRk0br2a6KB8IItX7l3eXI6dNQb4hu5WM9/j9q98thtq9qxTopeOH9dM3Fj
8+
GySv893bAgMBAAECggEABOmt3aVnlYQERv8FlJhPSR7x58oAXXoTHDlpMZ3pUhma
9+
OLtEi0MID5CEEzU/VPi4/fMRXp/kgIX/w+O2x+3wuMUaa+ZnGSMfZubrpaZQ+b4B
10+
qY62MLNOoFWYuR2y62SnkwuGTZ+TRv5YkDEDtDSjxg7GUp2Yl+sz+bEu45ejRACB
11+
+cwcLzpCroCTmSFRUVXtM1zGSN49OSyfLRZ5lrbwDl2zZYnaolNBOrI18spQwK9G
12+
/WFaV+aLL0aHbbtBS/yl7TwKqPpk/1AKSS1nHm0Y+bkFiwEYvxqu3K5GogAPPHUn
13+
9nYQxLwee0mWpcqSm6hWKuTbQKjOlUlzw7eyZlRrEQKBgQDJoNU0e9Vdo/L4vrXZ
14+
/t/lquSK/1UvZ13XgeojFL9dEAekk9lCcUNQNVmUGZNAkXyDNY3e2ANZlkYxik2q
15+
1v32yrCwjaRnFSUqnhV6RDhxMIaiCG7NdKgYk2j6k7SlejGHdJ5O24R19VmPRNQa
16+
6PwxPsCEKy1Gygf575PKp2KIiwKBgQDPkATtY6SWgORrEOwYvOGP6ooQcD4jKRa8
17+
NhMzzGEtDd9zPH6dLB0Y2Qrf8HR6w0ayPgWmL8DgRypv1VPyHdg927Gn/l86KpR6
18+
1z7YM9T63j7YMfM+9hcWsMH/QpDXV6Qlg3N5UFvDW4oTqVCaANOPSoRi8IzYxAnc
19+
CRQKzktZ8QKBgDOu3VfhsjSZlOt7/yNM+NlnL8QNZSmMhnp6W6j4ZYEWXc8q8tLc
20+
M5P4yOh0kdFIObFsZdxMZLdvFLkYKYZ0K486L4ZiGFUwD2HYOcsod4tUE/6uyLAz
21+
ie8awhsRB4ovQ0jkdLvj+xU9eeKGkxP+yr5YxoJaivWNTfQcHDcjJte3AoGBAJef
22+
Oxo2icqviSx1BiLkB1ncGNL9S0bgAw2l6s0R5YLF+Y7yiANEcFTwZ7NCsbPj5kba
23+
a8IEbD7pfaSID3R0PLyjOdngRav14tUBW5UP9+ryYrIHewtpNWCL6osPE0NbcDs/
24+
FSFvhDjnK6xFKO324JRx+NdVpW3LdvBXaV6jaAPhAoGARFuptdv5KbqHlqLgIa/E
25+
uH8GZfvL0oNGEldW/2Mz/mS1k63MwF563zhY1StD7NPgF7rSNZevAUg2iBaGJ7f+
26+
RSbmY2XZwR55ST2lPL14fivoTcH4U19lWVx3lAH7uxjbFv9tWEKV85Nuc3yVhFHB
27+
xnioJi1sp+ODi956s7OnKy4=
28+
-----END PRIVATE KEY-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package smoketest.tomcat.ssl;
18+
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.nio.file.Files;
22+
import java.nio.file.Path;
23+
import java.nio.file.Paths;
24+
import java.time.Duration;
25+
26+
import org.awaitility.Awaitility;
27+
import org.junit.jupiter.api.Test;
28+
29+
import org.springframework.beans.factory.annotation.Autowired;
30+
import org.springframework.beans.factory.annotation.Value;
31+
import org.springframework.boot.io.ApplicationResourceLoader;
32+
import org.springframework.boot.test.context.SpringBootTest;
33+
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
34+
import org.springframework.boot.test.web.server.LocalServerPort;
35+
import org.springframework.context.ApplicationContextInitializer;
36+
import org.springframework.context.ConfigurableApplicationContext;
37+
import org.springframework.core.io.Resource;
38+
import org.springframework.http.HttpStatus;
39+
import org.springframework.http.ResponseEntity;
40+
import org.springframework.test.context.ContextConfiguration;
41+
import org.springframework.web.client.ResourceAccessException;
42+
import org.springframework.web.client.RestTemplate;
43+
44+
import static org.assertj.core.api.Assertions.assertThat;
45+
import static org.junit.jupiter.api.Assertions.assertThrows;
46+
47+
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = {
48+
"spring.ssl.bundle.pem.server.keystore.certificates=classpath:certs/server.crt",
49+
"spring.ssl.bundle.pem.server.keystore.private-key=classpath:certs/server.key",
50+
"spring.ssl.bundle.pem.server.keystore.private-key-password=123456",
51+
"spring.ssl.bundle.pem.server.truststore.certificates[0]=optional:${user.dir}/build/resources/main/newca.crt",
52+
"spring.ssl.bundle.pem.server.truststore.certificates[1]=optional:classpath:certs/ca_cert.crt",
53+
"spring.ssl.bundle.pem.server.reload-on-update=true",
54+
"spring.ssl.bundle.pem.rest.keystore.certificates=classpath:certs/client.crt",
55+
"spring.ssl.bundle.pem.rest.keystore.private-key=classpath:certs/client.key",
56+
"spring.ssl.bundle.pem.rest.truststore.certificates=classpath:certs/ca.crt",
57+
"server.ssl.client-auth=need",
58+
"server.port=8443",
59+
"server.ssl.bundle=server",
60+
"spring.ssl.bundle.watch.file.quiet-period=5ms"})
61+
@ContextConfiguration(initializers = { SampleTomcatSslWithOptionalMultipleCertsApplicationTests.Initializer.class})
62+
class SampleTomcatSslWithOptionalMultipleCertsApplicationTests {
63+
64+
private static final String OPTIONAL_PREFIX = "optional:";
65+
66+
@LocalServerPort
67+
int port;
68+
69+
@Autowired
70+
private RestTemplate restTemplate;
71+
72+
@Value("${spring.ssl.bundle.pem.server.truststore.certificates[0]}")
73+
private String targetFile;
74+
75+
public static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
76+
77+
@Override
78+
public void initialize(ConfigurableApplicationContext applicationContext) {
79+
try {
80+
Resource resource = new ApplicationResourceLoader().getResource("classpath:newca.crt");
81+
if (resource.exists()) {
82+
resource.getFile().delete();
83+
}
84+
}
85+
catch (IOException e) {
86+
throw new RuntimeException(e);
87+
}
88+
}
89+
}
90+
91+
@Test
92+
void testHome() {
93+
final String URL = "https://localhost:%s/".formatted(this.port);
94+
95+
ResourceAccessException exception = assertThrows(ResourceAccessException.class, () -> this.restTemplate.getForEntity(URL, String.class));
96+
String expectedMessage = "Received fatal alert: bad_certificate";
97+
assertThat(exception.getCause().getMessage()).isEqualTo(expectedMessage);
98+
99+
copyCaFileFromResourceToExpectTruststoreCertificateProperty();
100+
101+
Awaitility.await().atMost(Duration.ofMinutes(1)).ignoreExceptions().untilAsserted(() -> {
102+
ResponseEntity<String> entity = this.restTemplate.getForEntity(URL, String.class);
103+
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
104+
assertThat(entity.getBody()).isEqualTo("Hello, world");
105+
});
106+
}
107+
108+
private void copyCaFileFromResourceToExpectTruststoreCertificateProperty() {
109+
Path sourcePath = Paths.get("src/main/resources/certs", "ca.crt");
110+
if (this.targetFile != null) {
111+
Path pathFile = Paths.get(this.targetFile.replace(OPTIONAL_PREFIX, ""));
112+
try {
113+
try (InputStream inputStream = Files.newInputStream(sourcePath)) {
114+
Files.copy(inputStream, pathFile);
115+
}
116+
}
117+
catch (IOException e) {
118+
e.printStackTrace();
119+
}
120+
}
121+
}
122+
123+
}

0 commit comments

Comments
 (0)
Please sign in to comment.