Skip to content

Commit

Permalink
Add support for optional trust certificates
Browse files Browse the repository at this point in the history
  • Loading branch information
elisabetesantos committed Aug 19, 2024
1 parent 2ef3195 commit c3efeaf
Show file tree
Hide file tree
Showing 10 changed files with 117 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
*/
record BundleContentProperty(String name, String value) {

private static final String OPTIONAL_URL_PREFIX = "optional:";

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

Path toWatchPath() {
boolean isOptional() {
return this.value.startsWith(OPTIONAL_URL_PREFIX);
}

String getRawValue() {
if (isOptional()) {
return this.value.substring(OPTIONAL_URL_PREFIX.length());
}
return this.value;
}

WatchablePath toWatchPath() {
try {
Resource resource = getResource();
Resource resource = getResource(getRawValue());
if (!resource.isFile()) {
throw new BundleContentNotWatchableException(this);
}
return Path.of(resource.getFile().getAbsolutePath());
return new WatchablePath(Path.of(resource.getFile().getAbsolutePath()), isOptional());
}
catch (Exception ex) {
if (ex instanceof BundleContentNotWatchableException bundleContentNotWatchableException) {
Expand All @@ -68,9 +81,9 @@ Path toWatchPath() {
}
}

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,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 +133,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 +214,23 @@ 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.path().toAbsolutePath(), watchablePath.optional()))
.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
Expand Up @@ -29,6 +29,7 @@
import org.springframework.boot.ssl.pem.PemSslStoreDetails;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
* {@link SslBundle} backed by {@link JksSslBundleProperties} or
Expand All @@ -40,6 +41,8 @@
*/
public final class PropertiesSslBundle implements SslBundle {

private static final String OPTIONAL_URL_PREFIX = "optional:";

private final SslStoreBundle stores;

private final SslBundleKey key;
Expand Down Expand Up @@ -118,8 +121,19 @@ 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());
return new PemSslStoreDetails(properties.getType(), getRawCertificate(properties.getCertificate()), properties.getPrivateKey(),
properties.getPrivateKeyPassword(), isCertificateOptional(properties.getCertificate()));
}

private static boolean isCertificateOptional(String certificate) {
return StringUtils.hasText(certificate) && certificate.startsWith(OPTIONAL_URL_PREFIX);
}

private static String getRawCertificate(String certificate) {
if (isCertificateOptional(certificate)) {
return certificate.substring(OPTIONAL_URL_PREFIX.length());
}
return certificate;
}

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

package org.springframework.boot.autoconfigure.ssl;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -54,13 +53,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 +70,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,15 +80,15 @@ 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<>();
watched
.add(new BundleContentProperty("keystore.private-key", bundle.properties().getKeystore().getPrivateKey()));
Expand All @@ -102,7 +101,7 @@ private Set<Path> watchedPemPaths(Bundle<PemSslBundleProperties> bundle) {
return watchedPaths(bundle.name(), watched);
}

private Set<Path> watchedPaths(String bundleName, List<BundleContentProperty> properties) {
private Set<WatchablePath> watchedPaths(String bundleName, List<BundleContentProperty> properties) {
try {
return properties.stream()
.filter(BundleContentProperty::hasValue)
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(Path path, Boolean optional) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ private static UncheckedIOException asUncheckedIOException(String message, Excep
}

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

@Override
public boolean optional() {
return this.details.optional();
}

@Override
public String type() {
return this.details.type();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,21 @@ public String toString() {
return this.text;
}

/**
* Load {@link PemContent} from the given content (either the PEM content itself or a
* reference to the resource to load).
* @param content the content to load
* @param isOptional the content to load may be optional
* @return a new {@link PemContent} instance
* @throws IOException on IO error
*/
static PemContent load(String content, Boolean isOptional) throws IOException {
if (isOptional && !Files.exists(Path.of(content))) {
return null;
}
return load(content);
}

/**
* Load {@link PemContent} from the given content (either the PEM content itself or a
* reference to the resource to load).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
*/
public interface PemSslStore {


boolean optional();

/**
* The key store type, for example {@code JKS} or {@code PKCS11}. A {@code null} value
* will use {@link KeyStore#getDefaultType()}).
Expand Down Expand Up @@ -164,6 +167,11 @@ public PrivateKey privateKey() {
return privateKey;
}

@Override
public boolean optional() {
return false; //TODO
}

};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public KeyStore getTrustStore() {
}

private static KeyStore createKeyStore(String name, PemSslStore pemSslStore) {
if (pemSslStore == null) {
if (pemSslStore == null || pemSslStore.optional() && pemSslStore.certificates() == null) {
return null;
}
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,20 @@
* @param password the password used
* {@link KeyStore#setKeyEntry(String, java.security.Key, char[], java.security.cert.Certificate[])
* setting key entries} in the {@link KeyStore}
* @param certificates the certificates content (either the PEM content itself or a
* @param certificates the certificates content (either the PEM content itself or or a
* reference to the resource to load). When a {@link #privateKey() private key} is present
* this value is treated as a certificate chain, otherwise it is treated a list of
* certificates that should all be registered.
* @param privateKey the private key content (either the PEM content itself or a reference
* to the resource to load)
* @param privateKeyPassword a password used to decrypt an encrypted private key
* @param optional certificates/privateKey may be optional
* @author Scott Frederick
* @author Phillip Webb
* @since 3.1.0
* @see PemSslStore#load(PemSslStoreDetails)
*/
public record PemSslStoreDetails(String type, String alias, String password, String certificates, String privateKey,
String privateKeyPassword) {
public record PemSslStoreDetails(String type, String alias, String password, String certificates, String privateKey, String privateKeyPassword, boolean optional) {

/**
* Create a new {@link PemSslStoreDetails} instance.
Expand Down Expand Up @@ -71,9 +71,10 @@ public record PemSslStoreDetails(String type, String alias, String password, Str
* @param privateKey the private key content (either the PEM content itself or a
* reference to the resource to load)
* @param privateKeyPassword a password used to decrypt an encrypted private key
* @param optional certificates/privateKey may be optional
*/
public PemSslStoreDetails(String type, String certificate, String privateKey, String privateKeyPassword) {
this(type, null, null, certificate, privateKey, privateKeyPassword);
public PemSslStoreDetails(String type, String certificate, String privateKey, String privateKeyPassword, boolean optional) {
this(type, null, null, certificate, privateKey, privateKeyPassword, optional);
}

/**
Expand All @@ -86,7 +87,7 @@ public PemSslStoreDetails(String type, String certificate, String privateKey, St
* reference to the resource to load)
*/
public PemSslStoreDetails(String type, String certificate, String privateKey) {
this(type, certificate, privateKey, null);
this(type, certificate, privateKey, null, false);
}

/**
Expand All @@ -96,8 +97,7 @@ public PemSslStoreDetails(String type, String certificate, String privateKey) {
* @since 3.2.0
*/
public PemSslStoreDetails withAlias(String alias) {
return new PemSslStoreDetails(this.type, alias, this.password, this.certificates, this.privateKey,
this.privateKeyPassword);
return new PemSslStoreDetails(this.type, alias, this.password, this.certificates, this.privateKey, this.privateKeyPassword, this.optional);
}

/**
Expand All @@ -107,8 +107,7 @@ public PemSslStoreDetails withAlias(String alias) {
* @since 3.2.0
*/
public PemSslStoreDetails withPassword(String password) {
return new PemSslStoreDetails(this.type, this.alias, password, this.certificates, this.privateKey,
this.privateKeyPassword);
return new PemSslStoreDetails(this.type, this.alias, password, this.certificates, this.privateKey, this.privateKeyPassword, this.optional);
}

/**
Expand All @@ -117,8 +116,7 @@ public PemSslStoreDetails withPassword(String password) {
* @return a new {@link PemSslStoreDetails} instance
*/
public PemSslStoreDetails withPrivateKey(String privateKey) {
return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificates, privateKey,
this.privateKeyPassword);
return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificates, privateKey, this.privateKeyPassword, this.optional);
}

/**
Expand All @@ -127,8 +125,7 @@ public PemSslStoreDetails withPrivateKey(String privateKey) {
* @return a new {@link PemSslStoreDetails} instance
*/
public PemSslStoreDetails withPrivateKeyPassword(String privateKeyPassword) {
return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificates, this.privateKey,
privateKeyPassword);
return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificates, this.privateKey, privateKeyPassword, this.optional);
}

boolean isEmpty() {
Expand Down

0 comments on commit c3efeaf

Please sign in to comment.