From 8d7292c049855ea38946cdddd01cd321a826feae Mon Sep 17 00:00:00 2001 From: Nanne Baars Date: Fri, 29 Sep 2023 14:50:13 +0200 Subject: [PATCH] Add `notAfter` and `userIds` to the certificate request. Closes: gh-477 Original pull request: gh-820 --- .../vault/core/VaultPkiOperations.java | 27 ++++ .../vault/core/VaultPkiTemplate.java | 46 ++++++- .../vault/support/Certificate.java | 99 +++++++++++++- .../vault/support/CertificateBundle.java | 60 ++++---- .../support/VaultCertificateRequest.java | 70 +++++++++- ...VaultIssuerCertificateRequestResponse.java | 25 ++++ .../VaultPkiTemplateIntegrationTests.java | 129 +++++++++++++++++- .../vault/support/CertificateUnitTests.java | 4 +- 8 files changed, 422 insertions(+), 38 deletions(-) create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/support/VaultIssuerCertificateRequestResponse.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 78c2efa4..a7d99f2a 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 @@ -22,6 +22,7 @@ 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.VaultIssuerCertificateRequestResponse; import org.springframework.vault.support.VaultSignCertificateRequestResponse; /** @@ -108,4 +109,30 @@ public interface VaultPkiOperations { } + /** + * Retrieves the specified issuer's certificate. Includes the full ca_chain of the + * issuer. + * @param issuer reference to an existing issuer, either by Vault-generated + * identifier, or the name assigned to an issuer. Pass the literal string 'default' to + * refer to the currently configured issuer. + * @return the {@link VaultIssuerCertificateRequestResponse} containing a + * {@link org.springframework.vault.support.Certificate} + * @see GET * + * /pki/issuer/:issuer_ref/json + * + */ + VaultIssuerCertificateRequestResponse getIssuerCertificate(String issuer) throws VaultException; + + /** + * Retrieves the specified issuer's certificate. Includes the full ca_chain of the + * issuer. + * @return {@link java.io.InputStream} containing the encoded certificate or + * {@literal null} + * @see GET + * /pki/issuer/:issuer_ref/{der, pem} + */ + InputStream getIssuerCertificate(String issuer, Encoding encoding) throws VaultException; + } 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 d21eadb5..2c2fde2f 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 @@ -21,8 +21,6 @@ import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.HashMap; import java.util.Map; - -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -30,6 +28,7 @@ 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.VaultIssuerCertificateRequestResponse; import org.springframework.vault.support.VaultSignCertificateRequestResponse; import org.springframework.web.client.HttpStatusCodeException; @@ -147,6 +146,47 @@ public class VaultPkiTemplate implements VaultPkiOperations { }); } + @Override + public VaultIssuerCertificateRequestResponse getIssuerCertificate(String issuer) throws VaultException { + + Assert.hasText(issuer, "Issuer must not be empty"); + + return this.vaultOperations.doWithSession(restOperations -> { + + try { + return restOperations.getForObject("{path}/issuer/{issuer}/json", + VaultIssuerCertificateRequestResponse.class, this.path, issuer); + } + catch (HttpStatusCodeException e) { + throw VaultResponses.buildException(e); + } + }); + } + + @Override + public InputStream getIssuerCertificate(String issuer, Encoding encoding) throws VaultException { + Assert.hasText(issuer, "Issuer must not be empty"); + Assert.notNull(encoding, "Encoding must not be null"); + + return this.vaultOperations.doWithSession(restOperations -> { + + String requestPath = encoding == Encoding.DER ? "{path}/issuer/{issuer}/der" : "{path}/issuer/{issuer}/pem"; + try { + ResponseEntity response = restOperations.getForEntity(requestPath, byte[].class, this.path, + issuer); + + if (response.getStatusCode().is2xxSuccessful() && response.hasBody()) { + return new ByteArrayInputStream(response.getBody()); + } + + return null; + } + catch (HttpStatusCodeException e) { + throw VaultResponses.buildException(e); + } + }); + } + /** * Create a request body stub for {@code pki/issue} and {@code pki/sign} from * {@link VaultCertificateRequest}. @@ -184,6 +224,8 @@ public class VaultPkiTemplate implements VaultPkiOperations { .to("exclude_cn_from_sans", request); mapper.from(certificateRequest::getFormat).whenHasText().to("format", request); mapper.from(certificateRequest::getPrivateKeyFormat).whenHasText().to("private_key_format", request); + mapper.from(certificateRequest::getNotAfter).whenHasText().as(i -> i.toString()).to("not_after", request); + mapper.from(certificateRequest::getUserIds).whenHasText().to("user_ids", request); 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 index a257f902..8f27276e 100644 --- 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 @@ -25,6 +25,7 @@ import java.util.List; import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.Base64Utils; import org.springframework.vault.VaultException; @@ -47,12 +48,19 @@ public class Certificate { private final String issuingCaCertificate; + private final List caChain; + + private final Long revocationTime; + Certificate(@JsonProperty("serial_number") String serialNumber, @JsonProperty("certificate") String certificate, - @JsonProperty("issuing_ca") String issuingCaCertificate) { + @JsonProperty("issuing_ca") String issuingCaCertificate, @JsonProperty("ca_chain") List caChain, + @JsonProperty("revocation_time") Long revocationTime) { this.serialNumber = serialNumber; this.certificate = certificate; this.issuingCaCertificate = issuingCaCertificate; + this.caChain = caChain; + this.revocationTime = revocationTime; } /** @@ -69,7 +77,49 @@ public class Certificate { 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 new Certificate(serialNumber, certificate, issuingCaCertificate, List.of(), null); + } + + /** + * 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}. + * @param caChain empty list allowed + * @return the {@link Certificate} + */ + public static Certificate of(String serialNumber, String certificate, String issuingCaCertificate, + List caChain) { + + 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"); + Assert.notNull(caChain, "CA chain must not be null"); + + return new Certificate(serialNumber, certificate, issuingCaCertificate, caChain, null); + } + + /** + * 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}. + * @param caChain empty list allowed + * @param revocationTime revocation time, must not be {@literal null} + * @return the {@link Certificate} + */ + public static Certificate of(String serialNumber, String certificate, String issuingCaCertificate, + List caChain, Long revocationTime) { + + 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"); + Assert.notNull(caChain, "CA chain must not be null"); + Assert.notNull(revocationTime, "Revocation time"); + + return new Certificate(serialNumber, certificate, issuingCaCertificate, caChain, revocationTime); } /** @@ -130,9 +180,27 @@ public class Certificate { * @return the {@link KeyStore} containing the private key and certificate chain. */ public KeyStore createTrustStore() { + return createTrustStore(false); + } + /** + * Create a trust store as {@link KeyStore} from this {@link Certificate} containing * + * the certificate chain. + * @param includeCaChain whether to include the certificate authority chain instead of + * just the issuer certificate. + * @return the {@link KeyStore} containing the certificate and certificate chain. + */ + public KeyStore createTrustStore(boolean includeCaChain) { try { - return KeystoreUtil.createKeyStore(getX509Certificate(), getX509IssuerCertificate()); + List certificates = new ArrayList<>(); + certificates.add(getX509Certificate()); + if (includeCaChain) { + certificates.addAll(getX509IssuerCertificates()); + } + else { + certificates.add(getX509IssuerCertificate()); + } + return KeystoreUtil.createKeyStore(certificates.toArray(new X509Certificate[0])); } catch (GeneralSecurityException | IOException e) { throw new VaultException("Cannot create KeyStore", e); @@ -161,4 +229,29 @@ public class Certificate { return result; } + /** + * Retrieve the issuing CA certificates as list of {@link X509Certificate}. + * @return the issuing CA {@link X509Certificate}. + * @since 2.3.3 + */ + public List getX509IssuerCertificates() { + + List certificates = new ArrayList<>(); + + for (String data : this.caChain) { + try { + certificates.addAll(getCertificates(data)); + } + catch (CertificateException e) { + throw new VaultException("Cannot create Certificate from issuing CA certificate", e); + } + } + + return certificates; + } + + public @Nullable Long getRevocationTime() { + return this.revocationTime; + } + } 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 4485fa88..7b47711e 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; import java.util.ArrayList; @@ -58,8 +57,6 @@ public class CertificateBundle extends Certificate { @Nullable private final String privateKeyType; - private final List caChain; - /** * Create a new {@link CertificateBundle}. * @param serialNumber the serial number. @@ -72,12 +69,12 @@ public class CertificateBundle extends Certificate { CertificateBundle(@JsonProperty("serial_number") String serialNumber, @JsonProperty("certificate") String certificate, @JsonProperty("issuing_ca") String issuingCaCertificate, @JsonProperty("ca_chain") List caChain, @JsonProperty("private_key") String privateKey, - @Nullable @JsonProperty("private_key_type") String privateKeyType) { + @Nullable @JsonProperty("private_key_type") String privateKeyType, + @JsonProperty("revocation_time") Long revocationTime) { - super(serialNumber, certificate, issuingCaCertificate); + super(serialNumber, certificate, issuingCaCertificate, caChain, revocationTime); this.privateKey = privateKey; this.privateKeyType = privateKeyType; - this.caChain = caChain; } /** @@ -98,7 +95,7 @@ public class CertificateBundle extends Certificate { Assert.hasText(privateKey, "Private key must not be empty"); return new CertificateBundle(serialNumber, certificate, issuingCaCertificate, - Collections.singletonList(issuingCaCertificate), privateKey, null); + Collections.singletonList(issuingCaCertificate), null, privateKey, null); } /** @@ -122,7 +119,33 @@ public class CertificateBundle extends Certificate { Assert.hasText(privateKeyType, "Private key type must not be empty"); return new CertificateBundle(serialNumber, certificate, issuingCaCertificate, - Collections.singletonList(issuingCaCertificate), privateKey, privateKeyType); + Collections.singletonList(issuingCaCertificate), privateKey, privateKeyType, null); + } + + /** + * Create a {@link CertificateBundle} 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}. + * @param privateKey must not be empty or {@literal null}. + * @param privateKeyType must not be empty or {@literal null}. + * @param revocationTime the revocation time. + * @return the {@link CertificateBundle} + * @since 2.4 + */ + public static CertificateBundle of(String serialNumber, String certificate, String issuingCaCertificate, + String privateKey, @Nullable String privateKeyType, Long revocationTime) { + + 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"); + Assert.hasText(privateKey, "Private key must not be empty"); + Assert.hasText(privateKeyType, "Private key type must not be empty"); + Assert.notNull(revocationTime, "Revocation time must not be null"); + + return new CertificateBundle(serialNumber, certificate, issuingCaCertificate, + Collections.singletonList(issuingCaCertificate), privateKey, privateKeyType, revocationTime); } /** @@ -276,27 +299,6 @@ public class CertificateBundle extends Certificate { } } - /** - * Retrieve the issuing CA certificates as list of {@link X509Certificate}. - * @return the issuing CA {@link X509Certificate}. - * @since 2.3.3 - */ - public List getX509IssuerCertificates() { - - List certificates = new ArrayList<>(); - - for (String data : this.caChain) { - try { - certificates.addAll(getCertificates(data)); - } - catch (CertificateException e) { - throw new VaultException("Cannot create Certificate from issuing CA certificate", e); - } - } - - return certificates; - } - private static KeySpec getPrivateKey(String privateKey, String keyType) throws GeneralSecurityException, IOException { diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultCertificateRequest.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultCertificateRequest.java index fbf8eae4..bd533215 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultCertificateRequest.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultCertificateRequest.java @@ -16,6 +16,8 @@ package org.springframework.vault.support; import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -88,6 +90,22 @@ public class VaultCertificateRequest { @Nullable private final String privateKeyFormat; + /** + * Set the Not After field of the certificate with specified date value. The value + * format should be given in UTC format YYYY-MM-ddTHH:MM:SSZ. Supports the Y10K end + * date for IEEE 802.1AR-2018 standard devices, 9999-12-31T23:59:59Z. + */ + @Nullable + private Instant notAfter; + + /** + * Specifies the comma-separated list of requested User ID (OID + * 0.9.2342.19200300.100.1.1) Subject values to be placed on the signed certificate. + * This field is validated against allowed_user_ids on the role. + */ + @Nullable + private String userIds; + /** * If {@literal true}, the given common name will not be included in DNS or Email * Subject Alternate Names (as appropriate). Useful if the CN is not a hostname or @@ -97,7 +115,8 @@ public class VaultCertificateRequest { private VaultCertificateRequest(String commonName, List altNames, List ipSubjectAltNames, List uriSubjectAltNames, List otherSans, @Nullable Duration ttl, String format, - @Nullable String privateKeyFormat, boolean excludeCommonNameFromSubjectAltNames) { + @Nullable String privateKeyFormat, boolean excludeCommonNameFromSubjectAltNames, @Nullable Instant notAfter, + @Nullable String userIds) { this.commonName = commonName; this.altNames = altNames; @@ -108,6 +127,8 @@ public class VaultCertificateRequest { this.excludeCommonNameFromSubjectAltNames = excludeCommonNameFromSubjectAltNames; this.format = format; this.privateKeyFormat = privateKeyFormat; + this.notAfter = notAfter; + this.userIds = userIds; } /** @@ -164,6 +185,16 @@ public class VaultCertificateRequest { return this.excludeCommonNameFromSubjectAltNames; } + @Nullable + public Instant getNotAfter() { + return this.notAfter; + } + + @Nullable + public String getUserIds() { + return this.userIds; + } + public static class VaultCertificateRequestBuilder { @Nullable @@ -187,6 +218,12 @@ public class VaultCertificateRequest { private boolean excludeCommonNameFromSubjectAltNames; + @Nullable + private Instant notAfter; + + @Nullable + private String userIds; + VaultCertificateRequestBuilder() { } @@ -386,6 +423,34 @@ public class VaultCertificateRequest { return this; } + /** + * Set the Not After field of the certificate with specified date value. The value + * format should be given in UTC format YYYY-MM-ddTHH:MM:SSZ. Supports the Y10K + * end date for IEEE 802.1AR-2018 standard devices, 9999-12-31T23:59:59Z. + * @return {@code this} {@link VaultCertificateRequestBuilder}. + */ + public VaultCertificateRequestBuilder notAfter(Instant notAfter) { + + Assert.notNull(notAfter, "Not after must not be null"); + + this.notAfter = Instant.from(notAfter).truncatedTo(ChronoUnit.SECONDS); + return this; + } + + /** + * Specifies the comma-separated list of requested User ID (OID + * 0.9.2342.19200300.100.1.1) Subject values to be placed on the signed + * certificate. This field is validated against allowed_user_ids on the role. + * @return {@code this} {@link VaultCertificateRequestBuilder}. + */ + public VaultCertificateRequestBuilder userIds(String userIds) { + + Assert.hasText(userIds, "User IDs must not be empty or null"); + + this.userIds = userIds; + return this; + } + /** * Build a new {@link VaultCertificateRequest} instance. Requires * {@link #commonName(String)} to be configured. @@ -446,7 +511,8 @@ public class VaultCertificateRequest { } return new VaultCertificateRequest(this.commonName, altNames, ipSubjectAltNames, uriSubjectAltNames, - otherSans, this.ttl, this.format, this.privateKeyFormat, this.excludeCommonNameFromSubjectAltNames); + otherSans, this.ttl, this.format, this.privateKeyFormat, this.excludeCommonNameFromSubjectAltNames, + notAfter, userIds); } private static List toList(Iterable iter) { diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultIssuerCertificateRequestResponse.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultIssuerCertificateRequestResponse.java new file mode 100644 index 00000000..05782636 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultIssuerCertificateRequestResponse.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2022 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 + * + * https://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 Nanne Baars + */ +public class VaultIssuerCertificateRequestResponse 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 ef751140..f26f5055 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 @@ -52,10 +52,12 @@ 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.VaultIssuerCertificateRequestResponse; import org.springframework.vault.support.VaultSignCertificateRequestResponse; import org.springframework.vault.util.IntegrationTestSupport; import org.springframework.vault.util.RequiresVaultVersion; import org.springframework.vault.util.Version; +import org.springframework.web.client.HttpClientErrorException; import static org.assertj.core.api.Assertions.*; import static org.springframework.vault.util.Settings.*; @@ -114,6 +116,7 @@ class VaultPkiTemplateIntegrationTests extends IntegrationTestSupport { role.put("allowed_domains", "localhost,example.com"); role.put("allow_subdomains", "true"); role.put("allow_localhost", "true"); + role.put("allowed_user_ids", "humanoid,robot"); role.put("allow_ip_sans", "true"); role.put("max_ttl", "72h"); @@ -124,7 +127,6 @@ class VaultPkiTemplateIntegrationTests extends IntegrationTestSupport { role.put("key_bits", "" + value.bits); this.vaultOperations.write("pki/roles/testrole-" + value.name(), role); } - } @Test @@ -264,6 +266,106 @@ class VaultPkiTemplateIntegrationTests extends IntegrationTestSupport { .isBefore(Date.from(now.plus(50, ChronoUnit.HOURS))); } + @Test + void signShouldSignCsrWithNotAfter() { + Instant notAfter = Instant.now().plus(50, ChronoUnit.DAYS); + 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.builder() + .commonName("hello.example.com") + .notAfter(notAfter) + .build(); + + VaultSignCertificateRequestResponse certificateResponse = this.pkiOperations.signCertificateRequest("testrole", + csr, request); + + Certificate data = certificateResponse.getRequiredData(); + assertThat(data.getX509Certificate().getNotAfter()).isEqualTo(notAfter.truncatedTo(ChronoUnit.SECONDS)); + } + + @Test + @RequiresVaultVersion("1.14.2") + void signShouldFailWithUnknownUserIds() { + 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.builder() + .commonName("hello.example.com") + .userIds("test1,test2") + .build(); + + assertThatThrownBy(() -> this.pkiOperations.signCertificateRequest("testrole", csr, request)) + .hasCauseInstanceOf(HttpClientErrorException.BadRequest.class) + .hasMessageContaining("user_id test1 is not allowed by this role"); + } + + @Test + @RequiresVaultVersion("1.14.2") + void signShouldSignWithKnownUserIds() { + 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.builder() + .commonName("hello.example.com") + .userIds("robot,humanoid") + .build(); + + VaultSignCertificateRequestResponse certificateResponse = this.pkiOperations.signCertificateRequest("testrole", + csr, request); + + Certificate data = certificateResponse.getRequiredData(); + + assertThat(data.getCertificate()).isNotEmpty(); + assertThat(data.getX509Certificate().getSubjectX500Principal().getName()).contains("UID=humanoid") + .contains("UID=robot"); + } + @Test void signShouldSignCsr() { @@ -346,4 +448,29 @@ class VaultPkiTemplateIntegrationTests extends IntegrationTestSupport { } } + @Test + void shouldReturnCA() throws Exception { + VaultIssuerCertificateRequestResponse certificateResponse = this.pkiOperations.getIssuerCertificate("default"); + + Certificate data = certificateResponse.getRequiredData(); + KeyStore trustStore = data.createTrustStore(true); + assertThat(trustStore.size()).isEqualTo(3); + assertThat(data.getCertificate()).isNotEmpty(); + assertThat(data.getX509IssuerCertificates()).hasSize(2); + + try (InputStream in = this.pkiOperations.getIssuerCertificate("default", Encoding.DER)) { + + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + + assertThat(cf.generateCertificate(in)).isInstanceOf(java.security.cert.Certificate.class); + } + + try (InputStream crl = this.pkiOperations.getIssuerCertificate("default", Encoding.PEM)) { + + byte[] bytes = StreamUtils.copyToByteArray(crl); + assertThat(bytes).isNotEmpty(); + } + + } + } 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 index 3e166fce..7d01f89d 100644 --- 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 @@ -17,6 +17,7 @@ package org.springframework.vault.support; import java.security.KeyStore; import java.security.cert.X509Certificate; +import java.util.List; import java.util.Map; import com.fasterxml.jackson.databind.ObjectMapper; @@ -41,7 +42,8 @@ class CertificateUnitTests { void before() throws Exception { Map data = this.OBJECT_MAPPER.readValue(getClass().getResource("/certificate.json"), Map.class); - this.certificate = Certificate.of(data.get("serial_number"), data.get("certificate"), data.get("issuing_ca")); + this.certificate = Certificate.of(data.get("serial_number"), data.get("certificate"), data.get("issuing_ca"), + List.of(), 0L); } @Test