From 2c6fca8df7ec67e1e7dd44c517a68cebbb261cc1 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 30 Oct 2023 16:04:58 -0700 Subject: [PATCH] Allow alias and password to be configured on a per PEM store basis Closes gh-38124 --- .../ssl/PropertiesSslBundle.java | 9 ++- .../boot/ssl/pem/PemSslStoreBundle.java | 44 +++++----- .../boot/ssl/pem/PemSslStoreDetails.java | 80 +++++++++++++++++-- .../boot/web/server/WebServerSslBundle.java | 8 +- .../boot/ssl/pem/PemSslStoreBundleTests.java | 31 ++++--- 5 files changed, 122 insertions(+), 50 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java index a1c8e9522f9..96188861a12 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java @@ -107,10 +107,11 @@ public final class PropertiesSslBundle implements SslBundle { } private static SslStoreBundle asSslStoreBundle(PemSslBundleProperties properties) { - PemSslStoreDetails keyStoreDetails = asStoreDetails(properties.getKeystore()); - PemSslStoreDetails trustStoreDetails = asStoreDetails(properties.getTruststore()); - return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, properties.getKey().getAlias(), null, - properties.isVerifyKeys()); + PemSslStoreDetails keyStoreDetails = asStoreDetails(properties.getKeystore()) + .withAlias(properties.getKey().getAlias()); + PemSslStoreDetails trustStoreDetails = asStoreDetails(properties.getTruststore()) + .withAlias(properties.getKey().getAlias()); + return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, properties.isVerifyKeys()); } private static PemSslStoreDetails asStoreDetails(PemSslBundleProperties.Store properties) { 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 becea0dbf16..0346b5395bd 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 @@ -26,7 +26,6 @@ import java.security.cert.X509Certificate; import java.util.List; import org.springframework.boot.ssl.SslStoreBundle; -import org.springframework.boot.ssl.pem.KeyVerifier.Result; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -61,39 +60,31 @@ public class PemSslStoreBundle implements SslStoreBundle { * @param keyStoreDetails the key store details * @param trustStoreDetails the trust store details * @param alias the alias to use or {@code null} to use a default alias + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of + * {@link PemSslStoreDetails#alias()} in the {@code keyStoreDetails} and + * {@code trustStoreDetails} */ + @Deprecated(since = "3.2.0", forRemoval = true) public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String alias) { - this(keyStoreDetails, trustStoreDetails, alias, null); + this(keyStoreDetails, trustStoreDetails, alias, false); } /** * Create a new {@link PemSslStoreBundle} instance. * @param keyStoreDetails the key store details * @param trustStoreDetails the trust store details - * @param alias the alias to use or {@code null} to use a default alias - * @param keyPassword the password to protect the key (if one is added) - * @since 3.2.0 - */ - public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String alias, - String keyPassword) { - this(keyStoreDetails, trustStoreDetails, alias, keyPassword, false); - } - - /** - * Create a new {@link PemSslStoreBundle} instance. - * @param keyStoreDetails the key store details - * @param trustStoreDetails the trust store details - * @param alias the key alias to use or {@code null} to use a default alias - * @param keyPassword the password to protect the key (if one is added) * @param verifyKeys whether to verify that the private key matches the public key * @since 3.2.0 */ - public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String alias, - String keyPassword, boolean verifyKeys) { - this.keyStore = createKeyStore("key", keyStoreDetails, (alias != null) ? alias : DEFAULT_ALIAS, keyPassword, - verifyKeys); - this.trustStore = createKeyStore("trust", trustStoreDetails, (alias != null) ? alias : DEFAULT_ALIAS, - keyPassword, verifyKeys); + public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, + boolean verifyKeys) { + this(keyStoreDetails, trustStoreDetails, null, verifyKeys); + } + + private PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String alias, + boolean verifyKeys) { + this.keyStore = createKeyStore("key", keyStoreDetails, alias, verifyKeys); + this.trustStore = createKeyStore("trust", trustStoreDetails, alias, verifyKeys); } @Override @@ -111,13 +102,14 @@ public class PemSslStoreBundle implements SslStoreBundle { return this.trustStore; } - private static KeyStore createKeyStore(String name, PemSslStoreDetails details, String alias, String keyPassword, - boolean verifyKeys) { + private static KeyStore createKeyStore(String name, PemSslStoreDetails details, String alias, boolean verifyKeys) { if (details == null || details.isEmpty()) { return null; } try { Assert.notNull(details.certificate(), "Certificate content must not be null"); + alias = (details.alias() != null) ? details.alias() : alias; + alias = (alias != null) ? alias : DEFAULT_ALIAS; KeyStore store = createKeyStore(details); X509Certificate[] certificates = loadCertificates(details); PrivateKey privateKey = loadPrivateKey(details); @@ -125,7 +117,7 @@ public class PemSslStoreBundle implements SslStoreBundle { if (verifyKeys) { verifyKeys(privateKey, certificates); } - addPrivateKey(store, privateKey, alias, keyPassword, certificates); + addPrivateKey(store, privateKey, alias, details.password(), certificates); } else { addCertificates(store, certificates, alias); 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 81d68eb6959..cdc580f4ce4 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 @@ -26,6 +26,10 @@ import org.springframework.util.StringUtils; * * @param type the key store type, for example {@code JKS} or {@code PKCS11}. A * {@code null} value will use {@link KeyStore#getDefaultType()}). + * @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 certificate 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 @@ -35,28 +39,94 @@ import org.springframework.util.StringUtils; * @author Phillip Webb * @since 3.1.0 */ -public record PemSslStoreDetails(String type, String certificate, String privateKey, String privateKeyPassword) { +public record PemSslStoreDetails(String type, String alias, String password, String certificate, String privateKey, + String privateKeyPassword) { + /** + * Create a new {@link PemSslStoreDetails} instance. + * @param type the key store type, for example {@code JKS} or {@code PKCS11}. A + * {@code null} value will use {@link KeyStore#getDefaultType()}). + * @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 certificate 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}) + * @param privateKeyPassword a password used to decrypt an encrypted private key + * @since 3.2.0 + */ + public PemSslStoreDetails { + } + + /** + * Create a new {@link PemSslStoreDetails} instance. + * @param type the key store type, for example {@code JKS} or {@code PKCS11}. A + * {@code null} value will use {@link KeyStore#getDefaultType()}). + * @param certificate 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}) + * @param privateKeyPassword a password used to decrypt an encrypted private key + */ + public PemSslStoreDetails(String type, String certificate, String privateKey, String privateKeyPassword) { + this(type, null, null, certificate, privateKey, null); + } + + /** + * Create a new {@link PemSslStoreDetails} instance. + * @param type the key store type, for example {@code JKS} or {@code PKCS11}. A + * {@code null} value will use {@link KeyStore#getDefaultType()}). + * @param certificate 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}) + */ public PemSslStoreDetails(String type, String certificate, String privateKey) { this(type, certificate, privateKey, null); } + /** + * Return a new {@link PemSslStoreDetails} instance with a new alias. + * @param alias the new alias + * @return a new {@link PemSslStoreDetails} instance + * @since 3.2.0 + */ + public PemSslStoreDetails withAlias(String alias) { + return new PemSslStoreDetails(this.type, alias, this.password, this.certificate, this.privateKey, + this.privateKeyPassword); + } + + /** + * Return a new {@link PemSslStoreDetails} instance with a new password. + * @param password the new password + * @return a new {@link PemSslStoreDetails} instance + * @since 3.2.0 + */ + public PemSslStoreDetails withPassword(String password) { + return new PemSslStoreDetails(this.type, this.alias, password, this.certificate, this.privateKey, + this.privateKeyPassword); + } + /** * Return a new {@link PemSslStoreDetails} instance with a new private key. * @param privateKey the new private key * @return a new {@link PemSslStoreDetails} instance */ public PemSslStoreDetails withPrivateKey(String privateKey) { - return new PemSslStoreDetails(this.type, this.certificate, privateKey, this.privateKeyPassword); + return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificate, privateKey, + this.privateKeyPassword); } /** * Return a new {@link PemSslStoreDetails} instance with a new private key password. - * @param password the new private key password + * @param privateKeyPassword the new private key password * @return a new {@link PemSslStoreDetails} instance */ - public PemSslStoreDetails withPrivateKeyPassword(String password) { - return new PemSslStoreDetails(this.type, this.certificate, this.privateKey, password); + public PemSslStoreDetails withPrivateKeyPassword(String privateKeyPassword) { + return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificate, this.privateKey, + privateKeyPassword); } boolean isEmpty() { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java index e722830aa49..c9f989bfae0 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java @@ -62,10 +62,12 @@ public final class WebServerSslBundle implements SslBundle { private static SslStoreBundle createPemStoreBundle(Ssl ssl) { PemSslStoreDetails keyStoreDetails = new PemSslStoreDetails(ssl.getKeyStoreType(), ssl.getCertificate(), - ssl.getCertificatePrivateKey()); + ssl.getCertificatePrivateKey()) + .withAlias(ssl.getKeyAlias()); PemSslStoreDetails trustStoreDetails = new PemSslStoreDetails(ssl.getTrustStoreType(), - ssl.getTrustCertificate(), ssl.getTrustCertificatePrivateKey()); - return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, ssl.getKeyAlias()); + ssl.getTrustCertificate(), ssl.getTrustCertificatePrivateKey()) + .withAlias(ssl.getKeyAlias()); + return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails); } private static SslStoreBundle createJksStoreBundle(Ssl ssl) { 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 84c0f408cd6..6414382683b 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 @@ -166,6 +166,7 @@ class PemSslStoreBundleTests { } @Test + @SuppressWarnings("removal") void whenHasKeyStoreDetailsAndTrustStoreDetailsAndAlias() { PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem") .withPrivateKey("classpath:test-key.pem"); @@ -190,21 +191,26 @@ class PemSslStoreBundleTests { @Test void whenHasKeyStoreDetailsAndTrustStoreDetailsAndKeyPassword() { PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem") - .withPrivateKey("classpath:test-key.pem"); + .withPrivateKey("classpath:test-key.pem") + .withAlias("ksa") + .withPassword("kss"); PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem") - .withPrivateKey("classpath:test-key.pem"); - PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, "test-alias", "keysecret"); - assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("test-alias", "keysecret".toCharArray())); - assertThat(bundle.getTrustStore()) - .satisfies(storeContainingCertAndKey("test-alias", "keysecret".toCharArray())); + .withPrivateKey("classpath:test-key.pem") + .withAlias("tsa") + .withPassword("tss"); + PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails); + assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("ksa", "kss".toCharArray())); + assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("tsa", "tss".toCharArray())); } @Test void shouldVerifyKeysIfEnabled() { PemSslStoreDetails keyStoreDetails = PemSslStoreDetails .forCertificate("classpath:org/springframework/boot/ssl/pem/key1.crt") - .withPrivateKey("classpath:org/springframework/boot/ssl/pem/key1.pem"); - PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, null, "test-alias", "keysecret", true); + .withPrivateKey("classpath:org/springframework/boot/ssl/pem/key1.pem") + .withAlias("test-alias") + .withPassword("keysecret"); + PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, null, true); assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("test-alias", "keysecret".toCharArray())); } @@ -212,8 +218,10 @@ class PemSslStoreBundleTests { void shouldVerifyKeysIfEnabledAndCertificateChainIsUsed() { PemSslStoreDetails keyStoreDetails = PemSslStoreDetails .forCertificate("classpath:org/springframework/boot/ssl/pem/key2-chain.crt") - .withPrivateKey("classpath:org/springframework/boot/ssl/pem/key2.pem"); - PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, null, "test-alias", "keysecret", true); + .withPrivateKey("classpath:org/springframework/boot/ssl/pem/key2.pem") + .withAlias("test-alias") + .withPassword("keysecret"); + PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, null, true); assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("test-alias", "keysecret".toCharArray())); } @@ -222,8 +230,7 @@ class PemSslStoreBundleTests { PemSslStoreDetails keyStoreDetails = PemSslStoreDetails .forCertificate("classpath:org/springframework/boot/ssl/pem/key2.crt") .withPrivateKey("classpath:org/springframework/boot/ssl/pem/key1.pem"); - assertThatIllegalStateException() - .isThrownBy(() -> new PemSslStoreBundle(keyStoreDetails, null, null, null, true)) + assertThatIllegalStateException().isThrownBy(() -> new PemSslStoreBundle(keyStoreDetails, null, true)) .withMessageContaining("Private key matches none of the certificates"); }