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.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.ssl;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.springframework.boot.ssl.pem.PemContent;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Helper utility to manage a single bundle content configuration property. May possibly
|
||||
* contain PEM content, a location or a directory search pattern.
|
||||
*
|
||||
* @param name the configuration property name (excluding any prefix)
|
||||
* @param value the configuration property value
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
record BundleContentProperty(String name, String value) {
|
||||
|
||||
/**
|
||||
* Return if the property value is PEM content.
|
||||
* @return if the value is PEM content
|
||||
*/
|
||||
boolean isPemContent() {
|
||||
return PemContent.isPresentInText(this.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if there is any property value present.
|
||||
* @return if the value is present
|
||||
*/
|
||||
boolean hasValue() {
|
||||
return StringUtils.hasText(this.value);
|
||||
}
|
||||
|
||||
private URL toUrl() throws FileNotFoundException {
|
||||
Assert.state(!isPemContent(), "Value contains PEM content");
|
||||
return ResourceUtils.getURL(this.value);
|
||||
}
|
||||
|
||||
Path toWatchPath() {
|
||||
return toPath();
|
||||
}
|
||||
|
||||
private Path toPath() {
|
||||
try {
|
||||
URL url = toUrl();
|
||||
Assert.state(isFileUrl(url), () -> "Vaule '%s' is not a file URL".formatted(url));
|
||||
return Path.of(url.toURI()).toAbsolutePath();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Unable to convert '%s' property to a path".formatted(this.name), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isFileUrl(URL url) {
|
||||
return "file".equalsIgnoreCase(url.getProtocol());
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ public class PemSslBundleProperties extends SslBundleProperties {
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* Location or content of the certificate in PEM format.
|
||||
* Location or content of the certificate or certificate chain in PEM format.
|
||||
*/
|
||||
private String certificate;
|
||||
|
||||
|
||||
@@ -99,7 +99,35 @@ public final class PropertiesSslBundle implements SslBundle {
|
||||
* @return an {@link SslBundle} instance
|
||||
*/
|
||||
public static SslBundle get(PemSslBundleProperties properties) {
|
||||
return new PropertiesSslBundle(asSslStoreBundle(properties), properties);
|
||||
try {
|
||||
PemSslStore keyStore = getPemSslStore("keystore", properties.getKeystore());
|
||||
if (keyStore != null) {
|
||||
keyStore = keyStore.withAlias(properties.getKey().getAlias())
|
||||
.withPassword(properties.getKey().getPassword());
|
||||
}
|
||||
PemSslStore trustStore = getPemSslStore("truststore", properties.getTruststore());
|
||||
SslStoreBundle storeBundle = new PemSslStoreBundle(keyStore, trustStore);
|
||||
return new PropertiesSslBundle(storeBundle, properties);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static PemSslStore getPemSslStore(String propertyName, PemSslBundleProperties.Store properties)
|
||||
throws IOException {
|
||||
PemSslStore pemSslStore = PemSslStore.load(asPemSslStoreDetails(properties));
|
||||
if (properties.isVerifyKeys()) {
|
||||
CertificateMatcher certificateMatcher = new CertificateMatcher(pemSslStore.privateKey());
|
||||
Assert.state(certificateMatcher.matchesAny(pemSslStore.certificates()),
|
||||
"Private key matches none of the certificates in the chain");
|
||||
}
|
||||
return pemSslStore;
|
||||
}
|
||||
|
||||
private static PemSslStoreDetails asPemSslStoreDetails(PemSslBundleProperties.Store properties) {
|
||||
return new PemSslStoreDetails(properties.getType(), properties.getCertificate(), properties.getPrivateKey(),
|
||||
properties.getPrivateKeyPassword());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,38 +136,8 @@ public final class PropertiesSslBundle implements SslBundle {
|
||||
* @return an {@link SslBundle} instance
|
||||
*/
|
||||
public static SslBundle get(JksSslBundleProperties properties) {
|
||||
return new PropertiesSslBundle(asSslStoreBundle(properties), properties);
|
||||
}
|
||||
|
||||
private static SslStoreBundle asSslStoreBundle(PemSslBundleProperties properties) {
|
||||
PemSslStore keyStore = asPemSslStore(properties.getKeystore());
|
||||
if (keyStore != null) {
|
||||
keyStore = keyStore.withAlias(properties.getKey().getAlias())
|
||||
.withPassword(properties.getKey().getPassword());
|
||||
}
|
||||
PemSslStore trustStore = asPemSslStore(properties.getTruststore());
|
||||
return new PemSslStoreBundle(keyStore, trustStore);
|
||||
}
|
||||
|
||||
private static PemSslStore asPemSslStore(PemSslBundleProperties.Store properties) {
|
||||
try {
|
||||
PemSslStoreDetails details = asStoreDetails(properties);
|
||||
PemSslStore pemSslStore = PemSslStore.load(details);
|
||||
if (properties.isVerifyKeys()) {
|
||||
CertificateMatcher certificateMatcher = new CertificateMatcher(pemSslStore.privateKey());
|
||||
Assert.state(certificateMatcher.matchesAny(pemSslStore.certificates()),
|
||||
"Private key matches none of the certificates in the chain");
|
||||
}
|
||||
return pemSslStore;
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static PemSslStoreDetails asStoreDetails(PemSslBundleProperties.Store properties) {
|
||||
return new PemSslStoreDetails(properties.getType(), properties.getCertificate(), properties.getPrivateKey(),
|
||||
properties.getPrivateKeyPassword());
|
||||
SslStoreBundle storeBundle = asSslStoreBundle(properties);
|
||||
return new PropertiesSslBundle(storeBundle, properties);
|
||||
}
|
||||
|
||||
private static SslStoreBundle asSslStoreBundle(JksSslBundleProperties properties) {
|
||||
|
||||
@@ -16,20 +16,17 @@
|
||||
|
||||
package org.springframework.boot.autoconfigure.ssl;
|
||||
|
||||
import java.net.URL;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.boot.ssl.SslBundle;
|
||||
import org.springframework.boot.ssl.SslBundleRegistry;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A {@link SslBundleRegistrar} that registers SSL bundles based
|
||||
@@ -41,8 +38,6 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
class SslPropertiesBundleRegistrar implements SslBundleRegistrar {
|
||||
|
||||
private static final Pattern PEM_CONTENT = Pattern.compile("-+BEGIN\\s+[^-]*-+", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private final SslProperties.Bundles properties;
|
||||
|
||||
private final FileWatcher fileWatcher;
|
||||
@@ -54,70 +49,58 @@ class SslPropertiesBundleRegistrar implements SslBundleRegistrar {
|
||||
|
||||
@Override
|
||||
public void registerBundles(SslBundleRegistry registry) {
|
||||
registerBundles(registry, this.properties.getPem(), PropertiesSslBundle::get, this::getLocations);
|
||||
registerBundles(registry, this.properties.getJks(), PropertiesSslBundle::get, this::getLocations);
|
||||
registerBundles(registry, this.properties.getPem(), PropertiesSslBundle::get, this::watchedPemPaths);
|
||||
registerBundles(registry, this.properties.getJks(), PropertiesSslBundle::get, this::watchedJksPaths);
|
||||
}
|
||||
|
||||
private <P extends SslBundleProperties> void registerBundles(SslBundleRegistry registry, Map<String, P> properties,
|
||||
Function<P, SslBundle> bundleFactory, Function<P, Set<Location>> locationsSupplier) {
|
||||
Function<P, SslBundle> bundleFactory, Function<P, Set<Path>> watchedPaths) {
|
||||
properties.forEach((bundleName, bundleProperties) -> {
|
||||
SslBundle bundle = bundleFactory.apply(bundleProperties);
|
||||
registry.registerBundle(bundleName, bundle);
|
||||
if (bundleProperties.isReloadOnUpdate()) {
|
||||
Set<Path> paths = locationsSupplier.apply(bundleProperties)
|
||||
.stream()
|
||||
.filter(Location::hasValue)
|
||||
.map((location) -> toPath(bundleName, location))
|
||||
.collect(Collectors.toSet());
|
||||
this.fileWatcher.watch(paths,
|
||||
() -> registry.updateBundle(bundleName, bundleFactory.apply(bundleProperties)));
|
||||
Supplier<SslBundle> bundleSupplier = () -> bundleFactory.apply(bundleProperties);
|
||||
try {
|
||||
registry.registerBundle(bundleName, bundleSupplier.get());
|
||||
if (bundleProperties.isReloadOnUpdate()) {
|
||||
Supplier<Set<Path>> pathsSupplier = () -> watchedPaths.apply(bundleProperties);
|
||||
watchForUpdates(registry, bundleName, pathsSupplier, bundleSupplier);
|
||||
}
|
||||
}
|
||||
catch (IllegalStateException ex) {
|
||||
throw new IllegalStateException("Unable to register SSL bundle '%s'".formatted(bundleName), ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Set<Location> getLocations(JksSslBundleProperties properties) {
|
||||
JksSslBundleProperties.Store keystore = properties.getKeystore();
|
||||
JksSslBundleProperties.Store truststore = properties.getTruststore();
|
||||
Set<Location> locations = new LinkedHashSet<>();
|
||||
locations.add(new Location("keystore.location", keystore.getLocation()));
|
||||
locations.add(new Location("truststore.location", truststore.getLocation()));
|
||||
return locations;
|
||||
}
|
||||
|
||||
private Set<Location> getLocations(PemSslBundleProperties properties) {
|
||||
PemSslBundleProperties.Store keystore = properties.getKeystore();
|
||||
PemSslBundleProperties.Store truststore = properties.getTruststore();
|
||||
Set<Location> locations = new LinkedHashSet<>();
|
||||
locations.add(new Location("keystore.private-key", keystore.getPrivateKey()));
|
||||
locations.add(new Location("keystore.certificate", keystore.getCertificate()));
|
||||
locations.add(new Location("truststore.private-key", truststore.getPrivateKey()));
|
||||
locations.add(new Location("truststore.certificate", truststore.getCertificate()));
|
||||
return locations;
|
||||
}
|
||||
|
||||
private Path toPath(String bundleName, Location watchableLocation) {
|
||||
String value = watchableLocation.value();
|
||||
String field = watchableLocation.field();
|
||||
Assert.state(!PEM_CONTENT.matcher(value).find(),
|
||||
() -> "SSL bundle '%s' '%s' is not a URL and can't be watched".formatted(bundleName, field));
|
||||
private void watchForUpdates(SslBundleRegistry registry, String bundleName, Supplier<Set<Path>> pathsSupplier,
|
||||
Supplier<SslBundle> bundleSupplier) {
|
||||
try {
|
||||
URL url = ResourceUtils.getURL(value);
|
||||
Assert.state("file".equalsIgnoreCase(url.getProtocol()),
|
||||
() -> "SSL bundle '%s' '%s' URL '%s' doesn't point to a file".formatted(bundleName, field, url));
|
||||
return Path.of(url.toURI()).toAbsolutePath();
|
||||
this.fileWatcher.watch(pathsSupplier.get(), () -> registry.updateBundle(bundleName, bundleSupplier.get()));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new RuntimeException(
|
||||
"SSL bundle '%s' '%s' location '%s' cannot be watched".formatted(bundleName, field, value), ex);
|
||||
catch (RuntimeException ex) {
|
||||
throw new IllegalStateException("Unable to watch for reload on update", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private record Location(String field, String value) {
|
||||
private Set<Path> watchedJksPaths(JksSslBundleProperties properties) {
|
||||
List<BundleContentProperty> watched = new ArrayList<>();
|
||||
watched.add(new BundleContentProperty("keystore.location", properties.getKeystore().getLocation()));
|
||||
watched.add(new BundleContentProperty("truststore.location", properties.getTruststore().getLocation()));
|
||||
return watchedPaths(watched);
|
||||
}
|
||||
|
||||
boolean hasValue() {
|
||||
return StringUtils.hasText(this.value);
|
||||
}
|
||||
private Set<Path> watchedPemPaths(PemSslBundleProperties properties) {
|
||||
List<BundleContentProperty> watched = new ArrayList<>();
|
||||
watched.add(new BundleContentProperty("keystore.private-key", properties.getKeystore().getPrivateKey()));
|
||||
watched.add(new BundleContentProperty("keystore.certificate", properties.getKeystore().getCertificate()));
|
||||
watched.add(new BundleContentProperty("truststore.private-key", properties.getTruststore().getPrivateKey()));
|
||||
watched.add(new BundleContentProperty("truststore.certificate", properties.getTruststore().getCertificate()));
|
||||
return watchedPaths(watched);
|
||||
}
|
||||
|
||||
private Set<Path> watchedPaths(List<BundleContentProperty> properties) {
|
||||
return properties.stream()
|
||||
.filter(BundleContentProperty::hasValue)
|
||||
.map(BundleContentProperty::toWatchPath)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user