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