Skip to content

Commit a89ae3f

Browse files
committed
Improve laziness of Pem and JKS SSL store bundles
Fixes gh-42119
1 parent f813079 commit a89ae3f

File tree

6 files changed

+97
-42
lines changed

6 files changed

+97
-42
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/jks/JksSslStoreBundle.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@
2424
import java.security.NoSuchAlgorithmException;
2525
import java.security.NoSuchProviderException;
2626
import java.security.cert.CertificateException;
27+
import java.util.function.Supplier;
2728

2829
import org.springframework.boot.ssl.SslStoreBundle;
2930
import org.springframework.core.style.ToStringCreator;
3031
import org.springframework.util.Assert;
3132
import org.springframework.util.ResourceUtils;
3233
import org.springframework.util.StringUtils;
34+
import org.springframework.util.function.SingletonSupplier;
3335

3436
/**
3537
* {@link SslStoreBundle} backed by a Java keystore.
@@ -43,9 +45,9 @@ public class JksSslStoreBundle implements SslStoreBundle {
4345

4446
private final JksSslStoreDetails keyStoreDetails;
4547

46-
private final KeyStore keyStore;
48+
private final Supplier<KeyStore> keyStore;
4749

48-
private final KeyStore trustStore;
50+
private final Supplier<KeyStore> trustStore;
4951

5052
/**
5153
* Create a new {@link JksSslStoreBundle} instance.
@@ -54,13 +56,13 @@ public class JksSslStoreBundle implements SslStoreBundle {
5456
*/
5557
public JksSslStoreBundle(JksSslStoreDetails keyStoreDetails, JksSslStoreDetails trustStoreDetails) {
5658
this.keyStoreDetails = keyStoreDetails;
57-
this.keyStore = createKeyStore("key", this.keyStoreDetails);
58-
this.trustStore = createKeyStore("trust", trustStoreDetails);
59+
this.keyStore = SingletonSupplier.of(() -> createKeyStore("key", this.keyStoreDetails));
60+
this.trustStore = SingletonSupplier.of(() -> createKeyStore("trust", trustStoreDetails));
5961
}
6062

6163
@Override
6264
public KeyStore getKeyStore() {
63-
return this.keyStore;
65+
return this.keyStore.get();
6466
}
6567

6668
@Override
@@ -70,7 +72,7 @@ public String getKeyStorePassword() {
7072

7173
@Override
7274
public KeyStore getTrustStore() {
73-
return this.trustStore;
75+
return this.trustStore.get();
7476
}
7577

7678
private KeyStore createKeyStore(String name, JksSslStoreDetails details) {
@@ -127,10 +129,12 @@ private void loadKeyStore(KeyStore store, String location, char[] password) {
127129
@Override
128130
public String toString() {
129131
ToStringCreator creator = new ToStringCreator(this);
130-
creator.append("keyStore.type", (this.keyStore != null) ? this.keyStore.getType() : "none");
132+
KeyStore keyStore = this.keyStore.get();
133+
creator.append("keyStore.type", (keyStore != null) ? keyStore.getType() : "none");
131134
String keyStorePassword = getKeyStorePassword();
132135
creator.append("keyStorePassword", (keyStorePassword != null) ? "******" : null);
133-
creator.append("trustStore.type", (this.trustStore != null) ? this.trustStore.getType() : "none");
136+
KeyStore trustStore = this.trustStore.get();
137+
creator.append("trustStore.type", (trustStore != null) ? trustStore.getType() : "none");
134138
return creator.toString();
135139
}
136140

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/LoadedPemSslStore.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -97,4 +97,14 @@ public PrivateKey privateKey() {
9797
return this.privateKeySupplier.get();
9898
}
9999

100+
@Override
101+
public PemSslStore withAlias(String alias) {
102+
return new LoadedPemSslStore(this.details.withAlias(alias));
103+
}
104+
105+
@Override
106+
public PemSslStore withPassword(String password) {
107+
return new LoadedPemSslStore(this.details.withPassword(password));
108+
}
109+
100110
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@
2424
import java.security.cert.CertificateException;
2525
import java.security.cert.X509Certificate;
2626
import java.util.List;
27+
import java.util.function.Supplier;
2728

2829
import org.springframework.boot.ssl.SslStoreBundle;
2930
import org.springframework.core.style.ToStringCreator;
3031
import org.springframework.util.Assert;
3132
import org.springframework.util.StringUtils;
33+
import org.springframework.util.function.SingletonSupplier;
3234

3335
/**
3436
* {@link SslStoreBundle} backed by PEM-encoded certificates and private keys.
@@ -42,9 +44,9 @@ public class PemSslStoreBundle implements SslStoreBundle {
4244

4345
private static final String DEFAULT_ALIAS = "ssl";
4446

45-
private final KeyStore keyStore;
47+
private final Supplier<KeyStore> keyStore;
4648

47-
private final KeyStore trustStore;
49+
private final Supplier<KeyStore> trustStore;
4850

4951
/**
5052
* Create a new {@link PemSslStoreBundle} instance.
@@ -66,8 +68,9 @@ public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails
6668
*/
6769
@Deprecated(since = "3.2.0", forRemoval = true)
6870
public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String alias) {
69-
this.keyStore = createKeyStore("key", PemSslStore.load(keyStoreDetails), alias);
70-
this.trustStore = createKeyStore("trust", PemSslStore.load(trustStoreDetails), alias);
71+
this.keyStore = SingletonSupplier.of(() -> createKeyStore("key", PemSslStore.load(keyStoreDetails), alias));
72+
this.trustStore = SingletonSupplier
73+
.of(() -> createKeyStore("trust", PemSslStore.load(trustStoreDetails), alias));
7174
}
7275

7376
/**
@@ -81,13 +84,13 @@ public PemSslStoreBundle(PemSslStore pemKeyStore, PemSslStore pemTrustStore) {
8184
}
8285

8386
private PemSslStoreBundle(PemSslStore pemKeyStore, PemSslStore pemTrustStore, String alias) {
84-
this.keyStore = createKeyStore("key", pemKeyStore, alias);
85-
this.trustStore = createKeyStore("trust", pemTrustStore, alias);
87+
this.keyStore = SingletonSupplier.of(() -> createKeyStore("key", pemKeyStore, alias));
88+
this.trustStore = SingletonSupplier.of(() -> createKeyStore("trust", pemTrustStore, alias));
8689
}
8790

8891
@Override
8992
public KeyStore getKeyStore() {
90-
return this.keyStore;
93+
return this.keyStore.get();
9194
}
9295

9396
@Override
@@ -97,19 +100,19 @@ public String getKeyStorePassword() {
97100

98101
@Override
99102
public KeyStore getTrustStore() {
100-
return this.trustStore;
103+
return this.trustStore.get();
101104
}
102105

103106
private static KeyStore createKeyStore(String name, PemSslStore pemSslStore, String alias) {
104107
if (pemSslStore == null) {
105108
return null;
106109
}
107110
try {
108-
Assert.notEmpty(pemSslStore.certificates(), "Certificates must not be empty");
111+
List<X509Certificate> certificates = pemSslStore.certificates();
112+
Assert.notEmpty(certificates, "Certificates must not be empty");
109113
alias = (pemSslStore.alias() != null) ? pemSslStore.alias() : alias;
110114
alias = (alias != null) ? alias : DEFAULT_ALIAS;
111115
KeyStore store = createKeyStore(pemSslStore.type());
112-
List<X509Certificate> certificates = pemSslStore.certificates();
113116
PrivateKey privateKey = pemSslStore.privateKey();
114117
if (privateKey != null) {
115118
addPrivateKey(store, privateKey, alias, pemSslStore.password(), certificates);
@@ -149,9 +152,11 @@ private static void addCertificates(KeyStore keyStore, List<X509Certificate> cer
149152
@Override
150153
public String toString() {
151154
ToStringCreator creator = new ToStringCreator(this);
152-
creator.append("keyStore.type", (this.keyStore != null) ? this.keyStore.getType() : "none");
155+
KeyStore keyStore = this.keyStore.get();
156+
KeyStore trustStore = this.trustStore.get();
157+
creator.append("keyStore.type", (keyStore != null) ? keyStore.getType() : "none");
153158
creator.append("keyStorePassword", null);
154-
creator.append("trustStore.type", (this.trustStore != null) ? this.trustStore.getType() : "none");
159+
creator.append("trustStore.type", (trustStore != null) ? trustStore.getType() : "none");
155160
return creator.toString();
156161
}
157162

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/jks/JksSslStoreBundleTests.java

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -58,12 +58,10 @@ void whenStoresHaveNoValues() {
5858
}
5959

6060
@Test
61-
void whenTypePKCS11AndLocationThrowsException() {
62-
assertThatIllegalStateException().isThrownBy(() -> {
63-
JksSslStoreDetails keyStoreDetails = new JksSslStoreDetails("PKCS11", null, "test.jks", null);
64-
JksSslStoreDetails trustStoreDetails = null;
65-
new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
66-
})
61+
void whenTypePKCS11AndLocationGetKeyStoreThrowsException() {
62+
JksSslStoreDetails keyStoreDetails = new JksSslStoreDetails("PKCS11", null, "test.jks", null);
63+
JksSslStoreBundle jksSslStoreBundle = new JksSslStoreBundle(keyStoreDetails, null);
64+
assertThatIllegalStateException().isThrownBy(jksSslStoreBundle::getKeyStore)
6765
.withMessageContaining(
6866
"Unable to create key store: Location is 'test.jks', but must be empty or null for PKCS11 hardware key stores");
6967
}
@@ -104,22 +102,28 @@ void whenHasTrustStoreType() {
104102

105103
@Test
106104
void whenHasKeyStoreProvider() {
107-
assertThatIllegalStateException().isThrownBy(() -> {
108-
JksSslStoreDetails keyStoreDetails = new JksSslStoreDetails(null, "com.example.KeyStoreProvider",
109-
"classpath:test.jks", "secret");
110-
JksSslStoreDetails trustStoreDetails = null;
111-
new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
112-
}).withMessageContaining("com.example.KeyStoreProvider");
105+
JksSslStoreDetails keyStoreDetails = new JksSslStoreDetails(null, "com.example.KeyStoreProvider",
106+
"classpath:test.jks", "secret");
107+
JksSslStoreBundle jksSslStoreBundle = new JksSslStoreBundle(keyStoreDetails, null);
108+
assertThatIllegalStateException().isThrownBy(jksSslStoreBundle::getKeyStore)
109+
.withMessageContaining("com.example.KeyStoreProvider");
113110
}
114111

115112
@Test
116113
void whenHasTrustStoreProvider() {
117-
assertThatIllegalStateException().isThrownBy(() -> {
118-
JksSslStoreDetails keyStoreDetails = null;
119-
JksSslStoreDetails trustStoreDetails = new JksSslStoreDetails(null, "com.example.KeyStoreProvider",
120-
"classpath:test.jks", "secret");
121-
new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
122-
}).withMessageContaining("com.example.KeyStoreProvider");
114+
JksSslStoreDetails trustStoreDetails = new JksSslStoreDetails(null, "com.example.KeyStoreProvider",
115+
"classpath:test.jks", "secret");
116+
JksSslStoreBundle jksSslStoreBundle = new JksSslStoreBundle(null, trustStoreDetails);
117+
assertThatIllegalStateException().isThrownBy(jksSslStoreBundle::getTrustStore)
118+
.withMessageContaining("com.example.KeyStoreProvider");
119+
}
120+
121+
@Test
122+
void storeCreationIsLazy() {
123+
JksSslStoreDetails details = new JksSslStoreDetails(null, null, "does-not-exist", null);
124+
JksSslStoreBundle bundle = new JksSslStoreBundle(details, details);
125+
assertThatIllegalStateException().isThrownBy(bundle::getKeyStore);
126+
assertThatIllegalStateException().isThrownBy(bundle::getTrustStore);
123127
}
124128

125129
private Consumer<KeyStore> storeContainingCertAndKey(String keyAlias, String keyPassword) {

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/LoadedPemSslStoreTests.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -45,4 +45,20 @@ void privateKeyIsLoadedLazily() {
4545
assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(store::privateKey);
4646
}
4747

48+
@Test
49+
void withAliasIsLazy() {
50+
PemSslStoreDetails details = PemSslStoreDetails.forCertificate("classpath:missing-test-cert.pem")
51+
.withPrivateKey("classpath:test-key.pem");
52+
PemSslStore store = new LoadedPemSslStore(details).withAlias("alias");
53+
assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(store::certificates);
54+
}
55+
56+
@Test
57+
void withPasswordIsLazy() {
58+
PemSslStoreDetails details = PemSslStoreDetails.forCertificate("classpath:missing-test-cert.pem")
59+
.withPrivateKey("classpath:test-key.pem");
60+
PemSslStore store = new LoadedPemSslStore(details).withPassword("password");
61+
assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(store::certificates);
62+
}
63+
4864
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,6 +27,10 @@
2727
import org.springframework.util.function.ThrowingConsumer;
2828

2929
import static org.assertj.core.api.Assertions.assertThat;
30+
import static org.mockito.BDDMockito.given;
31+
import static org.mockito.BDDMockito.then;
32+
import static org.mockito.Mockito.mock;
33+
import static org.mockito.Mockito.times;
3034

3135
/**
3236
* Tests for {@link PemSslStoreBundle}.
@@ -215,6 +219,18 @@ void createWithPemSslStoreCreatesInstance() {
215219
assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("ssl"));
216220
}
217221

222+
@Test
223+
void storeCreationIsLazy() {
224+
PemSslStore pemSslStore = mock(PemSslStore.class);
225+
PemSslStoreBundle bundle = new PemSslStoreBundle(pemSslStore, pemSslStore);
226+
given(pemSslStore.certificates()).willReturn(PemContent.of(CERTIFICATE).getCertificates());
227+
then(pemSslStore).shouldHaveNoInteractions();
228+
bundle.getKeyStore();
229+
then(pemSslStore).should().certificates();
230+
bundle.getTrustStore();
231+
then(pemSslStore).should(times(2)).certificates();
232+
}
233+
218234
private Consumer<KeyStore> storeContainingCert(String keyAlias) {
219235
return storeContainingCert(KeyStore.getDefaultType(), keyAlias);
220236
}

0 commit comments

Comments
 (0)