From 660065b08e64390fd75c2052cfa3ad19a20aac3a Mon Sep 17 00:00:00 2001 From: Stefan Kalscheuer Date: Thu, 12 Mar 2026 20:16:51 +0100 Subject: [PATCH] feat: add support for PKI module (#131) * generate certificates and keys * revoke certificates * read CA and issuer certificates --- CHANGELOG.md | 1 + README.md | 1 + .../jvault/connector/HTTPVaultConnector.java | 41 ++ .../stklcode/jvault/connector/PkiClient.java | 90 ++++ .../jvault/connector/VaultConnector.java | 8 + .../connector/internal/VaultApiPath.java | 8 + .../jvault/connector/model/PkiRequest.java | 435 ++++++++++++++++++ .../model/response/PkiCaResponse.java | 59 +++ .../connector/model/response/PkiResponse.java | 59 +++ .../model/response/PkiRevocationResponse.java | 50 ++ .../connector/HTTPVaultConnectorIT.java | 231 +++++++++- .../connector/model/PkiRequestTest.java | 192 ++++++++ .../model/response/PkiCaResponseTest.java | 132 ++++++ .../model/response/PkiResponseTest.java | 97 ++++ .../response/PkiRevocationResponseTest.java | 81 ++++ src/test/resources/data_dir/core/_mounts | 2 +- .../0d6ef259-2cb5-a42c-f29c-ed991995598a/_ca | 1 + .../0d6ef259-2cb5-a42c-f29c-ed991995598a/_crl | 1 + ...-2c-95-ad-f9-3a-30-c8-dc-15-ad-da-e3-2a-58 | 1 + .../config/_ca_bundle | 1 + .../role/_example-com | 1 + .../_casesensitivity | 2 +- .../data_dir/sys/counters/requests/2026/_07 | 1 + 23 files changed, 1489 insertions(+), 6 deletions(-) create mode 100644 src/main/java/de/stklcode/jvault/connector/PkiClient.java create mode 100644 src/main/java/de/stklcode/jvault/connector/model/PkiRequest.java create mode 100644 src/main/java/de/stklcode/jvault/connector/model/response/PkiCaResponse.java create mode 100644 src/main/java/de/stklcode/jvault/connector/model/response/PkiResponse.java create mode 100644 src/main/java/de/stklcode/jvault/connector/model/response/PkiRevocationResponse.java create mode 100644 src/test/java/de/stklcode/jvault/connector/model/PkiRequestTest.java create mode 100644 src/test/java/de/stklcode/jvault/connector/model/response/PkiCaResponseTest.java create mode 100644 src/test/java/de/stklcode/jvault/connector/model/response/PkiResponseTest.java create mode 100644 src/test/java/de/stklcode/jvault/connector/model/response/PkiRevocationResponseTest.java create mode 100644 src/test/resources/data_dir/logical/0d6ef259-2cb5-a42c-f29c-ed991995598a/_ca create mode 100644 src/test/resources/data_dir/logical/0d6ef259-2cb5-a42c-f29c-ed991995598a/_crl create mode 100644 src/test/resources/data_dir/logical/0d6ef259-2cb5-a42c-f29c-ed991995598a/certs/_40-97-29-6b-a8-02-2c-95-ad-f9-3a-30-c8-dc-15-ad-da-e3-2a-58 create mode 100644 src/test/resources/data_dir/logical/0d6ef259-2cb5-a42c-f29c-ed991995598a/config/_ca_bundle create mode 100644 src/test/resources/data_dir/logical/0d6ef259-2cb5-a42c-f29c-ed991995598a/role/_example-com create mode 100644 src/test/resources/data_dir/sys/counters/requests/2026/_07 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e41f583..559d5661 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ### Features * Allow parsing JSON from full dataset in `SecretResponse` (#130) +* Introduce `pki()` client to issue certificates via Vault PKI engine (#131) ### Removal * Remove deprecated `read...Credentials()` methods (#112) diff --git a/README.md b/README.md index 09d0ac74..5313d188 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Java Vault Connector is a connector library for [Vault](https://www.vaultproject * KV v1 and v2 support * Database secret handling * Transit API support +* PKI certificate support * Connector Factory with builder pattern * Tested against Vault 1.3 to 2.0 diff --git a/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java b/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java index babd8aae..9b23a1f6 100644 --- a/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java +++ b/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java @@ -252,6 +252,11 @@ public SysClientImpl sys() { return new SysClientImpl(); } + @Override + public PkiClientImpl pki() { + return new PkiClientImpl(); + } + @Override public final void close() { authorized = false; @@ -764,4 +769,40 @@ public final List getAuthBackends() throws VaultConnectorException return amr.supportedMethods().values().stream().map(AuthMethod::type).toList(); } } + + /** + * Sub-client for PKI methods. + */ + public class PkiClientImpl implements PkiClient { + + @Override + public PkiResponse generateCertificateAndKey(final String name, final PkiRequest pkiRequest) throws VaultConnectorException { + return request.post(PKI_ISSUE + encode(name), pkiRequest, token, PkiResponse.class); + } + + @Override + public PkiResponse generateCertificateAndKey(final String issuer, final String name, final PkiRequest pkiRequest) throws VaultConnectorException { + return request.post(PKI_ISSUER_ISSUE.formatted(encode(issuer), encode(name)), pkiRequest, token, PkiResponse.class); + } + + @Override + public PkiRevocationResponse revokeBySerial(String serial) throws VaultConnectorException { + return request.post(PKI_REVOKE, Map.of("serial_number", serial), token, PkiRevocationResponse.class); + } + + @Override + public PkiRevocationResponse revokeCertificate(String certificate) throws VaultConnectorException { + return request.post(PKI_REVOKE, Map.of("certificate", certificate), token, PkiRevocationResponse.class); + } + + @Override + public PkiCaResponse readCaCert() throws VaultConnectorException { + return request.get(PKI_CA_CERT, emptyMap(), token, PkiCaResponse.class); + } + + @Override + public PkiCaResponse readIssuerCert(String issuer) throws VaultConnectorException { + return request.get(PKI_ISSUER_CERT.formatted(encode(issuer)), emptyMap(), token, PkiCaResponse.class); + } + } } diff --git a/src/main/java/de/stklcode/jvault/connector/PkiClient.java b/src/main/java/de/stklcode/jvault/connector/PkiClient.java new file mode 100644 index 00000000..27b76fa7 --- /dev/null +++ b/src/main/java/de/stklcode/jvault/connector/PkiClient.java @@ -0,0 +1,90 @@ +/* + * Copyright 2016-2026 Stefan Kalscheuer + * + * 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 de.stklcode.jvault.connector; + +import de.stklcode.jvault.connector.exception.VaultConnectorException; +import de.stklcode.jvault.connector.model.PkiRequest; +import de.stklcode.jvault.connector.model.response.PkiCaResponse; +import de.stklcode.jvault.connector.model.response.PkiResponse; +import de.stklcode.jvault.connector.model.response.PkiRevocationResponse; + +/** + * PKI client interface. + * Provides methods to interact with Vault's PKI API. + * + * @since 2.0.0 + */ +public interface PkiClient { + + /** + * Generate a new set of credentials (certificate and private key) based on the given role name using. + * The issuer is determined by role. + * + * @param role The role name + * @param request The request + * @return PKI response + * @throws VaultConnectorException on error + */ + PkiResponse generateCertificateAndKey(String role, PkiRequest request) throws VaultConnectorException; + + /** + * Generate a new set of credentials (certificate and private key) based on the given role and issuer. + * + * @param issuer The issuer reference + * @param role The role name + * @param request The request + * @return PKI response + * @throws VaultConnectorException on error + */ + PkiResponse generateCertificateAndKey(String role, String issuer, PkiRequest request) throws VaultConnectorException; + + /** + * Request revocation of a certificate by serial number. + * + * @param serial Serial number of the certificate to revoke, in hyphen-separated or colon-separated hexadecimal + * @return Revocation response + * @throws VaultConnectorException on error + */ + PkiRevocationResponse revokeBySerial(String serial) throws VaultConnectorException; + + /** + * Request revocation of a certificate by serial number. + * + * @param certificate Certificate to revoke, in PEM format + * @return Revocation response + * @throws VaultConnectorException on error + */ + PkiRevocationResponse revokeCertificate(String certificate) throws VaultConnectorException; + + /** + * Read the default issuer's CA certificate. + * + * @return CA/issuer response + * @throws VaultConnectorException on error + */ + PkiCaResponse readCaCert() throws VaultConnectorException; + + /** + * Read the certificate of a specific issuer. + * + * @param issuer The issuer reference (may be "default") + * @return CA/issuer response + * @throws VaultConnectorException on error + */ + PkiCaResponse readIssuerCert(String issuer) throws VaultConnectorException; + +} diff --git a/src/main/java/de/stklcode/jvault/connector/VaultConnector.java b/src/main/java/de/stklcode/jvault/connector/VaultConnector.java index c0248514..2a842477 100644 --- a/src/main/java/de/stklcode/jvault/connector/VaultConnector.java +++ b/src/main/java/de/stklcode/jvault/connector/VaultConnector.java @@ -219,6 +219,14 @@ default SecretResponse renew(final String leaseID) throws VaultConnectorExceptio */ SysClient sys(); + /** + * Get client for PKI API. + * + * @return PKI client + * @since 2.0.0 + */ + PkiClient pki(); + /** * Read credentials for database backends. * diff --git a/src/main/java/de/stklcode/jvault/connector/internal/VaultApiPath.java b/src/main/java/de/stklcode/jvault/connector/internal/VaultApiPath.java index 7c620c8a..5af330ef 100644 --- a/src/main/java/de/stklcode/jvault/connector/internal/VaultApiPath.java +++ b/src/main/java/de/stklcode/jvault/connector/internal/VaultApiPath.java @@ -27,6 +27,7 @@ public final class VaultApiPath { private static final String SYS = "sys"; private static final String AUTH = "auth"; private static final String TRANSIT = "transit"; + private static final String PKI = "pki"; // System paths public static final String SYS_AUTH = SYS + "/auth"; @@ -62,6 +63,13 @@ public final class VaultApiPath { public static final String TRANSIT_DECRYPT = TRANSIT + "/decrypt/"; public static final String TRANSIT_HASH = TRANSIT + "/hash/"; + // PKI engine paths + public static final String PKI_ISSUE = PKI + "/issue/"; + public static final String PKI_ISSUER_ISSUE = PKI + "/%s/issue/%s"; + public static final String PKI_REVOKE = PKI + "/revoke"; + public static final String PKI_CA_CERT = PKI + "/cert/ca"; + public static final String PKI_ISSUER_CERT = PKI + "/issuer/%s/json"; + /** * Private constructor to prevent instantiation. */ diff --git a/src/main/java/de/stklcode/jvault/connector/model/PkiRequest.java b/src/main/java/de/stklcode/jvault/connector/model/PkiRequest.java new file mode 100644 index 00000000..4e895685 --- /dev/null +++ b/src/main/java/de/stklcode/jvault/connector/model/PkiRequest.java @@ -0,0 +1,435 @@ +/* + * Copyright 2016-2026 Stefan Kalscheuer + * + * 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 de.stklcode.jvault.connector.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Collection; + +/** + * PKI request model. + * + * @param commonName CN for the certificate + * @param altNames Subject Alternative Names + * @param ipSans IP Subject Alternative Names + * @param urlSans URI Subject Alternative Names + * @param otherSans custom OID/UTF8-string SANs + * @param ttl Time To Live + * @param format Certificate format + * @param keyFormat Private key format + * @param excludeCnFromSans Exclude CN from SANs? + * @param notAfter Not After (expiration date) + * @param removeRootsFromChain Remove root certificates from chain? + * @param userIds User IDs (OID 0.9.2342.19200300.100.1.1) + * @param certMetadata Base64 encoded certificate metadata + * @author Stefan Kalscheuer + * @since 2.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record PkiRequest( + @JsonProperty("common_name") String commonName, + @JsonProperty("alt_names") String altNames, + @JsonProperty("ip_sans") String ipSans, + @JsonProperty("url_sans") String urlSans, + @JsonProperty("other_sans") String otherSans, + @JsonProperty("ttl") String ttl, + @JsonProperty("format") String format, + @JsonProperty("private_key_format") String keyFormat, + @JsonProperty("exclude_cn_from_sans") Boolean excludeCnFromSans, + @JsonProperty("not_after") String notAfter, + @JsonProperty("remove_roots_from_chain") Boolean removeRootsFromChain, + @JsonProperty("user_ids") String userIds, + @JsonProperty("cert_metadata") String certMetadata +) implements Serializable { + + /** + * Construct {@link PkiRequest} object from {@link Builder}. + * + * @param builder Token builder. + */ + private PkiRequest(final Builder builder) { + this( + builder.commonName, + builder.altNames, + builder.ipSans, + builder.urlSans, + builder.otherSans, + builder.ttl, + builder.format, + builder.keyFormat, + builder.excludeCnFromSans, + builder.notAfter, + builder.removeRootsFromChain, + builder.userIds, + builder.certMetadata + ); + } + + /** + * Get {@link Builder} instance. + * + * @return Token Builder. + * @since 0.8 + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Constants for certificate formats. + */ + public enum Format { + PEM("pem"), + DER("der"), + PEM_BUNDLE("pem_bundle"); + + private final String value; + + Format(String value) { + this.value = value; + } + + public String value() { + return value; + } + } + + /** + * Constants for private key formats. + */ + public enum KeyFormat { + DER("der"), + PKCS8("pkcs8"); + + private final String value; + + KeyFormat(String value) { + this.value = value; + } + + public String value() { + return value; + } + } + + + /** + * A builder for PKI requests. + * + * @author Stefan Kalscheuer + */ + public static final class Builder { + private String commonName; + private String altNames; + private String ipSans; + private String urlSans; + private String otherSans; + private String ttl; + private String format; + private String keyFormat; + private Boolean excludeCnFromSans; + private String notAfter; + private Boolean removeRootsFromChain; + private String userIds; + private String certMetadata; + + /** + * Specifies the requested CN for the certificate. + * If the CN is allowed by role policy, it will be issued. If more than one common_name is desired, specify the + * alternative names in the {@link #} list. + * + * @param commonName Certificate CN + * @return self + */ + public Builder withCommonName(final String commonName) { + this.commonName = commonName; + return this; + } + + /** + * Specifies requested Subject Alternative Names, in a comma-delimited list. + * These can be host names or email addresses; they will be parsed into their respective fields. If any + * requested names do not match role policy, the entire request will be denied. + * + * @param altNames Alternative names, comma-delimited + * @return self + */ + public Builder withAltNames(final String altNames) { + this.altNames = altNames; + return this; + } + + /** + * Specifies requested Subject Alternative Names. + * These can be host names or email addresses; they will be parsed into their respective fields. If any + * requested names do not match role policy, the entire request will be denied. + * + * @param altNames Alternative names + * @return self + */ + public Builder withAltNames(final Collection altNames) { + if (altNames != null) { + this.altNames = String.join(",", altNames); + } else { + this.altNames = null; + } + return this; + } + + /** + * Specifies requested IP Subject Alternative Names, in a comma-delimited list. + * Only valid if the role allows IP SANs (which is the default). + * + * @param ipSans IP SANs, comma-delimited + * @return self + */ + public Builder withIpSans(final String ipSans) { + this.ipSans = ipSans; + return this; + } + + /** + * Specifies requested IP Subject Alternative Names. + * Only valid if the role allows IP SANs (which is the default). + * + * @param ipSans IP SANs + * @return self + */ + public Builder withIpSans(final Collection ipSans) { + if (ipSans != null) { + this.ipSans = String.join(",", ipSans); + } else { + this.ipSans = null; + } + + return this; + } + + /** + * Specifies the requested URI Subject Alternative Names, in a comma-delimited list. + * If any requested URIs do not match role policy, the entire request will be denied. + * + * @param urlSans URL SANs, comma-delimited + * @return self + */ + public Builder withUrlSans(final String urlSans) { + this.urlSans = urlSans; + return this; + } + + /** + * Specifies the requested URI Subject Alternative Names. + * If any requested URIs do not match role policy, the entire request will be denied. + * + * @param urlSans URL SANs + * @return self + */ + public Builder withUrlSans(final Collection urlSans) { + if (urlSans != null) { + this.urlSans = String.join(",", urlSans); + } else { + this.urlSans = null; + } + return this; + } + + /** + * Specifies custom OID/UTF8-string SANs. + * These must match values specified on the role in "allowed_other_sans". + * The format is the same as OpenSSL: {@code ;:} where the only current valid type is UTF8. + * This can be a comma-delimited list or a JSON string slice. + * + * @param otherSans Other SANs, comma-delimited + * @return self + */ + public Builder withOtherSans(final String otherSans) { + this.otherSans = otherSans; + return this; + } + + /** + * Specifies the requested URI Subject Alternative Names. + * If any requested URIs do not match role policy, the entire request will be denied. + * + * @param otherSans Other SANs + * @return self + */ + public Builder withOtherSans(final Collection otherSans) { + if (otherSans != null) { + this.otherSans = String.join(",", otherSans); + } else { + this.otherSans = null; + } + return this; + } + + /** + * Specifies requested Time To Live. + * Cannot be greater than the role's "max_ttl" value. If not provided, the role's "ttl" value will be used. + * + * @param ttl Time to live + * @return self + */ + public Builder withTtl(final String ttl) { + this.ttl = ttl; + return this; + } + + /** + * Specifies the format for returned data. + * Defaults to {@link Format#PEM} + * + * @param format Format for returned data + * @return self + */ + public Builder withFormat(final Format format) { + if (format != null) { + this.format = format.value(); + } else { + this.format = null; + } + return this; + } + + /** + * Specifies the key format for returned data. + * Defaults to {@link KeyFormat#DER} which returns either base64-encoded DER or PEM-encoded DER depending in + * {@link #withFormat(Format) format}. + * + * @param keyFormat Key format for returned data + * @return self + */ + public Builder withKeyFormat(final KeyFormat keyFormat) { + if (keyFormat != null) { + this.keyFormat = keyFormat.value(); + } else { + this.keyFormat = null; + } + return this; + } + + /** + * If {@code true}, the given common_name will not be included in DNS or Email Subject Alternate Names. + * Useful if the CN is not a hostname or email address, but is instead some human-readable identifier. + *

+ * Note that this does not apply to the private key within the certificate field if format + * {@link Format#PEM_BUNDLE} parameter is specified. + * + * @param excludeCnFromSans Exclude common name from SANs + * @return self + */ + public Builder withExcludeCnFromSans(final boolean excludeCnFromSans) { + this.excludeCnFromSans = excludeCnFromSans; + 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. + * + * @param notAfter Not after date + * @return self + */ + public Builder withNotAfter(final String notAfter) { + this.notAfter = notAfter; + return this; + } + + /** + * Set the Not After field of the certificate with specified date value. + * + * @param notAfter Not after date + * @return self + */ + public Builder withNotAfter(final ZonedDateTime notAfter) { + if (notAfter != null) { + this.notAfter = notAfter.format(DateTimeFormatter.ISO_ZONED_DATE_TIME); + } else { + this.notAfter = null; + } + return this; + } + + /** + * If {@code true}, the returned ca_chain field will not include any self-signed CA certificates. + * Useful if end-users already have the root CA in their trust store. + * + * @param removeRootsFromChain Remove root CA certificate from CA chain + * @return self + */ + public Builder withRemoveRootsFromChain(final boolean removeRootsFromChain) { + this.removeRootsFromChain = removeRootsFromChain; + 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. + * + * @param userIds User IDs, comma-delimited + * @return self + */ + public Builder withUserIds(final String userIds) { + this.userIds = userIds; + 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. + * + * @param userIds Alternative names + * @return self + */ + public Builder withUserIds(final Collection userIds) { + if (userIds != null) { + this.userIds = String.join(",", userIds); + } else { + this.userIds = null; + } + return this; + } + + /** + * A base 64 encoded value or an empty string to associate with the certificate's serial number. + * The role's "no_store_metadata" must be set to {@code false}, otherwise an error is returned when specified. + *

+ * Only available in Vault Enterprise. + * + * @param certMetadata Certificate metadata, base64 encoded + * @return self + */ + public Builder withCertMetadata(final String certMetadata) { + this.certMetadata = certMetadata; + return this; + } + + /** + * Build the token based on given parameters. + * + * @return the token + */ + public PkiRequest build() { + return new PkiRequest(this); + } + } +} diff --git a/src/main/java/de/stklcode/jvault/connector/model/response/PkiCaResponse.java b/src/main/java/de/stklcode/jvault/connector/model/response/PkiCaResponse.java new file mode 100644 index 00000000..bb48893e --- /dev/null +++ b/src/main/java/de/stklcode/jvault/connector/model/response/PkiCaResponse.java @@ -0,0 +1,59 @@ +/* + * Copyright 2016-2026 Stefan Kalscheuer + * + * 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 de.stklcode.jvault.connector.model.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonUnwrapped; + +import java.io.Serializable; +import java.util.List; + +/** + * Vault response for PKI CA/issuer certificate. + * + * @param responseHeader Response metadata + * @param data PKI response data + * @author Stefan Kalscheuer + * @since 2.0.0 + */ +public record PkiCaResponse( + @JsonUnwrapped Header responseHeader, + @JsonProperty("data") Data data +) implements VaultDataResponse { + + /** + * Vault CA certificate data. + * + * @param authorityKeyId CA key ID (serial number) + * @param certificate CA certificate (PEM encoded) + * @param revocationTime CA certificate revocation timestamp (0 if not revoked) + * @param revocationTimeRFC3339 CA certificate revocation time as RFC3339 string (empty if not revoked) + * @param caChain CA certificate chain (PEM encoded), only available if issuer was requested + * @param issuerId Issuer ID, only available if issuer was requested + * @param issuerName Issuer name, only available if issuer was requested + */ + public record Data( + @JsonProperty("authority_key_id") String authorityKeyId, + @JsonProperty("certificate") String certificate, + @JsonProperty("revocation_time") Long revocationTime, + @JsonProperty("revocation_time_rfc3339") String revocationTimeRFC3339, + @JsonProperty("ca_chain") List caChain, + @JsonProperty("issuer_id") String issuerId, + @JsonProperty("issuer_name") String issuerName + ) implements Serializable { + } +} diff --git a/src/main/java/de/stklcode/jvault/connector/model/response/PkiResponse.java b/src/main/java/de/stklcode/jvault/connector/model/response/PkiResponse.java new file mode 100644 index 00000000..ea504fee --- /dev/null +++ b/src/main/java/de/stklcode/jvault/connector/model/response/PkiResponse.java @@ -0,0 +1,59 @@ +/* + * Copyright 2016-2026 Stefan Kalscheuer + * + * 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 de.stklcode.jvault.connector.model.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonUnwrapped; + +import java.io.Serializable; +import java.util.List; + +/** + * Vault response for PKI certificates. + * + * @param responseHeader Response metadata + * @param data PKI response data + * @author Stefan Kalscheuer + * @since 2.0.0 + */ +public record PkiResponse( + @JsonUnwrapped Header responseHeader, + @JsonProperty("data") Data data +) implements VaultDataResponse { + + /** + * + * PKI data object. + * @param expiration Certificate expiration timestamp + * @param certificate Certificate (PEM encoded) + * @param issuingCa Issuing CA certificate (PEM or Base64 encoded) + * @param caChain Full CA certificate chain (PEM or Base64 encoded) + * @param privateKey Certificate private key (PEM or Base64 encoded) + * @param privateKeyType Certificate private key type + * @param serialNumber Certificate serial number + */ + public record Data( + @JsonProperty("expiration") Long expiration, + @JsonProperty("certificate") String certificate, + @JsonProperty("issuing_ca") String issuingCa, + @JsonProperty("ca_chain") List caChain, + @JsonProperty("private_key") String privateKey, + @JsonProperty("private_key_type") String privateKeyType, + @JsonProperty("serial_number") String serialNumber + ) implements Serializable { + } +} diff --git a/src/main/java/de/stklcode/jvault/connector/model/response/PkiRevocationResponse.java b/src/main/java/de/stklcode/jvault/connector/model/response/PkiRevocationResponse.java new file mode 100644 index 00000000..9c3fb965 --- /dev/null +++ b/src/main/java/de/stklcode/jvault/connector/model/response/PkiRevocationResponse.java @@ -0,0 +1,50 @@ +/* + * Copyright 2016-2026 Stefan Kalscheuer + * + * 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 de.stklcode.jvault.connector.model.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonUnwrapped; + +import java.io.Serializable; + +/** + * Vault response for PKI certificate revocations. + * + * @param responseHeader Response metadata + * @param data PKI response data + * @author Stefan Kalscheuer + * @since 2.0.0 + */ +public record PkiRevocationResponse( + @JsonUnwrapped Header responseHeader, + @JsonProperty("data") Data data +) implements VaultDataResponse { + + /** + * Vault revocation response data. + * + * @param revocationTime Revocation timestamp + * @param revocationTimeRFC3339 Revocation time as RFC3339 string + * @param state Revocation state (typically "revoked") + */ + public record Data( + @JsonProperty("revocation_time") Long revocationTime, + @JsonProperty("revocation_time_rfc3339") String revocationTimeRFC3339, + @JsonProperty("state") String state + ) implements Serializable { + } +} diff --git a/src/test/java/de/stklcode/jvault/connector/HTTPVaultConnectorIT.java b/src/test/java/de/stklcode/jvault/connector/HTTPVaultConnectorIT.java index 817e1806..635e16c4 100644 --- a/src/test/java/de/stklcode/jvault/connector/HTTPVaultConnectorIT.java +++ b/src/test/java/de/stklcode/jvault/connector/HTTPVaultConnectorIT.java @@ -17,10 +17,7 @@ package de.stklcode.jvault.connector; import de.stklcode.jvault.connector.exception.*; -import de.stklcode.jvault.connector.model.AppRole; -import de.stklcode.jvault.connector.model.AuthBackend; -import de.stklcode.jvault.connector.model.Token; -import de.stklcode.jvault.connector.model.TokenRole; +import de.stklcode.jvault.connector.model.*; import de.stklcode.jvault.connector.model.response.*; import de.stklcode.jvault.connector.test.Credentials; import de.stklcode.jvault.connector.test.VaultConfiguration; @@ -32,6 +29,15 @@ import java.net.ServerSocket; import java.nio.file.Files; import java.nio.file.Paths; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; @@ -1058,6 +1064,196 @@ void transitHashText() { } } + @Nested + @DisplayName("PKI Tests") + class PkiTests { + + private static final String PKI_CA_PEM = """ + -----BEGIN CERTIFICATE----- + MIIDLTCCAhWgAwIBAgIUQJcpa6gCLJWt+TowyNwVrdrjKlgwDQYJKoZIhvcNAQEL + BQAwHjEcMBoGA1UEAxMTSlZhdWx0IFRlc3QgUm9vdCBDQTAeFw0yNjA3MTQxODIw + MzlaFw0yNjA4MTUxODIxMDlaMB4xHDAaBgNVBAMTE0pWYXVsdCBUZXN0IFJvb3Qg + Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDI0guomBRZlG9pJOBO + y6lRqV7W616f6OS4mWryfICmE7C9emRahsjmlQSQGWO2mct3pwRyLFgSWpusiIkh + jssnHM1qyaWeFv1EcjUByQM8xf8KFKqxxw5mX7jH0P0qfGOljvBAlpRa0HzPEYDT + fhghYDo86a8JxW33VLha10MZJ+DU5r8SpbvzRfc4xdVF9PDDkxzq1hNMrVw2T/9l + m3ycRWQ/T/uUT5Amx94yUPSQXZydcUjmA51hfdkmC5agSPSL1A1TBpAuTcv77M0I + 8wIejUbMCJOl8fFNAalySMg/1a2ZzFuRw7iXNuXcfNIH22z73hLnYfZ+hbElEa/2 + xUkzAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0G + A1UdDgQWBBT5z9dFfwewhrtgEpHj4q7t+1fFcjAfBgNVHSMEGDAWgBT5z9dFfwew + hrtgEpHj4q7t+1fFcjANBgkqhkiG9w0BAQsFAAOCAQEAnXecUvthG+PqJ2czn6Ag + 6vqhDtRcEc2DJv6VQWMEUt9R8QzWEQ7+XodyGlFtDx20O9Nhrhp3tKlQP6wqFsbs + k8rkVGJ7fPVa/6aKjkSZ8BVWDEBfkkNE9pdtDlN7G2NFktG1ODco6i3pacEQtSLm + 6j7zmxJVxb3HGNgdZKdhHfGf0ABA9ErsiKf2Qwj0NPxa6Xhl+TsZKi8X+gwanYUs + sx7kgm9uh9kurhKlaSrj8uV18RwyorsKqYxnFMUTRJ9QkNEhFFr1uc32W8Kj1mDo + 5e2D7/dUGCYLI95vqkyzynt0TWQEc43cZj0/LWlSRA+2wmDBDUqL1OwQfX1TDv2N + TQ== + -----END CERTIFICATE-----"""; + + @BeforeEach + void authorize() { + assertDoesNotThrow(() -> connector.authToken(TOKEN_ROOT)); + assumeTrue(connector.isAuthorized()); + } + + @Test + @DisplayName("Generate certificate and key") + void generateCertificateAndKeyTest() { + PkiResponse pkiResponse = assertDoesNotThrow( + () -> connector.pki().generateCertificateAndKey( + "example-com", + PkiRequest.builder() + .withCommonName("test.example.com") + .withAltNames("test2.example.com") + .withIpSans("192.0.2.1") + .withKeyFormat(PkiRequest.KeyFormat.PKCS8) + .build() + ), + "Failed to issue certificate" + ); + + assertEquals("rsa", pkiResponse.data().privateKeyType(), "unexpected private key type"); + assertTrue(pkiResponse.data().expiration() > System.currentTimeMillis() / 1000L, "expiration timestamp should be in future"); + + assertEquals(PKI_CA_PEM, pkiResponse.data().issuingCa(), "unexpected issuing CA certificate"); + if (compareVersions(VAULT_VERSION, "1.11.0") >= 0) { + assertEquals(List.of(PKI_CA_PEM), pkiResponse.data().caChain(), "unexpected CA chain"); + } + + PublicKey caCert = parseCertificate(PKI_CA_PEM).getPublicKey(); + X509Certificate cert = parseCertificate(pkiResponse.data().certificate()); + assertNotNull(cert, "failed o parse certificate"); + assertNotNull(parsePrivateKey(pkiResponse.data().privateKey()), "failed o parse private key"); + assertDoesNotThrow(() -> cert.verify(caCert), "certificate was not signed by the issuing CA"); + + assertHasSAN(cert, 2, "test.example.com"); + assertHasSAN(cert, 2, "test2.example.com"); + assertHasSAN(cert, 7, "192.0.2.1"); + } + + @Test + @DisplayName("Revoke certificates") + void revokeCertificateAndKeyTest() { + // First, generate two certificates + PkiResponse pkiResponse1 = assertDoesNotThrow( + () -> connector.pki().generateCertificateAndKey("example-com", + PkiRequest.builder().withCommonName("a.example.com").build()), + "Failed to issue certificate 1" + ); + PkiResponse pkiResponse2 = assertDoesNotThrow( + () -> connector.pki().generateCertificateAndKey("example-com", + PkiRequest.builder().withCommonName("b.example.com").build()), + "Failed to issue certificate 2" + ); + + // Revoke first by serial + PkiRevocationResponse res1 = assertDoesNotThrow( + () -> connector.pki().revokeBySerial(pkiResponse1.data().serialNumber()), + "Failed to revoke certificate 1 by serial" + ); + assertNotNull(res1.data().revocationTime(), "missing revocation time in response"); + assertNotNull(res1.data().revocationTimeRFC3339(), "missing revocation time (RFC 3339) in response"); + if (compareVersions(VAULT_VERSION, "1.14.0") >= 0) { + assertEquals("revoked", res1.data().state(), "unexpected state in response"); + } + + if (compareVersions(VAULT_VERSION, "1.12.0") >= 0) { + // Revoke second by certificate + PkiRevocationResponse res2 = assertDoesNotThrow( + () -> connector.pki().revokeCertificate(pkiResponse2.data().certificate()), + "Failed to revoke certificate 2 by PEM" + ); + assertNotNull(res2.data().revocationTime(), "missing revocation time in response"); + assertNotNull(res2.data().revocationTimeRFC3339(), "missing revocation time (RFC 3339) in response"); + + if (compareVersions(VAULT_VERSION, "1.14.0") >= 0) { + assertEquals("revoked", res2.data().state(), "unexpected state in response"); + } + } + + InvalidResponseException ex = assertThrows(InvalidResponseException.class, + () -> connector.pki().revokeBySerial("00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00"), + "Expected exception on revoking non-existent certificate"); + assertEquals(400, ex.getStatusCode(), "unexpected status code in response"); + assertTrue(ex.getResponse().startsWith("certificate with serial 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 not found"), + "unexpected error response message"); + } + + @Test + @DisplayName("Read CA/issuer certificate") + void readCaCertificateTest() { + PkiCaResponse pkiResponse = assertDoesNotThrow(() -> connector.pki().readCaCert(), + "Failed to read CA certificate"); + + assertEquals(PKI_CA_PEM, pkiResponse.data().certificate(), "unexpected CA certificate"); + assertEquals(0, pkiResponse.data().revocationTime(), "unexpected revocation time"); + assertNull(pkiResponse.data().issuerId(), "unexpected issuer ID"); + assertNull(pkiResponse.data().issuerName(), "unexpected issuer name"); + + if (pkiResponse.data().authorityKeyId() != null) { + // Available in Vault 1.21.1+, but not in OpenBao (checked with 2.6.0) + assertEquals("f9:cf:d7:45:7f:07:b0:86:bb:60:12:91:e3:e2:ae:ed:fb:57:c5:72", + pkiResponse.data().authorityKeyId(), + "unexpected authority key ID"); + } + + if (compareVersions(VAULT_VERSION, "1.11.0") >= 0) { + assertEquals("", pkiResponse.data().revocationTimeRFC3339(), "unexpected revocation time (RFC 3339)"); + + // Request a specific issuer + pkiResponse = assertDoesNotThrow(() -> connector.pki().readIssuerCert("default"), + "Failed to read issuer certificate"); + + assertEquals(PKI_CA_PEM + "\n", pkiResponse.data().certificate(), "unexpected CA certificate"); + assertEquals(List.of(PKI_CA_PEM + "\n"), pkiResponse.data().caChain(), "unexpected CA chain"); + assertNull(pkiResponse.data().revocationTime(), "unexpected revocation time"); + assertNull(pkiResponse.data().revocationTimeRFC3339(), "unexpected revocation time (RFC 3339)"); + // Issuers are not initialized in Vaul 1.3.0 test data, so dynamically assigned during upgrade + assertNotNull(pkiResponse.data().issuerId(), "unexpected issuer ID"); + assertNotNull(pkiResponse.data().issuerName(), "unexpected issuer name"); + } + } + + private static X509Certificate parseCertificate(String pem) { + try { + return (X509Certificate) CertificateFactory.getInstance("X.509") + .generateCertificate(new ByteArrayInputStream(pem.getBytes(UTF_8))); + } catch (CertificateException e) { + fail("Failed to parse certificate", e); + return null; + } + } + + private static PrivateKey parsePrivateKey(String pem) { + try { + return KeyFactory.getInstance("RSA") + .generatePrivate(new PKCS8EncodedKeySpec( + Base64.getDecoder().decode( + pem + .replace("-----BEGIN PRIVATE KEY-----", "") + .replaceAll(System.lineSeparator(), "") + .replace("-----END PRIVATE KEY-----", "") + .replaceAll("\\s", "")) + )); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + fail("Failed to parse private key", e); + return null; + } + } + + private static void assertHasSAN(X509Certificate cert, Integer type, String san) { + var rawSans = assertDoesNotThrow(cert::getSubjectAlternativeNames, "unable to extract SANs from certificate"); + assertNotNull(rawSans, "missing SANs in certificate"); + for (var item : rawSans) { + if (item.size() == 2 && type.equals(item.get(0)) && san.equals(item.get(1))) { + return; + } + } + + fail("certificate does not contain SAN of type " + type + " and value " + san); + } + } + @Nested @DisplayName("Misc Tests") class MiscTests { @@ -1297,4 +1493,31 @@ private static String stackTrace(final Throwable th) { th.printStackTrace(new PrintWriter(sw, true)); return sw.getBuffer().toString(); } + + /** + * Compare two version strings. + * + * @param version1 Version 1 + * @param version2 Version 2 + * @return negative value if version 1 is smaller than version2, positive value of version 1 is greater, 0 if equal + */ + private static int compareVersions(String version1, String version2) { + int comparisonResult = 0; + + String[] version1Splits = version1.split("\\."); + String[] version2Splits = version2.split("\\."); + int maxLengthOfVersionSplits = Math.max(version1Splits.length, version2Splits.length); + + for (int i = 0; i < maxLengthOfVersionSplits; i++) { + Integer v1 = i < version1Splits.length ? Integer.parseInt(version1Splits[i]) : 0; + Integer v2 = i < version2Splits.length ? Integer.parseInt(version2Splits[i]) : 0; + int compare = v1.compareTo(v2); + if (compare != 0) { + comparisonResult = compare; + break; + } + } + + return comparisonResult; + } } diff --git a/src/test/java/de/stklcode/jvault/connector/model/PkiRequestTest.java b/src/test/java/de/stklcode/jvault/connector/model/PkiRequestTest.java new file mode 100644 index 00000000..ef9833b0 --- /dev/null +++ b/src/test/java/de/stklcode/jvault/connector/model/PkiRequestTest.java @@ -0,0 +1,192 @@ +/* + * Copyright 2016-2026 Stefan Kalscheuer + * + * 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 de.stklcode.jvault.connector.model; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * JUnit Test for {@link PkiRequest} model. + * + * @author Stefan Kalscheuer + */ +class PkiRequestTest extends AbstractModelTest { + + private static final String COMMON_NAME = "test.example.com"; + private static final List ALT_NAMES = List.of("www.example.com", "test.example.com"); + private static final String ALT_NAMES_STRING = "www.example.com,test.example.com"; + private static final List IP_SANS = List.of("192.168.1.1", "10.0.0.5"); + private static final String IP_SANS_STRING = "192.168.1.1,10.0.0.5"; + private static final List URL_SANS = List.of("https://api.example.com/v1"); + private static final String URL_SANS_STRING = "https://api.example.com/v1"; + private static final List OTHER_SANS = List.of("DNS:dns.example.com", "email:mail@example.com"); + private static final String OTHER_SANS_STRING = "DNS:dns.example.com,email:mail@example.com"; + private static final String TTL = "3600"; // 1 hour + private static final PkiRequest.Format FORMAT = PkiRequest.Format.PEM_BUNDLE; + private static final PkiRequest.KeyFormat KEY_FORMAT = PkiRequest.KeyFormat.PKCS8; + private static final boolean EXCLUDE_CN_FROM_SANS = true; + private static final String NOT_AFTER_DATE = "2030-12-31T23:59:59Z"; + private static final boolean REMOVE_ROOTS_FROM_CHAIN = true; + private static final List USER_IDS = List.of("user-role","group-admin"); + private static final String USER_IDS_STRING = "user-role,group-admin"; + private static final String CERT_METADATA = "dGVzdCBtZXRhZGF0YQ=="; + + PkiRequestTest() { + super(PkiRequest.class); + } + + @Override + protected PkiRequest createFull() { + return PkiRequest.builder() + .withCommonName(COMMON_NAME) + .withAltNames(ALT_NAMES) + .withIpSans(IP_SANS) + .withUrlSans(URL_SANS) + .withOtherSans(OTHER_SANS) + .withTtl(TTL) + .withFormat(FORMAT) + .withKeyFormat(KEY_FORMAT) + .withExcludeCnFromSans(EXCLUDE_CN_FROM_SANS) + .withNotAfter(NOT_AFTER_DATE) + .withRemoveRootsFromChain(REMOVE_ROOTS_FROM_CHAIN) + .withUserIds(USER_IDS) + .withCertMetadata(CERT_METADATA) + .build(); + } + + @Test + void buildFullTest() { + PkiRequest request = createFull(); + + assertEquals(COMMON_NAME, request.commonName(), "commonName mismatch"); + assertEquals(ALT_NAMES_STRING, request.altNames(), "altNames mismatch"); + assertEquals(IP_SANS_STRING, request.ipSans(), "ipSans mismatch"); + assertEquals(URL_SANS_STRING, request.urlSans(), "urlSans mismatch"); + assertEquals(OTHER_SANS_STRING, request.otherSans(), "otherSans mismatch"); + assertEquals(TTL, request.ttl(), "ttl mismatch"); + assertEquals(PkiRequest.Format.PEM_BUNDLE.value(), request.format(), "format mismatch"); + assertEquals(PkiRequest.KeyFormat.PKCS8.value(), request.keyFormat(), "keyFormat mismatch"); + assertEquals(EXCLUDE_CN_FROM_SANS, request.excludeCnFromSans(), "excludeCnFromSans mismatch"); + assertEquals(NOT_AFTER_DATE, request.notAfter(), "notAfter mismatch"); + assertEquals(REMOVE_ROOTS_FROM_CHAIN, request.removeRootsFromChain(), "removeRootsFromChain mismatch"); + assertEquals(USER_IDS_STRING, request.userIds(), "userIds mismatch"); + assertEquals(CERT_METADATA, request.certMetadata(), "certMetadata mismatch"); + + assertEquals( + "{" + + "\"common_name\":\"" + COMMON_NAME + "\"," + + "\"alt_names\":\"" + ALT_NAMES_STRING + "\"," + + "\"ip_sans\":\"" + IP_SANS_STRING + "\"," + + "\"url_sans\":\"" + URL_SANS_STRING + "\"," + + "\"other_sans\":\"" + OTHER_SANS_STRING + "\"," + + "\"ttl\":\"" + TTL + "\"," + + "\"format\":\"" + FORMAT.value() + "\"," + + "\"private_key_format\":\"" + KEY_FORMAT.value() + "\"," + + "\"exclude_cn_from_sans\":" + EXCLUDE_CN_FROM_SANS + "," + + "\"not_after\":\"" + NOT_AFTER_DATE + "\"," + + "\"remove_roots_from_chain\":" + REMOVE_ROOTS_FROM_CHAIN + "," + + "\"user_ids\":\"" + USER_IDS_STRING + "\"," + + "\"cert_metadata\":\"" + CERT_METADATA + "\"" + + "}", + objectMapper.writeValueAsString(request), + "unexpected JSON output for full request" + ); + } + + @Test + void buildFullStringsTest() { + PkiRequest request = PkiRequest.builder() + .withCommonName(COMMON_NAME) + .withAltNames(ALT_NAMES_STRING) + .withIpSans(IP_SANS_STRING) + .withUrlSans(URL_SANS_STRING) + .withOtherSans(OTHER_SANS_STRING) + .withTtl(TTL) + .withFormat(FORMAT) + .withKeyFormat(KEY_FORMAT) + .withExcludeCnFromSans(EXCLUDE_CN_FROM_SANS) + .withNotAfter(NOT_AFTER_DATE) + .withRemoveRootsFromChain(REMOVE_ROOTS_FROM_CHAIN) + .withUserIds(USER_IDS_STRING) + .withCertMetadata(CERT_METADATA) + .build(); + + assertEquals(COMMON_NAME, request.commonName(), "commonName mismatch"); + assertEquals(ALT_NAMES_STRING, request.altNames(), "altNames mismatch"); + assertEquals(IP_SANS_STRING, request.ipSans(), "ipSans mismatch"); + assertEquals(URL_SANS_STRING, request.urlSans(), "urlSans mismatch"); + assertEquals(OTHER_SANS_STRING, request.otherSans(), "otherSans mismatch"); + assertEquals(TTL, request.ttl(), "ttl mismatch"); + assertEquals(PkiRequest.Format.PEM_BUNDLE.value(), request.format(), "format mismatch"); + assertEquals(PkiRequest.KeyFormat.PKCS8.value(), request.keyFormat(), "keyFormat mismatch"); + assertEquals(EXCLUDE_CN_FROM_SANS, request.excludeCnFromSans(), "excludeCnFromSans mismatch"); + assertEquals(NOT_AFTER_DATE, request.notAfter(), "notAfter mismatch"); + assertEquals(REMOVE_ROOTS_FROM_CHAIN, request.removeRootsFromChain(), "removeRootsFromChain mismatch"); + assertEquals(USER_IDS_STRING, request.userIds(), "userIds mismatch"); + assertEquals(CERT_METADATA, request.certMetadata(), "certMetadata mismatch"); + + assertEquals( + "{" + + "\"common_name\":\"" + COMMON_NAME + "\"," + + "\"alt_names\":\"" + ALT_NAMES_STRING + "\"," + + "\"ip_sans\":\"" + IP_SANS_STRING + "\"," + + "\"url_sans\":\"" + URL_SANS_STRING + "\"," + + "\"other_sans\":\"" + OTHER_SANS_STRING + "\"," + + "\"ttl\":\"" + TTL + "\"," + + "\"format\":\"" + FORMAT.value() + "\"," + + "\"private_key_format\":\"" + KEY_FORMAT.value() + "\"," + + "\"exclude_cn_from_sans\":" + EXCLUDE_CN_FROM_SANS + "," + + "\"not_after\":\"" + NOT_AFTER_DATE + "\"," + + "\"remove_roots_from_chain\":" + REMOVE_ROOTS_FROM_CHAIN + "," + + "\"user_ids\":\"" + USER_IDS_STRING + "\"," + + "\"cert_metadata\":\"" + CERT_METADATA + "\"" + + "}", + objectMapper.writeValueAsString(request), + "unexpected JSON output for full request" + ); + } + + @Test + void buildMinimalTest() { + PkiRequest request = PkiRequest.builder() + .withCommonName(COMMON_NAME) + .build(); + + assertEquals(COMMON_NAME, request.commonName(), "commonName mismatch"); + assertNull(request.altNames(), "unexpected altNames"); + assertNull( request.ipSans(), "unexpected ipSans"); + assertNull(request.urlSans(), "unexpected urlSans"); + assertNull(request.otherSans(), "unexpected otherSans"); + assertNull(request.ttl(), "unexpected ttl"); + assertNull(request.format(), "unexpected format"); + assertNull(request.keyFormat(), "unexpected keyFormat"); + assertNull(request.excludeCnFromSans(), "unexpected excludeCnFromSans"); + assertNull(request.notAfter(), "unexpected notAfter"); + assertNull(request.removeRootsFromChain(), "unexpected removeRootsFromChain"); + assertNull(request.userIds(), "unexpected userIds"); + assertNull(request.certMetadata(), "unexpected certMetadata"); + + assertEquals( + "{\"common_name\":\"" + COMMON_NAME + "\"}", + objectMapper.writeValueAsString(request), + "unexpected JSON output for minimal request" + ); + } +} diff --git a/src/test/java/de/stklcode/jvault/connector/model/response/PkiCaResponseTest.java b/src/test/java/de/stklcode/jvault/connector/model/response/PkiCaResponseTest.java new file mode 100644 index 00000000..cfabd617 --- /dev/null +++ b/src/test/java/de/stklcode/jvault/connector/model/response/PkiCaResponseTest.java @@ -0,0 +1,132 @@ +/* + * Copyright 2016-2026 Stefan Kalscheuer + * + * 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 de.stklcode.jvault.connector.model.response; + +import de.stklcode.jvault.connector.model.AbstractModelTest; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * JUnit Test for {@link PkiCaResponse} model. + * + * @author Stefan Kalscheuer + */ +class PkiCaResponseTest extends AbstractModelTest { + private static final String LEASE_ID = "pki/ca/cert/1e26c095-b50e-483e-ab63-07612e6d6602"; + private static final Boolean RES_RENEWABLE = false; + private static final Integer RES_LEASE_DURATION = 21600; + private static final String AUTH_KEY_ID = "8b:e4:1a:d9:63:cf:8f:2b:e0:54:97:11:7c:da:02:f3:6a:5e:bc:4d"; + private static final String PEM_1 = "-----BEGIN CERTIFICATE-----\\nMIIDUTCCAjmgAwIBAgIJAKM+z4MSfw2mMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV\\n...\\nG/7g4koczXLoUM3OQXd5Aq2cs4SS1vODrYmgbioFsQ3eDHd1fg==\\n-----END CERTIFICATE-----\\n"; + private static final String PEM_2 = "-----BEGIN CERTIFICATE-----\\nMIIDLTCCAhWgAwIBAgIUQJcpa6gCLJWt+TowyNwVrdrjKlgwDQYJKoZIhvcNAQEL\\n...\\nTQ==\\n-----END CERTIFICATE-----\\n"; + private static final String ISSUER_ID = "fd10c11b-d5aa-4fb4-9ea2-94741e0b5f98"; + private static final String ISSUER_NAME = "my-issuer"; + + private static final String RES_CA_JSON = "{\n" + + " \"lease_id\": \"" + LEASE_ID + "\",\n" + + " \"renewable\": " + RES_RENEWABLE + ",\n" + + " \"lease_duration\": " + RES_LEASE_DURATION + ",\n" + + " \"data\": {\n" + + " \"authority_key_id\": \"" + AUTH_KEY_ID + "\",\n" + + " \"certificate\": \"" + PEM_2 + "\",\n" + + " \"revocation_time\": 0,\n" + + " \"revocation_time_rfc3339\": \"\"\n" + + " },\n" + + " \"warnings\": null,\n" + + " \"auth\": null\n" + + "}"; + private static final String RES_ISSUER_JSON = "{\n" + + " \"lease_id\": \"" + LEASE_ID + "\",\n" + + " \"renewable\": " + RES_RENEWABLE + ",\n" + + " \"lease_duration\": " + RES_LEASE_DURATION + ",\n" + + " \"data\": {\n" + + " \"ca_chain\": [" + + " \"" + PEM_1 + "\",\n" + + " \"" + PEM_2 + "\"\n" + + " ],\n" + + " \"certificate\": \"" + PEM_1 + "\",\n" + + " \"issuer_id\": \"" + ISSUER_ID + "\",\n" + + " \"issuer_name\": \"" + ISSUER_NAME + "\"\n" + + " },\n" + + " \"warnings\": null,\n" + + " \"auth\": null\n" + + "}"; + + PkiCaResponseTest() { + super(PkiCaResponse.class); + } + + @Override + protected PkiCaResponse createFull() { + return assertDoesNotThrow( + () -> objectMapper.readValue(RES_CA_JSON, PkiCaResponse.class), + "Creation of full model instance failed" + ); + } + + @Test + void jsonRoundtripCa() { + PkiCaResponse res = assertDoesNotThrow( + () -> objectMapper.readValue(RES_CA_JSON, PkiCaResponse.class), + "PkiCaResponse deserialization failed" + ); + assertNotNull(res, "Parsed response is NULL"); + assertEquals(LEASE_ID, res.responseHeader().leaseId(), "Incorrect leaseId"); + assertEquals(RES_RENEWABLE, res.responseHeader().renewable(), "Incorrect response renewable flag"); + assertEquals(RES_LEASE_DURATION, res.responseHeader().leaseDuration(), "Incorrect leaseDuration"); + assertNull(res.responseHeader().warnings(), "Incorrect warnings"); + assertNull(res.responseHeader().mountType(), "Incorrect mount type"); + + PkiCaResponse.Data data = res.data(); + assertNotNull(data, "PKI data is NULL"); + assertEquals(AUTH_KEY_ID, data.authorityKeyId(), "Incorrect authorityKeyId"); + assertEquals(PEM_2.replaceAll("\\\\n", "\n"), data.certificate(), "Incorrect certificate"); + assertEquals(0, data.revocationTime(), "Incorrect revocationTime"); + assertEquals("", data.revocationTimeRFC3339(), "Incorrect revocationTimeRFC3339"); + assertNull(data.caChain(), "Incorrect caChain"); + assertNull(data.issuerId(), "Incorrect issuerId"); + assertNull(data.issuerName(), "Incorrect issuerName"); + } + + @Test + void jsonRoundtripIssuer() { + PkiCaResponse res = assertDoesNotThrow( + () -> objectMapper.readValue(RES_ISSUER_JSON, PkiCaResponse.class), + "PkiCaResponse deserialization failed" + ); + assertNotNull(res, "Parsed response is NULL"); + + PkiCaResponse.Data data = res.data(); + assertNotNull(data, "PKI data is NULL"); + assertNull(data.authorityKeyId(), "Incorrect authorityKeyId"); + assertEquals(PEM_1.replaceAll("\\\\n", "\n"), data.certificate(), "Incorrect certificate"); + assertNull(data.revocationTime(), "Incorrect revocationTime"); + assertNull(data.revocationTimeRFC3339(), "Incorrect revocationTimeRFC3339"); + assertEquals( + List.of( + PEM_1.replaceAll("\\\\n", "\n"), + PEM_2.replaceAll("\\\\n", "\n") + ), + data.caChain(), + "Incorrect caChain" + ); + assertEquals(ISSUER_ID, data.issuerId(), "Incorrect issuerId"); + assertEquals(ISSUER_NAME, data.issuerName(), "Incorrect issuerName"); + } +} diff --git a/src/test/java/de/stklcode/jvault/connector/model/response/PkiResponseTest.java b/src/test/java/de/stklcode/jvault/connector/model/response/PkiResponseTest.java new file mode 100644 index 00000000..a45bbf64 --- /dev/null +++ b/src/test/java/de/stklcode/jvault/connector/model/response/PkiResponseTest.java @@ -0,0 +1,97 @@ +/* + * Copyright 2016-2026 Stefan Kalscheuer + * + * 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 de.stklcode.jvault.connector.model.response; + +import de.stklcode.jvault.connector.model.AbstractModelTest; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * JUnit Test for {@link PkiResponse} model. + * + * @author Stefan Kalscheuer + */ +class PkiResponseTest extends AbstractModelTest { + private static final String LEASE_ID = "pki/issue/test/7ad6cfa5-f04f-c62a-d477-f33210475d05"; + private static final Boolean RES_RENEWABLE = false; + private static final Integer RES_LEASE_DURATION = 21600; + private static final Long PKI_EXPIRATION = 1654105687L; + private static final String PKI_CERTIFICATE = "-----BEGIN CERTIFICATE-----\\nMIIDzDCCAragAwIBAgIUOd0ukLcjH43TfTHFG9qE0FtlMVgwCwYJKoZIhvcNAQEL\\n...\\numkqeYeO30g1uYvDuWLXVA==\\n-----END CERTIFICATE-----\\n"; + private static final String PKI_ISSUING_CA = "-----BEGIN CERTIFICATE-----\\nMIIDUTCCAjmgAwIBAgIJAKM+z4MSfw2mMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV\\n...\\nG/7g4koczXLoUM3OQXd5Aq2cs4SS1vODrYmgbioFsQ3eDHd1fg==\\n-----END CERTIFICATE-----\\n"; + private static final String PKI_CA_CHAIN_0 = "-----BEGIN CERTIFICATE-----\\nMIIDUTCCAjmgAwIBAgIJAKM+z4MSfw2mMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV\\n...\\nG/7g4koczXLoUM3OQXd5Aq2cs4SS1vODrYmgbioFsQ3eDHd1fg==\\n-----END CERTIFICATE-----\\n"; + private static final String PKI_PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\\nMIIEowIBAAKCAQEAnVHfwoKsUG1GDVyWB1AFroaKl2ImMBO8EnvGLRrmobIkQvh+\\n...\\nQN351pgTphi6nlCkGPzkDuwvtxSxiCWXQcaxrHAL7MiJpPzkIBq1\\n-----END RSA PRIVATE KEY-----\\n"; + private static final String PKI_PRIVATE_KEY_TYPE = "rsa"; + private static final String PKI_SERIAL_NUMBER = "39:dd:2e:90:b7:23:1f:8d:d3:7d:31:c5:1b:da:84:d0:5b:65:31:58"; + + private static final String RES_JSON = "{\n" + + " \"lease_id\": \"" + LEASE_ID + "\",\n" + + " \"renewable\": " + RES_RENEWABLE + ",\n" + + " \"lease_duration\": " + RES_LEASE_DURATION + ",\n" + + " \"data\": {\n" + + " \"expiration\": \"" + PKI_EXPIRATION + "\",\n" + + " \"certificate\": \"" + PKI_CERTIFICATE + "\",\n" + + " \"issuing_ca\": \"" + PKI_ISSUING_CA + "\",\n" + + " \"ca_chain\": [\n" + + " \"" + PKI_CA_CHAIN_0 + "\"\n" + + " ],\n" + + " \"private_key\": \"" + PKI_PRIVATE_KEY + "\",\n" + + " \"private_key_type\": \"" + PKI_PRIVATE_KEY_TYPE + "\",\n" + + " \"serial_number\": \"" + PKI_SERIAL_NUMBER + "\"\n" + + " },\n" + + " \"warnings\": null,\n" + + " \"auth\": null\n" + + "}"; + + PkiResponseTest() { + super(PkiResponse.class); + } + + @Override + protected PkiResponse createFull() { + return assertDoesNotThrow( + () -> objectMapper.readValue(RES_JSON, PkiResponse.class), + "Creation of full model instance failed" + ); + } + + @Test + void jsonRoundtrip() { + PkiResponse res = assertDoesNotThrow( + () -> objectMapper.readValue(RES_JSON, PkiResponse.class), + "PkiResponse deserialization failed" + ); + assertNotNull(res, "Parsed response is NULL"); + assertEquals(LEASE_ID, res.responseHeader().leaseId(), "Incorrect leaseId"); + assertEquals(RES_RENEWABLE, res.responseHeader().renewable(), "Incorrect response renewable flag"); + assertEquals(RES_LEASE_DURATION, res.responseHeader().leaseDuration(), "Incorrect leaseDuration"); + assertNull(res.responseHeader().warnings(), "Incorrect warnings"); + assertNull(res.responseHeader().mountType(), "Incorrect mount type"); + + PkiResponse.Data data = res.data(); + assertNotNull(data, "PKI data is NULL"); + assertEquals(PKI_EXPIRATION, data.expiration(), "Incorrect pki expiration"); + assertEquals(PKI_CERTIFICATE.replaceAll("\\\\n", "\n"), data.certificate(), "Incorrect pki certificate"); + assertEquals(PKI_ISSUING_CA.replaceAll("\\\\n", "\n"), data.issuingCa(), "Incorrect pki issuingCa"); + assertEquals(List.of(PKI_CA_CHAIN_0.replaceAll("\\\\n", "\n")), data.caChain(), "Incorrect pki caChain"); + assertEquals(PKI_PRIVATE_KEY.replaceAll("\\\\n", "\n"), data.privateKey(), "Incorrect pki privateKey"); + assertEquals(PKI_PRIVATE_KEY_TYPE, data.privateKeyType(), "Incorrect pki privateKeyType"); + assertEquals(PKI_SERIAL_NUMBER, data.serialNumber(), "Incorrect pki serialNumber"); + } +} diff --git a/src/test/java/de/stklcode/jvault/connector/model/response/PkiRevocationResponseTest.java b/src/test/java/de/stklcode/jvault/connector/model/response/PkiRevocationResponseTest.java new file mode 100644 index 00000000..0eb4cace --- /dev/null +++ b/src/test/java/de/stklcode/jvault/connector/model/response/PkiRevocationResponseTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2016-2026 Stefan Kalscheuer + * + * 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 de.stklcode.jvault.connector.model.response; + +import de.stklcode.jvault.connector.model.AbstractModelTest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * JUnit Test for {@link PkiRevocationResponse} model. + * + * @author Stefan Kalscheuer + */ +class PkiRevocationResponseTest extends AbstractModelTest { + private static final String LEASE_ID = "pki/revoke/test/0215cc7e-cadd-4553-baab-25869500a772"; + private static final Boolean RES_RENEWABLE = false; + private static final Integer RES_LEASE_DURATION = 21600; + private static final Long REVOCATION_TIME = 1784147440L; + private static final String REVOCATION_TIME_RFC3339 = "2026-07-15T20:30:40Z"; + private static final String STATE = "revoked"; + + private static final String RES_JSON = "{\n" + + " \"lease_id\": \"" + LEASE_ID + "\",\n" + + " \"renewable\": " + RES_RENEWABLE + ",\n" + + " \"lease_duration\": " + RES_LEASE_DURATION + ",\n" + + " \"data\": {\n" + + " \"revocation_time\": \"" + REVOCATION_TIME + "\",\n" + + " \"revocation_time_rfc3339\": \"" + REVOCATION_TIME_RFC3339 + "\",\n" + + " \"state\": \"" + STATE + "\"\n" + + " },\n" + + " \"warnings\": null,\n" + + " \"auth\": null\n" + + "}"; + + PkiRevocationResponseTest() { + super(PkiRevocationResponse.class); + } + + @Override + protected PkiRevocationResponse createFull() { + return assertDoesNotThrow( + () -> objectMapper.readValue(RES_JSON, PkiRevocationResponse.class), + "Creation of full model instance failed" + ); + } + + @Test + void jsonRoundtrip() { + PkiRevocationResponse res = assertDoesNotThrow( + () -> objectMapper.readValue(RES_JSON, PkiRevocationResponse.class), + "PkiResponse deserialization failed" + ); + assertNotNull(res, "Parsed response is NULL"); + assertEquals(LEASE_ID, res.responseHeader().leaseId(), "Incorrect leaseId"); + assertEquals(RES_RENEWABLE, res.responseHeader().renewable(), "Incorrect response renewable flag"); + assertEquals(RES_LEASE_DURATION, res.responseHeader().leaseDuration(), "Incorrect leaseDuration"); + assertNull(res.responseHeader().warnings(), "Incorrect warnings"); + assertNull(res.responseHeader().mountType(), "Incorrect mount type"); + + PkiRevocationResponse.Data data = res.data(); + assertNotNull(data, "PKI data is NULL"); + assertEquals(REVOCATION_TIME, data.revocationTime(), "Incorrect revocationTime"); + assertEquals(REVOCATION_TIME_RFC3339, data.revocationTimeRFC3339(), "Incorrect revocationTimeRFC3339"); + assertEquals(STATE, data.state(), "Incorrect state"); + } +} diff --git a/src/test/resources/data_dir/core/_mounts b/src/test/resources/data_dir/core/_mounts index fe4d71c4..1ebd45d1 100644 --- a/src/test/resources/data_dir/core/_mounts +++ b/src/test/resources/data_dir/core/_mounts @@ -1 +1 @@ -{"Value":"AAAAAQJ7mykBIbHX5k81qdXEpvlLgRF1ZSlODETcB1JBZ7nj0Muskpvvl3jofN5XH1Td8ibJlrR0o5o/OUjZAz9t0Da+ZzCy4ga9G2SmgWkUAravTqfPO/ZxWh2hqTso2WPBXRM3/IeR0SAv/zh7m7JILxjKybJmnl9U6fkjPID/us0AscckZ9kgJ4g8jwaTzPfRp5U8jMebHYbABXZ65PeUOvNiDVcOvQDWPJrMxICz12xbeJ8mKs5MHpcNkLtPoRCQSpsh0YYTwkuF7NvpIciqIJ4/Yb7wYO+vp9AATbiIs3sSFBWxXEl0OAg/SAmOvaR27Y7/NHN//mg81jkMOHv62/Fxf11I8t1d63oyWFQhP0xR9eoq5hGNQ/30I2m1prhZtLRC2ieKASBDMxTzyNS5G5bsVXvhCsxn8tiC0Ma+ySOfxMQzBRfbx8rtoGmLFP+l/d6VMOPGFxmYqzLS5HvvpCryscCqLn7A8i6TMSrZiF7ZevyfEBpThqhJiYHzUxf07O5IAe6JBSGuNV9gLE+uJXaiYLedJwSfjRKwdQyzer730dU1IegW3KYTb/5hSW4eaETKkjc/alC536WlvAv+5FyckDBc3aVC+hHB7lKZG5YANkOUS3m5I8epemOmuKQ5pnXLOdDkQ14DyNCC79NwLltkp0d6sTNstQ44XAbs0HlLjs4EFwg5Hps9qHeXgTOXeAvwUerJgM20nKszlB+Oy+JzZm0xOK5xoJwy+z0/U3PtJ+7pwAipesIa2QEzqMt3EneuPuwEcv3bPUcowukq542sCEK0CuZjLqTUU9nNqiZan5f5pWuL0hYw4NFIkNfQyRlqgKpMaplDk/2fBqaV98yn0DWceEMWRY4NXpEMS+ysPDIeamX99auWqakb95AZ7tySpkRAkZYtq1nY5Nu0w7NyJrJZ1lhBHs2ZjW0tpXn2CL0MhMVArg=="} +{"Value":"AAAAAQI2YmUzdWwiF66Y0VmuBjdQEAjgGNDFeaqQuPWlPVb4wD93V+CeqKz+qXCGtyOiZPDW2C4ElS7+sDE2uhRGR10VI3/xkmbrg17fWNJYpfT7WPXzObieA717limFZ1MIL7DVYRKvrqdAE/Vzk6FwTCU+2JDgs94xO1z0VdSWWpmJCl7QxeTdVpMkMOoOqLY+w6N/1a8en349N/C9mM2nl1ztWt4KlwxWgGsDIi9kn64pdLfbutP2H2iuwdmgKdtk25P2153+e1UIPBdOuAIBLhpJ8KCEK7T5gZeW3MWRa1MYw8qjpaFKWReBT1eZbZhPgqVX8fFy1zT8d1hwgyeoqyALTZUW5gLIchbZUQlVAq17j5dc0wbuPsWUVXmm2t1fZpx/tUmC0Nv4CzyJXAyk5DTtzgaXO7xOZlBiyl8Dk8nEcFznzxd2Ob9Nhq6JuRV3iY17ir0cIDBCNsodOD3y6RMosxW8Tu7+XbfauihokEgqHY9r3Kk3fouE2YiDYH3n7WMORepHXGI3DRRCjhMKZy/uNMcnBLa7x6kSBp5w9CgcpMRUriOKGuj94oBUkzjzkYm2gOr+lQbm8R6/Is7erd9NQBXk8fcch8KgJo6yTrU87DJ2cF7M81nuAcQP95qEDk61gQgYXRievrItKTo4tVaQToxqz22EXaPpvwV+dnlfpdrbi0XA1qxMOgPIsVTPOLx32lppJF08cShyxgT5VAXm8DSUszlbzZgHdhiRAf1xlphfP1vd3DufgALmrPNVXGD0FXr/FX3s5ct70BMBRnI6mMwie7Q55PlsKVko6phL7J0Dm5LMwevxA2VOCk4P5+o7P8pnxedw0pMLa1QgDlOS2gj4iRBIouQ/xBjOSnMXBKFfP7qGSROL7E/+POVjfhPOj8VCWck7bNt6G382+C40zfG0V2dz7S52YSJvT2rdgDUlkAVwf/R9QXOYNwnwWuKEPt2EMJaEsGP/Vs+lCcAkLmGu5b2tyi9a3gZBSjAptUNyYOMEvm15dgx+0KQGGwndfg1Z7tJJ4pEmt9FzDjb6UEB2ppttyw=="} diff --git a/src/test/resources/data_dir/logical/0d6ef259-2cb5-a42c-f29c-ed991995598a/_ca b/src/test/resources/data_dir/logical/0d6ef259-2cb5-a42c-f29c-ed991995598a/_ca new file mode 100644 index 00000000..b8fcf7aa --- /dev/null +++ b/src/test/resources/data_dir/logical/0d6ef259-2cb5-a42c-f29c-ed991995598a/_ca @@ -0,0 +1 @@ +{"Value":"AAAAAQL7ePakf6pq5W18Ff1Tei0hF8e58ClCCeIiDSCEdOQ0x7lx7DZ5ZxM8vcHKvjZBtPJ+ishr1yiyv1335qVxAmW9mBN+z3onEB995p0idCO7zdPYO/V9GdSzwXBQLe5kB2ISnOXsE2i/jkLekNIRzsXsHxxaLjNbEK83ae6IMkpnkcGCB8Y5M7ELPw+n4cV4qhJdn2PChu8iERBvHt+Wo+IxYT8N7MmOZL6ZA7x+CO0mKCLj3BPL+ibaHpaEY9Ao6pPZ63AGiQIHqRDzNq203oIgd6KpkL/2TkkxENpgzG78nCaHH7p7qbcKKlzZWnrY6No30iSaZBVkgdZ1sBa5APsEYfH4Dww291iwKV6NtiPV5V29lqGXMBlLMYne/rTOlujXbSLov/f3H5Ekm/BrVQ9BDypAHIdua2BLBO+sHjRol8JSAWVoCM4M/F4ANkiOQSOUVLUBG1cu265XqjoHd/OMat/3Yyd66RJ6X+QESZLv3YksN7S8wYOk+n2pgnmrc2CtaTK8XHynyMiiI74O0tm1fMxmkjfDG+y/IN7SNxUB8YjDQfnNwiZu4PIIOODbkMNAULR1kvOpMrE1UYljlpwzWJ/7y5l/WKpmZZ2sEuJNqo4ZcMkATT4qlw6CCMQwTDCfZIF/tj/QtUqbW5ildJuovp82v3SlkvSCLmcnN3EPO2BoAn5/1PKmNCrx+acAp8pld9YDCyP+qGgycrlKKIXSaXb3VljHt1TaK8f+jQwvpLfW/5iOsl+XvkVMW4QYJ+bOR3hrkQq4wr9RkylRBr4veij599yRw/53f8laUQUr4KoBW/HaPo40JyB9pCO9kEf6tZwH1Yx51NRKbi/VdQ9Eq24ciHs1aXURIA6h4tgG4xcGtc7Bko9mdoLMkH/kxkNbp7rEmXP8NM5ML9NBX+Kam/664OLJ8CQRncF/mBgD1PyfubC5EwlvelXgRpk5rpxnNQ+HeLcTsx6s8piFyAqlfdZYve/RG3ppalhA2nabhxFKPqGJ/+LAIYr0A6Iv/4Gh1clk3s9Q2KFvkfXCujM2pI+Zik3L3pT3MRxO4E09a7bhwhD5JNWjB5sOF5rv9HKisFftlxYHNqnpLV45cHPqw+qqw6uKMpYSaTHvXw=="} diff --git a/src/test/resources/data_dir/logical/0d6ef259-2cb5-a42c-f29c-ed991995598a/_crl b/src/test/resources/data_dir/logical/0d6ef259-2cb5-a42c-f29c-ed991995598a/_crl new file mode 100644 index 00000000..44b95de4 --- /dev/null +++ b/src/test/resources/data_dir/logical/0d6ef259-2cb5-a42c-f29c-ed991995598a/_crl @@ -0,0 +1 @@ +{"Value":"AAAAAQIokCJpo5ROVX8IDvGBvtA6aKWMrsEpMvzgCwRntif0ZkPseb8P781bggINPAOA4yGQKNWxA/3UuZYBjzVGM/X8oZ+NiXl+Ufw/V+iN1gDVzb031/diiUrOOBnstXD0aDoeHzKfc49CpR45uHBb1BfaO/RfSIGDWDjrPkfSIakfEIK4ciLT3euIsz/dPTDtSCzJu3kyoqT5YB7vLx9k351D3bd57znvf7JrrDxMPvQhhJLV6+VsMizhPmJS8A/oD9vvmKXcBOnrjlMxZWicnOEpEIIhYDXHbAHDFhjrMlGENNs4xIpl5FNS1JhkAlorYIfhGIP7+iC9+DUMczyHLpGBuFS4R+19I1C3wxMG+KaA+4Jr4SgaNnrl0dyNYe3J1cizyENIuVWI9I452ge6vPNq4eu9hkhVeeitWCemWfLg1QEkc8GnLSDNFx3DqsswbA4c+lGEyCdlTWLlhHuLB6456DvjbzjgwbyGTihGeXSuo2455oXqfbZB7BsqMlMpOJ8gHT91IoN8Q6nRzPj8EmhRuMqjw1IHzYKMmqzy68wT3VRNmw2NJTa6i8hEDxU="} diff --git a/src/test/resources/data_dir/logical/0d6ef259-2cb5-a42c-f29c-ed991995598a/certs/_40-97-29-6b-a8-02-2c-95-ad-f9-3a-30-c8-dc-15-ad-da-e3-2a-58 b/src/test/resources/data_dir/logical/0d6ef259-2cb5-a42c-f29c-ed991995598a/certs/_40-97-29-6b-a8-02-2c-95-ad-f9-3a-30-c8-dc-15-ad-da-e3-2a-58 new file mode 100644 index 00000000..25fb0a3e --- /dev/null +++ b/src/test/resources/data_dir/logical/0d6ef259-2cb5-a42c-f29c-ed991995598a/certs/_40-97-29-6b-a8-02-2c-95-ad-f9-3a-30-c8-dc-15-ad-da-e3-2a-58 @@ -0,0 +1 @@ +{"Value":"AAAAAQJ+9fR43a0F4HFTZXXuzBrZzN/WgZbsxPWDQfkM43SoI8jI+3LKuB2XpcHWIxwx31/5TM0/tB3d2uTD2sn36zxRqGxUwL0Ng5RaL/zRVNc4z4y4eajE8gqwoZm1a6ZJSv6Auw27hyVkpKp9O4UttIGg13UmSUIiG2XyMk3L35d47g2WmuO6AQRDL9zLEnzFLO+ryxXKR3ekX65m2Lo3ou+0ZaEIvFV/2AnBXh8FJxXHdjdcHoFkZ7XRERBYyX7g+/Zjpe40TKMImfSjs3YPLGW4DRvDFJjZLz/iOoxfXRslX0N8ZqIOsV7LdvqzRahcjSNRVkFdyhl162iWx0RJVRqAS/vyXT7vvH0R2Td1mghjqnTeZuWJlawxynl+7a4jDVa5MQPJPWJYe/Oe34mwbGs0aL9vXZpCpaOolHB8pllKF41OTPhvrCwn/2X2wgJn8sp+mwid8dIQBgNcTauXUv437h+j9/iUc8QrQR3rsJl4bvzFsmRKhS0LeQtBK2I9lezSy9cpMPIWd0z39pDUWKepo2MZH5bwipi7E/jokbfDNg1G80Sl85TloF4lYkXVshD/P9Yxzz3OiQaziHjbxRWg7IJNqV5J2yxNbd3YjVaZkP8pIudqfxkf8olkVEGc9DNRL71Vyl4xcryBJXnYTSut5aYqIitZwLqPmvJMybeTTuMq2pOE4WIiC//7npkgv4X7l81XusiY8WDDO2KfTTZMcJEyxxrbr6i+zC7+xwr4hEozBVVObWIMqZgXqE5jE6HIKFi1YMOsgWNAVxPwDVO9nOnzZkLkNHWuM9PFBYB9J/n/8557XUmDJCIYwYghs+QRJ/zlBSxJBIvcu1YV6Mza9OPMFWv3+PcWCzYvnjEdtqM93RmRccMWpawoAX8FurChm1AIU0EZ1xKvQHeIhkSJdT6l86zPgqdgxc7n91As8rTS7wuj+/IewgZAWaZyr0s61E0OJL5wl4ZmBG2dLMb4JF5qfVKHeKs3uxdBkAaejslGSsJVRZ+wWQBx5F4VsmXC4OfZFOGUXA6EPRRHqBrrMlbLk7zErr4AL4kr/X+9tu1aPUyJQaJKKFlmUGU5PI9PjW6Lp8pjcgpuOMj0XENtxzM6Dw0FGryeo2t3Kw=="} diff --git a/src/test/resources/data_dir/logical/0d6ef259-2cb5-a42c-f29c-ed991995598a/config/_ca_bundle b/src/test/resources/data_dir/logical/0d6ef259-2cb5-a42c-f29c-ed991995598a/config/_ca_bundle new file mode 100644 index 00000000..beccc4cb --- /dev/null +++ b/src/test/resources/data_dir/logical/0d6ef259-2cb5-a42c-f29c-ed991995598a/config/_ca_bundle @@ -0,0 +1 @@ +{"Value":"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"} diff --git a/src/test/resources/data_dir/logical/0d6ef259-2cb5-a42c-f29c-ed991995598a/role/_example-com b/src/test/resources/data_dir/logical/0d6ef259-2cb5-a42c-f29c-ed991995598a/role/_example-com new file mode 100644 index 00000000..1fc85163 --- /dev/null +++ b/src/test/resources/data_dir/logical/0d6ef259-2cb5-a42c-f29c-ed991995598a/role/_example-com @@ -0,0 +1 @@ +{"Value":"AAAAAQL7CVgjaBH2v8KBrE/Xxs1xITQS92Xwzi7+nuJGdriZ4a0X5IAuzCYUy/BFSiB972l04VY03Wfmb6q0n88B3tiWFpNX2Sc+QEhGXq2N+JqKXEwhLm3UnxH5pmsl51gMP3JjI+xcZf2gWaJLvHWjVIhbIwYeATE1IebnjeeM7vINyI2NBr2AfUhhlkn1bo5gLUrwZ4XQfDtfDj5VM0xkgK1A8oSQVhVZCDW4dyZ/4X09jhjqWBhgebzVzlkHQE4+UgZXcDc57RGFAYx8FRPpbVO8rjIj9KR4T9KK7zN4hJae47/Go2DGZ2Jk3O3NRboMFvpPgXAaDjIrX5IGhk2Nk7i9hyKwPv5wNDYadFHtQ30hYKxDckeD/VVKkrvZPKAhH9bXms39bRVgKdzIn67l0bA6XIEdVKmv9Pcj9q/WP1j1IQPINugYY54EljSf24O0d/zhkwHboyzTRArc2N046GJKnkfaRVDMOeu90WkxFWt8oMfYiI3ce96CMMWCGhMCeKRVQ+gP87cDENhdeXx4BwGJnOIK+gXSRhMRQvTfccfsfSLuKJScFgyn/sJHpjzEyzDzbzUUKBYlUyDkcsRig+sja5iHeEFp6mnoyBrkLRI4lM25wc36O9nmzhTGK6aN8/CD7bRW2/XTsdGd+qNiCKaBQeE0S9ThJC/z08AIFKIMWyO8AFKEFi2JCV3JvCJ3gvM460qOELqGP9SrG2QlyUi2kAOnOqsIQwonjXBUQ0CHQu2W6V1TW8XYJCFwyNX7QojyiCJwRz61vYCHJhr9cmWpd2yPa61KP8QlT7gokS/EQgOl7AiooM65iuSDX+Uba3U2o52c94gP1gAhHmvnsXFo3jRzLn2ioM236jTl0f71G+My/NgOb9IJWxxz89Jllp6F33nZuj5axLCf/YvfhJdTnELDK3jS8ZPUuSrAo2bKD/DhSth1N6Kd97mV9RsFDy9y2Ubb6QvfZVq/+1ye2JcB6LQFmD3RNmWQUCHdbOK55EYMfWXz1O0S1nJS16451My1scJKec4pPCYHVVx1zyCTs9LzeqYg0Vq8kM+lKK4avdrD0ty201XEgAJn/O+LaGAYABd/6XDXavneWhUVBZ8sMo6T+r1qsXvbKTCywnoi2iTAo2q6OwcZZuFjdx7/z90UIZwqtyfxEapQRR9aI60PrgDqRtsVsC/E0ktD14eK3Xk1wUtN4bhUU+TvjxhcwAVaG5E3VSZu2f9nlND5XxM+qS8Utt0NgZh17I3L+Yyi1i03Car3nqVCnPKE27BvjNC3AAPp74hQQ1F3XrBZxUukXsxCrY18ZNv3Ebpp4V/RvRTtW0dAJE0CnYV9BqOyyGW81Oh4f5MFCVW82SJRM2cc5v/3DsFOcp6objwLxH+3EkfDIOQ6XifGBWfAHrwyO1vzFpKOBwK7FBpDNTmHxzP28LTz/B9E9QbpfEZ7+iQ1ZG8ELtOA5DXyMoLVQZ/YN/JhQrZSh2BFHXSdDX7+vXdb"} diff --git a/src/test/resources/data_dir/logical/18ec1ee7-6a2c-5dc8-6dfe-dc15ff1352c0/_casesensitivity b/src/test/resources/data_dir/logical/18ec1ee7-6a2c-5dc8-6dfe-dc15ff1352c0/_casesensitivity index a70aa699..57548919 100644 --- a/src/test/resources/data_dir/logical/18ec1ee7-6a2c-5dc8-6dfe-dc15ff1352c0/_casesensitivity +++ b/src/test/resources/data_dir/logical/18ec1ee7-6a2c-5dc8-6dfe-dc15ff1352c0/_casesensitivity @@ -1 +1 @@ -{"Value":"AAAAAQLVD0p5guJ0hefKlgCk5K1tDfy949Rg1345BrwUiaCo42Bitynx//GwWbCW86oz06mHuebzxht4+FE6LCnrsIb+"} +{"Value":"AAAAAQKEKZlnZ/NuOmiNauEFPqzYp3UyIHzBuR1OV8ZbcWpDk0/d3wA5hFsurfN2zLOSK3WA5lO2csWfbdXtk7MNQ9eb"} diff --git a/src/test/resources/data_dir/sys/counters/requests/2026/_07 b/src/test/resources/data_dir/sys/counters/requests/2026/_07 new file mode 100644 index 00000000..72217be7 --- /dev/null +++ b/src/test/resources/data_dir/sys/counters/requests/2026/_07 @@ -0,0 +1 @@ +{"Value":"AAAAAQKJ+D5+KNpRUaddxaDaJrKbQIWvnXuetdnu1zHFEojQKe1KwoR4w9SX"}