Introduce PemSslStore as an alternative to PemSslStoreDetails.

Add a `PemSslStore` interface that can be used as an alternative
to `PemSslStoreDetails` when PEM content has already been loaded
and parsed.

Closes gh-38175
This commit is contained in:
Phillip Webb 2023-10-30 19:44:37 -07:00
parent 2b39ec6f60
commit 5e5d2265f5
6 changed files with 456 additions and 72 deletions

View File

@ -0,0 +1,88 @@
/*
* Copyright 2012-2023 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl.pem;
import java.io.IOException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.List;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* {@link PemSslStore} loaded from {@link PemSslStoreDetails}.
*
* @author Phillip Webb
* @see PemSslStore#load(PemSslStoreDetails)
*/
final class LoadedPemSslStore implements PemSslStore {
private final PemSslStoreDetails details;
private final List<X509Certificate> certificates;
private final PrivateKey privateKey;
LoadedPemSslStore(PemSslStoreDetails details) throws IOException {
Assert.notNull(details, "Details must not be null");
this.details = details;
this.certificates = loadCertificates(details);
this.privateKey = loadPrivateKey(details);
}
private static List<X509Certificate> loadCertificates(PemSslStoreDetails details) throws IOException {
PemContent pemContent = PemContent.load(details.certificates());
if (pemContent == null) {
return null;
}
List<X509Certificate> certificates = pemContent.getCertificates();
Assert.state(!CollectionUtils.isEmpty(certificates), "Loaded certificates are empty");
return certificates;
}
private static PrivateKey loadPrivateKey(PemSslStoreDetails details) throws IOException {
PemContent pemContent = PemContent.load(details.privateKey());
return (pemContent != null) ? pemContent.getPrivateKey(details.privateKeyPassword()) : null;
}
@Override
public String type() {
return this.details.type();
}
@Override
public String alias() {
return this.details.alias();
}
@Override
public String password() {
return this.details.password();
}
@Override
public List<X509Certificate> certificates() {
return this.certificates;
}
@Override
public PrivateKey privateKey() {
return this.privateKey;
}
}

View File

@ -0,0 +1,172 @@
/*
* Copyright 2012-2023 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl.pem;
import java.io.IOException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.List;
import org.springframework.util.Assert;
/**
* An individual trust or key store that has been loaded from PEM content.
*
* @author Phillip Webb
* @since 3.2.0
* @see PemSslStoreDetails
* @see PemContent
*/
public interface PemSslStore {
/**
* The key store type, for example {@code JKS} or {@code PKCS11}. A {@code null} value
* will use {@link KeyStore#getDefaultType()}).
* @return the key store type
*/
String type();
/**
* The alias used when setting entries in the {@link KeyStore}.
* @return the alias
*/
String alias();
/**
* the password used
* {@link KeyStore#setKeyEntry(String, java.security.Key, char[], java.security.cert.Certificate[])
* setting key entries} in the {@link KeyStore}.
* @return the password
*/
String password();
/**
* The certificates for this store. When a {@link #privateKey() private key} is
* present the returned value is treated as a certificate chain, otherwise it is
* treated a list of certificates that should all be registered.
* @return the X509 certificates
*/
List<X509Certificate> certificates();
/**
* The private key for this store or {@code null}.
* @return the private key
*/
PrivateKey privateKey();
/**
* Return a new {@link PemSslStore} instance with a new alias.
* @param alias the new alias
* @return a new {@link PemSslStore} instance
*/
default PemSslStore withAlias(String alias) {
return of(type(), alias, password(), certificates(), privateKey());
}
/**
* Return a new {@link PemSslStore} instance with a new password.
* @param password the new password
* @return a new {@link PemSslStore} instance
*/
default PemSslStore withPassword(String password) {
return of(type(), alias(), password, certificates(), privateKey());
}
/**
* Return a {@link PemSslStore} instance loaded using the given
* {@link PemSslStoreDetails}.
* @param details the PEM store details
* @return a loaded {@link PemSslStore} or {@code null}.
* @throws IOException on IO error
*/
static PemSslStore load(PemSslStoreDetails details) throws IOException {
if (details == null || details.isEmpty()) {
return null;
}
return new LoadedPemSslStore(details);
}
/**
* Factory method that can be used to create a new {@link PemSslStore} with the given
* values.
* @param type the key store type
* @param certificates the certificates for this store
* @param privateKey the private key
* @return a new {@link PemSslStore} instance
*/
static PemSslStore of(String type, List<X509Certificate> certificates, PrivateKey privateKey) {
return of(type, null, null, certificates, privateKey);
}
/**
* Factory method that can be used to create a new {@link PemSslStore} with the given
* values.
* @param certificates the certificates for this store
* @param privateKey the private key
* @return a new {@link PemSslStore} instance
*/
static PemSslStore of(List<X509Certificate> certificates, PrivateKey privateKey) {
return of(null, null, null, certificates, privateKey);
}
/**
* Factory method that can be used to create a new {@link PemSslStore} with the given
* values.
* @param type the key store type
* @param alias the alias used when setting entries in the {@link KeyStore}
* @param password the password used
* {@link KeyStore#setKeyEntry(String, java.security.Key, char[], java.security.cert.Certificate[])
* setting key entries} in the {@link KeyStore}
* @param certificates the certificates for this store
* @param privateKey the private key
* @return a new {@link PemSslStore} instance
*/
static PemSslStore of(String type, String alias, String password, List<X509Certificate> certificates,
PrivateKey privateKey) {
Assert.notEmpty(certificates, "Certificates must not be empty");
return new PemSslStore() {
@Override
public String type() {
return type;
}
@Override
public String alias() {
return alias;
}
@Override
public String password() {
return password;
}
@Override
public List<X509Certificate> certificates() {
return certificates;
}
@Override
public PrivateKey privateKey() {
return privateKey;
}
};
}
}

View File

@ -17,6 +17,7 @@
package org.springframework.boot.ssl.pem;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
@ -27,7 +28,6 @@ import java.util.List;
import org.springframework.boot.ssl.SslStoreBundle;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
@ -52,7 +52,7 @@ public class PemSslStoreBundle implements SslStoreBundle {
* @param trustStoreDetails the trust store details
*/
public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails) {
this(keyStoreDetails, trustStoreDetails, null);
this(keyStoreDetails, trustStoreDetails, null, false);
}
/**
@ -83,8 +83,26 @@ public class PemSslStoreBundle implements SslStoreBundle {
private PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String alias,
boolean verifyKeys) {
this.keyStore = createKeyStore("key", keyStoreDetails, alias, verifyKeys);
this.trustStore = createKeyStore("trust", trustStoreDetails, alias, verifyKeys);
try {
this.keyStore = createKeyStore("key", PemSslStore.load(keyStoreDetails), alias, verifyKeys);
this.trustStore = createKeyStore("trust", PemSslStore.load(trustStoreDetails), alias, verifyKeys);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
/**
* Create a new {@link PemSslStoreBundle} instance.
* @param pemKeyStore the PEM key store
* @param pemTrustStore the PEM trust store
* @param alias the alias to use or {@code null} to use a default alias
* @param verifyKeys whether to verify that the private key matches the public key
* @since 3.2.0
*/
public PemSslStoreBundle(PemSslStore pemKeyStore, PemSslStore pemTrustStore, String alias, boolean verifyKeys) {
this.keyStore = createKeyStore("key", pemKeyStore, alias, verifyKeys);
this.trustStore = createKeyStore("trust", pemTrustStore, alias, verifyKeys);
}
@Override
@ -102,22 +120,22 @@ public class PemSslStoreBundle implements SslStoreBundle {
return this.trustStore;
}
private static KeyStore createKeyStore(String name, PemSslStoreDetails details, String alias, boolean verifyKeys) {
if (details == null || details.isEmpty()) {
private static KeyStore createKeyStore(String name, PemSslStore pemSslStore, String alias, boolean verifyKeys) {
if (pemSslStore == null) {
return null;
}
try {
Assert.notNull(details.certificate(), "Certificate content must not be null");
alias = (details.alias() != null) ? details.alias() : alias;
Assert.notEmpty(pemSslStore.certificates(), "Certificates must not be empty");
alias = (pemSslStore.alias() != null) ? pemSslStore.alias() : alias;
alias = (alias != null) ? alias : DEFAULT_ALIAS;
KeyStore store = createKeyStore(details);
X509Certificate[] certificates = loadCertificates(details);
PrivateKey privateKey = loadPrivateKey(details);
KeyStore store = createKeyStore(pemSslStore.type());
List<X509Certificate> certificates = pemSslStore.certificates();
PrivateKey privateKey = pemSslStore.privateKey();
if (privateKey != null) {
if (verifyKeys) {
verifyKeys(privateKey, certificates);
}
addPrivateKey(store, privateKey, alias, details.password(), certificates);
addPrivateKey(store, privateKey, alias, pemSslStore.password(), certificates);
}
else {
addCertificates(store, certificates, alias);
@ -129,50 +147,37 @@ public class PemSslStoreBundle implements SslStoreBundle {
}
}
private static void verifyKeys(PrivateKey privateKey, X509Certificate[] certificates) {
KeyVerifier keyVerifier = new KeyVerifier();
// Key should match one of the certificates
for (X509Certificate certificate : certificates) {
Result result = keyVerifier.matches(privateKey, certificate.getPublicKey());
if (result == Result.YES) {
return;
}
}
throw new IllegalStateException("Private key matches none of the certificates");
}
private static PrivateKey loadPrivateKey(PemSslStoreDetails details) throws IOException {
PemContent pemContent = PemContent.load(details.privateKey());
if (pemContent == null) {
return null;
}
return pemContent.getPrivateKey(details.privateKeyPassword());
}
private static X509Certificate[] loadCertificates(PemSslStoreDetails details) throws IOException {
PemContent pemContent = PemContent.load(details.certificate());
List<X509Certificate> certificates = pemContent.getCertificates();
Assert.state(!CollectionUtils.isEmpty(certificates), "Loaded certificates are empty");
return certificates.toArray(X509Certificate[]::new);
}
private static KeyStore createKeyStore(PemSslStoreDetails details)
private static KeyStore createKeyStore(String type)
throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
String type = StringUtils.hasText(details.type()) ? details.type() : KeyStore.getDefaultType();
KeyStore store = KeyStore.getInstance(type);
KeyStore store = KeyStore.getInstance(StringUtils.hasText(type) ? type : KeyStore.getDefaultType());
store.load(null);
return store;
}
private static void addPrivateKey(KeyStore keyStore, PrivateKey privateKey, String alias, String keyPassword,
X509Certificate[] certificates) throws KeyStoreException {
keyStore.setKeyEntry(alias, privateKey, (keyPassword != null) ? keyPassword.toCharArray() : null, certificates);
private static void verifyKeys(PrivateKey privateKey, List<X509Certificate> certificateChain) {
KeyVerifier keyVerifier = new KeyVerifier();
// Key should match one of the certificates
for (X509Certificate certificate : certificateChain) {
KeyVerifier.Result result = keyVerifier.matches(privateKey, certificate.getPublicKey());
if (result == KeyVerifier.Result.YES) {
return;
}
}
throw new IllegalStateException("Private key matches none of the certificates in the chain");
}
private static void addCertificates(KeyStore keyStore, X509Certificate[] certificates, String alias)
private static void addPrivateKey(KeyStore keyStore, PrivateKey privateKey, String alias, String keyPassword,
List<X509Certificate> certificateChain) throws KeyStoreException {
keyStore.setKeyEntry(alias, privateKey, (keyPassword != null) ? keyPassword.toCharArray() : null,
certificateChain.toArray(X509Certificate[]::new));
}
private static void addCertificates(KeyStore keyStore, List<X509Certificate> certificates, String alias)
throws KeyStoreException {
for (int index = 0; index < certificates.length; index++) {
keyStore.setCertificateEntry(alias + "-" + index, certificates[index]);
for (int index = 0; index < certificates.size(); index++) {
String entryAlias = alias + ((certificates.size() == 1) ? "" : "-" + index);
X509Certificate certificate = certificates.get(index);
keyStore.setCertificateEntry(entryAlias, certificate);
}
}

View File

@ -30,16 +30,19 @@ import org.springframework.util.StringUtils;
* @param password the password used
* {@link KeyStore#setKeyEntry(String, java.security.Key, char[], java.security.cert.Certificate[])
* setting key entries} in the {@link KeyStore}
* @param certificate the certificate content (either the PEM content itself or something
* that can be loaded by {@link ResourceUtils#getURL})
* @param certificates the certificates content (either the PEM content itself or
* something that can be loaded by {@link ResourceUtils#getURL}). When a
* {@link #privateKey() private key} is present this value is treated as a certificate
* chain, otherwise it is treated a list of certificates that should all be registered.
* @param privateKey the private key content (either the PEM content itself or something
* that can be loaded by {@link ResourceUtils#getURL})
* @param privateKeyPassword a password used to decrypt an encrypted private key
* @author Scott Frederick
* @author Phillip Webb
* @since 3.1.0
* @see PemSslStore#load(PemSslStoreDetails)
*/
public record PemSslStoreDetails(String type, String alias, String password, String certificate, String privateKey,
public record PemSslStoreDetails(String type, String alias, String password, String certificates, String privateKey,
String privateKeyPassword) {
/**
@ -50,7 +53,7 @@ public record PemSslStoreDetails(String type, String alias, String password, Str
* @param password the password used
* {@link KeyStore#setKeyEntry(String, java.security.Key, char[], java.security.cert.Certificate[])
* setting key entries} in the {@link KeyStore}
* @param certificate the certificate content (either the PEM content itself or
* @param certificates the certificate content (either the PEM content itself or
* something that can be loaded by {@link ResourceUtils#getURL})
* @param privateKey the private key content (either the PEM content itself or
* something that can be loaded by {@link ResourceUtils#getURL})
@ -87,6 +90,16 @@ public record PemSslStoreDetails(String type, String alias, String password, Str
this(type, certificate, privateKey, null);
}
/**
* Return the certificate content.
* @return the certificate content
* @deprecated since 3.2.0 for removal in 3.4.0 in favor of {@link #certificates()}
*/
@Deprecated(since = "3.2.0", forRemoval = true)
public String certificate() {
return certificates();
}
/**
* Return a new {@link PemSslStoreDetails} instance with a new alias.
* @param alias the new alias
@ -94,7 +107,7 @@ public record PemSslStoreDetails(String type, String alias, String password, Str
* @since 3.2.0
*/
public PemSslStoreDetails withAlias(String alias) {
return new PemSslStoreDetails(this.type, alias, this.password, this.certificate, this.privateKey,
return new PemSslStoreDetails(this.type, alias, this.password, this.certificates, this.privateKey,
this.privateKeyPassword);
}
@ -105,7 +118,7 @@ public record PemSslStoreDetails(String type, String alias, String password, Str
* @since 3.2.0
*/
public PemSslStoreDetails withPassword(String password) {
return new PemSslStoreDetails(this.type, this.alias, password, this.certificate, this.privateKey,
return new PemSslStoreDetails(this.type, this.alias, password, this.certificates, this.privateKey,
this.privateKeyPassword);
}
@ -115,7 +128,7 @@ public record PemSslStoreDetails(String type, String alias, String password, Str
* @return a new {@link PemSslStoreDetails} instance
*/
public PemSslStoreDetails withPrivateKey(String privateKey) {
return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificate, privateKey,
return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificates, privateKey,
this.privateKeyPassword);
}
@ -125,12 +138,12 @@ public record PemSslStoreDetails(String type, String alias, String password, Str
* @return a new {@link PemSslStoreDetails} instance
*/
public PemSslStoreDetails withPrivateKeyPassword(String privateKeyPassword) {
return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificate, this.privateKey,
return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificates, this.privateKey,
privateKeyPassword);
}
boolean isEmpty() {
return isEmpty(this.type) && isEmpty(this.certificate) && isEmpty(this.privateKey);
return isEmpty(this.type) && isEmpty(this.certificates) && isEmpty(this.privateKey);
}
private boolean isEmpty(String value) {
@ -139,12 +152,27 @@ public record PemSslStoreDetails(String type, String alias, String password, Str
/**
* Factory method to create a new {@link PemSslStoreDetails} instance for the given
* certificate.
* @param certificate the certificate
* certificate. <b>Note:</b> This method doesn't actually check if the provided value
* only contains a single certificate. It is functionally equivalent to
* {@link #forCertificates(String)}.
* @param certificate the certificate content (either the PEM content itself or
* something that can be loaded by {@link ResourceUtils#getURL})
* @return a new {@link PemSslStoreDetails} instance.
*/
public static PemSslStoreDetails forCertificate(String certificate) {
return new PemSslStoreDetails(null, certificate, null);
return forCertificates(certificate);
}
/**
* Factory method to create a new {@link PemSslStoreDetails} instance for the given
* certificates.
* @param certificates the certificates content (either the PEM content itself or
* something that can be loaded by {@link ResourceUtils#getURL})
* @return a new {@link PemSslStoreDetails} instance.
* @since 3.2.0
*/
public static PemSslStoreDetails forCertificates(String certificates) {
return new PemSslStoreDetails(null, certificates, null);
}
}

View File

@ -17,6 +17,9 @@
package org.springframework.boot.ssl.pem;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
@ -94,7 +97,7 @@ class PemSslStoreBundleTests {
private static final char[] EMPTY_KEY_PASSWORD = new char[] {};
@Test
void whenNullStores() {
void createWithDetailsWhenNullStores() {
PemSslStoreDetails keyStoreDetails = null;
PemSslStoreDetails trustStoreDetails = null;
PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails);
@ -104,7 +107,7 @@ class PemSslStoreBundleTests {
}
@Test
void whenStoresHaveNoValues() {
void createWithDetailsWhenStoresHaveNoValues() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate(null);
PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate(null);
PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails);
@ -114,7 +117,7 @@ class PemSslStoreBundleTests {
}
@Test
void whenHasKeyStoreDetailsCertAndKey() {
void createWithDetailsWhenHasKeyStoreDetailsCertAndKey() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem")
.withPrivateKey("classpath:test-key.pem");
PemSslStoreDetails trustStoreDetails = null;
@ -124,7 +127,7 @@ class PemSslStoreBundleTests {
}
@Test
void whenHasKeyStoreDetailsCertAndEncryptedKey() {
void createWithDetailsWhenHasKeyStoreDetailsCertAndEncryptedKey() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem")
.withPrivateKey("classpath:ssl/pkcs8/key-rsa-encrypted.pem")
.withPrivateKeyPassword("test");
@ -135,17 +138,17 @@ class PemSslStoreBundleTests {
}
@Test
void whenHasKeyStoreDetailsAndTrustStoreDetailsWithoutKey() {
void createWithDetailsWhenHasKeyStoreDetailsAndTrustStoreDetailsWithoutKey() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem")
.withPrivateKey("classpath:test-key.pem");
PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem");
PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails);
assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("ssl"));
assertThat(bundle.getTrustStore()).satisfies(storeContainingCert("ssl-0"));
assertThat(bundle.getTrustStore()).satisfies(storeContainingCert("ssl"));
}
@Test
void whenHasKeyStoreDetailsAndTrustStoreDetails() {
void createWithDetailsWhenHasKeyStoreDetailsAndTrustStoreDetails() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem")
.withPrivateKey("classpath:test-key.pem");
PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem")
@ -156,7 +159,7 @@ class PemSslStoreBundleTests {
}
@Test
void whenHasEmbeddedKeyStoreDetailsAndTrustStoreDetails() {
void createWithDetailsWhenHasEmbeddedKeyStoreDetailsAndTrustStoreDetails() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate(CERTIFICATE).withPrivateKey(PRIVATE_KEY);
PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate(CERTIFICATE)
.withPrivateKey(PRIVATE_KEY);
@ -167,7 +170,7 @@ class PemSslStoreBundleTests {
@Test
@SuppressWarnings("removal")
void whenHasKeyStoreDetailsAndTrustStoreDetailsAndAlias() {
void createWithDetailsWhenHasKeyStoreDetailsAndTrustStoreDetailsAndAlias() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem")
.withPrivateKey("classpath:test-key.pem");
PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem")
@ -178,7 +181,7 @@ class PemSslStoreBundleTests {
}
@Test
void whenHasStoreType() {
void createWithDetailsWhenHasStoreType() {
PemSslStoreDetails keyStoreDetails = new PemSslStoreDetails("PKCS12", "classpath:test-cert.pem",
"classpath:test-key.pem");
PemSslStoreDetails trustStoreDetails = new PemSslStoreDetails("PKCS12", "classpath:test-cert.pem",
@ -189,7 +192,7 @@ class PemSslStoreBundleTests {
}
@Test
void whenHasKeyStoreDetailsAndTrustStoreDetailsAndKeyPassword() {
void createWithDetailsWhenHasKeyStoreDetailsAndTrustStoreDetailsAndKeyPassword() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem")
.withPrivateKey("classpath:test-key.pem")
.withAlias("ksa")
@ -217,7 +220,7 @@ class PemSslStoreBundleTests {
@Test
void shouldVerifyKeysIfEnabledAndCertificateChainIsUsed() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails
.forCertificate("classpath:org/springframework/boot/ssl/pem/key2-chain.crt")
.forCertificates("classpath:org/springframework/boot/ssl/pem/key2-chain.crt")
.withPrivateKey("classpath:org/springframework/boot/ssl/pem/key2.pem")
.withAlias("test-alias")
.withPassword("keysecret");
@ -234,6 +237,16 @@ class PemSslStoreBundleTests {
.withMessageContaining("Private key matches none of the certificates");
}
@Test
void createWithPemSslStoreCreatesInstance() {
List<X509Certificate> certificates = PemContent.of(CERTIFICATE).getCertificates();
PrivateKey privateKey = PemContent.of(PRIVATE_KEY).getPrivateKey();
PemSslStore pemSslStore = PemSslStore.of(certificates, privateKey);
PemSslStoreBundle bundle = new PemSslStoreBundle(pemSslStore, pemSslStore, null, false);
assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("ssl"));
assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("ssl"));
}
private Consumer<KeyStore> storeContainingCert(String keyAlias) {
return storeContainingCert(KeyStore.getDefaultType(), keyAlias);
}

View File

@ -0,0 +1,78 @@
/*
* Copyright 2012-2023 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ssl.pem;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link PemSslStore}.
*
* @author Phillip Webb
*/
class PemSslStoreTests {
@Test
void withAliasReturnsStoreWithNewAlias() {
List<X509Certificate> certificates = List.of(mock(X509Certificate.class));
PrivateKey privateKey = mock(PrivateKey.class);
PemSslStore store = PemSslStore.of("type", "alias", "secret", certificates, privateKey);
assertThat(store.withAlias("newalias").alias()).isEqualTo("newalias");
}
@Test
void withPasswordReturnsStoreWithNewPassword() {
List<X509Certificate> certificates = List.of(mock(X509Certificate.class));
PrivateKey privateKey = mock(PrivateKey.class);
PemSslStore store = PemSslStore.of("type", "alias", "secret", certificates, privateKey);
assertThat(store.withPassword("newsecret").password()).isEqualTo("newsecret");
}
@Test
void ofWhenNullCertificatesThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> PemSslStore.of(null, null, null, null, null))
.withMessage("Certificates must not be empty");
}
@Test
void ofWhenEmptyCertificatesThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> PemSslStore.of(null, null, null, Collections.emptyList(), null))
.withMessage("Certificates must not be empty");
}
@Test
void ofReturnsPemSslStore() {
List<X509Certificate> certificates = List.of(mock(X509Certificate.class));
PrivateKey privateKey = mock(PrivateKey.class);
PemSslStore store = PemSslStore.of("type", "alias", "password", certificates, privateKey);
assertThat(store.type()).isEqualTo("type");
assertThat(store.alias()).isEqualTo("alias");
assertThat(store.password()).isEqualTo("password");
assertThat(store.certificates()).isEqualTo(certificates);
assertThat(store.privateKey()).isEqualTo(privateKey);
}
}