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 c6e3267bc36..c5b5c510d56 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 @@ -23,6 +23,7 @@ import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.cert.CertificateException; +import java.util.function.Supplier; import org.springframework.boot.io.ApplicationResourceLoader; import org.springframework.boot.ssl.SslStoreBundle; @@ -30,6 +31,7 @@ import org.springframework.core.io.Resource; import org.springframework.core.style.ToStringCreator; import org.springframework.util.Assert; import org.springframework.util.StringUtils; +import org.springframework.util.function.SingletonSupplier; /** * {@link SslStoreBundle} backed by a Java keystore. @@ -43,9 +45,9 @@ public class JksSslStoreBundle implements SslStoreBundle { private final JksSslStoreDetails keyStoreDetails; - private final KeyStore keyStore; + private final Supplier keyStore; - private final KeyStore trustStore; + private final Supplier trustStore; /** * Create a new {@link JksSslStoreBundle} instance. @@ -54,13 +56,13 @@ public class JksSslStoreBundle implements SslStoreBundle { */ public JksSslStoreBundle(JksSslStoreDetails keyStoreDetails, JksSslStoreDetails trustStoreDetails) { this.keyStoreDetails = keyStoreDetails; - this.keyStore = createKeyStore("key", this.keyStoreDetails); - this.trustStore = createKeyStore("trust", trustStoreDetails); + this.keyStore = SingletonSupplier.of(() -> createKeyStore("key", this.keyStoreDetails)); + this.trustStore = SingletonSupplier.of(() -> createKeyStore("trust", trustStoreDetails)); } @Override public KeyStore getKeyStore() { - return this.keyStore; + return this.keyStore.get(); } @Override @@ -70,7 +72,7 @@ public class JksSslStoreBundle implements SslStoreBundle { @Override public KeyStore getTrustStore() { - return this.trustStore; + return this.trustStore.get(); } private KeyStore createKeyStore(String name, JksSslStoreDetails details) { @@ -127,10 +129,12 @@ public class JksSslStoreBundle implements SslStoreBundle { @Override public String toString() { ToStringCreator creator = new ToStringCreator(this); - creator.append("keyStore.type", (this.keyStore != null) ? this.keyStore.getType() : "none"); + KeyStore keyStore = this.keyStore.get(); + creator.append("keyStore.type", (keyStore != null) ? keyStore.getType() : "none"); String keyStorePassword = getKeyStorePassword(); creator.append("keyStorePassword", (keyStorePassword != null) ? "******" : null); - creator.append("trustStore.type", (this.trustStore != null) ? this.trustStore.getType() : "none"); + KeyStore trustStore = this.trustStore.get(); + creator.append("trustStore.type", (trustStore != null) ? trustStore.getType() : "none"); return creator.toString(); } 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 5edacd360e6..5f001421f57 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 @@ -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. @@ -97,4 +97,14 @@ final class LoadedPemSslStore implements PemSslStore { return this.privateKeySupplier.get(); } + @Override + public PemSslStore withAlias(String alias) { + return new LoadedPemSslStore(this.details.withAlias(alias)); + } + + @Override + public PemSslStore withPassword(String password) { + return new LoadedPemSslStore(this.details.withPassword(password)); + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java index 7412b904e23..6bb2d37ab24 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java @@ -24,11 +24,13 @@ import java.security.PrivateKey; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.List; +import java.util.function.Supplier; import org.springframework.boot.ssl.SslStoreBundle; import org.springframework.core.style.ToStringCreator; import org.springframework.util.Assert; import org.springframework.util.StringUtils; +import org.springframework.util.function.SingletonSupplier; /** * {@link SslStoreBundle} backed by PEM-encoded certificates and private keys. @@ -42,9 +44,9 @@ public class PemSslStoreBundle implements SslStoreBundle { private static final String DEFAULT_ALIAS = "ssl"; - private final KeyStore keyStore; + private final Supplier keyStore; - private final KeyStore trustStore; + private final Supplier trustStore; /** * Create a new {@link PemSslStoreBundle} instance. @@ -62,13 +64,13 @@ public class PemSslStoreBundle implements SslStoreBundle { * @since 3.2.0 */ public PemSslStoreBundle(PemSslStore pemKeyStore, PemSslStore pemTrustStore) { - this.keyStore = createKeyStore("key", pemKeyStore); - this.trustStore = createKeyStore("trust", pemTrustStore); + this.keyStore = SingletonSupplier.of(() -> createKeyStore("key", pemKeyStore)); + this.trustStore = SingletonSupplier.of(() -> createKeyStore("trust", pemTrustStore)); } @Override public KeyStore getKeyStore() { - return this.keyStore; + return this.keyStore.get(); } @Override @@ -78,7 +80,7 @@ public class PemSslStoreBundle implements SslStoreBundle { @Override public KeyStore getTrustStore() { - return this.trustStore; + return this.trustStore.get(); } private static KeyStore createKeyStore(String name, PemSslStore pemSslStore) { @@ -86,10 +88,10 @@ public class PemSslStoreBundle implements SslStoreBundle { return null; } try { - Assert.notEmpty(pemSslStore.certificates(), "Certificates must not be empty"); + List certificates = pemSslStore.certificates(); + Assert.notEmpty(certificates, "Certificates must not be empty"); String alias = (pemSslStore.alias() != null) ? pemSslStore.alias() : DEFAULT_ALIAS; KeyStore store = createKeyStore(pemSslStore.type()); - List certificates = pemSslStore.certificates(); PrivateKey privateKey = pemSslStore.privateKey(); if (privateKey != null) { addPrivateKey(store, privateKey, alias, pemSslStore.password(), certificates); @@ -129,9 +131,11 @@ public class PemSslStoreBundle implements SslStoreBundle { @Override public String toString() { ToStringCreator creator = new ToStringCreator(this); - creator.append("keyStore.type", (this.keyStore != null) ? this.keyStore.getType() : "none"); + KeyStore keyStore = this.keyStore.get(); + KeyStore trustStore = this.trustStore.get(); + creator.append("keyStore.type", (keyStore != null) ? keyStore.getType() : "none"); creator.append("keyStorePassword", null); - creator.append("trustStore.type", (this.trustStore != null) ? this.trustStore.getType() : "none"); + creator.append("trustStore.type", (trustStore != null) ? trustStore.getType() : "none"); return creator.toString(); } 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 cbbd14a715d..c081224dec4 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 @@ -63,12 +63,10 @@ class JksSslStoreBundleTests { } @Test - void whenTypePKCS11AndLocationThrowsException() { - assertThatIllegalStateException().isThrownBy(() -> { - JksSslStoreDetails keyStoreDetails = new JksSslStoreDetails("PKCS11", null, "test.jks", null); - JksSslStoreDetails trustStoreDetails = null; - new JksSslStoreBundle(keyStoreDetails, trustStoreDetails); - }) + void whenTypePKCS11AndLocationGetKeyStoreThrowsException() { + JksSslStoreDetails keyStoreDetails = new JksSslStoreDetails("PKCS11", null, "test.jks", null); + JksSslStoreBundle jksSslStoreBundle = new JksSslStoreBundle(keyStoreDetails, null); + assertThatIllegalStateException().isThrownBy(jksSslStoreBundle::getKeyStore) .withMessageContaining( "Unable to create key store: Location is 'test.jks', but must be empty or null for PKCS11 hardware key stores"); } @@ -109,22 +107,28 @@ class JksSslStoreBundleTests { @Test void whenHasKeyStoreProvider() { - assertThatIllegalStateException().isThrownBy(() -> { - JksSslStoreDetails keyStoreDetails = new JksSslStoreDetails(null, "com.example.KeyStoreProvider", - "classpath:test.jks", "secret"); - JksSslStoreDetails trustStoreDetails = null; - new JksSslStoreBundle(keyStoreDetails, trustStoreDetails); - }).withMessageContaining("com.example.KeyStoreProvider"); + JksSslStoreDetails keyStoreDetails = new JksSslStoreDetails(null, "com.example.KeyStoreProvider", + "classpath:test.jks", "secret"); + JksSslStoreBundle jksSslStoreBundle = new JksSslStoreBundle(keyStoreDetails, null); + assertThatIllegalStateException().isThrownBy(jksSslStoreBundle::getKeyStore) + .withMessageContaining("com.example.KeyStoreProvider"); } @Test void whenHasTrustStoreProvider() { - assertThatIllegalStateException().isThrownBy(() -> { - JksSslStoreDetails keyStoreDetails = null; - JksSslStoreDetails trustStoreDetails = new JksSslStoreDetails(null, "com.example.KeyStoreProvider", - "classpath:test.jks", "secret"); - new JksSslStoreBundle(keyStoreDetails, trustStoreDetails); - }).withMessageContaining("com.example.KeyStoreProvider"); + JksSslStoreDetails trustStoreDetails = new JksSslStoreDetails(null, "com.example.KeyStoreProvider", + "classpath:test.jks", "secret"); + JksSslStoreBundle jksSslStoreBundle = new JksSslStoreBundle(null, trustStoreDetails); + assertThatIllegalStateException().isThrownBy(jksSslStoreBundle::getTrustStore) + .withMessageContaining("com.example.KeyStoreProvider"); + } + + @Test + void storeCreationIsLazy() { + JksSslStoreDetails details = new JksSslStoreDetails(null, null, "does-not-exist", null); + JksSslStoreBundle bundle = new JksSslStoreBundle(details, details); + assertThatIllegalStateException().isThrownBy(bundle::getKeyStore); + assertThatIllegalStateException().isThrownBy(bundle::getTrustStore); } @Test @@ -141,7 +145,8 @@ class JksSslStoreBundleTests { @Test void invalidBase64EncodedLocationThrowsException() { JksSslStoreDetails keyStoreDetails = JksSslStoreDetails.forLocation("base64:not base 64"); - assertThatIllegalStateException().isThrownBy(() -> new JksSslStoreBundle(keyStoreDetails, null)) + JksSslStoreBundle jksSslStoreBundle = new JksSslStoreBundle(keyStoreDetails, null); + assertThatIllegalStateException().isThrownBy(jksSslStoreBundle::getKeyStore) .withMessageContaining("key store") .withMessageContaining("base64:not base 64") .havingRootCause() @@ -152,7 +157,8 @@ class JksSslStoreBundleTests { @Test void invalidLocationThrowsException() { JksSslStoreDetails trustStoreDetails = JksSslStoreDetails.forLocation("does-not-exist.p12"); - assertThatIllegalStateException().isThrownBy(() -> new JksSslStoreBundle(null, trustStoreDetails)) + JksSslStoreBundle jksSslStoreBundle = new JksSslStoreBundle(null, trustStoreDetails); + assertThatIllegalStateException().isThrownBy(jksSslStoreBundle::getTrustStore) .withMessageContaining("trust store") .withMessageContaining("does-not-exist.p12"); } 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 b7bccce838a..f78b777921c 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 @@ -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. @@ -45,4 +45,20 @@ class LoadedPemSslStoreTests { assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(store::privateKey); } + @Test + void withAliasIsLazy() { + PemSslStoreDetails details = PemSslStoreDetails.forCertificate("classpath:missing-test-cert.pem") + .withPrivateKey("classpath:test-key.pem"); + PemSslStore store = new LoadedPemSslStore(details).withAlias("alias"); + assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(store::certificates); + } + + @Test + void withPasswordIsLazy() { + PemSslStoreDetails details = PemSslStoreDetails.forCertificate("classpath:missing-test-cert.pem") + .withPrivateKey("classpath:test-key.pem"); + PemSslStore store = new LoadedPemSslStore(details).withPassword("password"); + assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(store::certificates); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java index 9cd7a266d44..52d7a63cdec 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java @@ -27,6 +27,10 @@ import org.junit.jupiter.api.Test; import org.springframework.util.function.ThrowingConsumer; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; /** * Tests for {@link PemSslStoreBundle}. @@ -203,6 +207,18 @@ class PemSslStoreBundleTests { assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("ssl")); } + @Test + void storeCreationIsLazy() { + PemSslStore pemSslStore = mock(PemSslStore.class); + PemSslStoreBundle bundle = new PemSslStoreBundle(pemSslStore, pemSslStore); + given(pemSslStore.certificates()).willReturn(PemContent.of(CERTIFICATE).getCertificates()); + then(pemSslStore).shouldHaveNoInteractions(); + bundle.getKeyStore(); + then(pemSslStore).should().certificates(); + bundle.getTrustStore(); + then(pemSslStore).should(times(2)).certificates(); + } + private Consumer storeContainingCert(String keyAlias) { return storeContainingCert(KeyStore.getDefaultType(), keyAlias); }