Browse Source

Merge pull request #48967 from garvit-joshi

* pr/48967:
  Polish "Show certificates from truststore in SSL info endpoint"
  Show certificates from truststore in SSL info endpoint

Closes gh-48967
pull/48957/head
Moritz Halbritter 1 month ago
parent
commit
472031e86b
  1. 32
      core/spring-boot/src/main/java/org/springframework/boot/info/SslInfo.java
  2. 69
      core/spring-boot/src/test/java/org/springframework/boot/info/SslInfoTests.java
  3. BIN
      core/spring-boot/src/test/resources/org/springframework/boot/info/keystore.jks
  4. BIN
      core/spring-boot/src/test/resources/org/springframework/boot/info/truststore.jks
  5. 40
      documentation/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/docs/info/InfoEndpointDocumentationTests.java

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

@ -104,9 +104,12 @@ public class SslInfo { @@ -104,9 +104,12 @@ public class SslInfo {
private final List<CertificateChainInfo> certificateChains;
private final List<CertificateChainInfo> trustStoreCertificateChains;
private BundleInfo(String name, SslBundle sslBundle) {
this.name = name;
this.certificateChains = extractCertificateChains(sslBundle.getStores().getKeyStore());
this.trustStoreCertificateChains = extractCertificateChains(sslBundle.getStores().getTrustStore());
}
private List<CertificateChainInfo> extractCertificateChains(@Nullable KeyStore keyStore) {
@ -132,6 +135,10 @@ public class SslInfo { @@ -132,6 +135,10 @@ public class SslInfo {
return this.certificateChains;
}
public List<CertificateChainInfo> getTrustStoreCertificateChains() {
return this.trustStoreCertificateChains;
}
}
/**
@ -150,15 +157,34 @@ public class SslInfo { @@ -150,15 +157,34 @@ public class SslInfo {
private List<CertificateInfo> extractCertificates(KeyStore keyStore, String alias) {
try {
Certificate[] certificates = keyStore.getCertificateChain(alias);
return (!ObjectUtils.isEmpty(certificates))
? Arrays.stream(certificates).map(CertificateInfo::new).toList() : Collections.emptyList();
List<CertificateInfo> certificates = readCertificateChain(keyStore, alias);
if (certificates != null) {
return certificates;
}
List<CertificateInfo> certificate = readCertificate(keyStore, alias);
if (certificate != null) {
return certificate;
}
return Collections.emptyList();
}
catch (KeyStoreException ex) {
return Collections.emptyList();
}
}
private @Nullable List<CertificateInfo> readCertificate(KeyStore keyStore, String alias)
throws KeyStoreException {
Certificate certificate = keyStore.getCertificate(alias);
return (certificate != null) ? List.of(new CertificateInfo(certificate)) : null;
}
private @Nullable List<CertificateInfo> readCertificateChain(KeyStore keyStore, String alias)
throws KeyStoreException {
Certificate[] certificates = keyStore.getCertificateChain(alias);
return ObjectUtils.isEmpty(certificates) ? null
: Arrays.stream(certificates).map(CertificateInfo::new).toList();
}
public String getAlias() {
return this.alias;
}

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

@ -60,9 +60,9 @@ class SslInfoTests { @@ -60,9 +60,9 @@ class SslInfoTests {
assertThat(bundle.getCertificateChains().get(1).getAlias()).isEqualTo("test-alias");
assertThat(bundle.getCertificateChains().get(1).getCertificates()).hasSize(1);
assertThat(bundle.getCertificateChains().get(2).getAlias()).isEqualTo("spring-boot-cert");
assertThat(bundle.getCertificateChains().get(2).getCertificates()).isEmpty();
assertThat(bundle.getCertificateChains().get(2).getCertificates()).hasSize(1);
assertThat(bundle.getCertificateChains().get(3).getAlias()).isEqualTo("test-alias-cert");
assertThat(bundle.getCertificateChains().get(3).getCertificates()).isEmpty();
assertThat(bundle.getCertificateChains().get(3).getCertificates()).hasSize(1);
CertificateInfo cert1 = bundle.getCertificateChains().get(0).getCertificates().get(0);
assertThat(cert1.getSubject()).isEqualTo("CN=localhost,OU=Spring,O=VMware,L=Palo Alto,ST=California,C=US");
assertThat(cert1.getIssuer()).isEqualTo(cert1.getSubject());
@ -85,6 +85,7 @@ class SslInfoTests { @@ -85,6 +85,7 @@ class SslInfoTests {
assertThat(cert2.getValidity()).isNotNull();
assertThat(cert2.getValidity().getStatus()).isSameAs(Status.VALID);
assertThat(cert2.getValidity().getMessage()).isNull();
assertThat(bundle.getTrustStoreCertificateChains()).isEmpty();
}
@Test
@ -149,7 +150,7 @@ class SslInfoTests { @@ -149,7 +150,7 @@ class SslInfoTests {
.flatMap((bundle) -> bundle.getCertificateChains().stream())
.flatMap((certificateChain) -> certificateChain.getCertificates().stream())
.toList();
assertThat(certs).hasSize(5);
assertThat(certs).hasSize(7);
assertThat(certs).allSatisfy((cert) -> {
assertThat(cert.getSubject()).isEqualTo("CN=localhost,OU=Spring,O=VMware,L=Palo Alto,ST=California,C=US");
assertThat(cert.getIssuer()).isEqualTo(cert.getSubject());
@ -188,6 +189,68 @@ class SslInfoTests { @@ -188,6 +189,68 @@ class SslInfoTests {
SslInfo sslInfo = new SslInfo(sslBundleRegistry, CLOCK);
assertThat(sslInfo.getBundles()).hasSize(1);
assertThat(sslInfo.getBundles().get(0).getCertificateChains()).isEmpty();
assertThat(sslInfo.getBundles().get(0).getTrustStoreCertificateChains()).isEmpty();
}
@Test
@WithPackageResources("test.p12")
void trustStoreCertificatesShouldProvideSslInfo() {
DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry();
JksSslStoreDetails trustStoreDetails = JksSslStoreDetails.forLocation("classpath:test.p12")
.withPassword("secret");
SslStoreBundle sslStoreBundle = new JksSslStoreBundle(null, trustStoreDetails);
sslBundleRegistry.registerBundle("test-trust", SslBundle.of(sslStoreBundle));
SslInfo sslInfo = new SslInfo(sslBundleRegistry, CLOCK);
assertThat(sslInfo.getBundles()).hasSize(1);
BundleInfo bundle = sslInfo.getBundles().get(0);
assertThat(bundle.getName()).isEqualTo("test-trust");
assertThat(bundle.getCertificateChains()).isEmpty();
assertThat(bundle.getTrustStoreCertificateChains()).hasSize(4);
assertThat(bundle.getTrustStoreCertificateChains().get(0).getAlias()).isEqualTo("spring-boot");
assertThat(bundle.getTrustStoreCertificateChains().get(1).getAlias()).isEqualTo("test-alias");
}
@Test
@WithPackageResources("test.p12")
void bothKeyStoreAndTrustStoreCertificatesShouldProvideSslInfo() {
DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry();
JksSslStoreDetails storeDetails = JksSslStoreDetails.forLocation("classpath:test.p12").withPassword("secret");
SslStoreBundle sslStoreBundle = new JksSslStoreBundle(storeDetails, storeDetails);
sslBundleRegistry.registerBundle("test-both", SslBundle.of(sslStoreBundle));
SslInfo sslInfo = new SslInfo(sslBundleRegistry, CLOCK);
assertThat(sslInfo.getBundles()).hasSize(1);
BundleInfo bundle = sslInfo.getBundles().get(0);
assertThat(bundle.getName()).isEqualTo("test-both");
assertThat(bundle.getCertificateChains()).hasSize(4);
assertThat(bundle.getTrustStoreCertificateChains()).hasSize(4);
}
@Test
@WithPackageResources({ "keystore.jks", "truststore.jks" })
void separateKeyStoreAndTrustStoreShouldProvideSslInfo() {
DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry();
JksSslStoreDetails keyStoreDetails = JksSslStoreDetails.forLocation("classpath:keystore.jks")
.withPassword("secret");
JksSslStoreDetails trustStoreDetails = JksSslStoreDetails.forLocation("classpath:truststore.jks")
.withPassword("secret");
SslStoreBundle sslStoreBundle = new JksSslStoreBundle(keyStoreDetails, trustStoreDetails);
sslBundleRegistry.registerBundle("test-separate", SslBundle.of(sslStoreBundle));
SslInfo sslInfo = new SslInfo(sslBundleRegistry, CLOCK);
assertThat(sslInfo.getBundles()).hasSize(1);
BundleInfo bundle = sslInfo.getBundles().get(0);
assertThat(bundle.getName()).isEqualTo("test-separate");
// Keystore has 2 PrivateKeyEntry entries
assertThat(bundle.getCertificateChains()).hasSize(2);
assertThat(bundle.getCertificateChains()).allSatisfy((chain) -> {
assertThat(chain.getCertificates()).hasSize(1);
assertThat(chain.getCertificates().get(0).getSubject()).startsWith("CN=localhost");
});
// Truststore has 3 trustedCertEntry entries
assertThat(bundle.getTrustStoreCertificateChains()).hasSize(3);
assertThat(bundle.getTrustStoreCertificateChains()).allSatisfy((chain) -> {
assertThat(chain.getCertificates()).hasSize(1);
assertThat(chain.getCertificates().get(0).getSubject()).startsWith("CN=localhost");
});
}
private SslInfo createSslInfo(String... locations) {

BIN
core/spring-boot/src/test/resources/org/springframework/boot/info/keystore.jks

Binary file not shown.

BIN
core/spring-boot/src/test/resources/org/springframework/boot/info/truststore.jks

Binary file not shown.

40
documentation/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/docs/info/InfoEndpointDocumentationTests.java

@ -208,6 +208,42 @@ class InfoEndpointDocumentationTests extends MockMvcEndpointDocumentationTests { @@ -208,6 +208,42 @@ class InfoEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
.description("Certificate validity status.")
.type(JsonFieldType.STRING),
fieldWithPath("bundles[].certificateChains[].certificates[].signatureAlgorithmName")
.description("Signature algorithm name.")
.type(JsonFieldType.STRING),
fieldWithPath("bundles[].trustStoreCertificateChains")
.description("Certificate chains in the trust store.")
.type(JsonFieldType.ARRAY),
fieldWithPath("bundles[].trustStoreCertificateChains[].alias")
.description("Alias of the certificate chain.")
.type(JsonFieldType.STRING),
fieldWithPath("bundles[].trustStoreCertificateChains[].certificates")
.description("Certificates in the chain.")
.type(JsonFieldType.ARRAY),
fieldWithPath("bundles[].trustStoreCertificateChains[].certificates[].subject")
.description("Subject of the certificate.")
.type(JsonFieldType.STRING),
fieldWithPath("bundles[].trustStoreCertificateChains[].certificates[].version")
.description("Version of the certificate.")
.type(JsonFieldType.STRING),
fieldWithPath("bundles[].trustStoreCertificateChains[].certificates[].issuer")
.description("Issuer of the certificate.")
.type(JsonFieldType.STRING),
fieldWithPath("bundles[].trustStoreCertificateChains[].certificates[].validityStarts")
.description("Certificate validity start date.")
.type(JsonFieldType.STRING),
fieldWithPath("bundles[].trustStoreCertificateChains[].certificates[].serialNumber")
.description("Serial number of the certificate.")
.type(JsonFieldType.STRING),
fieldWithPath("bundles[].trustStoreCertificateChains[].certificates[].validityEnds")
.description("Certificate validity end date.")
.type(JsonFieldType.STRING),
fieldWithPath("bundles[].trustStoreCertificateChains[].certificates[].validity")
.description("Certificate validity information.")
.type(JsonFieldType.OBJECT),
fieldWithPath("bundles[].trustStoreCertificateChains[].certificates[].validity.status")
.description("Certificate validity status.")
.type(JsonFieldType.STRING),
fieldWithPath("bundles[].trustStoreCertificateChains[].certificates[].signatureAlgorithmName")
.description("Signature algorithm name.")
.type(JsonFieldType.STRING));
}
@ -259,9 +295,9 @@ class InfoEndpointDocumentationTests extends MockMvcEndpointDocumentationTests { @@ -259,9 +295,9 @@ class InfoEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
@Bean
SslInfo sslInfo() {
DefaultSslBundleRegistry sslBundleRegistry = new DefaultSslBundleRegistry();
JksSslStoreDetails keyStoreDetails = JksSslStoreDetails.forLocation("classpath:test.p12")
JksSslStoreDetails storeDetails = JksSslStoreDetails.forLocation("classpath:test.p12")
.withPassword("secret");
SslStoreBundle sslStoreBundle = new JksSslStoreBundle(keyStoreDetails, null);
SslStoreBundle sslStoreBundle = new JksSslStoreBundle(storeDetails, storeDetails);
sslBundleRegistry.registerBundle("test-0", SslBundle.of(sslStoreBundle));
return new SslInfo(sslBundleRegistry);
}

Loading…
Cancel
Save