Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for multiple/optional trust certificates #41932

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.Set;

import javax.net.ssl.TrustManagerFactory;

Expand Down Expand Up @@ -53,6 +54,7 @@
import org.springframework.boot.io.ApplicationResourceLoader;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.ssl.pem.PemCertificate;
import org.springframework.boot.ssl.pem.PemSslStore;
import org.springframework.boot.ssl.pem.PemSslStoreDetails;
import org.springframework.context.annotation.Bean;
Expand Down Expand Up @@ -110,7 +112,7 @@ public Authenticator couchbaseAuthenticator(CouchbaseConnectionDetails connectio
}
Pem pem = this.properties.getAuthentication().getPem();
if (pem.getCertificates() != null) {
PemSslStoreDetails details = new PemSslStoreDetails(null, pem.getCertificates(), pem.getPrivateKey());
PemSslStoreDetails details = new PemSslStoreDetails(null, Set.of(new PemCertificate(pem.getCertificates())), pem.getPrivateKey());
PemSslStore store = PemSslStore.load(details);
return CertificateAuthenticator.fromKey(store.privateKey(), pem.getPrivateKeyPassword(),
store.certificates());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import org.springframework.boot.io.ApplicationResourceLoader;
import org.springframework.boot.ssl.pem.PemContent;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
Expand All @@ -33,7 +32,12 @@
* @author Phillip Webb
* @author Moritz Halbritter
*/
record BundleContentProperty(String name, String value) {
record BundleContentProperty(String name, String value, boolean optional) {

BundleContentProperty(String name, String value)
{
this(name, value,false);
}

/**
* Return if the property value is PEM content.
Expand All @@ -51,13 +55,16 @@ boolean hasValue() {
return StringUtils.hasText(this.value);
}

Path toWatchPath() {
WatchablePath toWatchPath() {
try {
if (isPemContent()) {
return null;
}
Resource resource = getResource();
if (!resource.isFile()) {
throw new BundleContentNotWatchableException(this);
}
return Path.of(resource.getFile().getAbsolutePath());
return new WatchablePath(this.optional, Path.of(resource.getFile().getAbsolutePath()));
}
catch (Exception ex) {
if (ex instanceof BundleContentNotWatchableException bundleContentNotWatchableException) {
Expand All @@ -69,7 +76,6 @@ Path toWatchPath() {
}

private Resource getResource() {
Assert.state(!isPemContent(), "Value contains PEM content");
return new ApplicationResourceLoader().getResource(this.value);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

/*
* Copyright 2012-2023 the original author or authors.
*
Expand Down Expand Up @@ -74,7 +75,7 @@ class FileWatcher implements Closeable {
* @param paths the files or directories to watch
* @param action the action to take when changes are detected
*/
void watch(Set<Path> paths, Runnable action) {
void watch(Set<WatchablePath> paths, Runnable action) {
Assert.notNull(paths, "Paths must not be null");
Assert.notNull(action, "Action must not be null");
if (paths.isEmpty()) {
Expand Down Expand Up @@ -133,7 +134,11 @@ private void onThreadException(Thread thread, Throwable throwable) {
}

void register(Registration registration) throws IOException {
for (Path path : registration.paths()) {
for (WatchablePath watchablePath : registration.paths()) {
Path path = watchablePath.path();
if (watchablePath.optional() && !Files.exists(path)) {
path = path.getParent();
}
if (!Files.isRegularFile(path) && !Files.isDirectory(path)) {
throw new IOException("'%s' is neither a file nor a directory".formatted(path));
}
Expand Down Expand Up @@ -210,19 +215,22 @@ public void close() throws IOException {
/**
* An individual watch registration.
*/
private record Registration(Set<Path> paths, Runnable action) {
private record Registration(Set<WatchablePath> paths, Runnable action) {

Registration {
paths = paths.stream().map(Path::toAbsolutePath).collect(Collectors.toSet());
paths = paths.stream().map(watchablePath -> new WatchablePath(watchablePath.optional(), watchablePath.path().toAbsolutePath()))
.collect(Collectors.toSet());
}

boolean manages(Path file) {
Path absolutePath = file.toAbsolutePath();
return this.paths.contains(absolutePath) || isInDirectories(absolutePath);
return this.paths.stream()
.map(WatchablePath::path)
.anyMatch(absolutePath::equals) || isInDirectories(absolutePath);
}

private boolean isInDirectories(Path file) {
return this.paths.stream().filter(Files::isDirectory).anyMatch(file::startsWith);
return this.paths.stream().map(WatchablePath::path).filter(Files::isDirectory).anyMatch(file::startsWith);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2012-2024 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 org.springframework.boot.ssl.pem.PemCertificate;

class PemCertificateParser {

public static final String OPTIONAL_PREFIX = "optional:";

public PemCertificate parse(String source) {
boolean optional = source.startsWith(OPTIONAL_PREFIX);
String location = optional ? source.substring(OPTIONAL_PREFIX.length()) : source;
return new PemCertificate(location, optional);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

package org.springframework.boot.autoconfigure.ssl;

import java.util.HashSet;
import java.util.Set;

import org.springframework.boot.ssl.pem.PemSslStoreBundle;

/**
Expand Down Expand Up @@ -60,8 +63,14 @@ public static class Store {
/**
* Location or content of the certificate or certificate chain in PEM format.
*/
@Deprecated
private String certificate;

/**
* Set with location or content of the certificate or certificate chain in PEM format.
*/
private Set<String> certificates = new HashSet<>();

/**
* Location or content of the private key in PEM format.
*/
Expand All @@ -85,14 +94,29 @@ public void setType(String type) {
this.type = type;
}

@Deprecated
public String getCertificate() {
return this.certificate;
}

@Deprecated
public void setCertificate(String certificate) {
this.certificate = certificate;
}

public Set<String> getCertificates() {
if (this.certificate != null) {
Set<String> allCertificates = new HashSet<>(this.certificates);
allCertificates.add(this.certificate);
return allCertificates;
}
return this.certificates;
}

public void setCertificates(Set<String> certificates) {
this.certificates = certificates;
}

public String getPrivateKey() {
return this.privateKey;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

package org.springframework.boot.autoconfigure.ssl;

import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.boot.autoconfigure.ssl.SslBundleProperties.Key;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundleKey;
Expand All @@ -24,6 +27,7 @@
import org.springframework.boot.ssl.SslStoreBundle;
import org.springframework.boot.ssl.jks.JksSslStoreBundle;
import org.springframework.boot.ssl.jks.JksSslStoreDetails;
import org.springframework.boot.ssl.pem.PemCertificate;
import org.springframework.boot.ssl.pem.PemSslStore;
import org.springframework.boot.ssl.pem.PemSslStoreBundle;
import org.springframework.boot.ssl.pem.PemSslStoreDetails;
Expand Down Expand Up @@ -118,8 +122,12 @@ private static PemSslStore getPemSslStore(String propertyName, PemSslBundlePrope
}

private static PemSslStoreDetails asPemSslStoreDetails(PemSslBundleProperties.Store properties) {
return new PemSslStoreDetails(properties.getType(), properties.getCertificate(), properties.getPrivateKey(),
properties.getPrivateKeyPassword());
PemCertificateParser converter = new PemCertificateParser();
Set<PemCertificate> pemCertificates = properties.getCertificates().stream()
.map(converter::parse)
.collect(Collectors.toSet());
return new PemSslStoreDetails(properties.getType(), pemCertificates, properties.getPrivateKey(),
properties.getPrivateKeyPassword());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,17 @@

package org.springframework.boot.autoconfigure.ssl;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.springframework.boot.ssl.pem.PemCertificate;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundleRegistry;

Expand Down Expand Up @@ -54,13 +56,13 @@ public void registerBundles(SslBundleRegistry registry) {
}

private <P extends SslBundleProperties> void registerBundles(SslBundleRegistry registry, Map<String, P> properties,
Function<P, SslBundle> bundleFactory, Function<Bundle<P>, Set<Path>> watchedPaths) {
Function<P, SslBundle> bundleFactory, Function<Bundle<P>, Set<WatchablePath>> watchedPaths) {
properties.forEach((bundleName, bundleProperties) -> {
Supplier<SslBundle> bundleSupplier = () -> bundleFactory.apply(bundleProperties);
try {
registry.registerBundle(bundleName, bundleSupplier.get());
if (bundleProperties.isReloadOnUpdate()) {
Supplier<Set<Path>> pathsSupplier = () -> watchedPaths
Supplier<Set<WatchablePath>> pathsSupplier = () -> watchedPaths
.apply(new Bundle<>(bundleName, bundleProperties));
watchForUpdates(registry, bundleName, pathsSupplier, bundleSupplier);
}
Expand All @@ -71,7 +73,7 @@ private <P extends SslBundleProperties> void registerBundles(SslBundleRegistry r
});
}

private void watchForUpdates(SslBundleRegistry registry, String bundleName, Supplier<Set<Path>> pathsSupplier,
private void watchForUpdates(SslBundleRegistry registry, String bundleName, Supplier<Set<WatchablePath>> pathsSupplier,
Supplier<SslBundle> bundleSupplier) {
try {
this.fileWatcher.watch(pathsSupplier.get(), () -> registry.updateBundle(bundleName, bundleSupplier.get()));
Expand All @@ -81,31 +83,43 @@ private void watchForUpdates(SslBundleRegistry registry, String bundleName, Supp
}
}

private Set<Path> watchedJksPaths(Bundle<JksSslBundleProperties> bundle) {
private Set<WatchablePath> watchedJksPaths(Bundle<JksSslBundleProperties> bundle) {
List<BundleContentProperty> watched = new ArrayList<>();
watched.add(new BundleContentProperty("keystore.location", bundle.properties().getKeystore().getLocation()));
watched
.add(new BundleContentProperty("truststore.location", bundle.properties().getTruststore().getLocation()));
return watchedPaths(bundle.name(), watched);
}

private Set<Path> watchedPemPaths(Bundle<PemSslBundleProperties> bundle) {
private Set<WatchablePath> watchedPemPaths(Bundle<PemSslBundleProperties> bundle) {
List<BundleContentProperty> watched = new ArrayList<>();
BiFunction<String, String, BundleContentProperty> contentKeyStoreCertificateProperty = locationToBundleContentProperty();
watched
.add(new BundleContentProperty("keystore.private-key", bundle.properties().getKeystore().getPrivateKey()));
watched
.add(new BundleContentProperty("keystore.certificate", bundle.properties().getKeystore().getCertificate()));
bundle.properties().getKeystore().getCertificates().stream()
.map(location -> contentKeyStoreCertificateProperty.apply(location, "keystore.certificate"))
.forEach(watched::add);
watched.add(new BundleContentProperty("truststore.private-key",
bundle.properties().getTruststore().getPrivateKey()));
watched.add(new BundleContentProperty("truststore.certificate",
bundle.properties().getTruststore().getCertificate()));
bundle.properties().getTruststore().getCertificates().stream()
.map(location -> contentKeyStoreCertificateProperty.apply(location, "truststore.certificate"))
.forEach(watched::add);
return watchedPaths(bundle.name(), watched);
}

private Set<Path> watchedPaths(String bundleName, List<BundleContentProperty> properties) {
private BiFunction<String, String, BundleContentProperty> locationToBundleContentProperty() {
PemCertificateParser certificateParser = new PemCertificateParser();
return (location, name) -> {
PemCertificate certificate = certificateParser.parse(location);
return new BundleContentProperty(name, certificate.location(), certificate.optional());
};
}

private Set<WatchablePath> watchedPaths(String bundleName, List<BundleContentProperty> properties) {
try {
return properties.stream()
.filter(BundleContentProperty::hasValue)
.filter(Predicate.not(BundleContentProperty::isPemContent))
.map(BundleContentProperty::toWatchPath)
.collect(Collectors.toSet());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2012-2024 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.nio.file.Path;

record WatchablePath(boolean optional, Path path) {
}
Loading