Skip to content

Commit 2cabf11

Browse files
Add SslInfoTests
1 parent 9e64df1 commit 2cabf11

File tree

5 files changed

+271
-2
lines changed

5 files changed

+271
-2
lines changed

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/ssl/SslHealthIndicator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ protected void doHealthCheck(Builder builder) throws Exception {
6969
}
7070
else if (statuses.contains(Validity.Status.WILL_EXPIRE_SOON)) {
7171
// TODO: Should we introduce Status.WARNING
72-
// (returns 200 but indicates that something is not right)?
72+
// (returns 200 but indicates that something is not right)?
7373
builder.status(WILL_EXPIRE_SOON_STATUS);
7474
}
7575
else {

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/info/SslInfo.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ private List<CertificateChain> createCertificateChains(KeyStore keyStore) {
8989

9090
private List<Certificate> getCertificates(String alias, KeyStore keyStore) {
9191
try {
92-
return List.of(keyStore.getCertificateChain(alias));
92+
Certificate[] certificateChain = keyStore.getCertificateChain(alias);
93+
return (certificateChain != null) ? List.of(certificateChain) : Collections.emptyList();
9394
}
9495
catch (KeyStoreException ex) {
9596
return Collections.emptyList();
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
/*
2+
* Copyright 2012-2024 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.info;
18+
19+
import java.io.BufferedReader;
20+
import java.io.IOException;
21+
import java.io.InputStreamReader;
22+
import java.nio.charset.StandardCharsets;
23+
import java.nio.file.Path;
24+
import java.time.Duration;
25+
import java.util.List;
26+
import java.util.stream.Collectors;
27+
28+
import org.junit.jupiter.api.Test;
29+
import org.junit.jupiter.api.io.TempDir;
30+
31+
import org.springframework.boot.info.SslInfo.Bundle;
32+
import org.springframework.boot.info.SslInfo.CertificateChain;
33+
import org.springframework.boot.info.SslInfo.CertificateInfo;
34+
import org.springframework.boot.info.SslInfo.CertificateInfo.Validity.Status;
35+
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
36+
import org.springframework.boot.ssl.SslBundle;
37+
import org.springframework.boot.ssl.SslStoreBundle;
38+
import org.springframework.boot.ssl.jks.JksSslStoreBundle;
39+
import org.springframework.boot.ssl.jks.JksSslStoreDetails;
40+
41+
import static org.assertj.core.api.Assertions.assertThat;
42+
43+
/**
44+
* Tests for {@link SslInfo}.
45+
*
46+
* @author Jonatan Ivanov
47+
*/
48+
class SslInfoTests {
49+
50+
@Test
51+
void validCertificatesShouldProvideSslInfo() {
52+
SslInfo sslInfo = createSslInfo("classpath:test.p12");
53+
assertThat(sslInfo.getBundles()).hasSize(1);
54+
Bundle bundle = sslInfo.getBundles().get(0);
55+
assertThat(bundle.getName()).isEqualTo("test-0");
56+
assertThat(bundle.getCertificateChains()).hasSize(4);
57+
assertThat(bundle.getCertificateChains().get(0).getAlias()).isEqualTo("spring-boot");
58+
assertThat(bundle.getCertificateChains().get(0).getCertificates()).hasSize(1);
59+
assertThat(bundle.getCertificateChains().get(1).getAlias()).isEqualTo("test-alias");
60+
assertThat(bundle.getCertificateChains().get(1).getCertificates()).hasSize(1);
61+
assertThat(bundle.getCertificateChains().get(2).getAlias()).isEqualTo("spring-boot-cert");
62+
assertThat(bundle.getCertificateChains().get(2).getCertificates()).isEmpty();
63+
assertThat(bundle.getCertificateChains().get(3).getAlias()).isEqualTo("test-alias-cert");
64+
assertThat(bundle.getCertificateChains().get(3).getCertificates()).isEmpty();
65+
66+
CertificateInfo cert1 = bundle.getCertificateChains().get(0).getCertificates().get(0);
67+
assertThat(cert1.getSubject()).isEqualTo("CN=localhost,OU=Spring,O=VMware,L=Palo Alto,ST=California,C=US");
68+
assertThat(cert1.getIssuer()).isEqualTo(cert1.getSubject());
69+
assertThat(cert1.getSerialNumber()).isNotEmpty();
70+
assertThat(cert1.getVersion()).isEqualTo("V3");
71+
assertThat(cert1.getSignatureAlgorithmName()).isEqualTo("SHA256withRSA");
72+
assertThat(cert1.getValidityStarts()).isInThePast();
73+
assertThat(cert1.getValidityEnds()).isInTheFuture();
74+
assertThat(cert1.getValidity()).isNotNull();
75+
assertThat(cert1.getValidity().getStatus()).isSameAs(Status.VALID);
76+
assertThat(cert1.getValidity().getMessage()).isNull();
77+
78+
CertificateInfo cert2 = bundle.getCertificateChains().get(1).getCertificates().get(0);
79+
assertThat(cert2.getSubject()).isEqualTo("CN=localhost,OU=Spring,O=VMware,L=Palo Alto,ST=California,C=US");
80+
assertThat(cert2.getIssuer()).isEqualTo(cert2.getSubject());
81+
assertThat(cert2.getSerialNumber()).isNotEmpty();
82+
assertThat(cert2.getVersion()).isEqualTo("V3");
83+
assertThat(cert2.getSignatureAlgorithmName()).isEqualTo("SHA256withRSA");
84+
assertThat(cert2.getValidityStarts()).isInThePast();
85+
assertThat(cert2.getValidityEnds()).isInTheFuture();
86+
assertThat(cert2.getValidity()).isNotNull();
87+
assertThat(cert2.getValidity().getStatus()).isSameAs(Status.VALID);
88+
assertThat(cert2.getValidity().getMessage()).isNull();
89+
}
90+
91+
@Test
92+
void notYetValidCertificateShouldProvideSslInfo() {
93+
SslInfo sslInfo = createSslInfo("classpath:test-not-yet-valid.p12");
94+
assertThat(sslInfo.getBundles()).hasSize(1);
95+
Bundle bundle = sslInfo.getBundles().get(0);
96+
assertThat(bundle.getName()).isEqualTo("test-0");
97+
assertThat(bundle.getCertificateChains()).hasSize(1);
98+
CertificateChain certificateChain = bundle.getCertificateChains().get(0);
99+
assertThat(certificateChain.getAlias()).isEqualTo("spring-boot");
100+
List<CertificateInfo> certs = certificateChain.getCertificates();
101+
assertThat(certs).hasSize(1);
102+
CertificateInfo cert = certs.get(0);
103+
assertThat(cert.getSubject()).isEqualTo("CN=localhost,OU=Spring,O=VMware,L=Palo Alto,ST=California,C=US");
104+
assertThat(cert.getIssuer()).isEqualTo(cert.getSubject());
105+
assertThat(cert.getSerialNumber()).isNotEmpty();
106+
assertThat(cert.getVersion()).isEqualTo("V3");
107+
assertThat(cert.getSignatureAlgorithmName()).isEqualTo("SHA256withRSA");
108+
assertThat(cert.getValidityStarts()).isInTheFuture();
109+
assertThat(cert.getValidityEnds()).isInTheFuture();
110+
assertThat(cert.getValidity()).isNotNull();
111+
assertThat(cert.getValidity().getStatus()).isSameAs(Status.NOT_YET_VALID);
112+
assertThat(cert.getValidity().getMessage()).startsWith("Not valid before");
113+
}
114+
115+
@Test
116+
void expiredCertificateShouldProvideSslInfo() {
117+
SslInfo sslInfo = createSslInfo("classpath:test-expired.p12");
118+
assertThat(sslInfo.getBundles()).hasSize(1);
119+
Bundle bundle = sslInfo.getBundles().get(0);
120+
assertThat(bundle.getName()).isEqualTo("test-0");
121+
assertThat(bundle.getCertificateChains()).hasSize(1);
122+
CertificateChain certificateChain = bundle.getCertificateChains().get(0);
123+
assertThat(certificateChain.getAlias()).isEqualTo("spring-boot");
124+
List<CertificateInfo> certs = certificateChain.getCertificates();
125+
assertThat(certs).hasSize(1);
126+
CertificateInfo cert = certs.get(0);
127+
assertThat(cert.getSubject()).isEqualTo("CN=localhost,OU=Spring,O=VMware,L=Palo Alto,ST=California,C=US");
128+
assertThat(cert.getIssuer()).isEqualTo(cert.getSubject());
129+
assertThat(cert.getSerialNumber()).isNotEmpty();
130+
assertThat(cert.getVersion()).isEqualTo("V3");
131+
assertThat(cert.getSignatureAlgorithmName()).isEqualTo("SHA256withRSA");
132+
assertThat(cert.getValidityStarts()).isInThePast();
133+
assertThat(cert.getValidityEnds()).isInThePast();
134+
assertThat(cert.getValidity()).isNotNull();
135+
assertThat(cert.getValidity().getStatus()).isSameAs(Status.EXPIRED);
136+
assertThat(cert.getValidity().getMessage()).startsWith("Not valid after");
137+
}
138+
139+
@Test
140+
void soonToBeExpiredCertificateShouldProvideSslInfo(@TempDir Path tempDir)
141+
throws IOException, InterruptedException {
142+
Path keyStore = createKeyStore(tempDir);
143+
SslInfo sslInfo = createSslInfo(keyStore.toString());
144+
assertThat(sslInfo.getBundles()).hasSize(1);
145+
Bundle bundle = sslInfo.getBundles().get(0);
146+
assertThat(bundle.getName()).isEqualTo("test-0");
147+
assertThat(bundle.getCertificateChains()).hasSize(1);
148+
CertificateChain certificateChain = bundle.getCertificateChains().get(0);
149+
assertThat(certificateChain.getAlias()).isEqualTo("spring-boot");
150+
List<CertificateInfo> certs = certificateChain.getCertificates();
151+
assertThat(certs).hasSize(1);
152+
CertificateInfo cert = certs.get(0);
153+
assertThat(cert.getSubject()).isEqualTo("CN=localhost,OU=Spring,O=VMware,L=Palo Alto,ST=California,C=US");
154+
assertThat(cert.getIssuer()).isEqualTo(cert.getSubject());
155+
assertThat(cert.getSerialNumber()).isNotEmpty();
156+
assertThat(cert.getVersion()).isEqualTo("V3");
157+
assertThat(cert.getSignatureAlgorithmName()).isEqualTo("SHA256withRSA");
158+
assertThat(cert.getValidityStarts()).isInThePast();
159+
assertThat(cert.getValidityEnds()).isInTheFuture();
160+
assertThat(cert.getValidity()).isNotNull();
161+
assertThat(cert.getValidity().getStatus()).isSameAs(Status.WILL_EXPIRE_SOON);
162+
assertThat(cert.getValidity().getMessage()).startsWith("Certificate will expire within threshold");
163+
}
164+
165+
@Test
166+
void multipleBundlesShouldProvideSslInfo(@TempDir Path tempDir) throws IOException, InterruptedException {
167+
Path keyStore = createKeyStore(tempDir);
168+
SslInfo sslInfo = createSslInfo("classpath:test.p12", "classpath:test-not-yet-valid.p12",
169+
"classpath:test-expired.p12", keyStore.toString());
170+
assertThat(sslInfo.getBundles()).hasSize(4);
171+
assertThat(sslInfo.getBundles()).allSatisfy((bundle) -> assertThat(bundle.getName()).startsWith("test-"));
172+
173+
List<CertificateInfo> certs = sslInfo.getBundles()
174+
.stream()
175+
.flatMap((bundle) -> bundle.getCertificateChains().stream())
176+
.flatMap((certificateChain) -> certificateChain.getCertificates().stream())
177+
.toList();
178+
179+
assertThat(certs).hasSize(5);
180+
assertThat(certs).allSatisfy((cert) -> {
181+
assertThat(cert.getSubject()).isEqualTo("CN=localhost,OU=Spring,O=VMware,L=Palo Alto,ST=California,C=US");
182+
assertThat(cert.getIssuer()).isEqualTo(cert.getSubject());
183+
assertThat(cert.getSerialNumber()).isNotEmpty();
184+
assertThat(cert.getVersion()).isEqualTo("V3");
185+
assertThat(cert.getSignatureAlgorithmName()).isEqualTo("SHA256withRSA");
186+
assertThat(cert.getValidity()).isNotNull();
187+
});
188+
189+
assertThat(certs).anySatisfy((cert) -> {
190+
assertThat(cert.getValidityStarts()).isInThePast();
191+
assertThat(cert.getValidityEnds()).isInTheFuture();
192+
assertThat(cert.getValidity()).isNotNull();
193+
assertThat(cert.getValidity().getStatus()).isSameAs(Status.VALID);
194+
assertThat(cert.getValidity().getMessage()).isNull();
195+
});
196+
197+
assertThat(certs).satisfiesOnlyOnce((cert) -> {
198+
assertThat(cert.getValidityStarts()).isInTheFuture();
199+
assertThat(cert.getValidityEnds()).isInTheFuture();
200+
assertThat(cert.getValidity()).isNotNull();
201+
assertThat(cert.getValidity().getStatus()).isSameAs(Status.NOT_YET_VALID);
202+
assertThat(cert.getValidity().getMessage()).startsWith("Not valid before");
203+
});
204+
205+
assertThat(certs).satisfiesOnlyOnce((cert) -> {
206+
assertThat(cert.getValidityStarts()).isInThePast();
207+
assertThat(cert.getValidityEnds()).isInThePast();
208+
assertThat(cert.getValidity()).isNotNull();
209+
assertThat(cert.getValidity().getStatus()).isSameAs(Status.EXPIRED);
210+
assertThat(cert.getValidity().getMessage()).startsWith("Not valid after");
211+
});
212+
213+
assertThat(certs).satisfiesOnlyOnce((cert) -> {
214+
assertThat(cert.getValidityStarts()).isInThePast();
215+
assertThat(cert.getValidityEnds()).isInTheFuture();
216+
assertThat(cert.getValidity()).isNotNull();
217+
assertThat(cert.getValidity().getStatus()).isSameAs(Status.WILL_EXPIRE_SOON);
218+
assertThat(cert.getValidity().getMessage()).startsWith("Certificate will expire within threshold");
219+
});
220+
}
221+
222+
private SslInfo createSslInfo(String... locations) {
223+
DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry();
224+
for (int i = 0; i < locations.length; i++) {
225+
JksSslStoreDetails keyStoreDetails = JksSslStoreDetails.forLocation(locations[i]).withPassword("secret");
226+
SslStoreBundle sslStoreBundle = new JksSslStoreBundle(keyStoreDetails, null);
227+
sslBundleRegistry.registerBundle("test-%d".formatted(i), SslBundle.of(sslStoreBundle));
228+
}
229+
230+
return new SslInfo(sslBundleRegistry, Duration.ofDays(7));
231+
}
232+
233+
private Path createKeyStore(Path directory) throws IOException, InterruptedException {
234+
Path keyStore = directory.resolve("test.p12");
235+
Process process = createProcessBuilder(keyStore).start();
236+
int exitCode = process.waitFor();
237+
if (exitCode != 0) {
238+
String out = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))
239+
.lines()
240+
.collect(Collectors.joining("\n"));
241+
throw new RuntimeException("Unexpected exit code from keytool: %d\n%s".formatted(exitCode, out));
242+
}
243+
244+
return keyStore;
245+
}
246+
247+
private ProcessBuilder createProcessBuilder(Path keystore) {
248+
// @formatter:off
249+
ProcessBuilder processBuilder = new ProcessBuilder(
250+
"keytool",
251+
"-genkeypair",
252+
"-storetype", "PKCS12",
253+
"-alias", "spring-boot",
254+
"-keyalg", "RSA",
255+
"-storepass", "secret",
256+
"-keypass", "secret",
257+
"-keystore", keystore.toString(),
258+
"-dname", "CN=localhost,OU=Spring,O=VMware,L=Palo Alto,ST=California,C=US",
259+
"-validity", "1",
260+
"-ext", "SAN=DNS:localhost,IP:::1,IP:127.0.0.1"
261+
);
262+
// @formatter:on
263+
processBuilder.redirectErrorStream(true);
264+
265+
return processBuilder;
266+
}
267+
268+
}
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)