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:
parent
5e5d2265f5
commit
1b61bc1f20
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue