From 5e5d2265f5143850db0d348a1d3432c0a8126d32 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 30 Oct 2023 19:44:37 -0700 Subject: [PATCH] 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 --- .../boot/ssl/pem/LoadedPemSslStore.java | 88 +++++++++ .../boot/ssl/pem/PemSslStore.java | 172 ++++++++++++++++++ .../boot/ssl/pem/PemSslStoreBundle.java | 101 +++++----- .../boot/ssl/pem/PemSslStoreDetails.java | 52 ++++-- .../boot/ssl/pem/PemSslStoreBundleTests.java | 37 ++-- .../boot/ssl/pem/PemSslStoreTests.java | 78 ++++++++ 6 files changed, 456 insertions(+), 72 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/LoadedPemSslStore.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreTests.java 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 new file mode 100644 index 00000000000..07f457a1b1f --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/LoadedPemSslStore.java @@ -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 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 loadCertificates(PemSslStoreDetails details) throws IOException { + PemContent pemContent = PemContent.load(details.certificates()); + if (pemContent == null) { + return null; + } + List 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 certificates() { + return this.certificates; + } + + @Override + public PrivateKey privateKey() { + return this.privateKey; + } + +} 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 new file mode 100644 index 00000000000..7eb3ce7b675 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java @@ -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 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 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 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 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 certificates() { + return certificates; + } + + @Override + public PrivateKey privateKey() { + return privateKey; + } + + }; + } + +} 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 ac565343b73..b5ad0b5b46e 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 @@ -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 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 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 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 certificateChain) throws KeyStoreException { + keyStore.setKeyEntry(alias, privateKey, (keyPassword != null) ? keyPassword.toCharArray() : null, + certificateChain.toArray(X509Certificate[]::new)); + } + + private static void addCertificates(KeyStore keyStore, List 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); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java index cdc580f4ce4..57d6d5db60c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java @@ -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. Note: 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); } } 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 6414382683b..c6759036d35 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 @@ -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 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 storeContainingCert(String keyAlias) { return storeContainingCert(KeyStore.getDefaultType(), keyAlias); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreTests.java new file mode 100644 index 00000000000..9e4ca402b14 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreTests.java @@ -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 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 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 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); + } + +}