Merge branch '3.3.x'

Closes gh-42839
This commit is contained in:
Phillip Webb 2024-10-22 18:45:19 -07:00
commit fdf7ca9093
13 changed files with 220 additions and 70 deletions

View File

@ -17,6 +17,7 @@
package org.springframework.boot.autoconfigure.ssl; package org.springframework.boot.autoconfigure.ssl;
import org.springframework.boot.autoconfigure.ssl.SslBundleProperties.Key; import org.springframework.boot.autoconfigure.ssl.SslBundleProperties.Key;
import org.springframework.boot.io.ApplicationResourceLoader;
import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundleKey; import org.springframework.boot.ssl.SslBundleKey;
import org.springframework.boot.ssl.SslManagerBundle; import org.springframework.boot.ssl.SslManagerBundle;
@ -27,6 +28,7 @@ import org.springframework.boot.ssl.jks.JksSslStoreDetails;
import org.springframework.boot.ssl.pem.PemSslStore; import org.springframework.boot.ssl.pem.PemSslStore;
import org.springframework.boot.ssl.pem.PemSslStoreBundle; import org.springframework.boot.ssl.pem.PemSslStoreBundle;
import org.springframework.boot.ssl.pem.PemSslStoreDetails; import org.springframework.boot.ssl.pem.PemSslStoreDetails;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.style.ToStringCreator; import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -97,18 +99,31 @@ public final class PropertiesSslBundle implements SslBundle {
* @return an {@link SslBundle} instance * @return an {@link SslBundle} instance
*/ */
public static SslBundle get(PemSslBundleProperties properties) { public static SslBundle get(PemSslBundleProperties properties) {
PemSslStore keyStore = getPemSslStore("keystore", properties.getKeystore()); return get(properties, new ApplicationResourceLoader());
}
/**
* Get an {@link SslBundle} for the given {@link PemSslBundleProperties}.
* @param properties the source properties
* @param resourceLoader the resource loader used to load content
* @return an {@link SslBundle} instance
* @since 3.3.5
*/
public static SslBundle get(PemSslBundleProperties properties, ResourceLoader resourceLoader) {
PemSslStore keyStore = getPemSslStore("keystore", properties.getKeystore(), resourceLoader);
if (keyStore != null) { if (keyStore != null) {
keyStore = keyStore.withAlias(properties.getKey().getAlias()) keyStore = keyStore.withAlias(properties.getKey().getAlias())
.withPassword(properties.getKey().getPassword()); .withPassword(properties.getKey().getPassword());
} }
PemSslStore trustStore = getPemSslStore("truststore", properties.getTruststore()); PemSslStore trustStore = getPemSslStore("truststore", properties.getTruststore(), resourceLoader);
SslStoreBundle storeBundle = new PemSslStoreBundle(keyStore, trustStore); SslStoreBundle storeBundle = new PemSslStoreBundle(keyStore, trustStore);
return new PropertiesSslBundle(storeBundle, properties); return new PropertiesSslBundle(storeBundle, properties);
} }
private static PemSslStore getPemSslStore(String propertyName, PemSslBundleProperties.Store properties) { private static PemSslStore getPemSslStore(String propertyName, PemSslBundleProperties.Store properties,
PemSslStore pemSslStore = PemSslStore.load(asPemSslStoreDetails(properties)); ResourceLoader resourceLoader) {
PemSslStoreDetails details = asPemSslStoreDetails(properties);
PemSslStore pemSslStore = PemSslStore.load(details, resourceLoader);
if (properties.isVerifyKeys()) { if (properties.isVerifyKeys()) {
CertificateMatcher certificateMatcher = new CertificateMatcher(pemSslStore.privateKey()); CertificateMatcher certificateMatcher = new CertificateMatcher(pemSslStore.privateKey());
Assert.state(certificateMatcher.matchesAny(pemSslStore.certificates()), Assert.state(certificateMatcher.matchesAny(pemSslStore.certificates()),
@ -128,14 +143,25 @@ public final class PropertiesSslBundle implements SslBundle {
* @return an {@link SslBundle} instance * @return an {@link SslBundle} instance
*/ */
public static SslBundle get(JksSslBundleProperties properties) { public static SslBundle get(JksSslBundleProperties properties) {
SslStoreBundle storeBundle = asSslStoreBundle(properties); return get(properties, new ApplicationResourceLoader());
}
/**
* Get an {@link SslBundle} for the given {@link JksSslBundleProperties}.
* @param properties the source properties
* @param resourceLoader the resource loader used to load content
* @return an {@link SslBundle} instance
* @since 3.3.5
*/
public static SslBundle get(JksSslBundleProperties properties, ResourceLoader resourceLoader) {
SslStoreBundle storeBundle = asSslStoreBundle(properties, resourceLoader);
return new PropertiesSslBundle(storeBundle, properties); return new PropertiesSslBundle(storeBundle, properties);
} }
private static SslStoreBundle asSslStoreBundle(JksSslBundleProperties properties) { private static SslStoreBundle asSslStoreBundle(JksSslBundleProperties properties, ResourceLoader resourceLoader) {
JksSslStoreDetails keyStoreDetails = asStoreDetails(properties.getKeystore()); JksSslStoreDetails keyStoreDetails = asStoreDetails(properties.getKeystore());
JksSslStoreDetails trustStoreDetails = asStoreDetails(properties.getTruststore()); JksSslStoreDetails trustStoreDetails = asStoreDetails(properties.getTruststore());
return new JksSslStoreBundle(keyStoreDetails, trustStoreDetails); return new JksSslStoreBundle(keyStoreDetails, trustStoreDetails, resourceLoader);
} }
private static JksSslStoreDetails asStoreDetails(JksSslBundleProperties.Store properties) { private static JksSslStoreDetails asStoreDetails(JksSslBundleProperties.Store properties) {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -21,10 +21,12 @@ import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.io.ApplicationResourceLoader;
import org.springframework.boot.ssl.DefaultSslBundleRegistry; import org.springframework.boot.ssl.DefaultSslBundleRegistry;
import org.springframework.boot.ssl.SslBundleRegistry; import org.springframework.boot.ssl.SslBundleRegistry;
import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.ssl.SslBundles;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ResourceLoader;
/** /**
* {@link EnableAutoConfiguration Auto-configuration} for SSL. * {@link EnableAutoConfiguration Auto-configuration} for SSL.
@ -36,9 +38,12 @@ import org.springframework.context.annotation.Bean;
@EnableConfigurationProperties(SslProperties.class) @EnableConfigurationProperties(SslProperties.class)
public class SslAutoConfiguration { public class SslAutoConfiguration {
private final ApplicationResourceLoader resourceLoader;
private final SslProperties sslProperties; private final SslProperties sslProperties;
SslAutoConfiguration(SslProperties sslProperties) { SslAutoConfiguration(ResourceLoader resourceLoader, SslProperties sslProperties) {
this.resourceLoader = new ApplicationResourceLoader(resourceLoader.getClassLoader());
this.sslProperties = sslProperties; this.sslProperties = sslProperties;
} }
@ -49,7 +54,7 @@ public class SslAutoConfiguration {
@Bean @Bean
SslPropertiesBundleRegistrar sslPropertiesSslBundleRegistrar(FileWatcher fileWatcher) { SslPropertiesBundleRegistrar sslPropertiesSslBundleRegistrar(FileWatcher fileWatcher) {
return new SslPropertiesBundleRegistrar(this.sslProperties, fileWatcher); return new SslPropertiesBundleRegistrar(this.sslProperties, fileWatcher, this.resourceLoader);
} }
@Bean @Bean

View File

@ -21,12 +21,14 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundleRegistry; import org.springframework.boot.ssl.SslBundleRegistry;
import org.springframework.core.io.ResourceLoader;
/** /**
* A {@link SslBundleRegistrar} that registers SSL bundles based * A {@link SslBundleRegistrar} that registers SSL bundles based
@ -42,9 +44,12 @@ class SslPropertiesBundleRegistrar implements SslBundleRegistrar {
private final FileWatcher fileWatcher; private final FileWatcher fileWatcher;
SslPropertiesBundleRegistrar(SslProperties properties, FileWatcher fileWatcher) { private final ResourceLoader resourceLoader;
SslPropertiesBundleRegistrar(SslProperties properties, FileWatcher fileWatcher, ResourceLoader resourceLoader) {
this.properties = properties.getBundle(); this.properties = properties.getBundle();
this.fileWatcher = fileWatcher; this.fileWatcher = fileWatcher;
this.resourceLoader = resourceLoader;
} }
@Override @Override
@ -54,9 +59,9 @@ class SslPropertiesBundleRegistrar implements SslBundleRegistrar {
} }
private <P extends SslBundleProperties> void registerBundles(SslBundleRegistry registry, Map<String, P> properties, private <P extends SslBundleProperties> void registerBundles(SslBundleRegistry registry, Map<String, P> properties,
Function<P, SslBundle> bundleFactory, Function<Bundle<P>, Set<Path>> watchedPaths) { BiFunction<P, ResourceLoader, SslBundle> bundleFactory, Function<Bundle<P>, Set<Path>> watchedPaths) {
properties.forEach((bundleName, bundleProperties) -> { properties.forEach((bundleName, bundleProperties) -> {
Supplier<SslBundle> bundleSupplier = () -> bundleFactory.apply(bundleProperties); Supplier<SslBundle> bundleSupplier = () -> bundleFactory.apply(bundleProperties, this.resourceLoader);
try { try {
registry.registerBundle(bundleName, bundleSupplier.get()); registry.registerBundle(bundleName, bundleSupplier.get());
if (bundleProperties.isReloadOnUpdate()) { if (bundleProperties.isReloadOnUpdate()) {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -25,10 +25,15 @@ import java.util.function.Consumer;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundle;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.function.ThrowingConsumer; import org.springframework.util.function.ThrowingConsumer;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.spy;
/** /**
* Tests for {@link PropertiesSslBundle}. * Tests for {@link PropertiesSslBundle}.
@ -137,6 +142,22 @@ class PropertiesSslBundleTests {
.withMessageContaining("Private key in keystore matches none of the certificates"); .withMessageContaining("Private key in keystore matches none of the certificates");
} }
@Test
void getWithResourceLoader() {
PemSslBundleProperties properties = new PemSslBundleProperties();
properties.getKeystore().setCertificate("classpath:org/springframework/boot/autoconfigure/ssl/key2-chain.crt");
properties.getKeystore().setPrivateKey("classpath:org/springframework/boot/autoconfigure/ssl/key2.pem");
properties.getKeystore().setVerifyKeys(true);
properties.getKey().setAlias("test-alias");
ResourceLoader resourceLoader = spy(new DefaultResourceLoader());
SslBundle bundle = PropertiesSslBundle.get(properties, resourceLoader);
assertThat(bundle.getStores().getKeyStore()).satisfies(storeContainingCertAndKey("test-alias"));
then(resourceLoader).should(atLeastOnce())
.getResource("classpath:org/springframework/boot/autoconfigure/ssl/key2-chain.crt");
then(resourceLoader).should(atLeastOnce())
.getResource("classpath:org/springframework/boot/autoconfigure/ssl/key2.pem");
}
private Consumer<KeyStore> storeContainingCertAndKey(String keyAlias) { private Consumer<KeyStore> storeContainingCertAndKey(String keyAlias) {
return ThrowingConsumer.of((keyStore) -> { return ThrowingConsumer.of((keyStore) -> {
assertThat(keyStore).isNotNull(); assertThat(keyStore).isNotNull();

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -53,29 +53,22 @@ class SslAutoConfigurationTests {
@Test @Test
void sslBundlesCreatedWithCertificates() { void sslBundlesCreatedWithCertificates() {
List<String> propertyValues = new ArrayList<>(); List<String> propertyValues = new ArrayList<>();
String location = "classpath:org/springframework/boot/autoconfigure/ssl/";
propertyValues.add("spring.ssl.bundle.pem.first.key.alias=alias1"); propertyValues.add("spring.ssl.bundle.pem.first.key.alias=alias1");
propertyValues.add("spring.ssl.bundle.pem.first.key.password=secret1"); propertyValues.add("spring.ssl.bundle.pem.first.key.password=secret1");
propertyValues.add( propertyValues.add("spring.ssl.bundle.pem.first.keystore.certificate=" + location + "rsa-cert.pem");
"spring.ssl.bundle.pem.first.keystore.certificate=classpath:org/springframework/boot/autoconfigure/ssl/rsa-cert.pem"); propertyValues.add("spring.ssl.bundle.pem.first.keystore.private-key=" + location + "rsa-key.pem");
propertyValues.add(
"spring.ssl.bundle.pem.first.keystore.private-key=classpath:org/springframework/boot/autoconfigure/ssl/rsa-key.pem");
propertyValues.add("spring.ssl.bundle.pem.first.keystore.type=PKCS12"); propertyValues.add("spring.ssl.bundle.pem.first.keystore.type=PKCS12");
propertyValues.add("spring.ssl.bundle.pem.first.truststore.type=PKCS12"); propertyValues.add("spring.ssl.bundle.pem.first.truststore.type=PKCS12");
propertyValues.add( propertyValues.add("spring.ssl.bundle.pem.first.truststore.certificate=" + location + "rsa-cert.pem");
"spring.ssl.bundle.pem.first.truststore.certificate=classpath:org/springframework/boot/autoconfigure/ssl/rsa-cert.pem"); propertyValues.add("spring.ssl.bundle.pem.first.truststore.private-key=" + location + "rsa-key.pem");
propertyValues.add(
"spring.ssl.bundle.pem.first.truststore.private-key=classpath:org/springframework/boot/autoconfigure/ssl/rsa-key.pem");
propertyValues.add("spring.ssl.bundle.pem.second.key.alias=alias2"); propertyValues.add("spring.ssl.bundle.pem.second.key.alias=alias2");
propertyValues.add("spring.ssl.bundle.pem.second.key.password=secret2"); propertyValues.add("spring.ssl.bundle.pem.second.key.password=secret2");
propertyValues.add( propertyValues.add("spring.ssl.bundle.pem.second.keystore.certificate=" + location + "ed25519-cert.pem");
"spring.ssl.bundle.pem.second.keystore.certificate=classpath:org/springframework/boot/autoconfigure/ssl/ed25519-cert.pem"); propertyValues.add("spring.ssl.bundle.pem.second.keystore.private-key=" + location + "ed25519-key.pem");
propertyValues.add(
"spring.ssl.bundle.pem.second.keystore.private-key=classpath:org/springframework/boot/autoconfigure/ssl/ed25519-key.pem");
propertyValues.add("spring.ssl.bundle.pem.second.keystore.type=PKCS12"); propertyValues.add("spring.ssl.bundle.pem.second.keystore.type=PKCS12");
propertyValues.add( propertyValues.add("spring.ssl.bundle.pem.second.truststore.certificate=" + location + "ed25519-cert.pem");
"spring.ssl.bundle.pem.second.truststore.certificate=classpath:org/springframework/boot/autoconfigure/ssl/ed25519-cert.pem"); propertyValues.add("spring.ssl.bundle.pem.second.truststore.private-key=" + location + "ed25519-key.pem");
propertyValues.add(
"spring.ssl.bundle.pem.second.truststore.private-key=classpath:org/springframework/boot/autoconfigure/ssl/ed25519-key.pem");
propertyValues.add("spring.ssl.bundle.pem.second.truststore.type=PKCS12"); propertyValues.add("spring.ssl.bundle.pem.second.truststore.type=PKCS12");
this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> { this.contextRunner.withPropertyValues(propertyValues.toArray(String[]::new)).run((context) -> {
assertThat(context).hasSingleBean(SslBundles.class); assertThat(context).hasSingleBean(SslBundles.class);
@ -102,14 +95,12 @@ class SslAutoConfigurationTests {
@Test @Test
void sslBundlesCreatedWithCustomSslBundle() { void sslBundlesCreatedWithCustomSslBundle() {
List<String> propertyValues = new ArrayList<>(); List<String> propertyValues = new ArrayList<>();
String location = "classpath:org/springframework/boot/autoconfigure/ssl/";
propertyValues.add("custom.ssl.key.alias=alias1"); propertyValues.add("custom.ssl.key.alias=alias1");
propertyValues.add("custom.ssl.key.password=secret1"); propertyValues.add("custom.ssl.key.password=secret1");
propertyValues propertyValues.add("custom.ssl.keystore.certificate=" + location + "rsa-cert.pem");
.add("custom.ssl.keystore.certificate=classpath:org/springframework/boot/autoconfigure/ssl/rsa-cert.pem"); propertyValues.add("custom.ssl.keystore.keystore.private-key=" + location + "rsa-key.pem");
propertyValues.add( propertyValues.add("custom.ssl.truststore.certificate=" + location + "rsa-cert.pem");
"custom.ssl.keystore.keystore.private-key=classpath:org/springframework/boot/autoconfigure/ssl/rsa-key.pem");
propertyValues
.add("custom.ssl.truststore.certificate=classpath:org/springframework/boot/autoconfigure/ssl/rsa-cert.pem");
propertyValues.add("custom.ssl.keystore.type=PKCS12"); propertyValues.add("custom.ssl.keystore.type=PKCS12");
propertyValues.add("custom.ssl.truststore.type=PKCS12"); propertyValues.add("custom.ssl.truststore.type=PKCS12");
this.contextRunner.withUserConfiguration(CustomSslBundleConfiguration.class) this.contextRunner.withUserConfiguration(CustomSslBundleConfiguration.class)

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -23,7 +23,9 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
import org.springframework.boot.ssl.SslBundleRegistry; import org.springframework.boot.ssl.SslBundleRegistry;
import org.springframework.core.io.DefaultResourceLoader;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
@ -31,6 +33,8 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.assertArg; import static org.mockito.ArgumentMatchers.assertArg;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.then; import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
/** /**
@ -44,6 +48,8 @@ class SslPropertiesBundleRegistrarTests {
private FileWatcher fileWatcher; private FileWatcher fileWatcher;
private DefaultResourceLoader resourceLoader;
private SslProperties properties; private SslProperties properties;
private SslBundleRegistry registry; private SslBundleRegistry registry;
@ -52,7 +58,8 @@ class SslPropertiesBundleRegistrarTests {
void setUp() { void setUp() {
this.properties = new SslProperties(); this.properties = new SslProperties();
this.fileWatcher = Mockito.mock(FileWatcher.class); this.fileWatcher = Mockito.mock(FileWatcher.class);
this.registrar = new SslPropertiesBundleRegistrar(this.properties, this.fileWatcher); this.resourceLoader = spy(new DefaultResourceLoader());
this.registrar = new SslPropertiesBundleRegistrar(this.properties, this.fileWatcher, this.resourceLoader);
this.registry = Mockito.mock(SslBundleRegistry.class); this.registry = Mockito.mock(SslBundleRegistry.class);
} }
@ -85,6 +92,21 @@ class SslPropertiesBundleRegistrarTests {
.watch(assertArg((set) -> pathEndingWith(set, "rsa-cert.pem", "rsa-key.pem")), any()); .watch(assertArg((set) -> pathEndingWith(set, "rsa-cert.pem", "rsa-key.pem")), any());
} }
@Test
void shouldUseResourceLoader() {
PemSslBundleProperties pem = new PemSslBundleProperties();
pem.getTruststore().setCertificate("classpath:org/springframework/boot/autoconfigure/ssl/ed25519-cert.pem");
pem.getTruststore().setPrivateKey("classpath:org/springframework/boot/autoconfigure/ssl/ed25519-key.pem");
this.properties.getBundle().getPem().put("bundle1", pem);
DefaultSslBundleRegistry registry = new DefaultSslBundleRegistry();
this.registrar.registerBundles(registry);
registry.getBundle("bundle1").createSslContext();
then(this.resourceLoader).should(atLeastOnce())
.getResource("classpath:org/springframework/boot/autoconfigure/ssl/ed25519-cert.pem");
then(this.resourceLoader).should(atLeastOnce())
.getResource("classpath:org/springframework/boot/autoconfigure/ssl/ed25519-key.pem");
}
@Test @Test
void shouldFailIfPemKeystoreCertificateIsEmbedded() { void shouldFailIfPemKeystoreCertificateIsEmbedded() {
PemSslBundleProperties pem = new PemSslBundleProperties(); PemSslBundleProperties pem = new PemSslBundleProperties();

View File

@ -27,7 +27,7 @@ import java.util.function.Supplier;
import org.springframework.boot.io.ApplicationResourceLoader; import org.springframework.boot.io.ApplicationResourceLoader;
import org.springframework.boot.ssl.SslStoreBundle; import org.springframework.boot.ssl.SslStoreBundle;
import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader;
import org.springframework.core.style.ToStringCreator; import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@ -45,6 +45,8 @@ public class JksSslStoreBundle implements SslStoreBundle {
private final JksSslStoreDetails keyStoreDetails; private final JksSslStoreDetails keyStoreDetails;
private final ResourceLoader resourceLoader;
private final Supplier<KeyStore> keyStore; private final Supplier<KeyStore> keyStore;
private final Supplier<KeyStore> trustStore; private final Supplier<KeyStore> trustStore;
@ -55,8 +57,22 @@ public class JksSslStoreBundle implements SslStoreBundle {
* @param trustStoreDetails the trust store details * @param trustStoreDetails the trust store details
*/ */
public JksSslStoreBundle(JksSslStoreDetails keyStoreDetails, JksSslStoreDetails trustStoreDetails) { public JksSslStoreBundle(JksSslStoreDetails keyStoreDetails, JksSslStoreDetails trustStoreDetails) {
this(keyStoreDetails, trustStoreDetails, new ApplicationResourceLoader());
}
/**
* Create a new {@link JksSslStoreBundle} instance.
* @param keyStoreDetails the key store details
* @param trustStoreDetails the trust store details
* @param resourceLoader the resource loader used to load content
* @since 3.3.5
*/
public JksSslStoreBundle(JksSslStoreDetails keyStoreDetails, JksSslStoreDetails trustStoreDetails,
ResourceLoader resourceLoader) {
Assert.notNull(resourceLoader, "ResourceLoader must not be null");
this.keyStoreDetails = keyStoreDetails; this.keyStoreDetails = keyStoreDetails;
this.keyStore = SingletonSupplier.of(() -> createKeyStore("key", this.keyStoreDetails)); this.resourceLoader = resourceLoader;
this.keyStore = SingletonSupplier.of(() -> createKeyStore("key", keyStoreDetails));
this.trustStore = SingletonSupplier.of(() -> createKeyStore("trust", trustStoreDetails)); this.trustStore = SingletonSupplier.of(() -> createKeyStore("trust", trustStoreDetails));
} }
@ -116,8 +132,7 @@ public class JksSslStoreBundle implements SslStoreBundle {
private void loadKeyStore(KeyStore store, String location, char[] password) { private void loadKeyStore(KeyStore store, String location, char[] password) {
Assert.state(StringUtils.hasText(location), () -> "Location must not be empty or null"); Assert.state(StringUtils.hasText(location), () -> "Location must not be empty or null");
try { try {
Resource resource = new ApplicationResourceLoader().getResource(location); try (InputStream stream = this.resourceLoader.getResource(location).getInputStream()) {
try (InputStream stream = resource.getInputStream()) {
store.load(stream, password); store.load(stream, password);
} }
} }

View File

@ -23,6 +23,7 @@ import java.security.cert.X509Certificate;
import java.util.List; import java.util.List;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.function.SingletonSupplier; import org.springframework.util.function.SingletonSupplier;
@ -38,15 +39,19 @@ final class LoadedPemSslStore implements PemSslStore {
private final PemSslStoreDetails details; private final PemSslStoreDetails details;
private final ResourceLoader resourceLoader;
private final Supplier<List<X509Certificate>> certificatesSupplier; private final Supplier<List<X509Certificate>> certificatesSupplier;
private final Supplier<PrivateKey> privateKeySupplier; private final Supplier<PrivateKey> privateKeySupplier;
LoadedPemSslStore(PemSslStoreDetails details) { LoadedPemSslStore(PemSslStoreDetails details, ResourceLoader resourceLoader) {
Assert.notNull(details, "Details must not be null"); Assert.notNull(details, "Details must not be null");
Assert.notNull(resourceLoader, "ResourceLoader must not be null");
this.details = details; this.details = details;
this.certificatesSupplier = supplier(() -> loadCertificates(details)); this.resourceLoader = resourceLoader;
this.privateKeySupplier = supplier(() -> loadPrivateKey(details)); this.certificatesSupplier = supplier(() -> loadCertificates(details, resourceLoader));
this.privateKeySupplier = supplier(() -> loadPrivateKey(details, resourceLoader));
} }
private static <T> Supplier<T> supplier(ThrowingSupplier<T> supplier) { private static <T> Supplier<T> supplier(ThrowingSupplier<T> supplier) {
@ -57,8 +62,9 @@ final class LoadedPemSslStore implements PemSslStore {
return new UncheckedIOException(message, (IOException) cause); return new UncheckedIOException(message, (IOException) cause);
} }
private static List<X509Certificate> loadCertificates(PemSslStoreDetails details) throws IOException { private static List<X509Certificate> loadCertificates(PemSslStoreDetails details, ResourceLoader resourceLoader)
PemContent pemContent = PemContent.load(details.certificates()); throws IOException {
PemContent pemContent = PemContent.load(details.certificates(), resourceLoader);
if (pemContent == null) { if (pemContent == null) {
return null; return null;
} }
@ -67,8 +73,9 @@ final class LoadedPemSslStore implements PemSslStore {
return certificates; return certificates;
} }
private static PrivateKey loadPrivateKey(PemSslStoreDetails details) throws IOException { private static PrivateKey loadPrivateKey(PemSslStoreDetails details, ResourceLoader resourceLoader)
PemContent pemContent = PemContent.load(details.privateKey()); throws IOException {
PemContent pemContent = PemContent.load(details.privateKey(), resourceLoader);
return (pemContent != null) ? pemContent.getPrivateKey(details.privateKeyPassword()) : null; return (pemContent != null) ? pemContent.getPrivateKey(details.privateKeyPassword()) : null;
} }
@ -99,12 +106,12 @@ final class LoadedPemSslStore implements PemSslStore {
@Override @Override
public PemSslStore withAlias(String alias) { public PemSslStore withAlias(String alias) {
return new LoadedPemSslStore(this.details.withAlias(alias)); return new LoadedPemSslStore(this.details.withAlias(alias), this.resourceLoader);
} }
@Override @Override
public PemSslStore withPassword(String password) { public PemSslStore withPassword(String password) {
return new LoadedPemSslStore(this.details.withPassword(password)); return new LoadedPemSslStore(this.details.withPassword(password), this.resourceLoader);
} }
} }

View File

@ -30,8 +30,7 @@ import java.util.Objects;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.boot.io.ApplicationResourceLoader; import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StreamUtils; import org.springframework.util.StreamUtils;
@ -109,10 +108,11 @@ public final class PemContent {
* Load {@link PemContent} from the given content (either the PEM content itself or a * Load {@link PemContent} from the given content (either the PEM content itself or a
* reference to the resource to load). * reference to the resource to load).
* @param content the content to load * @param content the content to load
* @param resourceLoader the resource loader used to load content
* @return a new {@link PemContent} instance * @return a new {@link PemContent} instance
* @throws IOException on IO error * @throws IOException on IO error
*/ */
static PemContent load(String content) throws IOException { static PemContent load(String content, ResourceLoader resourceLoader) throws IOException {
if (content == null) { if (content == null) {
return null; return null;
} }
@ -120,8 +120,7 @@ public final class PemContent {
return new PemContent(content); return new PemContent(content);
} }
try { try {
Resource resource = new ApplicationResourceLoader().getResource(content); return load(resourceLoader.getResource(content).getInputStream());
return load(resource.getInputStream());
} }
catch (IOException | UncheckedIOException ex) { catch (IOException | UncheckedIOException ex) {
throw new IOException("Error reading certificate or key from file '%s'".formatted(content), ex); throw new IOException("Error reading certificate or key from file '%s'".formatted(content), ex);

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -21,6 +21,8 @@ import java.security.PrivateKey;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.List; import java.util.List;
import org.springframework.boot.io.ApplicationResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
@ -93,10 +95,22 @@ public interface PemSslStore {
* @return a loaded {@link PemSslStore} or {@code null}. * @return a loaded {@link PemSslStore} or {@code null}.
*/ */
static PemSslStore load(PemSslStoreDetails details) { static PemSslStore load(PemSslStoreDetails details) {
return load(details, new ApplicationResourceLoader());
}
/**
* Return a {@link PemSslStore} instance loaded using the given
* {@link PemSslStoreDetails}.
* @param details the PEM store details
* @param resourceLoader the resource loader used to load content
* @return a loaded {@link PemSslStore} or {@code null}.
* @since 3.3.5
*/
static PemSslStore load(PemSslStoreDetails details, ResourceLoader resourceLoader) {
if (details == null || details.isEmpty()) { if (details == null || details.isEmpty()) {
return null; return null;
} }
return new LoadedPemSslStore(details); return new LoadedPemSslStore(details, resourceLoader);
} }
/** /**

View File

@ -26,11 +26,16 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.io.ApplicationResourceLoader; import org.springframework.boot.io.ApplicationResourceLoader;
import org.springframework.boot.web.embedded.test.MockPkcs11Security; import org.springframework.boot.web.embedded.test.MockPkcs11Security;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.function.ThrowingConsumer; import org.springframework.util.function.ThrowingConsumer;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.spy;
/** /**
* Tests for {@link JksSslStoreBundle}. * Tests for {@link JksSslStoreBundle}.
@ -163,6 +168,16 @@ class JksSslStoreBundleTests {
.withMessageContaining("does-not-exist.p12"); .withMessageContaining("does-not-exist.p12");
} }
@Test
void usesResourceLoader() {
JksSslStoreDetails keyStoreDetails = null;
JksSslStoreDetails trustStoreDetails = new JksSslStoreDetails("jks", null, "classpath:test.jks", "secret");
ResourceLoader resourceLoader = spy(new DefaultResourceLoader());
JksSslStoreBundle bundle = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails, resourceLoader);
assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("jks", "test-alias", "password"));
then(resourceLoader).should(atLeastOnce()).getResource("classpath:test.jks");
}
private Consumer<KeyStore> storeContainingCertAndKey(String keyAlias, String keyPassword) { private Consumer<KeyStore> storeContainingCertAndKey(String keyAlias, String keyPassword) {
return storeContainingCertAndKey(KeyStore.getDefaultType(), keyAlias, keyPassword); return storeContainingCertAndKey(KeyStore.getDefaultType(), keyAlias, keyPassword);
} }

View File

@ -20,7 +20,14 @@ import java.io.UncheckedIOException;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.io.ApplicationResourceLoader;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.spy;
/** /**
* Tests for {@link LoadedPemSslStore}. * Tests for {@link LoadedPemSslStore}.
@ -33,7 +40,7 @@ class LoadedPemSslStoreTests {
void certificatesAreLoadedLazily() { void certificatesAreLoadedLazily() {
PemSslStoreDetails details = PemSslStoreDetails.forCertificate("classpath:missing-test-cert.pem") PemSslStoreDetails details = PemSslStoreDetails.forCertificate("classpath:missing-test-cert.pem")
.withPrivateKey("classpath:test-key.pem"); .withPrivateKey("classpath:test-key.pem");
LoadedPemSslStore store = new LoadedPemSslStore(details); LoadedPemSslStore store = new LoadedPemSslStore(details, new ApplicationResourceLoader());
assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(store::certificates); assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(store::certificates);
} }
@ -41,7 +48,7 @@ class LoadedPemSslStoreTests {
void privateKeyIsLoadedLazily() { void privateKeyIsLoadedLazily() {
PemSslStoreDetails details = PemSslStoreDetails.forCertificate("classpath:test-cert.pem") PemSslStoreDetails details = PemSslStoreDetails.forCertificate("classpath:test-cert.pem")
.withPrivateKey("classpath:missing-test-key.pem"); .withPrivateKey("classpath:missing-test-key.pem");
LoadedPemSslStore store = new LoadedPemSslStore(details); LoadedPemSslStore store = new LoadedPemSslStore(details, new ApplicationResourceLoader());
assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(store::privateKey); assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(store::privateKey);
} }
@ -49,7 +56,7 @@ class LoadedPemSslStoreTests {
void withAliasIsLazy() { void withAliasIsLazy() {
PemSslStoreDetails details = PemSslStoreDetails.forCertificate("classpath:missing-test-cert.pem") PemSslStoreDetails details = PemSslStoreDetails.forCertificate("classpath:missing-test-cert.pem")
.withPrivateKey("classpath:test-key.pem"); .withPrivateKey("classpath:test-key.pem");
PemSslStore store = new LoadedPemSslStore(details).withAlias("alias"); PemSslStore store = new LoadedPemSslStore(details, new ApplicationResourceLoader()).withAlias("alias");
assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(store::certificates); assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(store::certificates);
} }
@ -57,8 +64,17 @@ class LoadedPemSslStoreTests {
void withPasswordIsLazy() { void withPasswordIsLazy() {
PemSslStoreDetails details = PemSslStoreDetails.forCertificate("classpath:missing-test-cert.pem") PemSslStoreDetails details = PemSslStoreDetails.forCertificate("classpath:missing-test-cert.pem")
.withPrivateKey("classpath:test-key.pem"); .withPrivateKey("classpath:test-key.pem");
PemSslStore store = new LoadedPemSslStore(details).withPassword("password"); PemSslStore store = new LoadedPemSslStore(details, new ApplicationResourceLoader()).withPassword("password");
assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(store::certificates); assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(store::certificates);
} }
@Test
void usesResourceLoader() {
PemSslStoreDetails details = PemSslStoreDetails.forCertificate("classpath:test-cert.pem");
ResourceLoader resourceLoader = spy(new DefaultResourceLoader());
LoadedPemSslStore store = new LoadedPemSslStore(details, resourceLoader);
store.certificates();
then(resourceLoader).should(atLeastOnce()).getResource("classpath:test-cert.pem");
}
} }

View File

@ -25,10 +25,16 @@ import java.util.List;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.io.ApplicationResourceLoader;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.spy;
/** /**
* Tests for {@link PemContent}. * Tests for {@link PemContent}.
@ -46,7 +52,8 @@ class PemContentTests {
@Test @Test
void getCertificateReturnsCertificates() throws Exception { void getCertificateReturnsCertificates() throws Exception {
PemContent content = PemContent.load(contentFromClasspath("/test-cert-chain.pem")); PemContent content = PemContent.load(contentFromClasspath("/test-cert-chain.pem"),
new ApplicationResourceLoader());
List<X509Certificate> certificates = content.getCertificates(); List<X509Certificate> certificates = content.getCertificates();
assertThat(certificates).isNotNull(); assertThat(certificates).isNotNull();
assertThat(certificates).hasSize(2); assertThat(certificates).hasSize(2);
@ -63,8 +70,8 @@ class PemContentTests {
@Test @Test
void getPrivateKeyReturnsPrivateKey() throws Exception { void getPrivateKeyReturnsPrivateKey() throws Exception {
PemContent content = PemContent PemContent content = PemContent.load(contentFromClasspath("/org/springframework/boot/web/server/pkcs8/dsa.key"),
.load(contentFromClasspath("/org/springframework/boot/web/server/pkcs8/dsa.key")); new ApplicationResourceLoader());
PrivateKey privateKey = content.getPrivateKey(); PrivateKey privateKey = content.getPrivateKey();
assertThat(privateKey).isNotNull(); assertThat(privateKey).isNotNull();
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
@ -88,7 +95,7 @@ class PemContentTests {
@Test @Test
void loadWithStringWhenContentIsNullReturnsNull() throws Exception { void loadWithStringWhenContentIsNullReturnsNull() throws Exception {
assertThat(PemContent.load((String) null)).isNull(); assertThat(PemContent.load((String) null, new ApplicationResourceLoader())).isNull();
} }
@Test @Test
@ -111,7 +118,7 @@ class PemContentTests {
+lGuHKdhNOVW9CmqPD1y76o6c8PQKuF7KZEoY2jvy3GeIfddBvqXgZ4PbWvFz1jO +lGuHKdhNOVW9CmqPD1y76o6c8PQKuF7KZEoY2jvy3GeIfddBvqXgZ4PbWvFz1jO
32C9XWHwRA4= 32C9XWHwRA4=
-----END CERTIFICATE-----"""; -----END CERTIFICATE-----""";
assertThat(PemContent.load(content)).hasToString(content); assertThat(PemContent.load(content, new ApplicationResourceLoader())).hasToString(content);
} }
@Test @Test
@ -180,14 +187,14 @@ class PemContentTests {
@Test @Test
void loadWithStringWhenClasspathLocationReturnsContent() throws IOException { void loadWithStringWhenClasspathLocationReturnsContent() throws IOException {
String actual = PemContent.load("classpath:test-cert.pem").toString(); String actual = PemContent.load("classpath:test-cert.pem", new ApplicationResourceLoader()).toString();
String expected = contentFromClasspath("test-cert.pem"); String expected = contentFromClasspath("test-cert.pem");
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
} }
@Test @Test
void loadWithStringWhenFileLocationReturnsContent() throws IOException { void loadWithStringWhenFileLocationReturnsContent() throws IOException {
String actual = PemContent.load("src/test/resources/test-cert.pem").toString(); String actual = PemContent.load("src/test/resources/test-cert.pem", new ApplicationResourceLoader()).toString();
String expected = contentFromClasspath("test-cert.pem"); String expected = contentFromClasspath("test-cert.pem");
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
} }
@ -200,6 +207,13 @@ class PemContentTests {
assertThat(actual).isEqualTo(expected); assertThat(actual).isEqualTo(expected);
} }
@Test
void loadWithResourceLoaderUsesResourceLoader() throws IOException {
ResourceLoader resourceLoader = spy(new DefaultResourceLoader());
PemContent.load("classpath:test-cert.pem", resourceLoader);
then(resourceLoader).should(atLeastOnce()).getResource("classpath:test-cert.pem");
}
@Test @Test
void ofWhenNullReturnsNull() { void ofWhenNullReturnsNull() {
assertThat(PemContent.of(null)).isNull(); assertThat(PemContent.of(null)).isNull();