From b30d81fa430b196f21c027d3eced8007c4b9a6bf Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 19 Oct 2017 08:16:16 +0200 Subject: [PATCH] Support CSR signing, certificate revocation and CRL retrieval. We now support CSR signing, certificate revocation and CRL retrieval via VaultPkiTemplate. Closes gh-125. --- .../vault/core/VaultPkiOperations.java | 54 ++++++- .../vault/core/VaultPkiTemplate.java | 121 +++++++++++--- .../vault/support/Certificate.java | 147 ++++++++++++++++++ .../vault/support/CertificateBundle.java | 71 +-------- .../vault/support/KeystoreUtil.java | 23 +++ .../VaultSignCertificateRequestResponse.java | 25 +++ .../VaultPkiTemplateIntegrationTests.java | 87 ++++++++++- .../support/CertificateBundleUnitTests.java | 21 +-- .../vault/support/CertificateUnitTests.java | 72 +++++++++ src/main/asciidoc/new-features.adoc | 5 +- 10 files changed, 514 insertions(+), 112 deletions(-) create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/support/Certificate.java create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/support/VaultSignCertificateRequestResponse.java create mode 100644 spring-vault-core/src/test/java/org/springframework/vault/support/CertificateUnitTests.java diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultPkiOperations.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultPkiOperations.java index 7847a693..7fbed1f3 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultPkiOperations.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultPkiOperations.java @@ -15,10 +15,13 @@ */ package org.springframework.vault.core; +import java.io.InputStream; + import org.springframework.vault.VaultException; import org.springframework.vault.support.CertificateBundle; import org.springframework.vault.support.VaultCertificateRequest; import org.springframework.vault.support.VaultCertificateResponse; +import org.springframework.vault.support.VaultSignCertificateRequestResponse; /** * Interface that specifies PKI backend-related operations. @@ -29,7 +32,7 @@ import org.springframework.vault.support.VaultCertificateResponse; * private key and CSR, submitting to a CA, and waiting for a verification and signing * process to complete. Vault's built-in authentication and authorization mechanisms * provide the verification functionality. - * + * * @author Mark Paluch * @see https://www.vaultproject.io/docs/secrets/pki/index.html @@ -52,4 +55,53 @@ public interface VaultPkiOperations { */ VaultCertificateResponse issueCertificate(String roleName, VaultCertificateRequest certificateRequest) throws VaultException; + + /** + * Signs a CSR using Vault's PKI backend given a {@code roleName}, {@code csr} and + * {@link VaultCertificateRequest}. The issuing CA certificate is returned as well, so + * that only the root CA need be in a client's trust store. Certificates use DER + * format and are base64 encoded. + * + * @param roleName must not be empty or {@literal null}. + * @param csr must not be empty or {@literal null}. + * @param certificateRequest must not be {@literal null}. + * @return the {@link VaultCertificateResponse} containing a + * {@link org.springframework.vault.support.Certificate} . + * @since 2.0 + * @see POST + * /pki/sign/[role name] + */ + VaultSignCertificateRequestResponse signCertificateRequest(String roleName, + String csr, VaultCertificateRequest certificateRequest) throws VaultException; + + /** + * Revokes a certificate using its serial number. This is an alternative option to the + * standard method of revoking using Vault lease IDs. A successful revocation will + * rotate the CRL + * + * @param serialNumber must not be empty or {@literal null}. + * @since 2.0 + * @see POST + * /pki/revoke + */ + void revoke(String serialNumber) throws VaultException; + + /** + * Retrieves the current CRL in raw form. This endpoint is suitable for usage in the + * CRL distribution points extension in a CA certificate. This is a bare endpoint that + * does not return a standard Vault data structure. Returns data {@link Encoding#DER} + * or {@link Encoding#PEM} encoded. + * + * @return {@link java.io.InputStream} containing the encoded CRL. + * @since 2.0 + * @see GET + * /pki/crl + */ + InputStream getCrl(Encoding encoding) throws VaultException; + + enum Encoding { + DER, PEM, + } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultPkiTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultPkiTemplate.java index f7c5a2d1..06d5b0dd 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultPkiTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultPkiTemplate.java @@ -15,15 +15,20 @@ */ package org.springframework.vault.core; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.Collections; import java.util.HashMap; import java.util.Map; +import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.vault.VaultException; import org.springframework.vault.client.VaultResponses; import org.springframework.vault.support.VaultCertificateRequest; import org.springframework.vault.support.VaultCertificateResponse; +import org.springframework.vault.support.VaultSignCertificateRequestResponse; import org.springframework.web.client.HttpStatusCodeException; /** @@ -54,13 +59,106 @@ public class VaultPkiTemplate implements VaultPkiOperations { } @Override - public VaultCertificateResponse issueCertificate(final String roleName, + public VaultCertificateResponse issueCertificate(String roleName, VaultCertificateRequest certificateRequest) throws VaultException { Assert.hasText(roleName, "Role name must not be empty"); Assert.notNull(certificateRequest, "Certificate request must not be null"); - final Map request = new HashMap<>(); + return requestCertificate(roleName, "{path}/issue/{roleName}", + createIssueRequest(certificateRequest), VaultCertificateResponse.class); + } + + @Override + public VaultSignCertificateRequestResponse signCertificateRequest(String roleName, + String csr, VaultCertificateRequest certificateRequest) throws VaultException { + + Assert.hasText(roleName, "Role name must not be empty"); + Assert.hasText(csr, "CSR name must not be empty"); + Assert.notNull(certificateRequest, "Certificate request must not be null"); + + Map body = createIssueRequest(certificateRequest); + body.put("csr", csr); + + return requestCertificate(roleName, "{path}/sign/{roleName}", body, + VaultSignCertificateRequestResponse.class); + } + + private T requestCertificate(String roleName, String requestPath, + Map request, Class responseType) { + + request.put("format", "der"); + + T response = vaultOperations.doWithSession(restOperations -> { + + try { + return restOperations.postForObject(requestPath, request, responseType, + path, roleName); + } + catch (HttpStatusCodeException e) { + throw VaultResponses.buildException(e); + } + }); + + Assert.state(response != null, "VaultCertificateResponse must not be null"); + + return response; + } + + @Override + public void revoke(String serialNumber) throws VaultException { + + Assert.hasText(serialNumber, "Serial number must not be null or empty"); + + vaultOperations.doWithSession(restOperations -> { + + try { + restOperations.postForObject("{path}/revoke", + Collections.singletonMap("serial_number", serialNumber), + Map.class, path); + + return null; + } + catch (HttpStatusCodeException e) { + throw VaultResponses.buildException(e); + } + }); + } + + @Override + public InputStream getCrl(Encoding encoding) throws VaultException { + + Assert.notNull(encoding, "Encoding must not be null"); + + return vaultOperations.doWithSession(restOperations -> { + + String requestPath = encoding == Encoding.DER ? "{path}/crl" + : "{path}/crl/pem"; + try { + ResponseEntity response = restOperations.getForEntity( + requestPath, byte[].class, path); + + return new ByteArrayInputStream(response.getBody()); + } + catch (HttpStatusCodeException e) { + throw VaultResponses.buildException(e); + } + }); + } + + /** + * Create a request body stub for {@code pki/issue} and {@code pki/sign} from + * {@link VaultCertificateRequest}. + * + * @param certificateRequest must not be {@literal null}. + * @return the body as {@link Map}. + */ + private static Map createIssueRequest( + VaultCertificateRequest certificateRequest) { + + Assert.notNull(certificateRequest, "Certificate request must not be null"); + + Map request = new HashMap<>(); request.put("common_name", certificateRequest.getCommonName()); if (!certificateRequest.getAltNames().isEmpty()) { @@ -81,26 +179,9 @@ public class VaultPkiTemplate implements VaultPkiOperations { request.put("ttl", certificateRequest.getTtl()); } - request.put("format", "der"); - if (certificateRequest.isExcludeCommonNameFromSubjectAltNames()) { request.put("exclude_cn_from_sans", true); } - - VaultCertificateResponse response = vaultOperations - .doWithSession(restOperations -> { - - try { - return restOperations.postForObject("{path}/issue/{roleName}", - request, VaultCertificateResponse.class, path, roleName); - } - catch (HttpStatusCodeException e) { - throw VaultResponses.buildException(e); - } - }); - - Assert.state(response != null, "VaultCertificateResponse must not be null"); - - return response; + return request; } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/Certificate.java b/spring-vault-core/src/main/java/org/springframework/vault/support/Certificate.java new file mode 100644 index 00000000..dc747aa8 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/Certificate.java @@ -0,0 +1,147 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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 com.fasterxml.jackson.annotation.JsonProperty; + +import org.springframework.util.Assert; +import org.springframework.vault.VaultException; + +/** + * Value object representing a certificate consisting of the certificate and the issuer + * certificate. Certificate and keys can be either DER or PEM encoded. DER-encoded + * certificates can be converted to a {@link X509Certificate}. + * + * @author Mark Paluch + * @since 2.0 + * @see #getX509Certificate() + * @see #getIssuingCaCertificate() + */ +public class Certificate { + + private final String serialNumber; + + private final String certificate; + + private final String issuingCaCertificate; + + Certificate(@JsonProperty("serial_number") String serialNumber, + @JsonProperty("certificate") String certificate, + @JsonProperty("issuing_ca") String issuingCaCertificate) { + + this.serialNumber = serialNumber; + this.certificate = certificate; + this.issuingCaCertificate = issuingCaCertificate; + } + + /** + * Create a {@link Certificate} given a private key with certificates and the serial + * number. + * + * @param serialNumber must not be empty or {@literal null}. + * @param certificate must not be empty or {@literal null}. + * @param issuingCaCertificate must not be empty or {@literal null}. + * @return the {@link Certificate} + */ + public static Certificate of(String serialNumber, String certificate, + String issuingCaCertificate) { + + Assert.hasText(serialNumber, "Serial number must not be empty"); + Assert.hasText(certificate, "Certificate must not be empty"); + Assert.hasText(issuingCaCertificate, "Issuing CA certificate must not be empty"); + + return new Certificate(serialNumber, certificate, issuingCaCertificate); + } + + /** + * @return the serial number. + */ + public String getSerialNumber() { + return this.serialNumber; + } + + /** + * @return encoded certificate (PEM or DER-encoded). + */ + public String getCertificate() { + return this.certificate; + } + + /** + * @return encoded certificate of the issuing CA (PEM or DER-encoded). + */ + public String getIssuingCaCertificate() { + return this.issuingCaCertificate; + } + + /** + * Retrieve the certificate as {@link X509Certificate}. Only supported if certificate + * is DER-encoded. + * + * @return the {@link X509Certificate}. + */ + public X509Certificate getX509Certificate() { + + try { + byte[] bytes = Base64.decode(getCertificate()); + return KeystoreUtil.getCertificate(bytes); + } + catch (IOException | CertificateException e) { + throw new VaultException("Cannot create Certificate from certificate", e); + } + } + + /** + * Retrieve the issuing CA certificate as {@link X509Certificate}. Only supported if + * certificate is DER-encoded. + * + * @return the issuing CA {@link X509Certificate}. + */ + public X509Certificate getX509IssuerCertificate() { + + try { + byte[] bytes = Base64.decode(getIssuingCaCertificate()); + return KeystoreUtil.getCertificate(bytes); + } + catch (IOException | CertificateException e) { + throw new VaultException( + "Cannot create Certificate from issuing CA certificate", e); + } + } + + /** + * Create a trust store as {@link KeyStore} from this {@link Certificate} containing + * the certificate chain. Only supported if certificate is DER-encoded. + * + * @return the {@link KeyStore} containing the private key and certificate chain. + */ + public KeyStore createTrustStore() { + + try { + return KeystoreUtil.createKeyStore(getX509Certificate(), + getX509IssuerCertificate()); + } + catch (GeneralSecurityException | IOException e) { + throw new VaultException("Cannot create KeyStore", e); + } + } +} 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 f0589a09..db2af576 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,7 +18,6 @@ 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; @@ -38,24 +37,16 @@ import org.springframework.vault.VaultException; * @see #getX509Certificate() * @see #getIssuingCaCertificate() */ -public class CertificateBundle { - - private final String serialNumber; - - private final String certificate; - - private final String issuingCaCertificate; +public class CertificateBundle extends Certificate { private final String privateKey; - private CertificateBundle(@JsonProperty("serial_number") String serialNumber, + CertificateBundle(@JsonProperty("serial_number") String serialNumber, @JsonProperty("certificate") String certificate, @JsonProperty("issuing_ca") String issuingCaCertificate, @JsonProperty("private_key") String privateKey) { - this.serialNumber = serialNumber; - this.certificate = certificate; - this.issuingCaCertificate = issuingCaCertificate; + super(serialNumber, certificate, issuingCaCertificate); this.privateKey = privateKey; } @@ -81,27 +72,6 @@ public class CertificateBundle { privateKey); } - /** - * @return the serial number. - */ - public String getSerialNumber() { - return this.serialNumber; - } - - /** - * @return encoded certificate (PEM or DER-encoded). - */ - public String getCertificate() { - return this.certificate; - } - - /** - * @return encoded certificate of the issuing CA (PEM or DER-encoded). - */ - public String getIssuingCaCertificate() { - return this.issuingCaCertificate; - } - /** * @return the private key (decrypted form, PEM or DER-encoded) */ @@ -127,41 +97,6 @@ public class CertificateBundle { } } - /** - * Retrieve the certificate as {@link X509Certificate}. Only supported if certificate - * is DER-encoded. - * - * @return the {@link X509Certificate}. - */ - public X509Certificate getX509Certificate() { - - try { - byte[] bytes = Base64.decode(getCertificate()); - return KeystoreUtil.getCertificate(bytes); - } - catch (IOException | CertificateException e) { - throw new VaultException("Cannot create Certificate from certificate", e); - } - } - - /** - * Retrieve the issuing CA certificate as {@link X509Certificate}. Only supported if - * certificate is DER-encoded. - * - * @return the issuing CA {@link X509Certificate}. - */ - public X509Certificate getX509IssuerCertificate() { - - try { - byte[] bytes = Base64.decode(getIssuingCaCertificate()); - return KeystoreUtil.getCertificate(bytes); - } - catch (IOException | CertificateException e) { - throw new VaultException( - "Cannot create Certificate from issuing CA certificate", e); - } - } - /** * Create a {@link KeyStore} from this {@link CertificateBundle} containing the * private key and certificate chain. Only supported if certificate and private key diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/KeystoreUtil.java b/spring-vault-core/src/main/java/org/springframework/vault/support/KeystoreUtil.java index 7dfcf634..af65ccd6 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/KeystoreUtil.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/KeystoreUtil.java @@ -67,6 +67,29 @@ class KeystoreUtil { return keyStore; } + /** + * Create a {@link KeyStore} containing the {@link X509Certificate certificates} + * stored with as {@code cert_0, cert_1...cert_N}. + * + * @param certificates + * @return + * @throws GeneralSecurityException + * @throws IOException + * @since 2.0 + */ + static KeyStore createKeyStore(X509Certificate... certificates) + throws GeneralSecurityException, IOException { + + KeyStore keyStore = createKeyStore(); + + int counter = 0; + for (X509Certificate certificate : certificates) { + keyStore.setCertificateEntry(String.format("cert_%d", counter++), certificate); + } + + return keyStore; + } + static X509Certificate getCertificate(byte[] source) throws CertificateException, IOException { diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultSignCertificateRequestResponse.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultSignCertificateRequestResponse.java new file mode 100644 index 00000000..4d4317df --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultSignCertificateRequestResponse.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.support; + +/** + * Value object to bind Vault HTTP PKI issue certificate API responses. + * + * @author Mark Paluch + */ +public class VaultSignCertificateRequestResponse extends + VaultResponseSupport { +} 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 74e3d0bc..19840535 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 @@ -16,6 +16,10 @@ package org.springframework.vault.core; import java.io.File; +import java.io.InputStream; +import java.math.BigInteger; +import java.security.cert.CertificateFactory; +import java.security.cert.X509CRL; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -28,10 +32,14 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.StreamUtils; import org.springframework.vault.VaultException; +import org.springframework.vault.core.VaultPkiOperations.Encoding; +import org.springframework.vault.support.Certificate; import org.springframework.vault.support.CertificateBundle; import org.springframework.vault.support.VaultCertificateRequest; import org.springframework.vault.support.VaultCertificateResponse; +import org.springframework.vault.support.VaultSignCertificateRequestResponse; import org.springframework.vault.util.IntegrationTestSupport; import static org.assertj.core.api.Assertions.assertThat; @@ -52,7 +60,7 @@ public class VaultPkiTemplateIntegrationTests extends IntegrationTestSupport { private VaultPkiOperations pkiOperations; @Before - public void before() throws Exception { + public void before() { pkiOperations = vaultOperations.opsForPki(); @@ -100,6 +108,43 @@ public class VaultPkiTemplateIntegrationTests extends IntegrationTestSupport { .isEqualTo("CN=hello.example.com"); } + @Test + public void signShouldSignCsr() { + + String csr = "-----BEGIN CERTIFICATE REQUEST-----\n" + + "MIICzTCCAbUCAQAwgYcxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpTb21lLVN0YXRl\n" + + "MRUwEwYDVQQHEwxTYW4gVmF1bHRpbm8xFTATBgNVBAoTDFNwcmluZyBWYXVsdDEY\n" + + "MBYGA1UEAxMPY3NyLmV4YW1wbGUuY29tMRswGQYJKoZIhvcNAQkBFgxzcHJpbmdA\n" + + "dmF1bHQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVlDBT1gAONIp4\n" + + "GQQ7BWDeqNzlscWqu5oQyfvw6oNFZzYWGVTgX/n72biv8d1Wx30MWpVYhbL0mk9m\n" + + "Uu15elMZHPb4F4bk8VDSiB9527SwAd/QpkNC1RsPp2h6g2LvGPJ2eidHSlLtF2To\n" + + "A4i6z0K0++nvYKSf9Af0sod2Z51xc9uPj/oN5z/8BQuGoCBpxJqgl7N/csMICixY\n" + + "2fQcCUbdPPqE9INIInUHe3mPE/yvxko9aYGZ5jnrdZyiQaRRKBdWpvbRLKXQ78Fz\n" + + "vXR3G33yn9JAN6wl1A916DiXzy2xHT19vyAn1hBUj2M6KFXChQ30oxTyTOqHCMLP\n" + + "m/BSEOsPAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAYFssueiUh3YGxnXcQ4dp\n" + + "ZqVWeVyOuGGaFJ4BA0drwJ9Mt/iNmPUTGE2oBNnh2R7e7HwGcNysFHZZOZBEQ0Hh\n" + + "Vn93GO7cfaTOetK0VtDqis1VFQD0eVPWf5s6UqT/+XGrFRhwJ9hM+2FQSrUDFecs\n" + + "+/605n1rD7qOj3vkGrtwvEUrxyRaQaKpPLHmVHENqV6F1NsO3Z27f2FWWAZF2VKN\n" + + "cCQQJNc//DbIN3J3JSElpIDBDHctoBoQVnMiwpCbSA+CaAtlWYJKnAfhTKeqnNMy\n" + + "qf3ACZ+1sBIuqSP7dEJ2KfIezaCPQ88+PAloRB52LFa+iq3yI7F5VzkwAvQFnTi+\n" + + "cQ==\n" + "-----END CERTIFICATE REQUEST-----"; + + VaultCertificateRequest request = VaultCertificateRequest + .create("hello.example.com"); + + VaultSignCertificateRequestResponse certificateResponse = pkiOperations + .signCertificateRequest("testrole", csr, request); + + Certificate data = certificateResponse.getData(); + + assertThat(data.getCertificate()).isNotEmpty(); + assertThat(data.getIssuingCaCertificate()).isNotEmpty(); + assertThat(data.getSerialNumber()).isNotEmpty(); + assertThat(data.getX509Certificate().getSubjectX500Principal().getName()) + .isEqualTo("CN=csr.example.com"); + assertThat(data.createTrustStore()).isNotNull(); + } + @Test(expected = VaultException.class) public void issueCertificateFail() { @@ -107,4 +152,44 @@ public class VaultPkiTemplateIntegrationTests extends IntegrationTestSupport { pkiOperations.issueCertificate("testrole", request); } + + @Test + public void shouldRevokeCertificate() throws Exception { + + VaultCertificateRequest request = VaultCertificateRequest + .create("foo.example.com"); + + VaultCertificateResponse certificateResponse = pkiOperations.issueCertificate( + "testrole", request); + + BigInteger serial = new BigInteger(certificateResponse.getData() + .getSerialNumber().replaceAll("\\:", ""), 16); + pkiOperations.revoke(certificateResponse.getData().getSerialNumber()); + + try (InputStream in = pkiOperations.getCrl(Encoding.DER)) { + + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + + X509CRL crl = (X509CRL) cf.generateCRL(in); + + assertThat(crl.getRevokedCertificate(serial)).isNotNull(); + } + } + + @Test + public void shouldReturnCrl() throws Exception { + + try (InputStream in = pkiOperations.getCrl(Encoding.DER)) { + + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + + assertThat(cf.generateCRL(in)).isInstanceOf(X509CRL.class); + } + + try (InputStream crl = pkiOperations.getCrl(Encoding.PEM)) { + + byte[] bytes = StreamUtils.copyToByteArray(crl); + assertThat(bytes).isNotEmpty(); + } + } } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/support/CertificateBundleUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/support/CertificateBundleUnitTests.java index b6f2b174..a729a816 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/support/CertificateBundleUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/support/CertificateBundleUnitTests.java @@ -18,7 +18,6 @@ package org.springframework.vault.support; import java.security.KeyFactory; import java.security.KeyStore; import java.security.PrivateKey; -import java.security.cert.X509Certificate; import java.util.Map; import com.fasterxml.jackson.databind.ObjectMapper; @@ -29,7 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link CertificateBundle}. - * + * * @author Mark Paluch */ public class CertificateBundleUnitTests { @@ -56,24 +55,6 @@ public class CertificateBundleUnitTests { assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); } - @Test - public void getX509CertificateShouldReturnCertificate() throws Exception { - - X509Certificate x509Certificate = certificateBundle.getX509Certificate(); - - assertThat(x509Certificate.getSubjectDN().getName()).isEqualTo( - "CN=hello.example.com"); - } - - @Test - public void getX509IssuerCertificateShouldReturnCertificate() throws Exception { - - X509Certificate x509Certificate = certificateBundle.getX509IssuerCertificate(); - - assertThat(x509Certificate.getSubjectDN().getName()).startsWith( - "CN=Intermediate CA Certificate"); - } - @Test public void getAsKeystore() throws Exception { diff --git a/spring-vault-core/src/test/java/org/springframework/vault/support/CertificateUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/support/CertificateUnitTests.java new file mode 100644 index 00000000..0458d906 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/support/CertificateUnitTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.support; + +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link Certificate}. + * + * @author Mark Paluch + */ +public class CertificateUnitTests { + + Certificate certificate; + + @SuppressWarnings("unchecked") + @Before + public void before() throws Exception { + Map data = new ObjectMapper().readValue( + getClass().getResource("/certificate.json"), Map.class); + + certificate = Certificate.of(data.get("serial_number"), data.get("certificate"), + data.get("issuing_ca")); + } + + @Test + public void getX509CertificateShouldReturnCertificate() { + + X509Certificate x509Certificate = certificate.getX509Certificate(); + + assertThat(x509Certificate.getSubjectDN().getName()).isEqualTo( + "CN=hello.example.com"); + } + + @Test + public void getX509IssuerCertificateShouldReturnCertificate() { + + X509Certificate x509Certificate = certificate.getX509IssuerCertificate(); + + assertThat(x509Certificate.getSubjectDN().getName()).startsWith( + "CN=Intermediate CA Certificate"); + } + + @Test + public void getAsTrustStore() throws Exception { + + KeyStore keyStore = certificate.createTrustStore(); + + assertThat(keyStore.size()).isEqualTo(2); + } +} diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 253131b5..6960c799 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -9,15 +9,16 @@ * <> based on Spring Data KeyValue. * Transit batch encrypt and decrypt support. * Policy management for policies stored as JSON. +* Support CSR signing, certificate revocation and CRL retrieval. [[new-features.1-1-0]] === What's new in Spring Vault 1.1.0 * <>. -* Configuration of encryption/decryption versions for transit keys. +* Configuration of encryption/decryption versions for transit keys. * Pull mode for <>. * Transit batch encrypt and decrypt support. -* TTL-based generic secret rotation. +* TTL-based generic secret rotation. [[new-features.1-0-0]] === What's new in Spring Vault 1.0