Browse Source

Merge branch '3.5.x'

Closes gh-46163
pull/46216/head
Moritz Halbritter 7 months ago
parent
commit
7fd6f457df
  1. 44
      spring-boot-project/spring-boot/src/main/java/org/springframework/boot/info/SslInfo.java
  2. 86
      spring-boot-project/spring-boot/src/test/java/org/springframework/boot/info/SslInfoTests.java
  3. BIN
      spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/info/will-expire-soon.p12

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

@ -19,9 +19,8 @@ package org.springframework.boot.info; @@ -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 { @@ -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 { @@ -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));
}

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

@ -16,16 +16,12 @@ @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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
spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/info/will-expire-soon.p12

Binary file not shown.
Loading…
Cancel
Save