Skip to content

Commit 0a16ec1

Browse files
committed
Add property to enable key verification on PEM SSL bundles
Closes gh-37727
1 parent 85aeede commit 0a16ec1

File tree

13 files changed

+494
-21
lines changed

13 files changed

+494
-21
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemSslBundleProperties.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
*
2424
* @author Scott Frederick
2525
* @author Phillip Webb
26+
* @author Moritz Halbritter
2627
* @since 3.1.0
2728
* @see PemSslStoreBundle
2829
*/
@@ -38,6 +39,11 @@ public class PemSslBundleProperties extends SslBundleProperties {
3839
*/
3940
private final Store truststore = new Store();
4041

42+
/**
43+
* Whether to verify that the private key matches the public key.
44+
*/
45+
private boolean verifyKeys;
46+
4147
public Store getKeystore() {
4248
return this.keystore;
4349
}
@@ -46,6 +52,14 @@ public Store getTruststore() {
4652
return this.truststore;
4753
}
4854

55+
public boolean isVerifyKeys() {
56+
return this.verifyKeys;
57+
}
58+
59+
public void setVerifyKeys(boolean verifyKeys) {
60+
this.verifyKeys = verifyKeys;
61+
}
62+
4963
/**
5064
* Store properties.
5165
*/

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ public static SslBundle get(JksSslBundleProperties properties) {
109109
private static SslStoreBundle asSslStoreBundle(PemSslBundleProperties properties) {
110110
PemSslStoreDetails keyStoreDetails = asStoreDetails(properties.getKeystore());
111111
PemSslStoreDetails trustStoreDetails = asStoreDetails(properties.getTruststore());
112-
return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, properties.getKey().getAlias());
112+
return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, properties.getKey().getAlias(), null,
113+
properties.isVerifyKeys());
113114
}
114115

115116
private static PemSslStoreDetails asStoreDetails(PemSslBundleProperties.Store properties) {
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.ssl.pem;
18+
19+
import java.nio.charset.StandardCharsets;
20+
import java.security.InvalidKeyException;
21+
import java.security.NoSuchAlgorithmException;
22+
import java.security.PrivateKey;
23+
import java.security.PublicKey;
24+
import java.security.Signature;
25+
import java.security.SignatureException;
26+
27+
/**
28+
* Performs checks on keys, e.g., if a public key and a private key belong together.
29+
*
30+
* @author Moritz Halbritter
31+
*/
32+
class KeyVerifier {
33+
34+
private static final byte[] DATA = "Just some piece of data which gets signed".getBytes(StandardCharsets.UTF_8);
35+
36+
/**
37+
* Checks if the given private key belongs to the given public key.
38+
* @param privateKey the private key
39+
* @param publicKey the public key
40+
* @return whether the keys belong together
41+
*/
42+
Result matches(PrivateKey privateKey, PublicKey publicKey) {
43+
try {
44+
if (!privateKey.getAlgorithm().equals(publicKey.getAlgorithm())) {
45+
// Keys are of different type
46+
return Result.NO;
47+
}
48+
String algorithm = getSignatureAlgorithm(privateKey.getAlgorithm());
49+
if (algorithm == null) {
50+
return Result.UNKNOWN;
51+
}
52+
byte[] signature = createSignature(privateKey, algorithm);
53+
return verifySignature(publicKey, algorithm, signature);
54+
}
55+
catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException ex) {
56+
return Result.UNKNOWN;
57+
}
58+
}
59+
60+
private static byte[] createSignature(PrivateKey privateKey, String algorithm)
61+
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
62+
Signature signer = Signature.getInstance(algorithm);
63+
signer.initSign(privateKey);
64+
signer.update(DATA);
65+
return signer.sign();
66+
}
67+
68+
private static Result verifySignature(PublicKey publicKey, String algorithm, byte[] signature)
69+
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
70+
Signature verifier = Signature.getInstance(algorithm);
71+
verifier.initVerify(publicKey);
72+
verifier.update(DATA);
73+
try {
74+
if (verifier.verify(signature)) {
75+
return Result.YES;
76+
}
77+
else {
78+
return Result.NO;
79+
}
80+
}
81+
catch (SignatureException ex) {
82+
return Result.NO;
83+
}
84+
}
85+
86+
private static String getSignatureAlgorithm(String keyAlgorithm) {
87+
// https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#signature-algorithms
88+
// https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#keypairgenerator-algorithms
89+
return switch (keyAlgorithm) {
90+
case "RSA" -> "SHA256withRSA";
91+
case "DSA" -> "SHA256withDSA";
92+
case "EC" -> "SHA256withECDSA";
93+
case "EdDSA" -> "EdDSA";
94+
default -> null;
95+
};
96+
}
97+
98+
enum Result {
99+
100+
YES, NO, UNKNOWN
101+
102+
}
103+
104+
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java

Lines changed: 76 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@
1616

1717
package org.springframework.boot.ssl.pem;
1818

19+
import java.io.IOException;
1920
import java.security.KeyStore;
2021
import java.security.KeyStoreException;
22+
import java.security.NoSuchAlgorithmException;
2123
import java.security.PrivateKey;
24+
import java.security.cert.CertificateException;
2225
import java.security.cert.X509Certificate;
2326

2427
import org.springframework.boot.ssl.SslStoreBundle;
28+
import org.springframework.boot.ssl.pem.KeyVerifier.Result;
2529
import org.springframework.util.Assert;
2630
import org.springframework.util.StringUtils;
2731

@@ -71,8 +75,24 @@ public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails
7175
*/
7276
public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String keyAlias,
7377
String keyPassword) {
74-
this.keyStore = createKeyStore("key", keyStoreDetails, keyAlias, keyPassword);
75-
this.trustStore = createKeyStore("trust", trustStoreDetails, keyAlias, keyPassword);
78+
this(keyStoreDetails, trustStoreDetails, keyAlias, keyPassword, false);
79+
}
80+
81+
/**
82+
* Create a new {@link PemSslStoreBundle} instance.
83+
* @param keyStoreDetails the key store details
84+
* @param trustStoreDetails the trust store details
85+
* @param keyAlias the key alias to use or {@code null} to use a default alias
86+
* @param keyPassword the password to use for the key
87+
* @param verifyKeys whether to verify that the private key matches the public key
88+
* @since 3.2.0
89+
*/
90+
public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String keyAlias,
91+
String keyPassword, boolean verifyKeys) {
92+
this.keyStore = createKeyStore("key", keyStoreDetails, (keyAlias != null) ? keyAlias : DEFAULT_KEY_ALIAS,
93+
keyPassword, verifyKeys);
94+
this.trustStore = createKeyStore("trust", trustStoreDetails, (keyAlias != null) ? keyAlias : DEFAULT_KEY_ALIAS,
95+
keyPassword, verifyKeys);
7696
}
7797

7898
@Override
@@ -90,38 +110,74 @@ public KeyStore getTrustStore() {
90110
return this.trustStore;
91111
}
92112

93-
private KeyStore createKeyStore(String name, PemSslStoreDetails details, String alias, String keyPassword) {
113+
private static KeyStore createKeyStore(String name, PemSslStoreDetails details, String keyAlias, String keyPassword,
114+
boolean verifyKeys) {
94115
if (details == null || details.isEmpty()) {
95116
return null;
96117
}
97118
try {
98119
Assert.notNull(details.certificate(), "Certificate content must not be null");
99-
String type = (!StringUtils.hasText(details.type())) ? KeyStore.getDefaultType() : details.type();
100-
KeyStore store = KeyStore.getInstance(type);
101-
store.load(null);
102-
String certificateContent = PemContent.load(details.certificate());
103-
String privateKeyContent = PemContent.load(details.privateKey());
104-
X509Certificate[] certificates = PemCertificateParser.parse(certificateContent);
105-
PrivateKey privateKey = PemPrivateKeyParser.parse(privateKeyContent, details.privateKeyPassword());
106-
addCertificates(store, certificates, privateKey, (alias != null) ? alias : DEFAULT_KEY_ALIAS, keyPassword);
120+
KeyStore store = createKeyStore(details);
121+
X509Certificate[] certificates = loadCertificates(details);
122+
PrivateKey privateKey = loadPrivateKey(details);
123+
if (privateKey != null) {
124+
if (verifyKeys) {
125+
verifyKeys(privateKey, certificates);
126+
}
127+
addPrivateKey(store, privateKey, keyAlias, keyPassword, certificates);
128+
}
129+
else {
130+
addCertificates(store, certificates, keyAlias);
131+
}
107132
return store;
108133
}
109134
catch (Exception ex) {
110135
throw new IllegalStateException("Unable to create %s store: %s".formatted(name, ex.getMessage()), ex);
111136
}
112137
}
113138

114-
private void addCertificates(KeyStore keyStore, X509Certificate[] certificates, PrivateKey privateKey, String alias,
115-
String keyPassword) throws KeyStoreException {
116-
if (privateKey != null) {
117-
keyStore.setKeyEntry(alias, privateKey, (keyPassword != null) ? keyPassword.toCharArray() : null,
118-
certificates);
119-
}
120-
else {
121-
for (int index = 0; index < certificates.length; index++) {
122-
keyStore.setCertificateEntry(alias + "-" + index, certificates[index]);
139+
private static void verifyKeys(PrivateKey privateKey, X509Certificate[] certificates) {
140+
KeyVerifier keyVerifier = new KeyVerifier();
141+
// Key should match one of the certificates
142+
for (X509Certificate certificate : certificates) {
143+
Result result = keyVerifier.matches(privateKey, certificate.getPublicKey());
144+
if (result == Result.YES) {
145+
return;
123146
}
124147
}
148+
throw new IllegalStateException("Private key matches none of the certificates");
149+
}
150+
151+
private static PrivateKey loadPrivateKey(PemSslStoreDetails details) {
152+
String privateKeyContent = PemContent.load(details.privateKey());
153+
return PemPrivateKeyParser.parse(privateKeyContent, details.privateKeyPassword());
154+
}
155+
156+
private static X509Certificate[] loadCertificates(PemSslStoreDetails details) {
157+
String certificateContent = PemContent.load(details.certificate());
158+
X509Certificate[] certificates = PemCertificateParser.parse(certificateContent);
159+
Assert.state(certificates != null && certificates.length > 0, "Loaded certificates are empty");
160+
return certificates;
161+
}
162+
163+
private static KeyStore createKeyStore(PemSslStoreDetails details)
164+
throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
165+
String type = StringUtils.hasText(details.type()) ? details.type() : KeyStore.getDefaultType();
166+
KeyStore store = KeyStore.getInstance(type);
167+
store.load(null);
168+
return store;
169+
}
170+
171+
private static void addPrivateKey(KeyStore keyStore, PrivateKey privateKey, String alias, String keyPassword,
172+
X509Certificate[] certificates) throws KeyStoreException {
173+
keyStore.setKeyEntry(alias, privateKey, (keyPassword != null) ? keyPassword.toCharArray() : null, certificates);
174+
}
175+
176+
private static void addCertificates(KeyStore keyStore, X509Certificate[] certificates, String alias)
177+
throws KeyStoreException {
178+
for (int index = 0; index < certificates.length; index++) {
179+
keyStore.setCertificateEntry(alias + "-" + index, certificates[index]);
180+
}
125181
}
126182

127183
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.ssl.pem;
18+
19+
import java.security.InvalidAlgorithmParameterException;
20+
import java.security.KeyPair;
21+
import java.security.KeyPairGenerator;
22+
import java.security.NoSuchAlgorithmException;
23+
import java.security.PrivateKey;
24+
import java.security.PublicKey;
25+
import java.security.spec.AlgorithmParameterSpec;
26+
import java.security.spec.ECGenParameterSpec;
27+
import java.util.LinkedList;
28+
import java.util.List;
29+
import java.util.stream.Stream;
30+
31+
import org.junit.jupiter.api.Named;
32+
import org.junit.jupiter.params.ParameterizedTest;
33+
import org.junit.jupiter.params.provider.Arguments;
34+
import org.junit.jupiter.params.provider.MethodSource;
35+
36+
import org.springframework.boot.ssl.pem.KeyVerifier.Result;
37+
38+
import static org.assertj.core.api.Assertions.assertThat;
39+
40+
/**
41+
* Tests for {@link KeyVerifier}.
42+
*
43+
* @author Moritz Halbritter
44+
*/
45+
class KeyVerifierTests {
46+
47+
private static final List<Algorithm> ALGORITHMS = List.of(Algorithm.of("RSA"), Algorithm.of("DSA"),
48+
Algorithm.of("ed25519"), Algorithm.of("ed448"), Algorithm.ec("secp256r1"), Algorithm.ec("secp521r1"));
49+
50+
private final KeyVerifier keyVerifier = new KeyVerifier();
51+
52+
@ParameterizedTest(name = "{0}")
53+
@MethodSource("arguments")
54+
void test(PrivateKey privateKey, PublicKey publicKey, List<PublicKey> invalidPublicKeys) {
55+
assertThat(this.keyVerifier.matches(privateKey, publicKey)).isEqualTo(Result.YES);
56+
for (PublicKey invalidPublicKey : invalidPublicKeys) {
57+
assertThat(this.keyVerifier.matches(privateKey, invalidPublicKey)).isEqualTo(Result.NO);
58+
}
59+
}
60+
61+
static Stream<Arguments> arguments() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
62+
List<KeyPair> keyPairs = new LinkedList<>();
63+
for (Algorithm algorithm : ALGORITHMS) {
64+
KeyPairGenerator generator = KeyPairGenerator.getInstance(algorithm.name());
65+
if (algorithm.spec() != null) {
66+
generator.initialize(algorithm.spec());
67+
}
68+
keyPairs.add(generator.generateKeyPair());
69+
keyPairs.add(generator.generateKeyPair());
70+
}
71+
return keyPairs.stream()
72+
.map((kp) -> Arguments.arguments(Named.named(kp.getPrivate().getAlgorithm(), kp.getPrivate()),
73+
kp.getPublic(), without(keyPairs, kp).map(KeyPair::getPublic).toList()));
74+
}
75+
76+
private static Stream<KeyPair> without(List<KeyPair> keyPairs, KeyPair without) {
77+
return keyPairs.stream().filter((kp) -> !kp.equals(without));
78+
}
79+
80+
private record Algorithm(String name, AlgorithmParameterSpec spec) {
81+
static Algorithm of(String name) {
82+
return new Algorithm(name, null);
83+
}
84+
85+
static Algorithm ec(String curve) {
86+
return new Algorithm("EC", new ECGenParameterSpec(curve));
87+
}
88+
}
89+
90+
}

0 commit comments

Comments
 (0)