Skip to content

Commit 4492041

Browse files
Add support for multiple/optional certificates
1 parent c3efeaf commit 4492041

File tree

26 files changed

+590
-84
lines changed

26 files changed

+590
-84
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+
}

0 commit comments

Comments
 (0)