Skip to content

Commit c3efeaf

Browse files
Add support for optional trust certificates
1 parent 2ef3195 commit c3efeaf

File tree

10 files changed

+117
-36
lines changed

10 files changed

+117
-36
lines changed

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

+18-5
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
*/
3636
record BundleContentProperty(String name, String value) {
3737

38+
private static final String OPTIONAL_URL_PREFIX = "optional:";
39+
3840
/**
3941
* Return if the property value is PEM content.
4042
* @return if the value is PEM content
@@ -51,13 +53,24 @@ boolean hasValue() {
5153
return StringUtils.hasText(this.value);
5254
}
5355

54-
Path toWatchPath() {
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+
67+
WatchablePath toWatchPath() {
5568
try {
56-
Resource resource = getResource();
69+
Resource resource = getResource(getRawValue());
5770
if (!resource.isFile()) {
5871
throw new BundleContentNotWatchableException(this);
5972
}
60-
return Path.of(resource.getFile().getAbsolutePath());
73+
return new WatchablePath(Path.of(resource.getFile().getAbsolutePath()), isOptional());
6174
}
6275
catch (Exception ex) {
6376
if (ex instanceof BundleContentNotWatchableException bundleContentNotWatchableException) {
@@ -68,9 +81,9 @@ Path toWatchPath() {
6881
}
6982
}
7083

71-
private Resource getResource() {
84+
private Resource getResource(String value) {
7285
Assert.state(!isPemContent(), "Value contains PEM content");
73-
return new ApplicationResourceLoader().getResource(this.value);
86+
return new ApplicationResourceLoader().getResource(value);
7487
}
7588

7689
}

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

+14-6
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ class FileWatcher implements Closeable {
7474
* @param paths the files or directories to watch
7575
* @param action the action to take when changes are detected
7676
*/
77-
void watch(Set<Path> paths, Runnable action) {
77+
void watch(Set<WatchablePath> paths, Runnable action) {
7878
Assert.notNull(paths, "Paths must not be null");
7979
Assert.notNull(action, "Action must not be null");
8080
if (paths.isEmpty()) {
@@ -133,7 +133,11 @@ private void onThreadException(Thread thread, Throwable throwable) {
133133
}
134134

135135
void register(Registration registration) throws IOException {
136-
for (Path path : registration.paths()) {
136+
for (WatchablePath watchablePath : registration.paths()) {
137+
Path path = watchablePath.path();
138+
if (watchablePath.optional() && !Files.exists(path)) {
139+
path = path.getParent();
140+
}
137141
if (!Files.isRegularFile(path) && !Files.isDirectory(path)) {
138142
throw new IOException("'%s' is neither a file nor a directory".formatted(path));
139143
}
@@ -210,19 +214,23 @@ public void close() throws IOException {
210214
/**
211215
* An individual watch registration.
212216
*/
213-
private record Registration(Set<Path> paths, Runnable action) {
217+
private record Registration(Set<WatchablePath> paths, Runnable action) {
214218

215219
Registration {
216-
paths = paths.stream().map(Path::toAbsolutePath).collect(Collectors.toSet());
220+
paths = paths.stream().map(watchablePath ->
221+
new WatchablePath(watchablePath.path().toAbsolutePath(), watchablePath.optional()))
222+
.collect(Collectors.toSet());
217223
}
218224

219225
boolean manages(Path file) {
220226
Path absolutePath = file.toAbsolutePath();
221-
return this.paths.contains(absolutePath) || isInDirectories(absolutePath);
227+
return this.paths.stream()
228+
.map(WatchablePath::path)
229+
.anyMatch(absolutePath::equals) || isInDirectories(absolutePath);
222230
}
223231

224232
private boolean isInDirectories(Path file) {
225-
return this.paths.stream().filter(Files::isDirectory).anyMatch(file::startsWith);
233+
return this.paths.stream().map(WatchablePath::path).filter(Files::isDirectory).anyMatch(file::startsWith);
226234
}
227235
}
228236

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

+16-2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.springframework.boot.ssl.pem.PemSslStoreDetails;
3030
import org.springframework.core.style.ToStringCreator;
3131
import org.springframework.util.Assert;
32+
import org.springframework.util.StringUtils;
3233

3334
/**
3435
* {@link SslBundle} backed by {@link JksSslBundleProperties} or
@@ -40,6 +41,8 @@
4041
*/
4142
public final class PropertiesSslBundle implements SslBundle {
4243

44+
private static final String OPTIONAL_URL_PREFIX = "optional:";
45+
4346
private final SslStoreBundle stores;
4447

4548
private final SslBundleKey key;
@@ -118,8 +121,19 @@ private static PemSslStore getPemSslStore(String propertyName, PemSslBundlePrope
118121
}
119122

120123
private static PemSslStoreDetails asPemSslStoreDetails(PemSslBundleProperties.Store properties) {
121-
return new PemSslStoreDetails(properties.getType(), properties.getCertificate(), properties.getPrivateKey(),
122-
properties.getPrivateKeyPassword());
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;
123137
}
124138

125139
/**

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

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

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

19-
import java.nio.file.Path;
2019
import java.util.ArrayList;
2120
import java.util.List;
2221
import java.util.Map;
@@ -54,13 +53,13 @@ public void registerBundles(SslBundleRegistry registry) {
5453
}
5554

5655
private <P extends SslBundleProperties> void registerBundles(SslBundleRegistry registry, Map<String, P> properties,
57-
Function<P, SslBundle> bundleFactory, Function<Bundle<P>, Set<Path>> watchedPaths) {
56+
Function<P, SslBundle> bundleFactory, Function<Bundle<P>, Set<WatchablePath>> watchedPaths) {
5857
properties.forEach((bundleName, bundleProperties) -> {
5958
Supplier<SslBundle> bundleSupplier = () -> bundleFactory.apply(bundleProperties);
6059
try {
6160
registry.registerBundle(bundleName, bundleSupplier.get());
6261
if (bundleProperties.isReloadOnUpdate()) {
63-
Supplier<Set<Path>> pathsSupplier = () -> watchedPaths
62+
Supplier<Set<WatchablePath>> pathsSupplier = () -> watchedPaths
6463
.apply(new Bundle<>(bundleName, bundleProperties));
6564
watchForUpdates(registry, bundleName, pathsSupplier, bundleSupplier);
6665
}
@@ -71,7 +70,7 @@ private <P extends SslBundleProperties> void registerBundles(SslBundleRegistry r
7170
});
7271
}
7372

74-
private void watchForUpdates(SslBundleRegistry registry, String bundleName, Supplier<Set<Path>> pathsSupplier,
73+
private void watchForUpdates(SslBundleRegistry registry, String bundleName, Supplier<Set<WatchablePath>> pathsSupplier,
7574
Supplier<SslBundle> bundleSupplier) {
7675
try {
7776
this.fileWatcher.watch(pathsSupplier.get(), () -> registry.updateBundle(bundleName, bundleSupplier.get()));
@@ -81,15 +80,15 @@ private void watchForUpdates(SslBundleRegistry registry, String bundleName, Supp
8180
}
8281
}
8382

84-
private Set<Path> watchedJksPaths(Bundle<JksSslBundleProperties> bundle) {
83+
private Set<WatchablePath> watchedJksPaths(Bundle<JksSslBundleProperties> bundle) {
8584
List<BundleContentProperty> watched = new ArrayList<>();
8685
watched.add(new BundleContentProperty("keystore.location", bundle.properties().getKeystore().getLocation()));
8786
watched
8887
.add(new BundleContentProperty("truststore.location", bundle.properties().getTruststore().getLocation()));
8988
return watchedPaths(bundle.name(), watched);
9089
}
9190

92-
private Set<Path> watchedPemPaths(Bundle<PemSslBundleProperties> bundle) {
91+
private Set<WatchablePath> watchedPemPaths(Bundle<PemSslBundleProperties> bundle) {
9392
List<BundleContentProperty> watched = new ArrayList<>();
9493
watched
9594
.add(new BundleContentProperty("keystore.private-key", bundle.properties().getKeystore().getPrivateKey()));
@@ -102,7 +101,7 @@ private Set<Path> watchedPemPaths(Bundle<PemSslBundleProperties> bundle) {
102101
return watchedPaths(bundle.name(), watched);
103102
}
104103

105-
private Set<Path> watchedPaths(String bundleName, List<BundleContentProperty> properties) {
104+
private Set<WatchablePath> watchedPaths(String bundleName, List<BundleContentProperty> properties) {
106105
try {
107106
return properties.stream()
108107
.filter(BundleContentProperty::hasValue)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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 java.nio.file.Path;
20+
21+
record WatchablePath(Path path, Boolean optional) {
22+
}

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ private static UncheckedIOException asUncheckedIOException(String message, Excep
5858
}
5959

6060
private static List<X509Certificate> loadCertificates(PemSslStoreDetails details) throws IOException {
61-
PemContent pemContent = PemContent.load(details.certificates());
61+
PemContent pemContent = PemContent.load(details.certificates(), details.optional());
6262
if (pemContent == null) {
6363
return null;
6464
}
@@ -72,6 +72,11 @@ private static PrivateKey loadPrivateKey(PemSslStoreDetails details) throws IOEx
7272
return (pemContent != null) ? pemContent.getPrivateKey(details.privateKeyPassword()) : null;
7373
}
7474

75+
@Override
76+
public boolean optional() {
77+
return this.details.optional();
78+
}
79+
7580
@Override
7681
public String type() {
7782
return this.details.type();

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

+15
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,21 @@ public String toString() {
104104
return this.text;
105105
}
106106

107+
/**
108+
* Load {@link PemContent} from the given content (either the PEM content itself or a
109+
* reference to the resource to load).
110+
* @param content the content to load
111+
* @param isOptional the content to load may be optional
112+
* @return a new {@link PemContent} instance
113+
* @throws IOException on IO error
114+
*/
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);
120+
}
121+
107122
/**
108123
* Load {@link PemContent} from the given content (either the PEM content itself or a
109124
* reference to the resource to load).

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,6 +33,9 @@
3333
*/
3434
public interface PemSslStore {
3535

36+
37+
boolean optional();
38+
3639
/**
3740
* The key store type, for example {@code JKS} or {@code PKCS11}. A {@code null} value
3841
* will use {@link KeyStore#getDefaultType()}).
@@ -164,6 +167,11 @@ public PrivateKey privateKey() {
164167
return privateKey;
165168
}
166169

170+
@Override
171+
public boolean optional() {
172+
return false; //TODO
173+
}
174+
167175
};
168176
}
169177

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) {
85+
if (pemSslStore == null || pemSslStore.optional() && pemSslStore.certificates() == null) {
8686
return null;
8787
}
8888
try {

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

+11-14
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,20 @@
2929
* @param password the password used
3030
* {@link KeyStore#setKeyEntry(String, java.security.Key, char[], java.security.cert.Certificate[])
3131
* setting key entries} in the {@link KeyStore}
32-
* @param certificates the certificates content (either the PEM content itself or a
32+
* @param certificates the certificates content (either the PEM content itself or or a
3333
* reference to the resource to load). When a {@link #privateKey() private key} is present
3434
* this value is treated as a certificate chain, otherwise it is treated a list of
3535
* certificates that should all be registered.
3636
* @param privateKey the private key content (either the PEM content itself or a reference
3737
* to the resource to load)
3838
* @param privateKeyPassword a password used to decrypt an encrypted private key
39+
* @param optional certificates/privateKey may be optional
3940
* @author Scott Frederick
4041
* @author Phillip Webb
4142
* @since 3.1.0
4243
* @see PemSslStore#load(PemSslStoreDetails)
4344
*/
44-
public record PemSslStoreDetails(String type, String alias, String password, String certificates, String privateKey,
45-
String privateKeyPassword) {
45+
public record PemSslStoreDetails(String type, String alias, String password, String certificates, String privateKey, String privateKeyPassword, boolean optional) {
4646

4747
/**
4848
* Create a new {@link PemSslStoreDetails} instance.
@@ -71,9 +71,10 @@ public record PemSslStoreDetails(String type, String alias, String password, Str
7171
* @param privateKey the private key content (either the PEM content itself or a
7272
* reference to the resource to load)
7373
* @param privateKeyPassword a password used to decrypt an encrypted private key
74+
* @param optional certificates/privateKey may be optional
7475
*/
75-
public PemSslStoreDetails(String type, String certificate, String privateKey, String privateKeyPassword) {
76-
this(type, null, null, certificate, privateKey, privateKeyPassword);
76+
public PemSslStoreDetails(String type, String certificate, String privateKey, String privateKeyPassword, boolean optional) {
77+
this(type, null, null, certificate, privateKey, privateKeyPassword, optional);
7778
}
7879

7980
/**
@@ -86,7 +87,7 @@ public PemSslStoreDetails(String type, String certificate, String privateKey, St
8687
* reference to the resource to load)
8788
*/
8889
public PemSslStoreDetails(String type, String certificate, String privateKey) {
89-
this(type, certificate, privateKey, null);
90+
this(type, certificate, privateKey, null, false);
9091
}
9192

9293
/**
@@ -96,8 +97,7 @@ public PemSslStoreDetails(String type, String certificate, String privateKey) {
9697
* @since 3.2.0
9798
*/
9899
public PemSslStoreDetails withAlias(String alias) {
99-
return new PemSslStoreDetails(this.type, alias, this.password, this.certificates, this.privateKey,
100-
this.privateKeyPassword);
100+
return new PemSslStoreDetails(this.type, alias, this.password, this.certificates, this.privateKey, this.privateKeyPassword, this.optional);
101101
}
102102

103103
/**
@@ -107,8 +107,7 @@ public PemSslStoreDetails withAlias(String alias) {
107107
* @since 3.2.0
108108
*/
109109
public PemSslStoreDetails withPassword(String password) {
110-
return new PemSslStoreDetails(this.type, this.alias, password, this.certificates, this.privateKey,
111-
this.privateKeyPassword);
110+
return new PemSslStoreDetails(this.type, this.alias, password, this.certificates, this.privateKey, this.privateKeyPassword, this.optional);
112111
}
113112

114113
/**
@@ -117,8 +116,7 @@ public PemSslStoreDetails withPassword(String password) {
117116
* @return a new {@link PemSslStoreDetails} instance
118117
*/
119118
public PemSslStoreDetails withPrivateKey(String privateKey) {
120-
return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificates, privateKey,
121-
this.privateKeyPassword);
119+
return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificates, privateKey, this.privateKeyPassword, this.optional);
122120
}
123121

124122
/**
@@ -127,8 +125,7 @@ public PemSslStoreDetails withPrivateKey(String privateKey) {
127125
* @return a new {@link PemSslStoreDetails} instance
128126
*/
129127
public PemSslStoreDetails withPrivateKeyPassword(String privateKeyPassword) {
130-
return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificates, this.privateKey,
131-
privateKeyPassword);
128+
return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificates, this.privateKey, privateKeyPassword, this.optional);
132129
}
133130

134131
boolean isEmpty() {

0 commit comments

Comments
 (0)