Skip to content

Commit 5dc5c2a

Browse files
committed
Rename KeyVerifier to CertificateMatcher
Rename `KeyVerifier` to `CertificateMatcher` and refactor some of the internals. This commit also adds test helper classes to help simplify some of the tests. See gh-38173
1 parent 1b61bc1 commit 5dc5c2a

File tree

8 files changed

+345
-207
lines changed

8 files changed

+345
-207
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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.autoconfigure.ssl;
18+
19+
import java.security.InvalidKeyException;
20+
import java.security.NoSuchAlgorithmException;
21+
import java.security.PrivateKey;
22+
import java.security.PublicKey;
23+
import java.security.Signature;
24+
import java.security.SignatureException;
25+
import java.security.cert.Certificate;
26+
import java.util.List;
27+
import java.util.Objects;
28+
29+
/**
30+
* Helper used to match certificates against a {@link PrivateKey}.
31+
*
32+
* @author Moritz Halbritter
33+
* @author Phillip Webb
34+
*/
35+
class CertificateMatcher {
36+
37+
private static final byte[] DATA = new byte[256];
38+
static {
39+
for (int i = 0; i < DATA.length; i++) {
40+
DATA[i] = (byte) i;
41+
}
42+
}
43+
44+
private final PrivateKey privateKey;
45+
46+
private final Signature signature;
47+
48+
private final byte[] generatedSignature;
49+
50+
CertificateMatcher(PrivateKey privateKey) {
51+
this.privateKey = privateKey;
52+
this.signature = createSignature(privateKey);
53+
this.generatedSignature = sign(this.signature, privateKey);
54+
}
55+
56+
private Signature createSignature(PrivateKey privateKey) {
57+
try {
58+
String algorithm = getSignatureAlgorithm(this.privateKey);
59+
return (algorithm != null) ? Signature.getInstance(algorithm) : null;
60+
}
61+
catch (NoSuchAlgorithmException ex) {
62+
return null;
63+
}
64+
}
65+
66+
private static String getSignatureAlgorithm(PrivateKey privateKey) {
67+
// https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#signature-algorithms
68+
// https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#keypairgenerator-algorithms
69+
return switch (privateKey.getAlgorithm()) {
70+
case "RSA" -> "SHA256withRSA";
71+
case "DSA" -> "SHA256withDSA";
72+
case "EC" -> "SHA256withECDSA";
73+
case "EdDSA" -> "EdDSA";
74+
default -> null;
75+
};
76+
}
77+
78+
boolean matchesAny(List<? extends Certificate> certificates) {
79+
return (this.generatedSignature != null) && certificates.stream().anyMatch(this::matches);
80+
}
81+
82+
boolean matches(Certificate certificate) {
83+
return matches(certificate.getPublicKey());
84+
}
85+
86+
private boolean matches(PublicKey publicKey) {
87+
return (this.generatedSignature != null)
88+
&& Objects.equals(this.privateKey.getAlgorithm(), publicKey.getAlgorithm()) && verify(publicKey);
89+
}
90+
91+
private boolean verify(PublicKey publicKey) {
92+
try {
93+
this.signature.initVerify(publicKey);
94+
this.signature.update(DATA);
95+
return this.signature.verify(this.generatedSignature);
96+
}
97+
catch (InvalidKeyException | SignatureException ex) {
98+
return false;
99+
}
100+
}
101+
102+
private static byte[] sign(Signature signature, PrivateKey privateKey) {
103+
try {
104+
signature.initSign(privateKey);
105+
signature.update(DATA);
106+
return signature.sign();
107+
}
108+
catch (InvalidKeyException | SignatureException ex) {
109+
return null;
110+
}
111+
}
112+
113+
}

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

Lines changed: 0 additions & 104 deletions
This file was deleted.

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

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import java.io.IOException;
2020
import java.io.UncheckedIOException;
21-
import java.security.cert.X509Certificate;
2221

2322
import org.springframework.boot.autoconfigure.ssl.SslBundleProperties.Key;
2423
import org.springframework.boot.ssl.SslBundle;
@@ -31,6 +30,7 @@
3130
import org.springframework.boot.ssl.pem.PemSslStore;
3231
import org.springframework.boot.ssl.pem.PemSslStoreBundle;
3332
import org.springframework.boot.ssl.pem.PemSslStoreDetails;
33+
import org.springframework.util.Assert;
3434

3535
/**
3636
* {@link SslBundle} backed by {@link JksSslBundleProperties} or
@@ -122,7 +122,9 @@ private static PemSslStore asPemSslStore(PemSslBundleProperties.Store properties
122122
PemSslStoreDetails details = asStoreDetails(properties, alias);
123123
PemSslStore pemSslStore = PemSslStore.load(details);
124124
if (properties.isVerifyKeys()) {
125-
verifyPemSslStoreKeys(pemSslStore);
125+
CertificateMatcher certificateMatcher = new CertificateMatcher(pemSslStore.privateKey());
126+
Assert.state(certificateMatcher.matchesAny(pemSslStore.certificates()),
127+
"Private key matches none of the certificates in the chain");
126128
}
127129
return pemSslStore;
128130
}
@@ -131,17 +133,6 @@ private static PemSslStore asPemSslStore(PemSslBundleProperties.Store properties
131133
}
132134
}
133135

134-
private static void verifyPemSslStoreKeys(PemSslStore pemSslStore) {
135-
KeyVerifier keyVerifier = new KeyVerifier();
136-
for (X509Certificate certificate : pemSslStore.certificates()) {
137-
KeyVerifier.Result result = keyVerifier.matches(pemSslStore.privateKey(), certificate.getPublicKey());
138-
if (result == KeyVerifier.Result.YES) {
139-
return;
140-
}
141-
}
142-
throw new IllegalStateException("Private key matches none of the certificates in the chain");
143-
}
144-
145136
private static PemSslStoreDetails asStoreDetails(PemSslBundleProperties.Store properties, String alias) {
146137
return new PemSslStoreDetails(properties.getType(), alias, null, properties.getCertificate(),
147138
properties.getPrivateKey(), properties.getPrivateKeyPassword());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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.autoconfigure.ssl;
18+
19+
import java.security.cert.Certificate;
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
23+
import static org.assertj.core.api.Assertions.assertThat;
24+
25+
/**
26+
* Tests for {@link CertificateMatcher}.
27+
*
28+
* @author Moritz Halbritter
29+
* @author Phillip Webb
30+
*/
31+
class CertificateMatcherTests {
32+
33+
@CertificateMatchingTest
34+
void matchesWhenMatchReturnsTrue(CertificateMatchingTestSource source) {
35+
CertificateMatcher matcher = new CertificateMatcher(source.privateKey());
36+
assertThat(matcher.matches(source.matchingCertificate())).isTrue();
37+
}
38+
39+
@CertificateMatchingTest
40+
void matchesWhenNoMatchReturnsFalse(CertificateMatchingTestSource source) {
41+
CertificateMatcher matcher = new CertificateMatcher(source.privateKey());
42+
for (Certificate nonMatchingCertificate : source.nonMatchingCertificates()) {
43+
assertThat(matcher.matches(nonMatchingCertificate)).isFalse();
44+
}
45+
}
46+
47+
@CertificateMatchingTest
48+
void matchesAnyWhenNoneMatchReturnsFalse(CertificateMatchingTestSource source) {
49+
CertificateMatcher matcher = new CertificateMatcher(source.privateKey());
50+
assertThat(matcher.matchesAny(source.nonMatchingCertificates())).isFalse();
51+
}
52+
53+
@CertificateMatchingTest
54+
void matchesAnyWhenOneMatchesReturnsTrue(CertificateMatchingTestSource source) {
55+
CertificateMatcher matcher = new CertificateMatcher(source.privateKey());
56+
List<Certificate> certificates = new ArrayList<>(source.nonMatchingCertificates());
57+
certificates.add(source.matchingCertificate());
58+
assertThat(matcher.matchesAny(certificates)).isTrue();
59+
}
60+
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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.autoconfigure.ssl;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
import org.junit.jupiter.params.ParameterizedTest;
26+
import org.junit.jupiter.params.provider.MethodSource;
27+
28+
/**
29+
* Annotation for a {@code ParameterizedTest @ParameterizedTest} with a
30+
* {@link CertificateMatchingTestSource} parameter.
31+
*
32+
* @author Phillip Webb
33+
*/
34+
@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD })
35+
@Retention(RetentionPolicy.RUNTIME)
36+
@Documented
37+
@ParameterizedTest(name = "{0}")
38+
@MethodSource("org.springframework.boot.autoconfigure.ssl.CertificateMatchingTestSource#create")
39+
public @interface CertificateMatchingTest {
40+
41+
}

0 commit comments

Comments
 (0)