Skip to content

Commit 9b71ef4

Browse files
committed
Polish and refactor some SSL internals
Polish and refactor some of the internal SSL code to make it easier to add additional functionality in the future.
1 parent 30a7426 commit 9b71ef4

File tree

8 files changed

+270
-118
lines changed

8 files changed

+270
-118
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2012-2023 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.io.FileNotFoundException;
20+
import java.net.URL;
21+
import java.nio.file.Path;
22+
23+
import org.springframework.boot.ssl.pem.PemContent;
24+
import org.springframework.util.Assert;
25+
import org.springframework.util.ResourceUtils;
26+
import org.springframework.util.StringUtils;
27+
28+
/**
29+
* Helper utility to manage a single bundle content configuration property. May possibly
30+
* contain PEM content, a location or a directory search pattern.
31+
*
32+
* @param name the configuration property name (excluding any prefix)
33+
* @param value the configuration property value
34+
* @author Phillip Webb
35+
*/
36+
record BundleContentProperty(String name, String value) {
37+
38+
/**
39+
* Return if the property value is PEM content.
40+
* @return if the value is PEM content
41+
*/
42+
boolean isPemContent() {
43+
return PemContent.isPresentInText(this.value);
44+
}
45+
46+
/**
47+
* Return if there is any property value present.
48+
* @return if the value is present
49+
*/
50+
boolean hasValue() {
51+
return StringUtils.hasText(this.value);
52+
}
53+
54+
private URL toUrl() throws FileNotFoundException {
55+
Assert.state(!isPemContent(), "Value contains PEM content");
56+
return ResourceUtils.getURL(this.value);
57+
}
58+
59+
Path toWatchPath() {
60+
return toPath();
61+
}
62+
63+
private Path toPath() {
64+
try {
65+
URL url = toUrl();
66+
Assert.state(isFileUrl(url), () -> "Vaule '%s' is not a file URL".formatted(url));
67+
return Path.of(url.toURI()).toAbsolutePath();
68+
}
69+
catch (Exception ex) {
70+
throw new IllegalStateException("Unable to convert '%s' property to a path".formatted(this.name), ex);
71+
}
72+
}
73+
74+
private boolean isFileUrl(URL url) {
75+
return "file".equalsIgnoreCase(url.getProtocol());
76+
}
77+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public static class Store {
5858
private String type;
5959

6060
/**
61-
* Location or content of the certificate in PEM format.
61+
* Location or content of the certificate or certificate chain in PEM format.
6262
*/
6363
private String certificate;
6464

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

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -99,49 +99,47 @@ public SslManagerBundle getManagers() {
9999
* @return an {@link SslBundle} instance
100100
*/
101101
public static SslBundle get(PemSslBundleProperties properties) {
102-
return new PropertiesSslBundle(asSslStoreBundle(properties), properties);
103-
}
104-
105-
/**
106-
* Get an {@link SslBundle} for the given {@link JksSslBundleProperties}.
107-
* @param properties the source properties
108-
* @return an {@link SslBundle} instance
109-
*/
110-
public static SslBundle get(JksSslBundleProperties properties) {
111-
return new PropertiesSslBundle(asSslStoreBundle(properties), properties);
112-
}
113-
114-
private static SslStoreBundle asSslStoreBundle(PemSslBundleProperties properties) {
115-
PemSslStore keyStore = asPemSslStore(properties.getKeystore());
116-
if (keyStore != null) {
117-
keyStore = keyStore.withAlias(properties.getKey().getAlias())
118-
.withPassword(properties.getKey().getPassword());
119-
}
120-
PemSslStore trustStore = asPemSslStore(properties.getTruststore());
121-
return new PemSslStoreBundle(keyStore, trustStore);
122-
}
123-
124-
private static PemSslStore asPemSslStore(PemSslBundleProperties.Store properties) {
125102
try {
126-
PemSslStoreDetails details = asStoreDetails(properties);
127-
PemSslStore pemSslStore = PemSslStore.load(details);
128-
if (properties.isVerifyKeys()) {
129-
CertificateMatcher certificateMatcher = new CertificateMatcher(pemSslStore.privateKey());
130-
Assert.state(certificateMatcher.matchesAny(pemSslStore.certificates()),
131-
"Private key matches none of the certificates in the chain");
103+
PemSslStore keyStore = getPemSslStore("keystore", properties.getKeystore());
104+
if (keyStore != null) {
105+
keyStore = keyStore.withAlias(properties.getKey().getAlias())
106+
.withPassword(properties.getKey().getPassword());
132107
}
133-
return pemSslStore;
108+
PemSslStore trustStore = getPemSslStore("truststore", properties.getTruststore());
109+
SslStoreBundle storeBundle = new PemSslStoreBundle(keyStore, trustStore);
110+
return new PropertiesSslBundle(storeBundle, properties);
134111
}
135112
catch (IOException ex) {
136113
throw new UncheckedIOException(ex);
137114
}
138115
}
139116

140-
private static PemSslStoreDetails asStoreDetails(PemSslBundleProperties.Store properties) {
117+
private static PemSslStore getPemSslStore(String propertyName, PemSslBundleProperties.Store properties)
118+
throws IOException {
119+
PemSslStore pemSslStore = PemSslStore.load(asPemSslStoreDetails(properties));
120+
if (properties.isVerifyKeys()) {
121+
CertificateMatcher certificateMatcher = new CertificateMatcher(pemSslStore.privateKey());
122+
Assert.state(certificateMatcher.matchesAny(pemSslStore.certificates()),
123+
"Private key matches none of the certificates in the chain");
124+
}
125+
return pemSslStore;
126+
}
127+
128+
private static PemSslStoreDetails asPemSslStoreDetails(PemSslBundleProperties.Store properties) {
141129
return new PemSslStoreDetails(properties.getType(), properties.getCertificate(), properties.getPrivateKey(),
142130
properties.getPrivateKeyPassword());
143131
}
144132

133+
/**
134+
* Get an {@link SslBundle} for the given {@link JksSslBundleProperties}.
135+
* @param properties the source properties
136+
* @return an {@link SslBundle} instance
137+
*/
138+
public static SslBundle get(JksSslBundleProperties properties) {
139+
SslStoreBundle storeBundle = asSslStoreBundle(properties);
140+
return new PropertiesSslBundle(storeBundle, properties);
141+
}
142+
145143
private static SslStoreBundle asSslStoreBundle(JksSslBundleProperties properties) {
146144
JksSslStoreDetails keyStoreDetails = asStoreDetails(properties.getKeystore());
147145
JksSslStoreDetails trustStoreDetails = asStoreDetails(properties.getTruststore());

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

Lines changed: 40 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,17 @@
1616

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

19-
import java.net.URL;
2019
import java.nio.file.Path;
21-
import java.util.LinkedHashSet;
20+
import java.util.ArrayList;
21+
import java.util.List;
2222
import java.util.Map;
2323
import java.util.Set;
2424
import java.util.function.Function;
25-
import java.util.regex.Pattern;
25+
import java.util.function.Supplier;
2626
import java.util.stream.Collectors;
2727

2828
import org.springframework.boot.ssl.SslBundle;
2929
import org.springframework.boot.ssl.SslBundleRegistry;
30-
import org.springframework.util.Assert;
31-
import org.springframework.util.ResourceUtils;
32-
import org.springframework.util.StringUtils;
3330

3431
/**
3532
* A {@link SslBundleRegistrar} that registers SSL bundles based
@@ -41,8 +38,6 @@
4138
*/
4239
class SslPropertiesBundleRegistrar implements SslBundleRegistrar {
4340

44-
private static final Pattern PEM_CONTENT = Pattern.compile("-+BEGIN\\s+[^-]*-+", Pattern.CASE_INSENSITIVE);
45-
4641
private final SslProperties.Bundles properties;
4742

4843
private final FileWatcher fileWatcher;
@@ -54,70 +49,58 @@ class SslPropertiesBundleRegistrar implements SslBundleRegistrar {
5449

5550
@Override
5651
public void registerBundles(SslBundleRegistry registry) {
57-
registerBundles(registry, this.properties.getPem(), PropertiesSslBundle::get, this::getLocations);
58-
registerBundles(registry, this.properties.getJks(), PropertiesSslBundle::get, this::getLocations);
52+
registerBundles(registry, this.properties.getPem(), PropertiesSslBundle::get, this::watchedPemPaths);
53+
registerBundles(registry, this.properties.getJks(), PropertiesSslBundle::get, this::watchedJksPaths);
5954
}
6055

6156
private <P extends SslBundleProperties> void registerBundles(SslBundleRegistry registry, Map<String, P> properties,
62-
Function<P, SslBundle> bundleFactory, Function<P, Set<Location>> locationsSupplier) {
57+
Function<P, SslBundle> bundleFactory, Function<P, Set<Path>> watchedPaths) {
6358
properties.forEach((bundleName, bundleProperties) -> {
64-
SslBundle bundle = bundleFactory.apply(bundleProperties);
65-
registry.registerBundle(bundleName, bundle);
66-
if (bundleProperties.isReloadOnUpdate()) {
67-
Set<Path> paths = locationsSupplier.apply(bundleProperties)
68-
.stream()
69-
.filter(Location::hasValue)
70-
.map((location) -> toPath(bundleName, location))
71-
.collect(Collectors.toSet());
72-
this.fileWatcher.watch(paths,
73-
() -> registry.updateBundle(bundleName, bundleFactory.apply(bundleProperties)));
59+
Supplier<SslBundle> bundleSupplier = () -> bundleFactory.apply(bundleProperties);
60+
try {
61+
registry.registerBundle(bundleName, bundleSupplier.get());
62+
if (bundleProperties.isReloadOnUpdate()) {
63+
Supplier<Set<Path>> pathsSupplier = () -> watchedPaths.apply(bundleProperties);
64+
watchForUpdates(registry, bundleName, pathsSupplier, bundleSupplier);
65+
}
66+
}
67+
catch (IllegalStateException ex) {
68+
throw new IllegalStateException("Unable to register SSL bundle '%s'".formatted(bundleName), ex);
7469
}
7570
});
7671
}
7772

78-
private Set<Location> getLocations(JksSslBundleProperties properties) {
79-
JksSslBundleProperties.Store keystore = properties.getKeystore();
80-
JksSslBundleProperties.Store truststore = properties.getTruststore();
81-
Set<Location> locations = new LinkedHashSet<>();
82-
locations.add(new Location("keystore.location", keystore.getLocation()));
83-
locations.add(new Location("truststore.location", truststore.getLocation()));
84-
return locations;
85-
}
86-
87-
private Set<Location> getLocations(PemSslBundleProperties properties) {
88-
PemSslBundleProperties.Store keystore = properties.getKeystore();
89-
PemSslBundleProperties.Store truststore = properties.getTruststore();
90-
Set<Location> locations = new LinkedHashSet<>();
91-
locations.add(new Location("keystore.private-key", keystore.getPrivateKey()));
92-
locations.add(new Location("keystore.certificate", keystore.getCertificate()));
93-
locations.add(new Location("truststore.private-key", truststore.getPrivateKey()));
94-
locations.add(new Location("truststore.certificate", truststore.getCertificate()));
95-
return locations;
96-
}
97-
98-
private Path toPath(String bundleName, Location watchableLocation) {
99-
String value = watchableLocation.value();
100-
String field = watchableLocation.field();
101-
Assert.state(!PEM_CONTENT.matcher(value).find(),
102-
() -> "SSL bundle '%s' '%s' is not a URL and can't be watched".formatted(bundleName, field));
73+
private void watchForUpdates(SslBundleRegistry registry, String bundleName, Supplier<Set<Path>> pathsSupplier,
74+
Supplier<SslBundle> bundleSupplier) {
10375
try {
104-
URL url = ResourceUtils.getURL(value);
105-
Assert.state("file".equalsIgnoreCase(url.getProtocol()),
106-
() -> "SSL bundle '%s' '%s' URL '%s' doesn't point to a file".formatted(bundleName, field, url));
107-
return Path.of(url.toURI()).toAbsolutePath();
76+
this.fileWatcher.watch(pathsSupplier.get(), () -> registry.updateBundle(bundleName, bundleSupplier.get()));
10877
}
109-
catch (Exception ex) {
110-
throw new RuntimeException(
111-
"SSL bundle '%s' '%s' location '%s' cannot be watched".formatted(bundleName, field, value), ex);
78+
catch (RuntimeException ex) {
79+
throw new IllegalStateException("Unable to watch for reload on update", ex);
11280
}
11381
}
11482

115-
private record Location(String field, String value) {
83+
private Set<Path> watchedJksPaths(JksSslBundleProperties properties) {
84+
List<BundleContentProperty> watched = new ArrayList<>();
85+
watched.add(new BundleContentProperty("keystore.location", properties.getKeystore().getLocation()));
86+
watched.add(new BundleContentProperty("truststore.location", properties.getTruststore().getLocation()));
87+
return watchedPaths(watched);
88+
}
11689

117-
boolean hasValue() {
118-
return StringUtils.hasText(this.value);
119-
}
90+
private Set<Path> watchedPemPaths(PemSslBundleProperties properties) {
91+
List<BundleContentProperty> watched = new ArrayList<>();
92+
watched.add(new BundleContentProperty("keystore.private-key", properties.getKeystore().getPrivateKey()));
93+
watched.add(new BundleContentProperty("keystore.certificate", properties.getKeystore().getCertificate()));
94+
watched.add(new BundleContentProperty("truststore.private-key", properties.getTruststore().getPrivateKey()));
95+
watched.add(new BundleContentProperty("truststore.certificate", properties.getTruststore().getCertificate()));
96+
return watchedPaths(watched);
97+
}
12098

99+
private Set<Path> watchedPaths(List<BundleContentProperty> properties) {
100+
return properties.stream()
101+
.filter(BundleContentProperty::hasValue)
102+
.map(BundleContentProperty::toWatchPath)
103+
.collect(Collectors.toSet());
121104
}
122105

123106
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2012-2023 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+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.api.io.TempDir;
23+
24+
import static org.assertj.core.api.Assertions.assertThat;
25+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
26+
27+
/**
28+
* Tests for {@link BundleContentProperty}.
29+
*
30+
* @author Moritz Halbritter
31+
* @author Phillip Webb
32+
*/
33+
class BundleContentPropertyTests {
34+
35+
private static final String PEM_TEXT = """
36+
-----BEGIN CERTIFICATE-----
37+
-----END CERTIFICATE-----
38+
""";
39+
40+
@TempDir
41+
Path temp;
42+
43+
@Test
44+
void isPemContentWhenValueIsPemTextReturnsTrue() {
45+
BundleContentProperty property = new BundleContentProperty("name", PEM_TEXT);
46+
assertThat(property.isPemContent()).isTrue();
47+
}
48+
49+
@Test
50+
void isPemContentWhenValueIsNotPemTextReturnsFalse() {
51+
BundleContentProperty property = new BundleContentProperty("name", "file.pem");
52+
assertThat(property.isPemContent()).isFalse();
53+
}
54+
55+
@Test
56+
void hasValueWhenHasValueReturnsTrue() {
57+
BundleContentProperty property = new BundleContentProperty("name", "file.pem");
58+
assertThat(property.hasValue()).isTrue();
59+
}
60+
61+
@Test
62+
void hasValueWhenHasNullValueReturnsFalse() {
63+
BundleContentProperty property = new BundleContentProperty("name", null);
64+
assertThat(property.hasValue()).isFalse();
65+
}
66+
67+
@Test
68+
void hasValueWhenHasEmptyValueReturnsFalse() {
69+
BundleContentProperty property = new BundleContentProperty("name", "");
70+
assertThat(property.hasValue()).isFalse();
71+
}
72+
73+
@Test
74+
void toWatchPathWhenNotPathThrowsException() {
75+
BundleContentProperty property = new BundleContentProperty("name", PEM_TEXT);
76+
assertThatIllegalStateException().isThrownBy(property::toWatchPath)
77+
.withMessage("Unable to convert 'name' property to a path");
78+
}
79+
80+
@Test
81+
void toWatchPathWhenPathReturnsPath() {
82+
Path file = this.temp.toAbsolutePath().resolve("file.txt");
83+
BundleContentProperty property = new BundleContentProperty("name", file.toString());
84+
assertThat(property.toWatchPath()).isEqualTo(file);
85+
}
86+
87+
}

0 commit comments

Comments
 (0)