Skip to content

Commit

Permalink
Add property to enable key verification on PEM SSL bundles
Browse files Browse the repository at this point in the history
Closes gh-37727
  • Loading branch information
mhalbritter committed Oct 5, 2023
1 parent 85aeede commit 0a16ec1
Show file tree
Hide file tree
Showing 13 changed files with 494 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
*
* @author Scott Frederick
* @author Phillip Webb
* @author Moritz Halbritter
* @since 3.1.0
* @see PemSslStoreBundle
*/
Expand All @@ -38,6 +39,11 @@ 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;
}
Expand All @@ -46,6 +52,14 @@ public Store getTruststore() {
return this.truststore;
}

public boolean isVerifyKeys() {
return this.verifyKeys;
}

public void setVerifyKeys(boolean verifyKeys) {
this.verifyKeys = verifyKeys;
}

/**
* Store properties.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ public static SslBundle get(JksSslBundleProperties properties) {
private static SslStoreBundle asSslStoreBundle(PemSslBundleProperties properties) {
PemSslStoreDetails keyStoreDetails = asStoreDetails(properties.getKeystore());
PemSslStoreDetails trustStoreDetails = asStoreDetails(properties.getTruststore());
return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, properties.getKey().getAlias());
return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, properties.getKey().getAlias(), null,
properties.isVerifyKeys());
}

private static PemSslStoreDetails asStoreDetails(PemSslBundleProperties.Store properties) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.boot.ssl.pem;

import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;

/**
* Performs checks on keys, e.g., if a public key and a private key belong together.
*
* @author Moritz Halbritter
*/
class KeyVerifier {

private static final byte[] DATA = "Just some piece of data which gets signed".getBytes(StandardCharsets.UTF_8);

/**
* Checks if the given private key belongs to the given public key.
* @param privateKey the private key
* @param publicKey the public key
* @return whether the keys belong together
*/
Result matches(PrivateKey privateKey, PublicKey publicKey) {
try {
if (!privateKey.getAlgorithm().equals(publicKey.getAlgorithm())) {
// Keys are of different type
return Result.NO;
}
String algorithm = getSignatureAlgorithm(privateKey.getAlgorithm());
if (algorithm == null) {
return Result.UNKNOWN;
}
byte[] signature = createSignature(privateKey, algorithm);
return verifySignature(publicKey, algorithm, signature);
}
catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException ex) {
return Result.UNKNOWN;
}
}

private static byte[] createSignature(PrivateKey privateKey, String algorithm)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
Signature signer = Signature.getInstance(algorithm);
signer.initSign(privateKey);
signer.update(DATA);
return signer.sign();
}

private static Result verifySignature(PublicKey publicKey, String algorithm, byte[] signature)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
Signature verifier = Signature.getInstance(algorithm);
verifier.initVerify(publicKey);
verifier.update(DATA);
try {
if (verifier.verify(signature)) {
return Result.YES;
}
else {
return Result.NO;
}
}
catch (SignatureException ex) {
return Result.NO;
}
}

private static String getSignatureAlgorithm(String keyAlgorithm) {
// https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#signature-algorithms
// https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#keypairgenerator-algorithms
return switch (keyAlgorithm) {
case "RSA" -> "SHA256withRSA";
case "DSA" -> "SHA256withDSA";
case "EC" -> "SHA256withECDSA";
case "EdDSA" -> "EdDSA";
default -> null;
};
}

enum Result {

YES, NO, UNKNOWN

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@

package org.springframework.boot.ssl.pem;

import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import org.springframework.boot.ssl.SslStoreBundle;
import org.springframework.boot.ssl.pem.KeyVerifier.Result;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

Expand Down Expand Up @@ -71,8 +75,24 @@ public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails
*/
public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String keyAlias,
String keyPassword) {
this.keyStore = createKeyStore("key", keyStoreDetails, keyAlias, keyPassword);
this.trustStore = createKeyStore("trust", trustStoreDetails, keyAlias, keyPassword);
this(keyStoreDetails, trustStoreDetails, keyAlias, keyPassword, false);
}

/**
* Create a new {@link PemSslStoreBundle} instance.
* @param keyStoreDetails the key store details
* @param trustStoreDetails the trust store details
* @param keyAlias the key alias to use or {@code null} to use a default alias
* @param keyPassword the password to use for the key
* @param verifyKeys whether to verify that the private key matches the public key
* @since 3.2.0
*/
public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String keyAlias,
String keyPassword, boolean verifyKeys) {
this.keyStore = createKeyStore("key", keyStoreDetails, (keyAlias != null) ? keyAlias : DEFAULT_KEY_ALIAS,
keyPassword, verifyKeys);
this.trustStore = createKeyStore("trust", trustStoreDetails, (keyAlias != null) ? keyAlias : DEFAULT_KEY_ALIAS,
keyPassword, verifyKeys);
}

@Override
Expand All @@ -90,38 +110,74 @@ public KeyStore getTrustStore() {
return this.trustStore;
}

private KeyStore createKeyStore(String name, PemSslStoreDetails details, String alias, String keyPassword) {
private static KeyStore createKeyStore(String name, PemSslStoreDetails details, String keyAlias, String keyPassword,
boolean verifyKeys) {
if (details == null || details.isEmpty()) {
return null;
}
try {
Assert.notNull(details.certificate(), "Certificate content must not be null");
String type = (!StringUtils.hasText(details.type())) ? KeyStore.getDefaultType() : details.type();
KeyStore store = KeyStore.getInstance(type);
store.load(null);
String certificateContent = PemContent.load(details.certificate());
String privateKeyContent = PemContent.load(details.privateKey());
X509Certificate[] certificates = PemCertificateParser.parse(certificateContent);
PrivateKey privateKey = PemPrivateKeyParser.parse(privateKeyContent, details.privateKeyPassword());
addCertificates(store, certificates, privateKey, (alias != null) ? alias : DEFAULT_KEY_ALIAS, keyPassword);
KeyStore store = createKeyStore(details);
X509Certificate[] certificates = loadCertificates(details);
PrivateKey privateKey = loadPrivateKey(details);
if (privateKey != null) {
if (verifyKeys) {
verifyKeys(privateKey, certificates);
}
addPrivateKey(store, privateKey, keyAlias, keyPassword, certificates);
}
else {
addCertificates(store, certificates, keyAlias);
}
return store;
}
catch (Exception ex) {
throw new IllegalStateException("Unable to create %s store: %s".formatted(name, ex.getMessage()), ex);
}
}

private void addCertificates(KeyStore keyStore, X509Certificate[] certificates, PrivateKey privateKey, String alias,
String keyPassword) throws KeyStoreException {
if (privateKey != null) {
keyStore.setKeyEntry(alias, privateKey, (keyPassword != null) ? keyPassword.toCharArray() : null,
certificates);
}
else {
for (int index = 0; index < certificates.length; index++) {
keyStore.setCertificateEntry(alias + "-" + index, certificates[index]);
private static void verifyKeys(PrivateKey privateKey, X509Certificate[] certificates) {
KeyVerifier keyVerifier = new KeyVerifier();
// Key should match one of the certificates
for (X509Certificate certificate : certificates) {
Result result = keyVerifier.matches(privateKey, certificate.getPublicKey());
if (result == Result.YES) {
return;
}
}
throw new IllegalStateException("Private key matches none of the certificates");
}

private static PrivateKey loadPrivateKey(PemSslStoreDetails details) {
String privateKeyContent = PemContent.load(details.privateKey());
return PemPrivateKeyParser.parse(privateKeyContent, details.privateKeyPassword());
}

private static X509Certificate[] loadCertificates(PemSslStoreDetails details) {
String certificateContent = PemContent.load(details.certificate());
X509Certificate[] certificates = PemCertificateParser.parse(certificateContent);
Assert.state(certificates != null && certificates.length > 0, "Loaded certificates are empty");
return certificates;
}

private static KeyStore createKeyStore(PemSslStoreDetails details)
throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
String type = StringUtils.hasText(details.type()) ? details.type() : KeyStore.getDefaultType();
KeyStore store = KeyStore.getInstance(type);
store.load(null);
return store;
}

private static void addPrivateKey(KeyStore keyStore, PrivateKey privateKey, String alias, String keyPassword,
X509Certificate[] certificates) throws KeyStoreException {
keyStore.setKeyEntry(alias, privateKey, (keyPassword != null) ? keyPassword.toCharArray() : null, certificates);
}

private static void addCertificates(KeyStore keyStore, X509Certificate[] certificates, String alias)
throws KeyStoreException {
for (int index = 0; index < certificates.length; index++) {
keyStore.setCertificateEntry(alias + "-" + index, certificates[index]);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.boot.ssl.pem;

import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.ECGenParameterSpec;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Stream;

import org.junit.jupiter.api.Named;
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 static org.assertj.core.api.Assertions.assertThat;

/**
* Tests for {@link KeyVerifier}.
*
* @author Moritz Halbritter
*/
class KeyVerifierTests {

private static final List<Algorithm> ALGORITHMS = List.of(Algorithm.of("RSA"), Algorithm.of("DSA"),
Algorithm.of("ed25519"), Algorithm.of("ed448"), Algorithm.ec("secp256r1"), Algorithm.ec("secp521r1"));

private final KeyVerifier keyVerifier = new KeyVerifier();

@ParameterizedTest(name = "{0}")
@MethodSource("arguments")
void test(PrivateKey privateKey, PublicKey publicKey, List<PublicKey> invalidPublicKeys) {
assertThat(this.keyVerifier.matches(privateKey, publicKey)).isEqualTo(Result.YES);
for (PublicKey invalidPublicKey : invalidPublicKeys) {
assertThat(this.keyVerifier.matches(privateKey, invalidPublicKey)).isEqualTo(Result.NO);
}
}

static Stream<Arguments> arguments() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
List<KeyPair> keyPairs = new LinkedList<>();
for (Algorithm algorithm : ALGORITHMS) {
KeyPairGenerator generator = KeyPairGenerator.getInstance(algorithm.name());
if (algorithm.spec() != null) {
generator.initialize(algorithm.spec());
}
keyPairs.add(generator.generateKeyPair());
keyPairs.add(generator.generateKeyPair());
}
return keyPairs.stream()
.map((kp) -> Arguments.arguments(Named.named(kp.getPrivate().getAlgorithm(), kp.getPrivate()),
kp.getPublic(), without(keyPairs, kp).map(KeyPair::getPublic).toList()));
}

private static Stream<KeyPair> without(List<KeyPair> keyPairs, KeyPair without) {
return keyPairs.stream().filter((kp) -> !kp.equals(without));
}

private record Algorithm(String name, AlgorithmParameterSpec spec) {
static Algorithm of(String name) {
return new Algorithm(name, null);
}

static Algorithm ec(String curve) {
return new Algorithm("EC", new ECGenParameterSpec(curve));
}
}

}
Loading

0 comments on commit 0a16ec1

Please sign in to comment.