Move PEM verification to spring-boot-autoconfigure

Move `KeyVerifier` to spring-boot-autoconfigure to reduce the
public API required in `PemSslStoreBundle`.

This commit also moves the verify property so that is can be set
per store.

Closes gh-38173
This commit is contained in:
Phillip Webb 2023-10-27 21:07:24 -07:00
parent 5e5d2265f5
commit 1b61bc1f20
12 changed files with 113 additions and 98 deletions

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.ssl.pem;
package org.springframework.boot.autoconfigure.ssl;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;

View File

@ -39,11 +39,6 @@ public class PemSslBundleProperties extends SslBundleProperties {
*/
private final Store truststore = new Store();
/**
* Whether to verify that the private key matches the public key.
*/
private boolean verifyKeys;
public Store getKeystore() {
return this.keystore;
}
@ -52,14 +47,6 @@ public class PemSslBundleProperties extends SslBundleProperties {
return this.truststore;
}
public boolean isVerifyKeys() {
return this.verifyKeys;
}
public void setVerifyKeys(boolean verifyKeys) {
this.verifyKeys = verifyKeys;
}
/**
* Store properties.
*/
@ -85,6 +72,11 @@ public class PemSslBundleProperties extends SslBundleProperties {
*/
private String privateKeyPassword;
/**
* Whether to verify that the private key matches the public key.
*/
private boolean verifyKeys;
public String getType() {
return this.type;
}
@ -117,6 +109,14 @@ public class PemSslBundleProperties extends SslBundleProperties {
this.privateKeyPassword = privateKeyPassword;
}
public boolean isVerifyKeys() {
return this.verifyKeys;
}
public void setVerifyKeys(boolean verifyKeys) {
this.verifyKeys = verifyKeys;
}
}
}

View File

@ -16,6 +16,10 @@
package org.springframework.boot.autoconfigure.ssl;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.cert.X509Certificate;
import org.springframework.boot.autoconfigure.ssl.SslBundleProperties.Key;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundleKey;
@ -24,6 +28,7 @@ import org.springframework.boot.ssl.SslOptions;
import org.springframework.boot.ssl.SslStoreBundle;
import org.springframework.boot.ssl.jks.JksSslStoreBundle;
import org.springframework.boot.ssl.jks.JksSslStoreDetails;
import org.springframework.boot.ssl.pem.PemSslStore;
import org.springframework.boot.ssl.pem.PemSslStoreBundle;
import org.springframework.boot.ssl.pem.PemSslStoreDetails;
@ -107,16 +112,39 @@ public final class PropertiesSslBundle implements SslBundle {
}
private static SslStoreBundle asSslStoreBundle(PemSslBundleProperties properties) {
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());
PemSslStore keyStore = asPemSslStore(properties.getKeystore(), properties.getKey().getAlias());
PemSslStore trustStore = asPemSslStore(properties.getTruststore(), properties.getKey().getAlias());
return new PemSslStoreBundle(keyStore, trustStore);
}
private static PemSslStoreDetails asStoreDetails(PemSslBundleProperties.Store properties) {
return new PemSslStoreDetails(properties.getType(), properties.getCertificate(), properties.getPrivateKey(),
properties.getPrivateKeyPassword());
private static PemSslStore asPemSslStore(PemSslBundleProperties.Store properties, String alias) {
try {
PemSslStoreDetails details = asStoreDetails(properties, alias);
PemSslStore pemSslStore = PemSslStore.load(details);
if (properties.isVerifyKeys()) {
verifyPemSslStoreKeys(pemSslStore);
}
return pemSslStore;
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
private static void verifyPemSslStoreKeys(PemSslStore pemSslStore) {
KeyVerifier keyVerifier = new KeyVerifier();
for (X509Certificate certificate : pemSslStore.certificates()) {
KeyVerifier.Result result = keyVerifier.matches(pemSslStore.privateKey(), certificate.getPublicKey());
if (result == KeyVerifier.Result.YES) {
return;
}
}
throw new IllegalStateException("Private key matches none of the certificates in the chain");
}
private static PemSslStoreDetails asStoreDetails(PemSslBundleProperties.Store properties, String alias) {
return new PemSslStoreDetails(properties.getType(), alias, null, properties.getCertificate(),
properties.getPrivateKey(), properties.getPrivateKeyPassword());
}
private static SslStoreBundle asSslStoreBundle(JksSslBundleProperties properties) {

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.ssl.pem;
package org.springframework.boot.autoconfigure.ssl;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
@ -33,7 +33,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.boot.ssl.pem.KeyVerifier.Result;
import org.springframework.boot.autoconfigure.ssl.KeyVerifier.Result;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -20,12 +20,15 @@ import java.security.Key;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.util.Set;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.util.function.ThrowingConsumer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link PropertiesSslBundle}.
@ -35,6 +38,8 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
class PropertiesSslBundleTests {
private static final char[] EMPTY_KEY_PASSWORD = new char[] {};
@Test
void pemPropertiesAreMappedToSslBundle() throws Exception {
PemSslBundleProperties properties = new PemSslBundleProperties();
@ -99,4 +104,47 @@ class PropertiesSslBundleTests {
assertThat(trustStore.getProvider().getName()).isEqualTo("SUN");
}
@Test
void getWithPemSslBundlePropertiesWhenVerifyKeyStoreAgainstSingleCertificateWithMatchCreatesBundle() {
PemSslBundleProperties properties = new PemSslBundleProperties();
properties.getKeystore().setCertificate("classpath:org/springframework/boot/autoconfigure/ssl/key1.crt");
properties.getKeystore().setPrivateKey("classpath:org/springframework/boot/autoconfigure/ssl/key1.pem");
properties.getKeystore().setVerifyKeys(true);
properties.getKey().setAlias("test-alias");
SslBundle bundle = PropertiesSslBundle.get(properties);
assertThat(bundle.getStores().getKeyStore()).satisfies(storeContainingCertAndKey("test-alias"));
}
@Test
void getWithPemSslBundlePropertiesWhenVerifyKeyStoreAgainstCertificateChainWithMatchCreatesBundle() {
PemSslBundleProperties properties = new PemSslBundleProperties();
properties.getKeystore().setCertificate("classpath:org/springframework/boot/autoconfigure/ssl/key2-chain.crt");
properties.getKeystore().setPrivateKey("classpath:org/springframework/boot/autoconfigure/ssl/key2.pem");
properties.getKeystore().setVerifyKeys(true);
properties.getKey().setAlias("test-alias");
SslBundle bundle = PropertiesSslBundle.get(properties);
assertThat(bundle.getStores().getKeyStore()).satisfies(storeContainingCertAndKey("test-alias"));
}
@Test
void getWithPemSslBundlePropertiesWhenVerifyKeyStoreWithNoMatchThrowsException() {
PemSslBundleProperties properties = new PemSslBundleProperties();
properties.getKeystore().setCertificate("classpath:org/springframework/boot/autoconfigure/ssl/key2.crt");
properties.getKeystore().setPrivateKey("classpath:org/springframework/boot/autoconfigure/ssl/key1.pem");
properties.getKeystore().setVerifyKeys(true);
properties.getKey().setAlias("test-alias");
assertThatIllegalStateException().isThrownBy(() -> PropertiesSslBundle.get(properties))
.withMessageContaining("Private key matches none of the certificates");
}
private Consumer<KeyStore> storeContainingCertAndKey(String keyAlias) {
return ThrowingConsumer.of((keyStore) -> {
assertThat(keyStore).isNotNull();
assertThat(keyStore.getType()).isEqualTo(KeyStore.getDefaultType());
assertThat(keyStore.containsAlias(keyAlias)).isTrue();
assertThat(keyStore.getCertificate(keyAlias)).isNotNull();
assertThat(keyStore.getKey(keyAlias, EMPTY_KEY_PASSWORD)).isNotNull();
});
}
}

View File

@ -51,8 +51,9 @@ public class PemSslStoreBundle implements SslStoreBundle {
* @param keyStoreDetails the key store details
* @param trustStoreDetails the trust store details
*/
@SuppressWarnings("removal")
public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails) {
this(keyStoreDetails, trustStoreDetails, null, false);
this(keyStoreDetails, trustStoreDetails, null);
}
/**
@ -66,26 +67,9 @@ public class PemSslStoreBundle implements SslStoreBundle {
*/
@Deprecated(since = "3.2.0", forRemoval = true)
public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String alias) {
this(keyStoreDetails, trustStoreDetails, alias, false);
}
/**
* Create a new {@link PemSslStoreBundle} instance.
* @param keyStoreDetails the key store details
* @param trustStoreDetails the trust store details
* @param verifyKeys whether to verify that the private key matches the public key
* @since 3.2.0
*/
public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails,
boolean verifyKeys) {
this(keyStoreDetails, trustStoreDetails, null, verifyKeys);
}
private PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String alias,
boolean verifyKeys) {
try {
this.keyStore = createKeyStore("key", PemSslStore.load(keyStoreDetails), alias, verifyKeys);
this.trustStore = createKeyStore("trust", PemSslStore.load(trustStoreDetails), alias, verifyKeys);
this.keyStore = createKeyStore("key", PemSslStore.load(keyStoreDetails), alias);
this.trustStore = createKeyStore("trust", PemSslStore.load(trustStoreDetails), alias);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
@ -96,13 +80,15 @@ public class PemSslStoreBundle implements SslStoreBundle {
* 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);
public PemSslStoreBundle(PemSslStore pemKeyStore, PemSslStore pemTrustStore) {
this(pemKeyStore, pemTrustStore, null);
}
private PemSslStoreBundle(PemSslStore pemKeyStore, PemSslStore pemTrustStore, String alias) {
this.keyStore = createKeyStore("key", pemKeyStore, alias);
this.trustStore = createKeyStore("trust", pemTrustStore, alias);
}
@Override
@ -120,7 +106,7 @@ public class PemSslStoreBundle implements SslStoreBundle {
return this.trustStore;
}
private static KeyStore createKeyStore(String name, PemSslStore pemSslStore, String alias, boolean verifyKeys) {
private static KeyStore createKeyStore(String name, PemSslStore pemSslStore, String alias) {
if (pemSslStore == null) {
return null;
}
@ -132,9 +118,6 @@ public class PemSslStoreBundle implements SslStoreBundle {
List<X509Certificate> certificates = pemSslStore.certificates();
PrivateKey privateKey = pemSslStore.privateKey();
if (privateKey != null) {
if (verifyKeys) {
verifyKeys(privateKey, certificates);
}
addPrivateKey(store, privateKey, alias, pemSslStore.password(), certificates);
}
else {
@ -154,18 +137,6 @@ public class PemSslStoreBundle implements SslStoreBundle {
return store;
}
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 addPrivateKey(KeyStore keyStore, PrivateKey privateKey, String alias, String keyPassword,
List<X509Certificate> certificateChain) throws KeyStoreException {
keyStore.setKeyEntry(alias, privateKey, (keyPassword != null) ? keyPassword.toCharArray() : null,

View File

@ -27,7 +27,6 @@ import org.junit.jupiter.api.Test;
import org.springframework.util.function.ThrowingConsumer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link PemSslStoreBundle}.
@ -206,43 +205,12 @@ class PemSslStoreBundleTests {
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")
.withAlias("test-alias")
.withPassword("keysecret");
PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, null, true);
assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("test-alias", "keysecret".toCharArray()));
}
@Test
void shouldVerifyKeysIfEnabledAndCertificateChainIsUsed() {
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails
.forCertificates("classpath:org/springframework/boot/ssl/pem/key2-chain.crt")
.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()));
}
@Test
void shouldFailIfVerifyKeysIsEnabledAndKeysDontMatch() {
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, true))
.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);
PemSslStoreBundle bundle = new PemSslStoreBundle(pemSslStore, pemSslStore);
assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("ssl"));
assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("ssl"));
}