Skip to content

Commit 7fd6f45

Browse files
committed
Merge branch '3.5.x'
Closes gh-46163
2 parents a08e40d + e99c3a9 commit 7fd6f45

File tree

3 files changed

+57
-73
lines changed

3 files changed

+57
-73
lines changed

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

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,8 @@
1919
import java.security.KeyStore;
2020
import java.security.KeyStoreException;
2121
import java.security.cert.Certificate;
22-
import java.security.cert.CertificateExpiredException;
23-
import java.security.cert.CertificateNotYetValidException;
2422
import java.security.cert.X509Certificate;
23+
import java.time.Clock;
2524
import java.time.Instant;
2625
import java.util.Arrays;
2726
import java.util.Collections;
@@ -48,13 +47,26 @@ public class SslInfo {
4847

4948
private final SslBundles sslBundles;
5049

50+
private final Clock clock;
51+
5152
/**
5253
* Creates a new instance.
5354
* @param sslBundles the {@link SslBundles} to extract the info from
5455
* @since 4.0.0
5556
*/
5657
public SslInfo(SslBundles sslBundles) {
58+
this(sslBundles, Clock.systemDefaultZone());
59+
}
60+
61+
/**
62+
* Creates a new instance.
63+
* @param sslBundles the {@link SslBundles} to extract the info from
64+
* @param clock the {@link Clock} to use
65+
* @since 4.0.0
66+
*/
67+
public SslInfo(SslBundles sslBundles, Clock clock) {
5768
this.sslBundles = sslBundles;
69+
this.clock = clock;
5870
}
5971

6072
/**
@@ -197,19 +209,27 @@ public CertificateValidityInfo getValidity() {
197209
return extract((certificate) -> {
198210
Instant starts = getValidityStarts();
199211
Instant ends = getValidityEnds();
200-
try {
201-
certificate.checkValidity();
202-
return CertificateValidityInfo.VALID;
203-
}
204-
catch (CertificateNotYetValidException ex) {
205-
return new CertificateValidityInfo(Status.NOT_YET_VALID, "Not valid before %s", starts);
206-
}
207-
catch (CertificateExpiredException ex) {
208-
return new CertificateValidityInfo(Status.EXPIRED, "Not valid after %s", ends);
209-
}
212+
CertificateValidityInfo.Status validity = checkValidity(starts, ends);
213+
return switch (validity) {
214+
case VALID -> CertificateValidityInfo.VALID;
215+
case EXPIRED -> new CertificateValidityInfo(Status.EXPIRED, "Not valid after %s", ends);
216+
case NOT_YET_VALID ->
217+
new CertificateValidityInfo(Status.NOT_YET_VALID, "Not valid before %s", starts);
218+
};
210219
});
211220
}
212221

222+
private CertificateValidityInfo.Status checkValidity(Instant starts, Instant ends) {
223+
Instant now = SslInfo.this.clock.instant();
224+
if (now.isBefore(starts)) {
225+
return CertificateValidityInfo.Status.NOT_YET_VALID;
226+
}
227+
if (now.isAfter(ends)) {
228+
return CertificateValidityInfo.Status.EXPIRED;
229+
}
230+
return CertificateValidityInfo.Status.VALID;
231+
}
232+
213233
private <V, R> R extract(Function<X509Certificate, V> valueExtractor, Function<V, R> resultExtractor) {
214234
return extract(valueExtractor.andThen(resultExtractor));
215235
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/info/SslInfoTests.java

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

1717
package org.springframework.boot.info;
1818

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;
19+
import java.time.Clock;
20+
import java.time.Instant;
21+
import java.time.ZoneId;
2422
import java.util.List;
25-
import java.util.stream.Collectors;
2623

2724
import org.junit.jupiter.api.Test;
28-
import org.junit.jupiter.api.io.TempDir;
2925

3026
import org.springframework.boot.info.SslInfo.BundleInfo;
3127
import org.springframework.boot.info.SslInfo.CertificateChainInfo;
@@ -45,9 +41,12 @@
4541
* Tests for {@link SslInfo}.
4642
*
4743
* @author Jonatan Ivanov
44+
* @author Moritz Halbritter
4845
*/
4946
class SslInfoTests {
5047

48+
private static final Clock CLOCK = Clock.fixed(Instant.parse("2025-06-18T13:00:00Z"), ZoneId.of("UTC"));
49+
5150
@Test
5251
@WithPackageResources("test.p12")
5352
void validCertificatesShouldProvideSslInfo() {
@@ -70,8 +69,8 @@ void validCertificatesShouldProvideSslInfo() {
7069
assertThat(cert1.getSerialNumber()).isNotEmpty();
7170
assertThat(cert1.getVersion()).isEqualTo("V3");
7271
assertThat(cert1.getSignatureAlgorithmName()).isEqualTo("SHA256withRSA");
73-
assertThat(cert1.getValidityStarts()).isInThePast();
74-
assertThat(cert1.getValidityEnds()).isInTheFuture();
72+
assertThat(cert1.getValidityStarts()).isBefore(CLOCK.instant());
73+
assertThat(cert1.getValidityEnds()).isAfter(CLOCK.instant());
7574
assertThat(cert1.getValidity()).isNotNull();
7675
assertThat(cert1.getValidity().getStatus()).isSameAs(Status.VALID);
7776
assertThat(cert1.getValidity().getMessage()).isNull();
@@ -81,8 +80,8 @@ void validCertificatesShouldProvideSslInfo() {
8180
assertThat(cert2.getSerialNumber()).isNotEmpty();
8281
assertThat(cert2.getVersion()).isEqualTo("V3");
8382
assertThat(cert2.getSignatureAlgorithmName()).isEqualTo("SHA256withRSA");
84-
assertThat(cert2.getValidityStarts()).isInThePast();
85-
assertThat(cert2.getValidityEnds()).isInTheFuture();
83+
assertThat(cert2.getValidityStarts()).isBefore(CLOCK.instant());
84+
assertThat(cert2.getValidityEnds()).isAfter(CLOCK.instant());
8685
assertThat(cert2.getValidity()).isNotNull();
8786
assertThat(cert2.getValidity().getStatus()).isSameAs(Status.VALID);
8887
assertThat(cert2.getValidity().getMessage()).isNull();
@@ -106,8 +105,8 @@ void notYetValidCertificateShouldProvideSslInfo() {
106105
assertThat(cert.getSerialNumber()).isNotEmpty();
107106
assertThat(cert.getVersion()).isEqualTo("V3");
108107
assertThat(cert.getSignatureAlgorithmName()).isEqualTo("SHA256withRSA");
109-
assertThat(cert.getValidityStarts()).isInTheFuture();
110-
assertThat(cert.getValidityEnds()).isInTheFuture();
108+
assertThat(cert.getValidityStarts()).isAfter(CLOCK.instant());
109+
assertThat(cert.getValidityEnds()).isAfter(CLOCK.instant());
111110
assertThat(cert.getValidity()).isNotNull();
112111
assertThat(cert.getValidity().getStatus()).isSameAs(Status.NOT_YET_VALID);
113112
assertThat(cert.getValidity().getMessage()).startsWith("Not valid before");
@@ -131,19 +130,18 @@ void expiredCertificateShouldProvideSslInfo() {
131130
assertThat(cert.getSerialNumber()).isNotEmpty();
132131
assertThat(cert.getVersion()).isEqualTo("V3");
133132
assertThat(cert.getSignatureAlgorithmName()).isEqualTo("SHA256withRSA");
134-
assertThat(cert.getValidityStarts()).isInThePast();
135-
assertThat(cert.getValidityEnds()).isInThePast();
133+
assertThat(cert.getValidityStarts()).isBefore(CLOCK.instant());
134+
assertThat(cert.getValidityEnds()).isBefore(CLOCK.instant());
136135
assertThat(cert.getValidity()).isNotNull();
137136
assertThat(cert.getValidity().getStatus()).isSameAs(Status.EXPIRED);
138137
assertThat(cert.getValidity().getMessage()).startsWith("Not valid after");
139138
}
140139

141140
@Test
142-
@WithPackageResources({ "test.p12", "test-not-yet-valid.p12", "test-expired.p12" })
143-
void multipleBundlesShouldProvideSslInfo(@TempDir Path tempDir) throws IOException, InterruptedException {
144-
Path keyStore = createKeyStore(tempDir);
141+
@WithPackageResources({ "test.p12", "test-not-yet-valid.p12", "test-expired.p12", "will-expire-soon.p12" })
142+
void multipleBundlesShouldProvideSslInfo() {
145143
SslInfo sslInfo = createSslInfo("classpath:test.p12", "classpath:test-not-yet-valid.p12",
146-
"classpath:test-expired.p12", keyStore.toString());
144+
"classpath:test-expired.p12", "classpath:will-expire-soon.p12");
147145
assertThat(sslInfo.getBundles()).hasSize(4);
148146
assertThat(sslInfo.getBundles()).allSatisfy((bundle) -> assertThat(bundle.getName()).startsWith("test-"));
149147
List<CertificateInfo> certs = sslInfo.getBundles()
@@ -161,22 +159,22 @@ void multipleBundlesShouldProvideSslInfo(@TempDir Path tempDir) throws IOExcepti
161159
assertThat(cert.getValidity()).isNotNull();
162160
});
163161
assertThat(certs).anySatisfy((cert) -> {
164-
assertThat(cert.getValidityStarts()).isInThePast();
165-
assertThat(cert.getValidityEnds()).isInTheFuture();
162+
assertThat(cert.getValidityStarts()).isBefore(CLOCK.instant());
163+
assertThat(cert.getValidityEnds()).isAfter(CLOCK.instant());
166164
assertThat(cert.getValidity()).isNotNull();
167165
assertThat(cert.getValidity().getStatus()).isSameAs(Status.VALID);
168166
assertThat(cert.getValidity().getMessage()).isNull();
169167
});
170168
assertThat(certs).satisfiesOnlyOnce((cert) -> {
171-
assertThat(cert.getValidityStarts()).isInTheFuture();
172-
assertThat(cert.getValidityEnds()).isInTheFuture();
169+
assertThat(cert.getValidityStarts()).isAfter(CLOCK.instant());
170+
assertThat(cert.getValidityEnds()).isAfter(CLOCK.instant());
173171
assertThat(cert.getValidity()).isNotNull();
174172
assertThat(cert.getValidity().getStatus()).isSameAs(Status.NOT_YET_VALID);
175173
assertThat(cert.getValidity().getMessage()).startsWith("Not valid before");
176174
});
177175
assertThat(certs).satisfiesOnlyOnce((cert) -> {
178-
assertThat(cert.getValidityStarts()).isInThePast();
179-
assertThat(cert.getValidityEnds()).isInThePast();
176+
assertThat(cert.getValidityStarts()).isBefore(CLOCK.instant());
177+
assertThat(cert.getValidityEnds()).isBefore(CLOCK.instant());
180178
assertThat(cert.getValidity()).isNotNull();
181179
assertThat(cert.getValidity().getStatus()).isSameAs(Status.EXPIRED);
182180
assertThat(cert.getValidity().getMessage()).startsWith("Not valid after");
@@ -187,7 +185,7 @@ void multipleBundlesShouldProvideSslInfo(@TempDir Path tempDir) throws IOExcepti
187185
void nullKeyStore() {
188186
DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry();
189187
sslBundleRegistry.registerBundle("test", SslBundle.of(SslStoreBundle.NONE, SslBundleKey.NONE));
190-
SslInfo sslInfo = new SslInfo(sslBundleRegistry);
188+
SslInfo sslInfo = new SslInfo(sslBundleRegistry, CLOCK);
191189
assertThat(sslInfo.getBundles()).hasSize(1);
192190
assertThat(sslInfo.getBundles().get(0).getCertificateChains()).isEmpty();
193191
}
@@ -199,41 +197,7 @@ private SslInfo createSslInfo(String... locations) {
199197
SslStoreBundle sslStoreBundle = new JksSslStoreBundle(keyStoreDetails, null);
200198
sslBundleRegistry.registerBundle("test-%d".formatted(i), SslBundle.of(sslStoreBundle));
201199
}
202-
return new SslInfo(sslBundleRegistry);
203-
}
204-
205-
private Path createKeyStore(Path directory) throws IOException, InterruptedException {
206-
Path keyStore = directory.resolve("test.p12");
207-
Process process = createProcessBuilder(keyStore).start();
208-
int exitCode = process.waitFor();
209-
if (exitCode != 0) {
210-
try (BufferedReader reader = new BufferedReader(
211-
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
212-
String out = reader.lines().collect(Collectors.joining("\n"));
213-
throw new RuntimeException("Unexpected exit code from keytool: %d\n%s".formatted(exitCode, out));
214-
}
215-
}
216-
return keyStore;
217-
}
218-
219-
private ProcessBuilder createProcessBuilder(Path keystore) {
220-
// @formatter:off
221-
ProcessBuilder processBuilder = new ProcessBuilder(
222-
"keytool",
223-
"-genkeypair",
224-
"-storetype", "PKCS12",
225-
"-alias", "spring-boot",
226-
"-keyalg", "RSA",
227-
"-storepass", "secret",
228-
"-keypass", "secret",
229-
"-keystore", keystore.toString(),
230-
"-dname", "CN=localhost,OU=Spring,O=VMware,L=Palo Alto,ST=California,C=US",
231-
"-validity", "1",
232-
"-ext", "SAN=DNS:localhost,IP:::1,IP:127.0.0.1"
233-
);
234-
// @formatter:on
235-
processBuilder.redirectErrorStream(true);
236-
return processBuilder;
200+
return new SslInfo(sslBundleRegistry, CLOCK);
237201
}
238202

239203
}

0 commit comments

Comments
 (0)