From 0a935e1b9e99fd97f0a8cc9e8ebe0056922b61b8 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 11 Jun 2021 11:07:31 +0200 Subject: [PATCH] Expose CA chain through CertificateBundle. We now expose access to the CA chain by providing getX509IssuerCertificates(). Additionally, a KeyStore can be created containing the full CA chain. Closes gh-648. --- .../vault/support/CertificateBundle.java | 64 +++++++++++++++++-- .../VaultPkiTemplateIntegrationTests.java | 14 +++- 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/CertificateBundle.java b/spring-vault-core/src/main/java/org/springframework/vault/support/CertificateBundle.java index 7ebc46fb..0f44755c 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/CertificateBundle.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/CertificateBundle.java @@ -18,8 +18,12 @@ package org.springframework.vault.support; import java.io.IOException; import java.security.GeneralSecurityException; import java.security.KeyStore; +import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.security.spec.KeySpec; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import com.fasterxml.jackson.annotation.JsonProperty; @@ -42,12 +46,15 @@ public class CertificateBundle extends Certificate { private final String privateKey; + private final List caChain; + CertificateBundle(@JsonProperty("serial_number") String serialNumber, @JsonProperty("certificate") String certificate, @JsonProperty("issuing_ca") String issuingCaCertificate, - @JsonProperty("private_key") String privateKey) { + @JsonProperty("ca_chain") List caChain, @JsonProperty("private_key") String privateKey) { super(serialNumber, certificate, issuingCaCertificate); this.privateKey = privateKey; + this.caChain = caChain; } /** @@ -67,7 +74,8 @@ public class CertificateBundle extends Certificate { Assert.hasText(issuingCaCertificate, "Issuing CA certificate must not be empty"); Assert.hasText(privateKey, "Private key must not be empty"); - return new CertificateBundle(serialNumber, certificate, issuingCaCertificate, privateKey); + return new CertificateBundle(serialNumber, certificate, issuingCaCertificate, + Collections.singletonList(issuingCaCertificate), privateKey); } /** @@ -102,16 +110,64 @@ public class CertificateBundle extends Certificate { * @return the {@link KeyStore} containing the private key and certificate chain. */ public KeyStore createKeyStore(String keyAlias) { + return createKeyStore(keyAlias, false); + } + + /** + * Create a {@link KeyStore} from this {@link CertificateBundle} containing the + * private key and certificate chain. Only supported if certificate and private key + * are DER-encoded. + * @param keyAlias the key alias to use. + * @param includeCaChain whether to include the certificate authority chain instead of + * just the issuer certificate. + * @return the {@link KeyStore} containing the private key and certificate chain. + * @since 2.3.3 + */ + public KeyStore createKeyStore(String keyAlias, boolean includeCaChain) { Assert.hasText(keyAlias, "Key alias must not be empty"); try { - return KeystoreUtil.createKeyStore(keyAlias, getPrivateKeySpec(), getX509Certificate(), - getX509IssuerCertificate()); + + List certificates = new ArrayList<>(); + certificates.add(getX509Certificate()); + + if (includeCaChain) { + certificates.addAll(getX509IssuerCertificates()); + } + else { + certificates.add(getX509IssuerCertificate()); + } + + return KeystoreUtil.createKeyStore(keyAlias, getPrivateKeySpec(), + certificates.toArray(new X509Certificate[0])); } catch (GeneralSecurityException | IOException e) { throw new VaultException("Cannot create KeyStore", e); } } + /** + * Retrieve the issuing CA certificates as list of {@link X509Certificate}. Only + * supported if certificates are DER-encoded. + * @return the issuing CA {@link X509Certificate}. + * @since 2.3.3 + */ + public List getX509IssuerCertificates() { + + List certificates = new ArrayList<>(); + + for (String data : caChain) { + try { + byte[] bytes = Base64Utils.decodeFromString(data); + certificates.add(KeystoreUtil.getCertificate(bytes)); + } + catch (CertificateException e) { + throw new VaultException("Cannot create Certificate from issuing CA certificate", e); + } + } + + return certificates; + } + } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultPkiTemplateIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultPkiTemplateIntegrationTests.java index 55a02404..786da55c 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultPkiTemplateIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultPkiTemplateIntegrationTests.java @@ -18,6 +18,8 @@ package org.springframework.vault.core; import java.io.File; import java.io.InputStream; import java.math.BigInteger; +import java.security.KeyStore; +import java.security.KeyStoreException; import java.security.cert.CertificateFactory; import java.security.cert.X509CRL; import java.security.cert.X509Certificate; @@ -78,10 +80,11 @@ class VaultPkiTemplateIntegrationTests extends IntegrationTestSupport { } File workDir = findWorkDir(new File(System.getProperty("user.dir"))); + String caCert = Files.contentOf(new File(workDir, "ca/certs/ca.cert.pem"), "US-ASCII"); String cert = Files.contentOf(new File(workDir, "ca/certs/intermediate.cert.pem"), "US-ASCII"); String key = Files.contentOf(new File(workDir, "ca/private/intermediate.decrypted.key.pem"), "US-ASCII"); - Map pembundle = Collections.singletonMap("pem_bundle", cert + key); + Map pembundle = Collections.singletonMap("pem_bundle", cert + key + caCert); this.vaultOperations.write("pki/config/ca", pembundle); @@ -96,7 +99,7 @@ class VaultPkiTemplateIntegrationTests extends IntegrationTestSupport { } @Test - void issueCertificateShouldCreateCertificate() { + void issueCertificateShouldCreateCertificate() throws KeyStoreException { VaultCertificateRequest request = VaultCertificateRequest.create("hello.example.com"); @@ -109,6 +112,13 @@ class VaultPkiTemplateIntegrationTests extends IntegrationTestSupport { assertThat(data.getIssuingCaCertificate()).isNotEmpty(); assertThat(data.getSerialNumber()).isNotEmpty(); assertThat(data.getX509Certificate().getSubjectX500Principal().getName()).isEqualTo("CN=hello.example.com"); + assertThat(data.getX509IssuerCertificates()).hasSize(2); + + KeyStore keyStore = data.createKeyStore("vault"); + assertThat(keyStore.getCertificateChain("vault")).hasSize(2); + + KeyStore keyStoreWithCaChain = data.createKeyStore("vault", true); + assertThat(keyStoreWithCaChain.getCertificateChain("vault")).hasSize(3); } @Test