Merge branch '3.3.x'

Closes gh-42170
This commit is contained in:
Andy Wilkinson 2024-09-06 11:50:52 +01:00
commit f6505f7a18
6 changed files with 96 additions and 40 deletions

View File

@ -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> keyStore;
private final KeyStore trustStore;
private final Supplier<KeyStore> 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();
}

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");
* 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));
}
}

View File

@ -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> keyStore;
private final KeyStore trustStore;
private final Supplier<KeyStore> 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<X509Certificate> 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<X509Certificate> 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();
}

View File

@ -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");
}

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");
* 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);
}
}

View File

@ -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<KeyStore> storeContainingCert(String keyAlias) {
return storeContainingCert(KeyStore.getDefaultType(), keyAlias);
}