Allow alias and password to be configured on a per PEM store basis

Closes gh-38124
This commit is contained in:
Phillip Webb 2023-10-30 16:04:58 -07:00
parent 8bf847e549
commit 2c6fca8df7
5 changed files with 122 additions and 50 deletions

View File

@ -107,10 +107,11 @@ public final class PropertiesSslBundle implements SslBundle {
} }
private static SslStoreBundle asSslStoreBundle(PemSslBundleProperties properties) { private static SslStoreBundle asSslStoreBundle(PemSslBundleProperties properties) {
PemSslStoreDetails keyStoreDetails = asStoreDetails(properties.getKeystore()); PemSslStoreDetails keyStoreDetails = asStoreDetails(properties.getKeystore())
PemSslStoreDetails trustStoreDetails = asStoreDetails(properties.getTruststore()); .withAlias(properties.getKey().getAlias());
return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, properties.getKey().getAlias(), null, PemSslStoreDetails trustStoreDetails = asStoreDetails(properties.getTruststore())
properties.isVerifyKeys()); .withAlias(properties.getKey().getAlias());
return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, properties.isVerifyKeys());
} }
private static PemSslStoreDetails asStoreDetails(PemSslBundleProperties.Store properties) { private static PemSslStoreDetails asStoreDetails(PemSslBundleProperties.Store properties) {

View File

@ -26,7 +26,6 @@ import java.security.cert.X509Certificate;
import java.util.List; import java.util.List;
import org.springframework.boot.ssl.SslStoreBundle; import org.springframework.boot.ssl.SslStoreBundle;
import org.springframework.boot.ssl.pem.KeyVerifier.Result;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@ -61,39 +60,31 @@ public class PemSslStoreBundle implements SslStoreBundle {
* @param keyStoreDetails the key store details * @param keyStoreDetails the key store details
* @param trustStoreDetails the trust store details * @param trustStoreDetails the trust store details
* @param alias the alias to use or {@code null} to use a default alias * @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) { public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String alias) {
this(keyStoreDetails, trustStoreDetails, alias, null); this(keyStoreDetails, trustStoreDetails, alias, false);
} }
/** /**
* Create a new {@link PemSslStoreBundle} instance. * Create a new {@link PemSslStoreBundle} instance.
* @param keyStoreDetails the key store details * @param keyStoreDetails the key store details
* @param trustStoreDetails the trust 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 * @param verifyKeys whether to verify that the private key matches the public key
* @since 3.2.0 * @since 3.2.0
*/ */
public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String alias, public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails,
String keyPassword, boolean verifyKeys) { boolean verifyKeys) {
this.keyStore = createKeyStore("key", keyStoreDetails, (alias != null) ? alias : DEFAULT_ALIAS, keyPassword, this(keyStoreDetails, trustStoreDetails, null, verifyKeys);
verifyKeys); }
this.trustStore = createKeyStore("trust", trustStoreDetails, (alias != null) ? alias : DEFAULT_ALIAS,
keyPassword, 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 @Override
@ -111,13 +102,14 @@ public class PemSslStoreBundle implements SslStoreBundle {
return this.trustStore; return this.trustStore;
} }
private static KeyStore createKeyStore(String name, PemSslStoreDetails details, String alias, String keyPassword, private static KeyStore createKeyStore(String name, PemSslStoreDetails details, String alias, boolean verifyKeys) {
boolean verifyKeys) {
if (details == null || details.isEmpty()) { if (details == null || details.isEmpty()) {
return null; return null;
} }
try { try {
Assert.notNull(details.certificate(), "Certificate content must not be null"); 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); KeyStore store = createKeyStore(details);
X509Certificate[] certificates = loadCertificates(details); X509Certificate[] certificates = loadCertificates(details);
PrivateKey privateKey = loadPrivateKey(details); PrivateKey privateKey = loadPrivateKey(details);
@ -125,7 +117,7 @@ public class PemSslStoreBundle implements SslStoreBundle {
if (verifyKeys) { if (verifyKeys) {
verifyKeys(privateKey, certificates); verifyKeys(privateKey, certificates);
} }
addPrivateKey(store, privateKey, alias, keyPassword, certificates); addPrivateKey(store, privateKey, alias, details.password(), certificates);
} }
else { else {
addCertificates(store, certificates, alias); addCertificates(store, certificates, alias);

View File

@ -26,6 +26,10 @@ import org.springframework.util.StringUtils;
* *
* @param type the key store type, for example {@code JKS} or {@code PKCS11}. A * @param type the key store type, for example {@code JKS} or {@code PKCS11}. A
* {@code null} value will use {@link KeyStore#getDefaultType()}). * {@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 * @param certificate the certificate content (either the PEM content itself or something
* that can be loaded by {@link ResourceUtils#getURL}) * that can be loaded by {@link ResourceUtils#getURL})
* @param privateKey the private key content (either the PEM content itself or something * @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 * @author Phillip Webb
* @since 3.1.0 * @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) { public PemSslStoreDetails(String type, String certificate, String privateKey) {
this(type, certificate, privateKey, null); 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. * Return a new {@link PemSslStoreDetails} instance with a new private key.
* @param privateKey the new private key * @param privateKey the new private key
* @return a new {@link PemSslStoreDetails} instance * @return a new {@link PemSslStoreDetails} instance
*/ */
public PemSslStoreDetails withPrivateKey(String privateKey) { 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. * 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 * @return a new {@link PemSslStoreDetails} instance
*/ */
public PemSslStoreDetails withPrivateKeyPassword(String password) { public PemSslStoreDetails withPrivateKeyPassword(String privateKeyPassword) {
return new PemSslStoreDetails(this.type, this.certificate, this.privateKey, password); return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificate, this.privateKey,
privateKeyPassword);
} }
boolean isEmpty() { boolean isEmpty() {

View File

@ -62,10 +62,12 @@ public final class WebServerSslBundle implements SslBundle {
private static SslStoreBundle createPemStoreBundle(Ssl ssl) { private static SslStoreBundle createPemStoreBundle(Ssl ssl) {
PemSslStoreDetails keyStoreDetails = new PemSslStoreDetails(ssl.getKeyStoreType(), ssl.getCertificate(), PemSslStoreDetails keyStoreDetails = new PemSslStoreDetails(ssl.getKeyStoreType(), ssl.getCertificate(),
ssl.getCertificatePrivateKey()); ssl.getCertificatePrivateKey())
.withAlias(ssl.getKeyAlias());
PemSslStoreDetails trustStoreDetails = new PemSslStoreDetails(ssl.getTrustStoreType(), PemSslStoreDetails trustStoreDetails = new PemSslStoreDetails(ssl.getTrustStoreType(),
ssl.getTrustCertificate(), ssl.getTrustCertificatePrivateKey()); ssl.getTrustCertificate(), ssl.getTrustCertificatePrivateKey())
return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, ssl.getKeyAlias()); .withAlias(ssl.getKeyAlias());
return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails);
} }
private static SslStoreBundle createJksStoreBundle(Ssl ssl) { private static SslStoreBundle createJksStoreBundle(Ssl ssl) {

View File

@ -166,6 +166,7 @@ class PemSslStoreBundleTests {
} }
@Test @Test
@SuppressWarnings("removal")
void whenHasKeyStoreDetailsAndTrustStoreDetailsAndAlias() { void whenHasKeyStoreDetailsAndTrustStoreDetailsAndAlias() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem") PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem")
.withPrivateKey("classpath:test-key.pem"); .withPrivateKey("classpath:test-key.pem");
@ -190,21 +191,26 @@ class PemSslStoreBundleTests {
@Test @Test
void whenHasKeyStoreDetailsAndTrustStoreDetailsAndKeyPassword() { void whenHasKeyStoreDetailsAndTrustStoreDetailsAndKeyPassword() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem") 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") PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem")
.withPrivateKey("classpath:test-key.pem"); .withPrivateKey("classpath:test-key.pem")
PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, "test-alias", "keysecret"); .withAlias("tsa")
assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("test-alias", "keysecret".toCharArray())); .withPassword("tss");
assertThat(bundle.getTrustStore()) PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails);
.satisfies(storeContainingCertAndKey("test-alias", "keysecret".toCharArray())); assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("ksa", "kss".toCharArray()));
assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("tsa", "tss".toCharArray()));
} }
@Test @Test
void shouldVerifyKeysIfEnabled() { void shouldVerifyKeysIfEnabled() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails PemSslStoreDetails keyStoreDetails = PemSslStoreDetails
.forCertificate("classpath:org/springframework/boot/ssl/pem/key1.crt") .forCertificate("classpath:org/springframework/boot/ssl/pem/key1.crt")
.withPrivateKey("classpath:org/springframework/boot/ssl/pem/key1.pem"); .withPrivateKey("classpath:org/springframework/boot/ssl/pem/key1.pem")
PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, null, "test-alias", "keysecret", true); .withAlias("test-alias")
.withPassword("keysecret");
PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, null, true);
assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("test-alias", "keysecret".toCharArray())); assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("test-alias", "keysecret".toCharArray()));
} }
@ -212,8 +218,10 @@ class PemSslStoreBundleTests {
void shouldVerifyKeysIfEnabledAndCertificateChainIsUsed() { void shouldVerifyKeysIfEnabledAndCertificateChainIsUsed() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails PemSslStoreDetails keyStoreDetails = PemSslStoreDetails
.forCertificate("classpath:org/springframework/boot/ssl/pem/key2-chain.crt") .forCertificate("classpath:org/springframework/boot/ssl/pem/key2-chain.crt")
.withPrivateKey("classpath:org/springframework/boot/ssl/pem/key2.pem"); .withPrivateKey("classpath:org/springframework/boot/ssl/pem/key2.pem")
PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, null, "test-alias", "keysecret", true); .withAlias("test-alias")
.withPassword("keysecret");
PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, null, true);
assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("test-alias", "keysecret".toCharArray())); assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("test-alias", "keysecret".toCharArray()));
} }
@ -222,8 +230,7 @@ class PemSslStoreBundleTests {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails PemSslStoreDetails keyStoreDetails = PemSslStoreDetails
.forCertificate("classpath:org/springframework/boot/ssl/pem/key2.crt") .forCertificate("classpath:org/springframework/boot/ssl/pem/key2.crt")
.withPrivateKey("classpath:org/springframework/boot/ssl/pem/key1.pem"); .withPrivateKey("classpath:org/springframework/boot/ssl/pem/key1.pem");
assertThatIllegalStateException() assertThatIllegalStateException().isThrownBy(() -> new PemSslStoreBundle(keyStoreDetails, null, true))
.isThrownBy(() -> new PemSslStoreBundle(keyStoreDetails, null, null, null, true))
.withMessageContaining("Private key matches none of the certificates"); .withMessageContaining("Private key matches none of the certificates");
} }