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) {
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) {

View File

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

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
* {@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() {

View File

@ -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) {

View File

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