Skip to content

Commit

Permalink
Add support for multiple/optional certificates
Browse files Browse the repository at this point in the history
  • Loading branch information
elisabetesantos committed Sep 10, 2024
1 parent c3efeaf commit fe27aca
Show file tree
Hide file tree
Showing 26 changed files with 594 additions and 85 deletions.
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,9 +32,12 @@
* @author Phillip Webb
* @author Moritz Halbritter
*/
record BundleContentProperty(String name, String value) {
record BundleContentProperty(String name, String value, boolean optional) {

private static final String OPTIONAL_URL_PREFIX = "optional:";
BundleContentProperty(String name, String value)
{
this(name, value,false);
}

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

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(getRawValue());
if (isPemContent()) {
return null;
}
Resource resource = getResource();
if (!resource.isFile()) {
throw new BundleContentNotWatchableException(this);
}
return new WatchablePath(Path.of(resource.getFile().getAbsolutePath()), isOptional());
return new WatchablePath(this.optional, Path.of(resource.getFile().getAbsolutePath()));
}
catch (Exception ex) {
if (ex instanceof BundleContentNotWatchableException bundleContentNotWatchableException) {
Expand All @@ -81,9 +75,8 @@ WatchablePath toWatchPath() {
}
}

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

}
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 @@ -217,8 +218,7 @@ public void close() throws IOException {
private record Registration(Set<WatchablePath> paths, Runnable action) {

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

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,12 +27,12 @@
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;
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 @@ -41,8 +44,6 @@
*/
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 @@ -121,19 +122,12 @@ private static PemSslStore getPemSslStore(String propertyName, PemSslBundlePrope
}

private static PemSslStoreDetails asPemSslStoreDetails(PemSslBundleProperties.Store properties) {
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;
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 @@ -20,10 +20,13 @@
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 @@ -90,21 +93,33 @@ private Set<WatchablePath> watchedJksPaths(Bundle<JksSslBundleProperties> 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 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
Expand Up @@ -18,5 +18,5 @@

import java.nio.file.Path;

record WatchablePath(Path path, Boolean optional) {
}
record WatchablePath(boolean optional, Path path) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.io.UncheckedIOException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;

Expand Down Expand Up @@ -58,11 +59,13 @@ private static UncheckedIOException asUncheckedIOException(String message, Excep
}

private static List<X509Certificate> loadCertificates(PemSslStoreDetails details) throws IOException {
PemContent pemContent = PemContent.load(details.certificates(), details.optional());
if (pemContent == null) {
return null;
List<X509Certificate> certificates = new ArrayList<>();
for (PemCertificate certificate : details.certificateSet()) {
PemContent pemContent = PemContent.load(certificate.location(), certificate.optional());
if (pemContent != null) {
certificates.addAll(pemContent.getCertificates());
}
}
List<X509Certificate> certificates = pemContent.getCertificates();
Assert.state(!CollectionUtils.isEmpty(certificates), "Loaded certificates are empty");
return certificates;
}
Expand All @@ -72,11 +75,6 @@ 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
@@ -0,0 +1,24 @@
/*
* 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.ssl.pem;

public record PemCertificate (String location, boolean optional) {

public PemCertificate(String location) {
this(location, false);
}
}
Loading

0 comments on commit fe27aca

Please sign in to comment.