mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-17 12:09:16 +00:00
Show certificates from truststore in SSL info endpoint
See gh-48967 Signed-off-by: Garvit Joshi <garvitjoshi9@gmail.com>
This commit is contained in:
committed by
Moritz Halbritter
parent
630643777c
commit
e1a1d0e233
@@ -104,9 +104,12 @@ public class SslInfo {
|
||||
|
||||
private final List<CertificateChainInfo> certificateChains;
|
||||
|
||||
private final List<CertificateChainInfo> trustStoreCertificates;
|
||||
|
||||
private BundleInfo(String name, SslBundle sslBundle) {
|
||||
this.name = name;
|
||||
this.certificateChains = extractCertificateChains(sslBundle.getStores().getKeyStore());
|
||||
this.trustStoreCertificates = extractCertificateChains(sslBundle.getStores().getTrustStore());
|
||||
}
|
||||
|
||||
private List<CertificateChainInfo> extractCertificateChains(@Nullable KeyStore keyStore) {
|
||||
@@ -132,6 +135,10 @@ public class SslInfo {
|
||||
return this.certificateChains;
|
||||
}
|
||||
|
||||
public List<CertificateChainInfo> getTrustStoreCertificates() {
|
||||
return this.trustStoreCertificates;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,9 +157,17 @@ public class SslInfo {
|
||||
|
||||
private List<CertificateInfo> extractCertificates(KeyStore keyStore, String alias) {
|
||||
try {
|
||||
// First try to get the certificate chain (works for PrivateKeyEntry)
|
||||
Certificate[] certificates = keyStore.getCertificateChain(alias);
|
||||
return (!ObjectUtils.isEmpty(certificates))
|
||||
? Arrays.stream(certificates).map(CertificateInfo::new).toList() : Collections.emptyList();
|
||||
if (!ObjectUtils.isEmpty(certificates)) {
|
||||
return Arrays.stream(certificates).map(CertificateInfo::new).toList();
|
||||
}
|
||||
// Fall back to single certificate (works for trustedCertEntry)
|
||||
Certificate certificate = keyStore.getCertificate(alias);
|
||||
if (certificate != null) {
|
||||
return List.of(new CertificateInfo(certificate));
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
catch (KeyStoreException ex) {
|
||||
return Collections.emptyList();
|
||||
|
||||
@@ -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 {
|
||||
assertThat(cert2.getValidity()).isNotNull();
|
||||
assertThat(cert2.getValidity().getStatus()).isSameAs(Status.VALID);
|
||||
assertThat(cert2.getValidity().getMessage()).isNull();
|
||||
assertThat(bundle.getTrustStoreCertificates()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -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 {
|
||||
SslInfo sslInfo = new SslInfo(sslBundleRegistry, CLOCK);
|
||||
assertThat(sslInfo.getBundles()).hasSize(1);
|
||||
assertThat(sslInfo.getBundles().get(0).getCertificateChains()).isEmpty();
|
||||
assertThat(sslInfo.getBundles().get(0).getTrustStoreCertificates()).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.getTrustStoreCertificates()).hasSize(4);
|
||||
assertThat(bundle.getTrustStoreCertificates().get(0).getAlias()).isEqualTo("spring-boot");
|
||||
assertThat(bundle.getTrustStoreCertificates().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.getTrustStoreCertificates()).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.getTrustStoreCertificates()).hasSize(3);
|
||||
assertThat(bundle.getTrustStoreCertificates()).allSatisfy((chain) -> {
|
||||
assertThat(chain.getCertificates()).hasSize(1);
|
||||
assertThat(chain.getCertificates().get(0).getSubject()).startsWith("CN=localhost");
|
||||
});
|
||||
}
|
||||
|
||||
private SslInfo createSslInfo(String... locations) {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+36
-2
@@ -208,6 +208,40 @@ 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[].trustStoreCertificates").description("Certificate chains in the trust store.")
|
||||
.type(JsonFieldType.ARRAY),
|
||||
fieldWithPath("bundles[].trustStoreCertificates[].alias").description("Alias of the certificate chain.")
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("bundles[].trustStoreCertificates[].certificates")
|
||||
.description("Certificates in the chain.")
|
||||
.type(JsonFieldType.ARRAY),
|
||||
fieldWithPath("bundles[].trustStoreCertificates[].certificates[].subject")
|
||||
.description("Subject of the certificate.")
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("bundles[].trustStoreCertificates[].certificates[].version")
|
||||
.description("Version of the certificate.")
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("bundles[].trustStoreCertificates[].certificates[].issuer")
|
||||
.description("Issuer of the certificate.")
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("bundles[].trustStoreCertificates[].certificates[].validityStarts")
|
||||
.description("Certificate validity start date.")
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("bundles[].trustStoreCertificates[].certificates[].serialNumber")
|
||||
.description("Serial number of the certificate.")
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("bundles[].trustStoreCertificates[].certificates[].validityEnds")
|
||||
.description("Certificate validity end date.")
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("bundles[].trustStoreCertificates[].certificates[].validity")
|
||||
.description("Certificate validity information.")
|
||||
.type(JsonFieldType.OBJECT),
|
||||
fieldWithPath("bundles[].trustStoreCertificates[].certificates[].validity.status")
|
||||
.description("Certificate validity status.")
|
||||
.type(JsonFieldType.STRING),
|
||||
fieldWithPath("bundles[].trustStoreCertificates[].certificates[].signatureAlgorithmName")
|
||||
.description("Signature algorithm name.")
|
||||
.type(JsonFieldType.STRING));
|
||||
}
|
||||
@@ -259,9 +293,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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user