mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-26 20:09:02 +00:00
Merge branch '3.5.x'
Closes gh-46163
This commit is contained in:
+32
-12
@@ -19,9 +19,8 @@ package org.springframework.boot.info;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateExpiredException;
|
||||
import java.security.cert.CertificateNotYetValidException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -48,13 +47,26 @@ public class SslInfo {
|
||||
|
||||
private final SslBundles sslBundles;
|
||||
|
||||
private final Clock clock;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param sslBundles the {@link SslBundles} to extract the info from
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public SslInfo(SslBundles sslBundles) {
|
||||
this(sslBundles, Clock.systemDefaultZone());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param sslBundles the {@link SslBundles} to extract the info from
|
||||
* @param clock the {@link Clock} to use
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public SslInfo(SslBundles sslBundles, Clock clock) {
|
||||
this.sslBundles = sslBundles;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -197,19 +209,27 @@ public class SslInfo {
|
||||
return extract((certificate) -> {
|
||||
Instant starts = getValidityStarts();
|
||||
Instant ends = getValidityEnds();
|
||||
try {
|
||||
certificate.checkValidity();
|
||||
return CertificateValidityInfo.VALID;
|
||||
}
|
||||
catch (CertificateNotYetValidException ex) {
|
||||
return new CertificateValidityInfo(Status.NOT_YET_VALID, "Not valid before %s", starts);
|
||||
}
|
||||
catch (CertificateExpiredException ex) {
|
||||
return new CertificateValidityInfo(Status.EXPIRED, "Not valid after %s", ends);
|
||||
}
|
||||
CertificateValidityInfo.Status validity = checkValidity(starts, ends);
|
||||
return switch (validity) {
|
||||
case VALID -> CertificateValidityInfo.VALID;
|
||||
case EXPIRED -> new CertificateValidityInfo(Status.EXPIRED, "Not valid after %s", ends);
|
||||
case NOT_YET_VALID ->
|
||||
new CertificateValidityInfo(Status.NOT_YET_VALID, "Not valid before %s", starts);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private CertificateValidityInfo.Status checkValidity(Instant starts, Instant ends) {
|
||||
Instant now = SslInfo.this.clock.instant();
|
||||
if (now.isBefore(starts)) {
|
||||
return CertificateValidityInfo.Status.NOT_YET_VALID;
|
||||
}
|
||||
if (now.isAfter(ends)) {
|
||||
return CertificateValidityInfo.Status.EXPIRED;
|
||||
}
|
||||
return CertificateValidityInfo.Status.VALID;
|
||||
}
|
||||
|
||||
private <V, R> R extract(Function<X509Certificate, V> valueExtractor, Function<V, R> resultExtractor) {
|
||||
return extract(valueExtractor.andThen(resultExtractor));
|
||||
}
|
||||
|
||||
+25
-61
@@ -16,16 +16,12 @@
|
||||
|
||||
package org.springframework.boot.info;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.boot.info.SslInfo.BundleInfo;
|
||||
import org.springframework.boot.info.SslInfo.CertificateChainInfo;
|
||||
@@ -45,9 +41,12 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* Tests for {@link SslInfo}.
|
||||
*
|
||||
* @author Jonatan Ivanov
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class SslInfoTests {
|
||||
|
||||
private static final Clock CLOCK = Clock.fixed(Instant.parse("2025-06-18T13:00:00Z"), ZoneId.of("UTC"));
|
||||
|
||||
@Test
|
||||
@WithPackageResources("test.p12")
|
||||
void validCertificatesShouldProvideSslInfo() {
|
||||
@@ -70,8 +69,8 @@ class SslInfoTests {
|
||||
assertThat(cert1.getSerialNumber()).isNotEmpty();
|
||||
assertThat(cert1.getVersion()).isEqualTo("V3");
|
||||
assertThat(cert1.getSignatureAlgorithmName()).isEqualTo("SHA256withRSA");
|
||||
assertThat(cert1.getValidityStarts()).isInThePast();
|
||||
assertThat(cert1.getValidityEnds()).isInTheFuture();
|
||||
assertThat(cert1.getValidityStarts()).isBefore(CLOCK.instant());
|
||||
assertThat(cert1.getValidityEnds()).isAfter(CLOCK.instant());
|
||||
assertThat(cert1.getValidity()).isNotNull();
|
||||
assertThat(cert1.getValidity().getStatus()).isSameAs(Status.VALID);
|
||||
assertThat(cert1.getValidity().getMessage()).isNull();
|
||||
@@ -81,8 +80,8 @@ class SslInfoTests {
|
||||
assertThat(cert2.getSerialNumber()).isNotEmpty();
|
||||
assertThat(cert2.getVersion()).isEqualTo("V3");
|
||||
assertThat(cert2.getSignatureAlgorithmName()).isEqualTo("SHA256withRSA");
|
||||
assertThat(cert2.getValidityStarts()).isInThePast();
|
||||
assertThat(cert2.getValidityEnds()).isInTheFuture();
|
||||
assertThat(cert2.getValidityStarts()).isBefore(CLOCK.instant());
|
||||
assertThat(cert2.getValidityEnds()).isAfter(CLOCK.instant());
|
||||
assertThat(cert2.getValidity()).isNotNull();
|
||||
assertThat(cert2.getValidity().getStatus()).isSameAs(Status.VALID);
|
||||
assertThat(cert2.getValidity().getMessage()).isNull();
|
||||
@@ -106,8 +105,8 @@ class SslInfoTests {
|
||||
assertThat(cert.getSerialNumber()).isNotEmpty();
|
||||
assertThat(cert.getVersion()).isEqualTo("V3");
|
||||
assertThat(cert.getSignatureAlgorithmName()).isEqualTo("SHA256withRSA");
|
||||
assertThat(cert.getValidityStarts()).isInTheFuture();
|
||||
assertThat(cert.getValidityEnds()).isInTheFuture();
|
||||
assertThat(cert.getValidityStarts()).isAfter(CLOCK.instant());
|
||||
assertThat(cert.getValidityEnds()).isAfter(CLOCK.instant());
|
||||
assertThat(cert.getValidity()).isNotNull();
|
||||
assertThat(cert.getValidity().getStatus()).isSameAs(Status.NOT_YET_VALID);
|
||||
assertThat(cert.getValidity().getMessage()).startsWith("Not valid before");
|
||||
@@ -131,19 +130,18 @@ class SslInfoTests {
|
||||
assertThat(cert.getSerialNumber()).isNotEmpty();
|
||||
assertThat(cert.getVersion()).isEqualTo("V3");
|
||||
assertThat(cert.getSignatureAlgorithmName()).isEqualTo("SHA256withRSA");
|
||||
assertThat(cert.getValidityStarts()).isInThePast();
|
||||
assertThat(cert.getValidityEnds()).isInThePast();
|
||||
assertThat(cert.getValidityStarts()).isBefore(CLOCK.instant());
|
||||
assertThat(cert.getValidityEnds()).isBefore(CLOCK.instant());
|
||||
assertThat(cert.getValidity()).isNotNull();
|
||||
assertThat(cert.getValidity().getStatus()).isSameAs(Status.EXPIRED);
|
||||
assertThat(cert.getValidity().getMessage()).startsWith("Not valid after");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithPackageResources({ "test.p12", "test-not-yet-valid.p12", "test-expired.p12" })
|
||||
void multipleBundlesShouldProvideSslInfo(@TempDir Path tempDir) throws IOException, InterruptedException {
|
||||
Path keyStore = createKeyStore(tempDir);
|
||||
@WithPackageResources({ "test.p12", "test-not-yet-valid.p12", "test-expired.p12", "will-expire-soon.p12" })
|
||||
void multipleBundlesShouldProvideSslInfo() {
|
||||
SslInfo sslInfo = createSslInfo("classpath:test.p12", "classpath:test-not-yet-valid.p12",
|
||||
"classpath:test-expired.p12", keyStore.toString());
|
||||
"classpath:test-expired.p12", "classpath:will-expire-soon.p12");
|
||||
assertThat(sslInfo.getBundles()).hasSize(4);
|
||||
assertThat(sslInfo.getBundles()).allSatisfy((bundle) -> assertThat(bundle.getName()).startsWith("test-"));
|
||||
List<CertificateInfo> certs = sslInfo.getBundles()
|
||||
@@ -161,22 +159,22 @@ class SslInfoTests {
|
||||
assertThat(cert.getValidity()).isNotNull();
|
||||
});
|
||||
assertThat(certs).anySatisfy((cert) -> {
|
||||
assertThat(cert.getValidityStarts()).isInThePast();
|
||||
assertThat(cert.getValidityEnds()).isInTheFuture();
|
||||
assertThat(cert.getValidityStarts()).isBefore(CLOCK.instant());
|
||||
assertThat(cert.getValidityEnds()).isAfter(CLOCK.instant());
|
||||
assertThat(cert.getValidity()).isNotNull();
|
||||
assertThat(cert.getValidity().getStatus()).isSameAs(Status.VALID);
|
||||
assertThat(cert.getValidity().getMessage()).isNull();
|
||||
});
|
||||
assertThat(certs).satisfiesOnlyOnce((cert) -> {
|
||||
assertThat(cert.getValidityStarts()).isInTheFuture();
|
||||
assertThat(cert.getValidityEnds()).isInTheFuture();
|
||||
assertThat(cert.getValidityStarts()).isAfter(CLOCK.instant());
|
||||
assertThat(cert.getValidityEnds()).isAfter(CLOCK.instant());
|
||||
assertThat(cert.getValidity()).isNotNull();
|
||||
assertThat(cert.getValidity().getStatus()).isSameAs(Status.NOT_YET_VALID);
|
||||
assertThat(cert.getValidity().getMessage()).startsWith("Not valid before");
|
||||
});
|
||||
assertThat(certs).satisfiesOnlyOnce((cert) -> {
|
||||
assertThat(cert.getValidityStarts()).isInThePast();
|
||||
assertThat(cert.getValidityEnds()).isInThePast();
|
||||
assertThat(cert.getValidityStarts()).isBefore(CLOCK.instant());
|
||||
assertThat(cert.getValidityEnds()).isBefore(CLOCK.instant());
|
||||
assertThat(cert.getValidity()).isNotNull();
|
||||
assertThat(cert.getValidity().getStatus()).isSameAs(Status.EXPIRED);
|
||||
assertThat(cert.getValidity().getMessage()).startsWith("Not valid after");
|
||||
@@ -187,7 +185,7 @@ class SslInfoTests {
|
||||
void nullKeyStore() {
|
||||
DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry();
|
||||
sslBundleRegistry.registerBundle("test", SslBundle.of(SslStoreBundle.NONE, SslBundleKey.NONE));
|
||||
SslInfo sslInfo = new SslInfo(sslBundleRegistry);
|
||||
SslInfo sslInfo = new SslInfo(sslBundleRegistry, CLOCK);
|
||||
assertThat(sslInfo.getBundles()).hasSize(1);
|
||||
assertThat(sslInfo.getBundles().get(0).getCertificateChains()).isEmpty();
|
||||
}
|
||||
@@ -199,41 +197,7 @@ class SslInfoTests {
|
||||
SslStoreBundle sslStoreBundle = new JksSslStoreBundle(keyStoreDetails, null);
|
||||
sslBundleRegistry.registerBundle("test-%d".formatted(i), SslBundle.of(sslStoreBundle));
|
||||
}
|
||||
return new SslInfo(sslBundleRegistry);
|
||||
}
|
||||
|
||||
private Path createKeyStore(Path directory) throws IOException, InterruptedException {
|
||||
Path keyStore = directory.resolve("test.p12");
|
||||
Process process = createProcessBuilder(keyStore).start();
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0) {
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String out = reader.lines().collect(Collectors.joining("\n"));
|
||||
throw new RuntimeException("Unexpected exit code from keytool: %d\n%s".formatted(exitCode, out));
|
||||
}
|
||||
}
|
||||
return keyStore;
|
||||
}
|
||||
|
||||
private ProcessBuilder createProcessBuilder(Path keystore) {
|
||||
// @formatter:off
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(
|
||||
"keytool",
|
||||
"-genkeypair",
|
||||
"-storetype", "PKCS12",
|
||||
"-alias", "spring-boot",
|
||||
"-keyalg", "RSA",
|
||||
"-storepass", "secret",
|
||||
"-keypass", "secret",
|
||||
"-keystore", keystore.toString(),
|
||||
"-dname", "CN=localhost,OU=Spring,O=VMware,L=Palo Alto,ST=California,C=US",
|
||||
"-validity", "1",
|
||||
"-ext", "SAN=DNS:localhost,IP:::1,IP:127.0.0.1"
|
||||
);
|
||||
// @formatter:on
|
||||
processBuilder.redirectErrorStream(true);
|
||||
return processBuilder;
|
||||
return new SslInfo(sslBundleRegistry, CLOCK);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
Reference in New Issue
Block a user