Add PemDirectorySslStoreBundle
parent
4d65fbd49c
commit
584e5b71f0
@ -0,0 +1,205 @@
|
||||
/*
|
||||
* 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.nio.file.Path;
|
||||
|
||||
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
|
||||
import org.springframework.boot.ssl.pem.PemDirectorySslStoreBundle;
|
||||
import org.springframework.boot.ssl.pem.PemDirectorySslStoreBundle.CertificateSelector;
|
||||
import org.springframework.boot.ssl.pem.PemDirectorySslStoreDetails;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link SslBundleProperties} for directories containing PEM-encoded certificates and
|
||||
* private keys.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
* @since 3.2.0
|
||||
* @see PemDirectorySslStoreBundle
|
||||
*/
|
||||
public class PemDirectorySslBundleProperties extends SslBundleProperties {
|
||||
|
||||
/**
|
||||
* Directory containing the certificates and private keys.
|
||||
*/
|
||||
private String directory;
|
||||
|
||||
/**
|
||||
* Password used to decrypt an encrypted private key.
|
||||
*/
|
||||
private String privateKeyPassword;
|
||||
|
||||
/**
|
||||
* File extension of the certificates, e.g. '.crt'.
|
||||
*/
|
||||
private String certificateExtension = ".crt";
|
||||
|
||||
/**
|
||||
* File extension of the keys, e.g. '.key'.
|
||||
*/
|
||||
private String keyExtension = ".key";
|
||||
|
||||
/**
|
||||
* Certificate selection strategy.
|
||||
*/
|
||||
private CertificateSelection certificateSelection = CertificateSelection.NEWEST_NOT_BEFORE;
|
||||
|
||||
/**
|
||||
* Keystore properties.
|
||||
*/
|
||||
private final Store keystore = new Store();
|
||||
|
||||
/**
|
||||
* Truststore properties.
|
||||
*/
|
||||
private final Store truststore = new Store();
|
||||
|
||||
/**
|
||||
* Whether to verify that the private key matches the public key in the certificate.
|
||||
*/
|
||||
private boolean verifyKeys = false;
|
||||
|
||||
public String getDirectory() {
|
||||
return this.directory;
|
||||
}
|
||||
|
||||
public void setDirectory(String directory) {
|
||||
this.directory = directory;
|
||||
}
|
||||
|
||||
public String getPrivateKeyPassword() {
|
||||
return this.privateKeyPassword;
|
||||
}
|
||||
|
||||
public void setPrivateKeyPassword(String privateKeyPassword) {
|
||||
this.privateKeyPassword = privateKeyPassword;
|
||||
}
|
||||
|
||||
public String getCertificateExtension() {
|
||||
return this.certificateExtension;
|
||||
}
|
||||
|
||||
public void setCertificateExtension(String certificateExtension) {
|
||||
this.certificateExtension = certificateExtension;
|
||||
}
|
||||
|
||||
public String getKeyExtension() {
|
||||
return this.keyExtension;
|
||||
}
|
||||
|
||||
public void setKeyExtension(String keyExtension) {
|
||||
this.keyExtension = keyExtension;
|
||||
}
|
||||
|
||||
public CertificateSelection getCertificateSelection() {
|
||||
return this.certificateSelection;
|
||||
}
|
||||
|
||||
public void setCertificateSelection(CertificateSelection certificateSelection) {
|
||||
this.certificateSelection = certificateSelection;
|
||||
}
|
||||
|
||||
public boolean isVerifyKeys() {
|
||||
return this.verifyKeys;
|
||||
}
|
||||
|
||||
public void setVerifyKeys(boolean verifyKeys) {
|
||||
this.verifyKeys = verifyKeys;
|
||||
}
|
||||
|
||||
public Store getKeystore() {
|
||||
return this.keystore;
|
||||
}
|
||||
|
||||
public Store getTruststore() {
|
||||
return this.truststore;
|
||||
}
|
||||
|
||||
void validate() {
|
||||
if (!StringUtils.hasLength(this.directory)) {
|
||||
throw new InvalidConfigurationPropertyValueException("spring.ssl.bundle.pemdir.*.directory", this.directory,
|
||||
"Must not be empty");
|
||||
}
|
||||
}
|
||||
|
||||
PemDirectorySslStoreDetails toDetails() {
|
||||
return new PemDirectorySslStoreDetails(Path.of(getDirectory()), getKeystore().getType(),
|
||||
getTruststore().getType(), getPrivateKeyPassword(), getKey().getAlias(), isVerifyKeys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Certificate selection strategy.
|
||||
*/
|
||||
public enum CertificateSelection {
|
||||
|
||||
/**
|
||||
* Selects the certificate with the longest 'Not After' field (which is usually
|
||||
* the longest usable certificate).
|
||||
*/
|
||||
LONGEST_LIFETIME {
|
||||
@Override
|
||||
CertificateSelector getCertificateSelector() {
|
||||
return CertificateSelector.maximumNotAfter();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Selects the certificate with the maximum 'Not Before' field (which is usually
|
||||
* the most recently created certificate).
|
||||
*/
|
||||
NEWEST_NOT_BEFORE {
|
||||
@Override
|
||||
CertificateSelector getCertificateSelector() {
|
||||
return CertificateSelector.maximumNotBefore();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Selects the certificate which has been created most recently.
|
||||
*/
|
||||
NEWEST_FILE {
|
||||
@Override
|
||||
CertificateSelector getCertificateSelector() {
|
||||
return CertificateSelector.newestFile();
|
||||
}
|
||||
};
|
||||
|
||||
abstract CertificateSelector getCertificateSelector();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Store properties.
|
||||
*/
|
||||
public static class Store {
|
||||
|
||||
/**
|
||||
* Type of the store to create, e.g. JKS.
|
||||
*/
|
||||
private String type;
|
||||
|
||||
public String getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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.ssl.pem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.ssl.pem.PemDirectorySslStoreBundle.Certificate;
|
||||
import org.springframework.boot.ssl.pem.PemDirectorySslStoreBundle.CertificateSelector;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link CertificateSelector} implementations.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
final class CertificateSelectors {
|
||||
|
||||
private CertificateSelectors() {
|
||||
}
|
||||
|
||||
abstract static class AbstractCertificateSelector implements CertificateSelector {
|
||||
|
||||
private final Clock clock;
|
||||
|
||||
AbstractCertificateSelector() {
|
||||
this(Clock.systemDefaultZone());
|
||||
}
|
||||
|
||||
AbstractCertificateSelector(Clock clock) {
|
||||
Assert.notNull(clock, "clock must not be null");
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Certificate select(List<Certificate> certificates) {
|
||||
Instant now = this.clock.instant();
|
||||
List<Certificate> preProcessed = certificates.stream().filter((c) -> isUsable(c, now)).toList();
|
||||
return doSelect(preProcessed);
|
||||
}
|
||||
|
||||
protected abstract Certificate doSelect(List<Certificate> candidates);
|
||||
|
||||
private boolean isUsable(Certificate certificate, Instant now) {
|
||||
return now.isAfter(certificate.certificate().getNotBefore().toInstant())
|
||||
&& now.isBefore(certificate.certificate().getNotAfter().toInstant());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class MaximumNotAfterCertificateSelector extends AbstractCertificateSelector {
|
||||
|
||||
MaximumNotAfterCertificateSelector() {
|
||||
super();
|
||||
}
|
||||
|
||||
MaximumNotAfterCertificateSelector(Clock clock) {
|
||||
super(clock);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Certificate doSelect(List<Certificate> candidates) {
|
||||
return candidates.stream().max(Comparator.comparing((c) -> c.certificate().getNotAfter())).orElse(null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class MaximumNotBeforeCertificateSelector extends AbstractCertificateSelector {
|
||||
|
||||
MaximumNotBeforeCertificateSelector() {
|
||||
super();
|
||||
}
|
||||
|
||||
MaximumNotBeforeCertificateSelector(Clock clock) {
|
||||
super(clock);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Certificate doSelect(List<Certificate> candidates) {
|
||||
return candidates.stream().max(Comparator.comparing((c) -> c.certificate().getNotBefore())).orElse(null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class NewestFileCertificateSelector extends AbstractCertificateSelector {
|
||||
|
||||
NewestFileCertificateSelector() {
|
||||
super();
|
||||
}
|
||||
|
||||
NewestFileCertificateSelector(Clock clock) {
|
||||
super(clock);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Certificate doSelect(List<Certificate> candidates) {
|
||||
if (candidates.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (candidates.size() == 1) {
|
||||
return candidates.get(0);
|
||||
}
|
||||
Certificate certificate = null;
|
||||
Instant created = null;
|
||||
for (Certificate candidate : candidates) {
|
||||
BasicFileAttributes attributes;
|
||||
try {
|
||||
attributes = Files.readAttributes(candidate.file(), BasicFileAttributes.class);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException("Failed to get creation time of file %s".formatted(candidate.file()),
|
||||
ex);
|
||||
}
|
||||
Instant candidateCreationTime = attributes.creationTime().toInstant();
|
||||
if (created == null || candidateCreationTime.isAfter(created)) {
|
||||
certificate = candidate;
|
||||
created = candidateCreationTime;
|
||||
}
|
||||
}
|
||||
return certificate;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,231 @@
|
||||
/*
|
||||
* 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.ssl.pem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyStore;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.boot.ssl.SslStoreBundle;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link SslStoreBundle} which uses a directory containing certificates and keys in PEM
|
||||
* encoding.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
* @since 3.2.0
|
||||
* @see PemSslStoreBundle
|
||||
*/
|
||||
public class PemDirectorySslStoreBundle implements SslStoreBundle {
|
||||
|
||||
private final SslStoreBundle delegate;
|
||||
|
||||
/**
|
||||
* Creates a new {@link PemDirectorySslStoreBundle}.
|
||||
* @param details the {@link PemDirectorySslStoreDetails} to create the bundle
|
||||
* @param certificateMatcher the strategy to find certificates
|
||||
* @param keyLocator the strategy to find the key for a certificate
|
||||
* @param certificateSelector the strategy to select a certificate
|
||||
*/
|
||||
public PemDirectorySslStoreBundle(PemDirectorySslStoreDetails details, CertificateMatcher certificateMatcher,
|
||||
KeyLocator keyLocator, CertificateSelector certificateSelector) {
|
||||
Assert.notNull(details, "details must not be null");
|
||||
Assert.notNull(certificateMatcher, "certificateMatcher must not be null");
|
||||
Assert.notNull(keyLocator, "keyLocator must not be null");
|
||||
Assert.notNull(certificateSelector, "certificateSelector must not be null");
|
||||
List<Path> files = listFiles(details.directory());
|
||||
List<Certificate> certificates = findCertificates(certificateMatcher, files);
|
||||
Certificate certificate = selectCertificate(certificateSelector, certificates);
|
||||
Path key = findKey(keyLocator, certificate, files);
|
||||
this.delegate = loadBundle(certificate.file(), key, details);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyStore getKeyStore() {
|
||||
return this.delegate.getKeyStore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeyStorePassword() {
|
||||
return this.delegate.getKeyStorePassword();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyStore getTrustStore() {
|
||||
return this.delegate.getTrustStore();
|
||||
}
|
||||
|
||||
private static PemSslStoreBundle loadBundle(Path certificate, Path key, PemDirectorySslStoreDetails details) {
|
||||
String certificateContent = readContent(certificate);
|
||||
String keyContent = readContent(key);
|
||||
return new PemSslStoreBundle(
|
||||
new PemSslStoreDetails(details.keyStoreType(), certificateContent, keyContent,
|
||||
details.privateKeyPassword()),
|
||||
new PemSslStoreDetails(details.trustStoreType(), certificateContent, null), details.alias(), null,
|
||||
details.verifyKeys());
|
||||
}
|
||||
|
||||
private static List<Path> listFiles(Path directory) {
|
||||
try (Stream<Path> fileStream = Files.list(directory)) {
|
||||
return fileStream.toList();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException("Failed to list files in directory '%s'".formatted(directory), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static Certificate selectCertificate(CertificateSelector certificateSelector,
|
||||
List<Certificate> certificates) {
|
||||
Certificate selected = certificateSelector.select(certificates);
|
||||
if (selected == null) {
|
||||
throw new IllegalStateException("No certificate could be selected. Candidates: %s".formatted(certificates));
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private static Path findKey(KeyLocator keyLocator, Certificate certificate, List<Path> files) {
|
||||
Path key = keyLocator.locate(certificate, files);
|
||||
if (key == null || !Files.exists(key)) {
|
||||
throw new IllegalStateException("Key for certificate '%s' not found".formatted(certificate.file()));
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
private static List<Certificate> findCertificates(CertificateMatcher certificateMatcher, List<Path> files) {
|
||||
List<Certificate> candidates = new ArrayList<>();
|
||||
for (Path file : files) {
|
||||
if (certificateMatcher.matches(file)) {
|
||||
String content = readContent(file);
|
||||
X509Certificate[] x509Certificates = PemCertificateParser.parse(content);
|
||||
if (x509Certificates == null || x509Certificates.length == 0) {
|
||||
throw new IllegalStateException("No certificates found in file '%s'".formatted(file));
|
||||
}
|
||||
// Always use the first certificate if it is a chain
|
||||
candidates.add(new Certificate(file, x509Certificates[0]));
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private static String readContent(Path file) {
|
||||
try {
|
||||
return Files.readString(file);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException("Failed to read content of file '%s'".formatted(file), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Certificate.
|
||||
*
|
||||
* @param file the certificate file
|
||||
* @param certificate the parsed certificate
|
||||
*/
|
||||
public record Certificate(Path file, X509Certificate certificate) {
|
||||
}
|
||||
|
||||
public interface KeyLocator {
|
||||
|
||||
/**
|
||||
* Locates the key belonging to the given {@code certificate}.
|
||||
* @param certificate the certificate to locate a key for
|
||||
* @param files the available files
|
||||
* @return the path to the key, or {@code null}
|
||||
*/
|
||||
Path locate(Certificate certificate, Collection<Path> files);
|
||||
|
||||
/**
|
||||
* Creates a {@link KeyLocator} which selects keys based on the file extension.
|
||||
* @param certificateExtension the extension of certificate files
|
||||
* @param keyExtension the extension of key files
|
||||
* @return the key locator
|
||||
*/
|
||||
static KeyLocator withExtension(String certificateExtension, String keyExtension) {
|
||||
return new SuffixKeyLocator(certificateExtension, keyExtension);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public interface CertificateMatcher {
|
||||
|
||||
/**
|
||||
* Decides whether the given {@code file} is a certificate.
|
||||
* @param file the file to decide on
|
||||
* @return whether the file is a certificate
|
||||
*/
|
||||
boolean matches(Path file);
|
||||
|
||||
/**
|
||||
* Creates a {@link CertificateMatcher} which selects certificates based on the
|
||||
* file extension.
|
||||
* @param certificateExtension the extension of certificate files
|
||||
* @return the certificate matcher
|
||||
*/
|
||||
static CertificateMatcher withExtension(String certificateExtension) {
|
||||
return new SuffixCertificateMatcher(certificateExtension);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public interface CertificateSelector {
|
||||
|
||||
/**
|
||||
* Selects a certificate from the given {@code certificates}.
|
||||
* @param certificates the certificates
|
||||
* @return the selected certificate
|
||||
*/
|
||||
Certificate select(List<Certificate> certificates);
|
||||
|
||||
/**
|
||||
* Creates a {@link CertificateSelector} which selects the certificate with the
|
||||
* maximum not after field.
|
||||
* @return the certificate selector
|
||||
*/
|
||||
static CertificateSelector maximumNotAfter() {
|
||||
return new CertificateSelectors.MaximumNotAfterCertificateSelector();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link CertificateSelector} which selects the certificate with the
|
||||
* maximum not before field.
|
||||
* @return the certificate selector
|
||||
*/
|
||||
static CertificateSelector maximumNotBefore() {
|
||||
return new CertificateSelectors.MaximumNotBeforeCertificateSelector();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link CertificateSelector} which selects the certificate with the
|
||||
* newest file creation date.
|
||||
* @return the certificate selector
|
||||
*/
|
||||
static CertificateSelector newestFile() {
|
||||
return new CertificateSelectors.NewestFileCertificateSelector();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.ssl.pem;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Details to create a {@link PemDirectorySslStoreBundle}.
|
||||
*
|
||||
* @param directory the directory to load the certificate and key from
|
||||
* @param keyStoreType the type of keystore or {@code null}
|
||||
* @param trustStoreType the type of truststore or {@code null}
|
||||
* @param privateKeyPassword the password for loading the private key or {@code null}
|
||||
* @param alias the alias for the certificate and key or {@code null}
|
||||
* @param verifyKeys whether to verify the certificate and key
|
||||
* @author Moritz Halbritter
|
||||
* @since 3.2.0
|
||||
*/
|
||||
public record PemDirectorySslStoreDetails(Path directory, String keyStoreType, String trustStoreType,
|
||||
String privateKeyPassword, String alias, boolean verifyKeys) {
|
||||
public PemDirectorySslStoreDetails {
|
||||
Assert.notNull(directory, "directory must not be null");
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.ssl.pem;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.springframework.boot.ssl.pem.PemDirectorySslStoreBundle.CertificateMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link CertificateMatcher} which matches files with a given suffix.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class SuffixCertificateMatcher implements CertificateMatcher {
|
||||
|
||||
private final String certificateSuffix;
|
||||
|
||||
SuffixCertificateMatcher(String certificateSuffix) {
|
||||
Assert.notNull(certificateSuffix, "certificateSuffix must not be null");
|
||||
this.certificateSuffix = certificateSuffix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(Path file) {
|
||||
return file.toString().endsWith(this.certificateSuffix);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.ssl.pem;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.boot.ssl.pem.PemDirectorySslStoreBundle.Certificate;
|
||||
import org.springframework.boot.ssl.pem.PemDirectorySslStoreBundle.KeyLocator;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link KeyLocator} which matches files with a given suffix.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class SuffixKeyLocator implements KeyLocator {
|
||||
|
||||
private final String certificateSuffix;
|
||||
|
||||
private final String keySuffix;
|
||||
|
||||
SuffixKeyLocator(String certificateSuffix, String keySuffix) {
|
||||
Assert.notNull(certificateSuffix, "certificateSuffix must not be null");
|
||||
Assert.notNull(keySuffix, "keySuffix must not be null");
|
||||
this.certificateSuffix = certificateSuffix;
|
||||
this.keySuffix = keySuffix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path locate(Certificate certificate, Collection<Path> files) {
|
||||
String path = certificate.file().toString();
|
||||
if (!path.endsWith(this.certificateSuffix)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Path '%s' does not end with '%s'".formatted(path, this.certificateSuffix));
|
||||
}
|
||||
path = path.substring(0, path.length() - this.certificateSuffix.length());
|
||||
path = path + this.keySuffix;
|
||||
return Path.of(path);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.ssl.pem;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.sql.Date;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.boot.ssl.pem.PemDirectorySslStoreBundle.Certificate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
|
||||
/**
|
||||
* Tests for {@link CertificateSelectors.MaximumNotAfterCertificateSelector}.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class MaximumNotAfterCertificateSelectorTests {
|
||||
|
||||
private static final Instant NOW = Instant.parse("2000-01-01T00:00:00Z");
|
||||
|
||||
private final CertificateSelectors.MaximumNotAfterCertificateSelector selector = new CertificateSelectors.MaximumNotAfterCertificateSelector(
|
||||
Clock.fixed(NOW, ZoneId.of("UTC")));
|
||||
|
||||
@Test
|
||||
void shouldSelectCertificateWithMaximumNotAfter() {
|
||||
// Valid until 10s
|
||||
Certificate cert1 = new Certificate(Path.of("certificate-1"),
|
||||
createCertificate(NOW.minusSeconds(10), NOW.plusSeconds(10)));
|
||||
// Not valid, starts in the future
|
||||
Certificate cert2 = new Certificate(Path.of("certificate-2"),
|
||||
createCertificate(NOW.plusSeconds(1), NOW.plusSeconds(15)));
|
||||
// Not valid, expired
|
||||
Certificate cert3 = new Certificate(Path.of("certificate-3"),
|
||||
createCertificate(NOW.minusSeconds(10), NOW.minusSeconds(1)));
|
||||
// Valid until 20s
|
||||
Certificate cert4 = new Certificate(Path.of("certificate-4"),
|
||||
createCertificate(NOW.minusSeconds(10), NOW.plusSeconds(20)));
|
||||
List<Certificate> candidates = List.of(cert1, cert2, cert3, cert4);
|
||||
Certificate selected = this.selector.select(candidates);
|
||||
assertThat(selected).isEqualTo(cert4);
|
||||
}
|
||||
|
||||
private X509Certificate createCertificate(Instant notBefore, Instant notAfter) {
|
||||
X509Certificate certificate = Mockito.mock(X509Certificate.class);
|
||||
given(certificate.getNotBefore()).willReturn(Date.from(notBefore));
|
||||
given(certificate.getNotAfter()).willReturn(Date.from(notAfter));
|
||||
return certificate;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.ssl.pem;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.sql.Date;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.boot.ssl.pem.PemDirectorySslStoreBundle.Certificate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
|
||||
/**
|
||||
* Tests for {@link CertificateSelectors.MaximumNotBeforeCertificateSelector}.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class MaximumNotBeforeCertificateSelectorTests {
|
||||
|
||||
private static final Instant NOW = Instant.parse("2000-01-01T00:00:00Z");
|
||||
|
||||
private final CertificateSelectors.MaximumNotBeforeCertificateSelector selector = new CertificateSelectors.MaximumNotBeforeCertificateSelector(
|
||||
Clock.fixed(NOW, ZoneId.of("UTC")));
|
||||
|
||||
@Test
|
||||
void shouldSelectCertificateWithMaximumNotBefore() {
|
||||
// Valid since -10s
|
||||
Certificate cert1 = new Certificate(Path.of("certificate-1"),
|
||||
createCertificate(NOW.minusSeconds(10), NOW.plusSeconds(1)));
|
||||
// Not valid, starts in the future
|
||||
Certificate cert2 = new Certificate(Path.of("certificate-2"),
|
||||
createCertificate(NOW.plusSeconds(1), NOW.plusSeconds(15)));
|
||||
// Not valid, expired
|
||||
Certificate cert3 = new Certificate(Path.of("certificate-3"),
|
||||
createCertificate(NOW.minusSeconds(10), NOW.minusSeconds(1)));
|
||||
// Valid since -20s
|
||||
Certificate cert4 = new Certificate(Path.of("certificate-4"),
|
||||
createCertificate(NOW.minusSeconds(20), NOW.plusSeconds(1)));
|
||||
List<Certificate> candidates = List.of(cert1, cert2, cert3, cert4);
|
||||
Certificate selected = this.selector.select(candidates);
|
||||
assertThat(selected).isEqualTo(cert1);
|
||||
}
|
||||
|
||||
private X509Certificate createCertificate(Instant notBefore, Instant notAfter) {
|
||||
X509Certificate certificate = Mockito.mock(X509Certificate.class);
|
||||
given(certificate.getNotBefore()).willReturn(Date.from(notBefore));
|
||||
given(certificate.getNotAfter()).willReturn(Date.from(notAfter));
|
||||
return certificate;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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.ssl.pem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.sql.Date;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.boot.ssl.pem.PemDirectorySslStoreBundle.Certificate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
|
||||
/**
|
||||
* Tests for {@link CertificateSelectors.NewestFileCertificateSelector}.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class NewestFileCertificateSelectorTests {
|
||||
|
||||
private static final Instant NOW = Instant.parse("2000-01-01T00:00:00Z");
|
||||
|
||||
private final CertificateSelectors.NewestFileCertificateSelector selector = new CertificateSelectors.NewestFileCertificateSelector(
|
||||
Clock.fixed(NOW, ZoneId.of("UTC")));
|
||||
|
||||
@Test
|
||||
void shouldSelectNewestFile(@TempDir Path tempDirectory) throws IOException, InterruptedException {
|
||||
// There's no portable way to set the file creation date, so we resort to sleeps
|
||||
// here
|
||||
Path cert1file = Files.createFile(tempDirectory.resolve("certificate-1"));
|
||||
Thread.sleep(10);
|
||||
Path cert2file = Files.createFile(tempDirectory.resolve("certificate-2"));
|
||||
Thread.sleep(10);
|
||||
Path cert3file = Files.createFile(tempDirectory.resolve("certificate-3"));
|
||||
Thread.sleep(10);
|
||||
Path cert4file = Files.createFile(tempDirectory.resolve("certificate-4"));
|
||||
// Valid
|
||||
Certificate cert1 = createCertificate(cert1file, NOW.minusSeconds(10), NOW.plusSeconds(10));
|
||||
// Not valid, starts in the future
|
||||
Certificate cert2 = createCertificate(cert2file, NOW.plusSeconds(1), NOW.plusSeconds(15));
|
||||
// Not valid, expired
|
||||
Certificate cert3 = createCertificate(cert3file, NOW.minusSeconds(1), NOW.minusSeconds(1));
|
||||
// Valid
|
||||
Certificate cert4 = createCertificate(cert4file, NOW.minusSeconds(1), NOW.plusSeconds(1));
|
||||
List<Certificate> candidates = List.of(cert1, cert2, cert3, cert4);
|
||||
Certificate selected = this.selector.select(candidates);
|
||||
assertThat(selected).isEqualTo(cert4);
|
||||
}
|
||||
|
||||
private Certificate createCertificate(Path file, Instant notBefore, Instant notAfter) {
|
||||
X509Certificate certificate = Mockito.mock(X509Certificate.class);
|
||||
given(certificate.getNotBefore()).willReturn(Date.from(notBefore));
|
||||
given(certificate.getNotAfter()).willReturn(Date.from(notAfter));
|
||||
return new Certificate(file, certificate);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
/*
|
||||
* 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.ssl.pem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.Key;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.boot.ssl.pem.PemDirectorySslStoreBundle.CertificateMatcher;
|
||||
import org.springframework.boot.ssl.pem.PemDirectorySslStoreBundle.CertificateSelector;
|
||||
import org.springframework.boot.ssl.pem.PemDirectorySslStoreBundle.KeyLocator;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for {@link PemDirectorySslStoreBundle}.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class PemDirectorySslStoreBundleTests {
|
||||
|
||||
private static final Instant NOW = Instant.parse("2023-12-31T00:00:00Z");
|
||||
|
||||
private final CertificateMatcher certificateMatcher = new SuffixCertificateMatcher(".crt");
|
||||
|
||||
private final KeyLocator keyLocator = new SuffixKeyLocator(".crt", ".key");
|
||||
|
||||
private final CertificateSelector certificateSelector = new CertificateSelectors.MaximumNotAfterCertificateSelector(
|
||||
Clock.fixed(NOW, ZoneId.of("UTC")));
|
||||
|
||||
@Test
|
||||
void shouldLoadFromDirectory(@TempDir Path tempDir)
|
||||
throws IOException, UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException {
|
||||
storeFile(tempDir, "key1.crt", "1.crt");
|
||||
storeFile(tempDir, "key1.pem", "1.key");
|
||||
storeFile(tempDir, "key2.crt", "2.crt");
|
||||
storeFile(tempDir, "key2.pem", "2.key");
|
||||
PemDirectorySslStoreBundle bundle = createBundle(tempDir);
|
||||
assertHasKey(bundle.getKeyStore(), "alias");
|
||||
// This is the serial number of key2.crt
|
||||
assertHasCertificate(bundle.getKeyStore(), "alias", "506931258775469093758931777760969589521779563942");
|
||||
assertHasCertificate(bundle.getTrustStore(), "alias-0", "506931258775469093758931777760969589521779563942");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailIfGivenNonExistentDirectory() {
|
||||
assertThatThrownBy(() -> createBundle(Path.of("/this/path/does/not/exist")))
|
||||
.isInstanceOf(UncheckedIOException.class)
|
||||
.hasMessage("Failed to list files in directory '/this/path/does/not/exist'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailIfGivenEmptyDirectory(@TempDir Path tempDir) {
|
||||
assertThatIllegalStateException().isThrownBy(() -> createBundle(tempDir))
|
||||
.withMessage("No certificate could be selected. Candidates: []");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailIfKeyIsMissing(@TempDir Path tempDir) throws IOException {
|
||||
storeFile(tempDir, "key1.crt", "1.crt");
|
||||
storeFile(tempDir, "key2.crt", "2.crt");
|
||||
assertThatIllegalStateException().isThrownBy(() -> createBundle(tempDir))
|
||||
.withMessageMatching("Key for certificate '.+' not found");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailIfNoCertificatesCanBeLoaded(@TempDir Path tempDir) throws IOException {
|
||||
Files.createFile(tempDir.resolve("1.crt"));
|
||||
assertThatIllegalStateException().isThrownBy(() -> createBundle(tempDir))
|
||||
.withMessageMatching("No certificates found in file '.+'");
|
||||
}
|
||||
|
||||
private PemDirectorySslStoreBundle createBundle(Path tempDir) {
|
||||
return new PemDirectorySslStoreBundle(
|
||||
new PemDirectorySslStoreDetails(tempDir, null, null, null, "alias", false), this.certificateMatcher,
|
||||
this.keyLocator, this.certificateSelector);
|
||||
}
|
||||
|
||||
private void assertHasCertificate(KeyStore keyStore, String alias, String serialNumber) throws KeyStoreException {
|
||||
Certificate certificate = keyStore.getCertificate(alias);
|
||||
assertThat(certificate).as("certificate").isNotNull();
|
||||
assertThat(certificate).isInstanceOf(X509Certificate.class);
|
||||
X509Certificate x509Certificate = (X509Certificate) certificate;
|
||||
assertThat(x509Certificate.getSerialNumber()).isEqualTo(serialNumber);
|
||||
}
|
||||
|
||||
private static void storeFile(Path tempDir, String resourceName, String fileName) throws IOException {
|
||||
try (InputStream resourceStream = PemDirectorySslStoreBundleTests.class.getResourceAsStream(resourceName);
|
||||
OutputStream fileStream = Files.newOutputStream(tempDir.resolve(fileName))) {
|
||||
assertThat(resourceStream).isNotNull();
|
||||
resourceStream.transferTo(fileStream);
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertHasKey(KeyStore keyStore, String alias)
|
||||
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
|
||||
Key key = keyStore.getKey(alias, null);
|
||||
assertThat(key).as("key").isNotNull();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.ssl.pem;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link SuffixCertificateMatcher}.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class SuffixCertificateMatcherTests {
|
||||
|
||||
private final SuffixCertificateMatcher predicate = new SuffixCertificateMatcher(".crt");
|
||||
|
||||
@Test
|
||||
void shouldMatch() {
|
||||
assertThat(this.predicate.matches(Path.of("1.crt"))).isTrue();
|
||||
assertThat(this.predicate.matches(Path.of(".crt"))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotMatch() {
|
||||
assertThat(this.predicate.matches(Path.of(".key"))).isFalse();
|
||||
assertThat(this.predicate.matches(Path.of(".pem"))).isFalse();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.ssl.pem;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.boot.ssl.pem.PemDirectorySslStoreBundle.Certificate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link SuffixKeyLocator}.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class SuffixKeyLocatorTests {
|
||||
|
||||
private final SuffixKeyLocator locator = new SuffixKeyLocator(".crt", ".key");
|
||||
|
||||
@Test
|
||||
void shouldMatch() {
|
||||
Certificate certificate = createCertificate("1.crt");
|
||||
Path key = this.locator.locate(certificate, Set.of(Path.of("1.crt"), Path.of("1.key")));
|
||||
assertThat(key).isEqualTo(Path.of("1.key"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionIfGivenCertificateDoesntEndWithSuffix() {
|
||||
Certificate certificate = createCertificate("1.pem");
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.locator.locate(certificate, Collections.emptySet()))
|
||||
.withMessageContaining("does not end with '.crt'");
|
||||
}
|
||||
|
||||
private Certificate createCertificate(String file) {
|
||||
return new Certificate(Path.of(file), Mockito.mock(X509Certificate.class));
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue