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.
This commit is contained in:
Mark Paluch
2021-06-11 11:07:31 +02:00
parent 9fef7ef9aa
commit 0a935e1b9e
2 changed files with 72 additions and 6 deletions

View File

@@ -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<String> 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<String> 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<X509Certificate> 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<X509Certificate> getX509IssuerCertificates() {
List<X509Certificate> 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;
}
}

View File

@@ -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<String, String> pembundle = Collections.singletonMap("pem_bundle", cert + key);
Map<String, String> 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