diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java index 29baa16e69e..e56ab890162 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.ssl; 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.SslBundleKey; 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.PemSslStoreBundle; import org.springframework.boot.ssl.pem.PemSslStoreDetails; +import org.springframework.core.io.ResourceLoader; import org.springframework.core.style.ToStringCreator; import org.springframework.util.Assert; @@ -97,18 +99,31 @@ public final class PropertiesSslBundle implements SslBundle { * @return an {@link SslBundle} instance */ 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) { keyStore = keyStore.withAlias(properties.getKey().getAlias()) .withPassword(properties.getKey().getPassword()); } - PemSslStore trustStore = getPemSslStore("truststore", properties.getTruststore()); + PemSslStore trustStore = getPemSslStore("truststore", properties.getTruststore(), resourceLoader); SslStoreBundle storeBundle = new PemSslStoreBundle(keyStore, trustStore); return new PropertiesSslBundle(storeBundle, properties); } - private static PemSslStore getPemSslStore(String propertyName, PemSslBundleProperties.Store properties) { - PemSslStore pemSslStore = PemSslStore.load(asPemSslStoreDetails(properties)); + private static PemSslStore getPemSslStore(String propertyName, PemSslBundleProperties.Store properties, + ResourceLoader resourceLoader) { + PemSslStoreDetails details = asPemSslStoreDetails(properties); + PemSslStore pemSslStore = PemSslStore.load(details, resourceLoader); if (properties.isVerifyKeys()) { CertificateMatcher certificateMatcher = new CertificateMatcher(pemSslStore.privateKey()); Assert.state(certificateMatcher.matchesAny(pemSslStore.certificates()), @@ -128,14 +143,25 @@ public final class PropertiesSslBundle implements SslBundle { * @return an {@link SslBundle} instance */ 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); } - private static SslStoreBundle asSslStoreBundle(JksSslBundleProperties properties) { + private static SslStoreBundle asSslStoreBundle(JksSslBundleProperties properties, ResourceLoader resourceLoader) { JksSslStoreDetails keyStoreDetails = asStoreDetails(properties.getKeystore()); JksSslStoreDetails trustStoreDetails = asStoreDetails(properties.getTruststore()); - return new JksSslStoreBundle(keyStoreDetails, trustStoreDetails); + return new JksSslStoreBundle(keyStoreDetails, trustStoreDetails, resourceLoader); } private static JksSslStoreDetails asStoreDetails(JksSslBundleProperties.Store properties) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.java index 1348f16b37b..c53356a71e1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.java @@ -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"); * 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.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.io.ApplicationResourceLoader; import org.springframework.boot.ssl.DefaultSslBundleRegistry; import org.springframework.boot.ssl.SslBundleRegistry; import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; +import org.springframework.core.io.ResourceLoader; /** * {@link EnableAutoConfiguration Auto-configuration} for SSL. @@ -36,9 +38,12 @@ import org.springframework.context.annotation.Bean; @EnableConfigurationProperties(SslProperties.class) public class SslAutoConfiguration { + private final ApplicationResourceLoader resourceLoader; + private final SslProperties sslProperties; - SslAutoConfiguration(SslProperties sslProperties) { + SslAutoConfiguration(ResourceLoader resourceLoader, SslProperties sslProperties) { + this.resourceLoader = new ApplicationResourceLoader(resourceLoader.getClassLoader()); this.sslProperties = sslProperties; } @@ -49,7 +54,7 @@ public class SslAutoConfiguration { @Bean SslPropertiesBundleRegistrar sslPropertiesSslBundleRegistrar(FileWatcher fileWatcher) { - return new SslPropertiesBundleRegistrar(this.sslProperties, fileWatcher); + return new SslPropertiesBundleRegistrar(this.sslProperties, fileWatcher, this.resourceLoader); } @Bean diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java index c8f595b0d25..6687e81a663 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java @@ -21,12 +21,14 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundleRegistry; +import org.springframework.core.io.ResourceLoader; /** * A {@link SslBundleRegistrar} that registers SSL bundles based @@ -42,9 +44,12 @@ class SslPropertiesBundleRegistrar implements SslBundleRegistrar { 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.fileWatcher = fileWatcher; + this.resourceLoader = resourceLoader; } @Override @@ -54,9 +59,9 @@ class SslPropertiesBundleRegistrar implements SslBundleRegistrar { } private

void registerBundles(SslBundleRegistry registry, Map properties, - Function bundleFactory, Function, Set> watchedPaths) { + BiFunction bundleFactory, Function, Set> watchedPaths) { properties.forEach((bundleName, bundleProperties) -> { - Supplier bundleSupplier = () -> bundleFactory.apply(bundleProperties); + Supplier bundleSupplier = () -> bundleFactory.apply(bundleProperties, this.resourceLoader); try { registry.registerBundle(bundleName, bundleSupplier.get()); if (bundleProperties.isReloadOnUpdate()) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundleTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundleTests.java index d6b770a3d92..66d1c6d1d66 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundleTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundleTests.java @@ -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"); * 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.springframework.boot.ssl.SslBundle; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.ResourceLoader; import org.springframework.util.function.ThrowingConsumer; import static org.assertj.core.api.Assertions.assertThat; 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}. @@ -137,6 +142,22 @@ class PropertiesSslBundleTests { .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 storeContainingCertAndKey(String keyAlias) { return ThrowingConsumer.of((keyStore) -> { assertThat(keyStore).isNotNull(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrarTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrarTests.java index 759bb474609..dafabd8801b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrarTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrarTests.java @@ -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"); * 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.mockito.Mockito; +import org.springframework.boot.ssl.DefaultSslBundleRegistry; 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.assertThatIllegalStateException; @@ -31,6 +33,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.assertArg; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; /** @@ -44,6 +48,8 @@ class SslPropertiesBundleRegistrarTests { private FileWatcher fileWatcher; + private DefaultResourceLoader resourceLoader; + private SslProperties properties; private SslBundleRegistry registry; @@ -52,7 +58,8 @@ class SslPropertiesBundleRegistrarTests { void setUp() { this.properties = new SslProperties(); 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); } @@ -85,6 +92,21 @@ class SslPropertiesBundleRegistrarTests { .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 void shouldFailIfPemKeystoreCertificateIsEmbedded() { PemSslBundleProperties pem = new PemSslBundleProperties(); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/jks/JksSslStoreBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/jks/JksSslStoreBundle.java index c5b5c510d56..7bc6b5f78d3 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/jks/JksSslStoreBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/jks/JksSslStoreBundle.java @@ -27,7 +27,7 @@ import java.util.function.Supplier; import org.springframework.boot.io.ApplicationResourceLoader; 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.util.Assert; import org.springframework.util.StringUtils; @@ -45,6 +45,8 @@ public class JksSslStoreBundle implements SslStoreBundle { private final JksSslStoreDetails keyStoreDetails; + private final ResourceLoader resourceLoader; + private final Supplier keyStore; private final Supplier trustStore; @@ -55,8 +57,22 @@ public class JksSslStoreBundle implements SslStoreBundle { * @param trustStoreDetails the trust store details */ 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.keyStore = SingletonSupplier.of(() -> createKeyStore("key", this.keyStoreDetails)); + this.resourceLoader = resourceLoader; + this.keyStore = SingletonSupplier.of(() -> createKeyStore("key", keyStoreDetails)); 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) { Assert.state(StringUtils.hasText(location), () -> "Location must not be empty or null"); try { - Resource resource = new ApplicationResourceLoader().getResource(location); - try (InputStream stream = resource.getInputStream()) { + try (InputStream stream = this.resourceLoader.getResource(location).getInputStream()) { store.load(stream, password); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/LoadedPemSslStore.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/LoadedPemSslStore.java index 5f001421f57..950cb9bdaeb 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/LoadedPemSslStore.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/LoadedPemSslStore.java @@ -23,6 +23,7 @@ import java.security.cert.X509Certificate; import java.util.List; import java.util.function.Supplier; +import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.function.SingletonSupplier; @@ -38,15 +39,19 @@ final class LoadedPemSslStore implements PemSslStore { private final PemSslStoreDetails details; + private final ResourceLoader resourceLoader; + private final Supplier> certificatesSupplier; private final Supplier privateKeySupplier; - LoadedPemSslStore(PemSslStoreDetails details) { + LoadedPemSslStore(PemSslStoreDetails details, ResourceLoader resourceLoader) { Assert.notNull(details, "Details must not be null"); + Assert.notNull(resourceLoader, "ResourceLoader must not be null"); this.details = details; - this.certificatesSupplier = supplier(() -> loadCertificates(details)); - this.privateKeySupplier = supplier(() -> loadPrivateKey(details)); + this.resourceLoader = resourceLoader; + this.certificatesSupplier = supplier(() -> loadCertificates(details, resourceLoader)); + this.privateKeySupplier = supplier(() -> loadPrivateKey(details, resourceLoader)); } private static Supplier supplier(ThrowingSupplier supplier) { @@ -57,8 +62,9 @@ final class LoadedPemSslStore implements PemSslStore { return new UncheckedIOException(message, (IOException) cause); } - private static List loadCertificates(PemSslStoreDetails details) throws IOException { - PemContent pemContent = PemContent.load(details.certificates()); + private static List loadCertificates(PemSslStoreDetails details, ResourceLoader resourceLoader) + throws IOException { + PemContent pemContent = PemContent.load(details.certificates(), resourceLoader); if (pemContent == null) { return null; } @@ -67,8 +73,9 @@ final class LoadedPemSslStore implements PemSslStore { return certificates; } - private static PrivateKey loadPrivateKey(PemSslStoreDetails details) throws IOException { - PemContent pemContent = PemContent.load(details.privateKey()); + private static PrivateKey loadPrivateKey(PemSslStoreDetails details, ResourceLoader resourceLoader) + throws IOException { + PemContent pemContent = PemContent.load(details.privateKey(), resourceLoader); return (pemContent != null) ? pemContent.getPrivateKey(details.privateKeyPassword()) : null; } @@ -99,12 +106,12 @@ final class LoadedPemSslStore implements PemSslStore { @Override public PemSslStore withAlias(String alias) { - return new LoadedPemSslStore(this.details.withAlias(alias)); + return new LoadedPemSslStore(this.details.withAlias(alias), this.resourceLoader); } @Override public PemSslStore withPassword(String password) { - return new LoadedPemSslStore(this.details.withPassword(password)); + return new LoadedPemSslStore(this.details.withPassword(password), this.resourceLoader); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java index 3a7e08e43d7..cadb6b215ad 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java @@ -29,8 +29,7 @@ import java.util.List; import java.util.Objects; import java.util.regex.Pattern; -import org.springframework.boot.io.ApplicationResourceLoader; -import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; import org.springframework.util.StreamUtils; @@ -108,10 +107,11 @@ public final class PemContent { * Load {@link PemContent} from the given content (either the PEM content itself or a * reference to the resource to load). * @param content the content to load + * @param resourceLoader the resource loader used to load content * @return a new {@link PemContent} instance * @throws IOException on IO error */ - static PemContent load(String content) throws IOException { + static PemContent load(String content, ResourceLoader resourceLoader) throws IOException { if (content == null) { return null; } @@ -119,8 +119,7 @@ public final class PemContent { return new PemContent(content); } try { - Resource resource = new ApplicationResourceLoader().getResource(content); - return load(resource.getInputStream()); + return load(resourceLoader.getResource(content).getInputStream()); } catch (IOException | UncheckedIOException ex) { throw new IOException("Error reading certificate or key from file '%s'".formatted(content), ex); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java index d58fc9a71be..7081aba6d16 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java @@ -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"); * 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.util.List; +import org.springframework.boot.io.ApplicationResourceLoader; +import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; /** @@ -93,10 +95,22 @@ public interface PemSslStore { * @return a loaded {@link PemSslStore} or {@code null}. */ 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()) { return null; } - return new LoadedPemSslStore(details); + return new LoadedPemSslStore(details, resourceLoader); } /** diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/jks/JksSslStoreBundleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/jks/JksSslStoreBundleTests.java index c081224dec4..2e6fd4b54e0 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/jks/JksSslStoreBundleTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/jks/JksSslStoreBundleTests.java @@ -26,11 +26,16 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.io.ApplicationResourceLoader; 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.ResourceLoader; import org.springframework.util.function.ThrowingConsumer; import static org.assertj.core.api.Assertions.assertThat; 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}. @@ -163,6 +168,16 @@ class JksSslStoreBundleTests { .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 storeContainingCertAndKey(String keyAlias, String keyPassword) { return storeContainingCertAndKey(KeyStore.getDefaultType(), keyAlias, keyPassword); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/LoadedPemSslStoreTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/LoadedPemSslStoreTests.java index f78b777921c..c590a721cef 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/LoadedPemSslStoreTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/LoadedPemSslStoreTests.java @@ -20,7 +20,14 @@ import java.io.UncheckedIOException; 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.mockito.BDDMockito.then; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.spy; /** * Tests for {@link LoadedPemSslStore}. @@ -33,7 +40,7 @@ class LoadedPemSslStoreTests { void certificatesAreLoadedLazily() { PemSslStoreDetails details = PemSslStoreDetails.forCertificate("classpath:missing-test-cert.pem") .withPrivateKey("classpath:test-key.pem"); - LoadedPemSslStore store = new LoadedPemSslStore(details); + LoadedPemSslStore store = new LoadedPemSslStore(details, new ApplicationResourceLoader()); assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(store::certificates); } @@ -41,7 +48,7 @@ class LoadedPemSslStoreTests { void privateKeyIsLoadedLazily() { PemSslStoreDetails details = PemSslStoreDetails.forCertificate("classpath:test-cert.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); } @@ -49,7 +56,7 @@ class LoadedPemSslStoreTests { void withAliasIsLazy() { PemSslStoreDetails details = PemSslStoreDetails.forCertificate("classpath:missing-test-cert.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); } @@ -57,8 +64,17 @@ class LoadedPemSslStoreTests { void withPasswordIsLazy() { PemSslStoreDetails details = PemSslStoreDetails.forCertificate("classpath:missing-test-cert.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); } + @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"); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java index fac38bc5fd5..9387362aa7c 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java @@ -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"); * you may not use this file except in compliance with the License. @@ -25,10 +25,16 @@ import java.util.List; import org.junit.jupiter.api.Test; +import org.springframework.boot.io.ApplicationResourceLoader; 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.assertThatIllegalStateException; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.spy; /** * Tests for {@link PemContent}. @@ -46,7 +52,8 @@ class PemContentTests { @Test 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 certificates = content.getCertificates(); assertThat(certificates).isNotNull(); assertThat(certificates).hasSize(2); @@ -63,8 +70,8 @@ class PemContentTests { @Test void getPrivateKeyReturnsPrivateKey() throws Exception { - PemContent content = PemContent - .load(contentFromClasspath("/org/springframework/boot/web/server/pkcs8/dsa.key")); + PemContent content = PemContent.load(contentFromClasspath("/org/springframework/boot/web/server/pkcs8/dsa.key"), + new ApplicationResourceLoader()); PrivateKey privateKey = content.getPrivateKey(); assertThat(privateKey).isNotNull(); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); @@ -88,7 +95,7 @@ class PemContentTests { @Test void loadWithStringWhenContentIsNullReturnsNull() throws Exception { - assertThat(PemContent.load((String) null)).isNull(); + assertThat(PemContent.load((String) null, new ApplicationResourceLoader())).isNull(); } @Test @@ -111,19 +118,19 @@ class PemContentTests { +lGuHKdhNOVW9CmqPD1y76o6c8PQKuF7KZEoY2jvy3GeIfddBvqXgZ4PbWvFz1jO 32C9XWHwRA4= -----END CERTIFICATE-----"""; - assertThat(PemContent.load(content)).hasToString(content); + assertThat(PemContent.load(content, new ApplicationResourceLoader())).hasToString(content); } @Test 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"); assertThat(actual).isEqualTo(expected); } @Test 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"); assertThat(actual).isEqualTo(expected); } @@ -136,6 +143,13 @@ class PemContentTests { 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 void ofWhenNullReturnsNull() { assertThat(PemContent.of(null)).isNull();