From 849c8c82cb04924c51a16cf0e85751a5da46c6ac Mon Sep 17 00:00:00 2001
From: Leo Galambos
Date: Thu, 30 Jul 2026 20:14:11 +0200
Subject: [PATCH] security(pki): enforce configuration-driven CA profiles
* add versioned root and intermediate CA profile documents
* extend the strict profile schema with closed certificate kinds
* package canonical built-in root and intermediate profiles
* enforce immutable import and explicit activation for CA profiles
* bind CA credentials to exact profile ID, version, and canonical hash
* resolve active profiles for root and intermediate issuance
* validate issuer-controlled CA requests before backend execution
* enforce complete CA DER and extension postconditions
* reject inactive, mismatched, and malicious profile/backend inputs
* preserve historical credential bindings across profile activation changes
* add root and intermediate profile version-switch coverage
BREAKING CHANGE: root and intermediate CA issuance now requires an explicitly imported and activated versioned CA profile.
---
.../java/zeroecho/pki/api/ProfileService.java | 10 +-
.../pki/api/credential/CaProfileBinding.java | 14 +-
.../BuiltInCertificateProfileCatalog.java | 16 +-
.../pki/api/profile/CaCertificatePolicy.java | 63 ++
.../zeroecho/pki/api/profile/CaKeyUsage.java | 15 +
.../pki/api/profile/CertificatePolicy.java | 11 +
.../pki/api/profile/CertificateProfile.java | 18 +-
.../profile/CertificateProfileDefinition.java | 74 +-
.../CertificateProfileDocumentCodec.java | 365 +++++--
.../api/profile/CertificateProfileKind.java | 17 +
.../api/profile/LeafCertificatePolicy.java | 17 +-
.../profile/SubjectAlternativeNamePolicy.java | 6 +-
.../pki/api/profile/SubjectPolicy.java | 5 +-
.../pki/api/profile/package-info.java | 14 +-
.../pki/impl/ProfileLifecycleFailure.java | 1 +
.../core/CaCertificateProfileValidator.java | 261 +++++
.../zeroecho/pki/impl/core/CaProofGate.java | 32 +-
.../core/CertificateProfileValidator.java | 29 +-
.../impl/core/CredentialProfileBindings.java | 4 +-
.../pki/impl/core/DefaultCaService.java | 333 +++++--
.../pki/impl/core/ManagedCaIssuance.java | 194 ----
.../core/ValidatedCaCertificateRequest.java | 138 +++
.../bc/BcX509CredentialIssuerBackend.java | 236 +----
.../pki/impl/fs/FilesystemPkiStore.java | 11 +
.../java/zeroecho/pki/impl/fs/FsCodec.java | 4 +-
.../framework/CredentialIssuerBackend.java | 16 +-
.../java/zeroecho/pki/spi/store/PkiStore.java | 19 +-
.../zeroecho/pki/profiles/v1/catalog.json | 2 +-
.../pki/profiles/v1/email-signing.json | 2 +-
.../pki/profiles/v1/intermediate-ca.json | 1 +
.../zeroecho/pki/profiles/v1/root-ca.json | 1 +
.../zeroecho/pki/profiles/v1/server-tls.json | 2 +-
.../zeroecho/pki/profiles/v1/vpn-client.json | 2 +-
.../zeroecho/pki/profiles/v1/vpn-server.json | 2 +-
.../BuiltInCertificateProfileCatalogTest.java | 82 +-
.../CertificateProfileDocumentCodecTest.java | 145 ++-
.../e2e/CaProfileIssuanceEnforcementTest.java | 919 ++++++++++++++++++
.../pki/e2e/H7EndEntityAcceptanceE2eTest.java | 29 +-
.../e2e/H7EndEntityCsrRejectionE2eTest.java | 8 +-
.../java/zeroecho/pki/e2e/PkiCoreE2eTest.java | 30 +-
.../zeroecho/pki/e2e/PkiProofGateE2eTest.java | 139 +--
.../DefaultStatusObjectServiceCrlTest.java | 4 +-
.../impl/core/H7ProfileEnforcementTest.java | 44 +-
...EffectiveCredentialStatusResolverTest.java | 4 +-
.../pki/impl/fs/FilesystemPkiStoreTest.java | 23 +-
.../fs/FilesystemProfileLifecycleTest.java | 53 +-
.../fs/FilesystemRevocationJournalTest.java | 2 +-
.../zeroecho/pki/impl/fs/FsCodecTest.java | 50 +-
.../pki/testkit/H7ProfileDocuments.java | 2 +-
.../zeroecho/pki/testkit/PkiTestRuntime.java | 38 +-
50 files changed, 2608 insertions(+), 899 deletions(-)
create mode 100644 pki/src/main/java/zeroecho/pki/api/profile/CaCertificatePolicy.java
create mode 100644 pki/src/main/java/zeroecho/pki/api/profile/CaKeyUsage.java
create mode 100644 pki/src/main/java/zeroecho/pki/api/profile/CertificatePolicy.java
create mode 100644 pki/src/main/java/zeroecho/pki/api/profile/CertificateProfileKind.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/core/CaCertificateProfileValidator.java
delete mode 100644 pki/src/main/java/zeroecho/pki/impl/core/ManagedCaIssuance.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/core/ValidatedCaCertificateRequest.java
create mode 100644 pki/src/main/resources/zeroecho/pki/profiles/v1/intermediate-ca.json
create mode 100644 pki/src/main/resources/zeroecho/pki/profiles/v1/root-ca.json
create mode 100644 pki/src/test/java/zeroecho/pki/e2e/CaProfileIssuanceEnforcementTest.java
diff --git a/pki/src/main/java/zeroecho/pki/api/ProfileService.java b/pki/src/main/java/zeroecho/pki/api/ProfileService.java
index 5dee9d8..68e8fb6 100644
--- a/pki/src/main/java/zeroecho/pki/api/ProfileService.java
+++ b/pki/src/main/java/zeroecho/pki/api/ProfileService.java
@@ -16,8 +16,14 @@ import zeroecho.pki.api.profile.ImportedCertificateProfileVersion;
/**
* Versioned certificate-profile import, activation, and lookup service.
*
- * Import never activates a profile. End-entity issuance must resolve only an
- * explicitly activated persisted version through {@link #requireActiveProfile(String)}.
+ *
+ * Import never activates a profile. All versions of one logical profile ID
+ * have one immutable certificate kind. End-entity issuance resolves only an
+ * explicitly activated persisted version through
+ * {@link #requireActiveProfile(String)}. Root and intermediate issuance use the
+ * same authoritative active-profile lookup and bind issued credentials to the
+ * exact resolved version.
+ *
*/
public interface ProfileService {
/** Imports a bounded strict JSON profile document. */
diff --git a/pki/src/main/java/zeroecho/pki/api/credential/CaProfileBinding.java b/pki/src/main/java/zeroecho/pki/api/credential/CaProfileBinding.java
index 6de67d8..82c8d04 100644
--- a/pki/src/main/java/zeroecho/pki/api/credential/CaProfileBinding.java
+++ b/pki/src/main/java/zeroecho/pki/api/credential/CaProfileBinding.java
@@ -4,16 +4,18 @@
******************************************************************************/
package zeroecho.pki.api.credential;
+import java.util.Objects;
+
+import zeroecho.pki.api.profile.CertificateProfileRef;
+
/**
- * Explicit CA policy identity for a root or intermediate credential.
+ * Exact versioned CA profile identity for a root or intermediate credential.
*
- * @param profileId existing CA policy identifier
+ * @param reference imported CA profile version used for issuance
*/
-public record CaProfileBinding(String profileId) implements CredentialProfileBinding {
+public record CaProfileBinding(CertificateProfileRef reference) implements CredentialProfileBinding {
/** Creates a CA profile binding. */
public CaProfileBinding {
- if (profileId == null || profileId.isBlank()) {
- throw new IllegalArgumentException("profileId must not be null/blank");
- }
+ Objects.requireNonNull(reference, "reference");
}
}
diff --git a/pki/src/main/java/zeroecho/pki/api/profile/BuiltInCertificateProfileCatalog.java b/pki/src/main/java/zeroecho/pki/api/profile/BuiltInCertificateProfileCatalog.java
index 5269ef3..0953fce 100644
--- a/pki/src/main/java/zeroecho/pki/api/profile/BuiltInCertificateProfileCatalog.java
+++ b/pki/src/main/java/zeroecho/pki/api/profile/BuiltInCertificateProfileCatalog.java
@@ -32,7 +32,7 @@ import tools.jackson.core.json.JsonReadFeature;
import zeroecho.pki.api.PkiException;
/**
- * Loader for the fixed built-in end-entity profile resource catalogue.
+ * Loader for the fixed built-in certificate-profile resource catalogue.
*
*
* The catalogue is stateless and thread-safe. Every load reparses the packaged
@@ -68,7 +68,8 @@ public final class BuiltInCertificateProfileCatalog {
private static final String ERROR_PREFIX =
"Built-in certificate profile catalogue rejected: code=";
private static final Set EXPECTED_PROFILE_IDS =
- Set.of("server-tls", "vpn-server", "vpn-client", "email-signing");
+ Set.of("server-tls", "vpn-server", "vpn-client", "email-signing",
+ "root-ca", "intermediate-ca");
private BuiltInCertificateProfileCatalog() {
}
@@ -222,6 +223,9 @@ public final class BuiltInCertificateProfileCatalog {
private final Set identities = new HashSet<>();
private final Set hashes = new HashSet<>();
private final Set profileIds = new HashSet<>();
+ private int endEntityCount;
+ private int rootCount;
+ private int intermediateCount;
private CatalogueAccumulator(int expectedSize) {
templates = new ArrayList<>(expectedSize);
@@ -240,12 +244,18 @@ public final class BuiltInCertificateProfileCatalog {
|| !profileIds.add(definition.profileId())) {
throw failure("BUILT_IN_PROFILE_SET_INVALID");
}
+ switch (definition.certificateType()) {
+ case END_ENTITY -> endEntityCount++;
+ case ROOT_CA -> rootCount++;
+ case INTERMEDIATE_CA -> intermediateCount++;
+ }
templates.add(template);
}
private List finish() {
if (templates.size() != EXPECTED_PROFILE_IDS.size()
- || !profileIds.equals(EXPECTED_PROFILE_IDS)) {
+ || !profileIds.equals(EXPECTED_PROFILE_IDS)
+ || endEntityCount != 4 || rootCount != 1 || intermediateCount != 1) {
throw failure("BUILT_IN_PROFILE_SET_INVALID");
}
return List.copyOf(templates);
diff --git a/pki/src/main/java/zeroecho/pki/api/profile/CaCertificatePolicy.java b/pki/src/main/java/zeroecho/pki/api/profile/CaCertificatePolicy.java
new file mode 100644
index 0000000..684c078
--- /dev/null
+++ b/pki/src/main/java/zeroecho/pki/api/profile/CaCertificatePolicy.java
@@ -0,0 +1,63 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Leo Galambos
+ * All rights reserved.
+ ******************************************************************************/
+package zeroecho.pki.api.profile;
+
+import java.util.Set;
+
+/**
+ * Immutable issuer-controlled CA certificate policy.
+ *
+ *
+ * CA status is always {@code true}. Basic Constraints and Key Usage are always
+ * critical. SAN and EKU are not representable for CA profiles. Subject and
+ * authority key identifiers, when emitted by a CA implementation, remain fixed
+ * repository invariants rather than configurable profile values.
+ *
+ *
+ * @param basicConstraintsCritical Basic Constraints criticality, required
+ * to be {@code true}
+ * @param pathLengthConstraint nonnegative maximum subordinate-CA depth
+ * @param keyUsageCritical Key Usage criticality, required to be
+ * {@code true}
+ * @param keyUsages exact CA key-usage set
+ * @param allowedSubjectKeyAlgorithmIds exact canonical ZeroEcho subject-key
+ * algorithm identifiers
+ */
+public record CaCertificatePolicy(boolean basicConstraintsCritical, int pathLengthConstraint,
+ boolean keyUsageCritical, Set keyUsages,
+ Set allowedSubjectKeyAlgorithmIds) implements CertificatePolicy {
+
+ /** Maximum supported path-length constraint. */
+ public static final int MAXIMUM_PATH_LENGTH = 32;
+
+ private static final Set SUPPORTED_SUBJECT_KEY_ALGORITHMS =
+ Set.of("RSA", "ECDSA", "Ed25519", "Ed448");
+
+ /** Validates and constructs the CA policy. */
+ public CaCertificatePolicy {
+ if (!basicConstraintsCritical) {
+ throw new IllegalArgumentException("CA Basic Constraints must be critical");
+ }
+ if (pathLengthConstraint < 0 || pathLengthConstraint > MAXIMUM_PATH_LENGTH) {
+ throw new IllegalArgumentException("CA path length is outside the supported range");
+ }
+ if (!keyUsageCritical) {
+ throw new IllegalArgumentException("CA Key Usage must be critical");
+ }
+ if (keyUsages == null || allowedSubjectKeyAlgorithmIds == null) {
+ throw new IllegalArgumentException("CA policy collections must not be null");
+ }
+ keyUsages = Set.copyOf(keyUsages);
+ allowedSubjectKeyAlgorithmIds = Set.copyOf(allowedSubjectKeyAlgorithmIds);
+ if (!keyUsages.contains(CaKeyUsage.KEY_CERT_SIGN)
+ || !keyUsages.contains(CaKeyUsage.CRL_SIGN)) {
+ throw new IllegalArgumentException("CA Key Usage must permit certificate and CRL signing");
+ }
+ if (allowedSubjectKeyAlgorithmIds.isEmpty()
+ || !SUPPORTED_SUBJECT_KEY_ALGORITHMS.containsAll(allowedSubjectKeyAlgorithmIds)) {
+ throw new IllegalArgumentException("At least one supported CA subject-key algorithm is required");
+ }
+ }
+}
diff --git a/pki/src/main/java/zeroecho/pki/api/profile/CaKeyUsage.java b/pki/src/main/java/zeroecho/pki/api/profile/CaKeyUsage.java
new file mode 100644
index 0000000..b418385
--- /dev/null
+++ b/pki/src/main/java/zeroecho/pki/api/profile/CaKeyUsage.java
@@ -0,0 +1,15 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Leo Galambos
+ * All rights reserved.
+ ******************************************************************************/
+package zeroecho.pki.api.profile;
+
+/**
+ * Closed X.509 key-usage set available to CA certificate profiles.
+ */
+public enum CaKeyUsage {
+ /** keyCertSign. */
+ KEY_CERT_SIGN,
+ /** cRLSign. */
+ CRL_SIGN
+}
diff --git a/pki/src/main/java/zeroecho/pki/api/profile/CertificatePolicy.java b/pki/src/main/java/zeroecho/pki/api/profile/CertificatePolicy.java
new file mode 100644
index 0000000..16a1bb1
--- /dev/null
+++ b/pki/src/main/java/zeroecho/pki/api/profile/CertificatePolicy.java
@@ -0,0 +1,11 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Leo Galambos
+ * All rights reserved.
+ ******************************************************************************/
+package zeroecho.pki.api.profile;
+
+/**
+ * Closed certificate-specific policy variant.
+ */
+public sealed interface CertificatePolicy permits LeafCertificatePolicy, CaCertificatePolicy {
+}
diff --git a/pki/src/main/java/zeroecho/pki/api/profile/CertificateProfile.java b/pki/src/main/java/zeroecho/pki/api/profile/CertificateProfile.java
index 006d7ca..f679d68 100644
--- a/pki/src/main/java/zeroecho/pki/api/profile/CertificateProfile.java
+++ b/pki/src/main/java/zeroecho/pki/api/profile/CertificateProfile.java
@@ -33,6 +33,8 @@
******************************************************************************/
package zeroecho.pki.api.profile;
+import java.time.Duration;
+
import zeroecho.pki.api.FormatId;
/**
@@ -51,10 +53,12 @@ import zeroecho.pki.api.FormatId;
* @param profileId stable profile identifier
* @param formatId framework/format supported by the profile
* @param displayName human-readable name
- * @param leafPolicy complete end-entity identity and extension policy
+ * @param maximumValidity positive maximum validity
+ * @param subjectPolicy complete end-entity subject policy
+ * @param leafPolicy complete end-entity extension policy
*/
public record CertificateProfile(String profileId, FormatId formatId, String displayName,
- LeafCertificatePolicy leafPolicy) {
+ Duration maximumValidity, SubjectPolicy subjectPolicy, LeafCertificatePolicy leafPolicy) {
/**
* Creates a certificate profile.
@@ -72,8 +76,9 @@ public record CertificateProfile(String profileId, FormatId formatId, String dis
if (displayName == null || displayName.isBlank()) {
throw new IllegalArgumentException("displayName must not be null/blank");
}
- if (leafPolicy == null) {
- throw new IllegalArgumentException("leafPolicy must not be null");
+ if (maximumValidity == null || maximumValidity.isZero() || maximumValidity.isNegative()
+ || subjectPolicy == null || leafPolicy == null) {
+ throw new IllegalArgumentException("profile policies and maximum validity must be valid");
}
}
@@ -87,7 +92,10 @@ public record CertificateProfile(String profileId, FormatId formatId, String dis
if (definition == null) {
throw new IllegalArgumentException("definition must not be null");
}
+ if (definition.certificateType() != CertificateProfileKind.END_ENTITY) {
+ throw new IllegalArgumentException("Only end-entity definitions have a runtime issuance projection");
+ }
return new CertificateProfile(definition.profileId(), definition.formatId(), definition.displayName(),
- definition.leafPolicy());
+ definition.maximumValidity(), definition.subjectPolicy(), definition.leafPolicy());
}
}
diff --git a/pki/src/main/java/zeroecho/pki/api/profile/CertificateProfileDefinition.java b/pki/src/main/java/zeroecho/pki/api/profile/CertificateProfileDefinition.java
index 495c52b..c8cbcce 100644
--- a/pki/src/main/java/zeroecho/pki/api/profile/CertificateProfileDefinition.java
+++ b/pki/src/main/java/zeroecho/pki/api/profile/CertificateProfileDefinition.java
@@ -4,28 +4,35 @@
******************************************************************************/
package zeroecho.pki.api.profile;
+import java.time.Duration;
+
import zeroecho.pki.api.FormatId;
/**
* Immutable, versioned certificate-profile configuration.
*
*
- * This definition deliberately excludes runtime activation state. Its
- * {@link LeafCertificatePolicy} is the authoritative typed policy used by the
- * issuance path.
+ * Schema version 2 replaces the pre-release version 1 shape. The certificate
+ * kind and its closed policy variant are immutable across the definition.
+ * Runtime activation state is deliberately excluded.
*
*
- * @param profileId stable profile identifier
- * @param profileVersion positive configuration version
- * @param formatId framework/format identifier
- * @param displayName human-readable profile name
- * @param leafPolicy complete leaf certificate policy
+ * @param certificateType kind of certificate governed by the profile
+ * @param profileId stable profile identifier
+ * @param profileVersion positive configuration version
+ * @param formatId framework/format identifier
+ * @param displayName human-readable profile name
+ * @param maximumValidity positive maximum validity
+ * @param subjectPolicy complete subject policy
+ * @param certificatePolicy closed certificate-specific policy variant
*/
-public record CertificateProfileDefinition(String profileId, long profileVersion, FormatId formatId,
- String displayName, LeafCertificatePolicy leafPolicy) {
+public record CertificateProfileDefinition(CertificateProfileKind certificateType,
+ String profileId, long profileVersion, FormatId formatId, String displayName,
+ Duration maximumValidity, SubjectPolicy subjectPolicy,
+ CertificatePolicy certificatePolicy) {
/** Current certificate-profile document schema version. */
- public static final int SCHEMA_VERSION = 1;
+ public static final int SCHEMA_VERSION = 2;
/**
* Creates a certificate-profile definition.
@@ -34,6 +41,9 @@ public record CertificateProfileDefinition(String profileId, long profileVersion
* profile version is not positive
*/
public CertificateProfileDefinition {
+ if (certificateType == null) {
+ throw new IllegalArgumentException("certificateType must not be null");
+ }
if (profileId == null || profileId.isBlank()) {
throw new IllegalArgumentException("profileId must not be null/blank");
}
@@ -46,8 +56,46 @@ public record CertificateProfileDefinition(String profileId, long profileVersion
if (displayName == null || displayName.isBlank()) {
throw new IllegalArgumentException("displayName must not be null/blank");
}
- if (leafPolicy == null) {
- throw new IllegalArgumentException("leafPolicy must not be null");
+ if (maximumValidity == null || maximumValidity.isZero() || maximumValidity.isNegative()) {
+ throw new IllegalArgumentException("maximumValidity must be positive");
+ }
+ if (subjectPolicy == null || certificatePolicy == null) {
+ throw new IllegalArgumentException("profile policies must not be null");
+ }
+ boolean leaf = certificatePolicy instanceof LeafCertificatePolicy;
+ if (certificateType == CertificateProfileKind.END_ENTITY != leaf) {
+ throw new IllegalArgumentException("certificate type and policy variant do not match");
+ }
+ if (certificateType != CertificateProfileKind.END_ENTITY
+ && (subjectPolicy.allowEmpty() || subjectPolicy.rules().isEmpty()
+ || subjectPolicy.rules().stream().noneMatch(rule -> rule.minimumOccurrences() > 0))) {
+ throw new IllegalArgumentException("CA profiles require a nonempty subject policy");
}
}
+
+ /**
+ * Returns the end-entity policy.
+ *
+ * @return leaf policy
+ * @throws IllegalStateException if this is a CA profile
+ */
+ public LeafCertificatePolicy leafPolicy() {
+ if (certificatePolicy instanceof LeafCertificatePolicy leaf) {
+ return leaf;
+ }
+ throw new IllegalStateException("Profile is not an end-entity profile");
+ }
+
+ /**
+ * Returns the CA policy.
+ *
+ * @return CA policy
+ * @throws IllegalStateException if this is an end-entity profile
+ */
+ public CaCertificatePolicy caPolicy() {
+ if (certificatePolicy instanceof CaCertificatePolicy ca) {
+ return ca;
+ }
+ throw new IllegalStateException("Profile is not a CA profile");
+ }
}
diff --git a/pki/src/main/java/zeroecho/pki/api/profile/CertificateProfileDocumentCodec.java b/pki/src/main/java/zeroecho/pki/api/profile/CertificateProfileDocumentCodec.java
index ff5c11a..3b59767 100644
--- a/pki/src/main/java/zeroecho/pki/api/profile/CertificateProfileDocumentCodec.java
+++ b/pki/src/main/java/zeroecho/pki/api/profile/CertificateProfileDocumentCodec.java
@@ -34,7 +34,7 @@ import zeroecho.pki.api.FormatId;
import zeroecho.pki.api.PkiException;
/**
- * Strict JSON parser and canonical writer for version 1 certificate-profile
+ * Strict JSON parser and canonical writer for version 2 certificate-profile
* documents.
*
*
@@ -42,9 +42,10 @@ import zeroecho.pki.api.PkiException;
* tokens and creates the existing typed policy model directly.
*
*/
+// The closed streaming grammar intentionally keeps all bounded field handlers in one codec.
@SuppressWarnings({ "PMD.AvoidDuplicateLiterals", "PMD.AvoidInstantiatingObjectsInLoops",
"PMD.AvoidUncheckedExceptionsInSignatures", "PMD.CyclomaticComplexity",
- "PMD.PreserveStackTrace" })
+ "PMD.PreserveStackTrace", "PMD.TooManyMethods" })
public final class CertificateProfileDocumentCodec {
/** Maximum accepted encoded document size. */
@@ -157,7 +158,7 @@ public final class CertificateProfileDocumentCodec {
* @return newly allocated canonical JSON bytes without a BOM or trailing
* whitespace
* @throws PkiException if the definition cannot be represented by schema
- * version 1
+ * version 2
*/
public static byte[] writeCanonical(CertificateProfileDefinition definition) {
validateDefinition(definition);
@@ -209,92 +210,155 @@ public final class CertificateProfileDocumentCodec {
private static CertificateProfileDefinition parseDocument(JsonParser parser) throws JacksonException {
requireToken(parser.nextToken(), JsonToken.START_OBJECT, "$");
- long seen = 0;
- int schemaVersion = 0;
- String profileId = null;
- long profileVersion = 0;
- String formatId = null;
- String displayName = null;
- Duration maximumValidity = null;
- SubjectSection subject = null;
- SanSection san = null;
- LeafSection leaf = null;
+ DocumentFields document = new DocumentFields();
while (parser.nextToken() != JsonToken.END_OBJECT) {
requireToken(parser.currentToken(), JsonToken.PROPERTY_NAME, "$");
String field = parser.currentName();
requireValue(parser, "$." + field);
- switch (field) {
- case "schemaVersion" -> {
- seen = mark(seen, 0, "$.schemaVersion");
- schemaVersion = readInt(parser, "$.schemaVersion");
- }
- case "profileId" -> {
- seen = mark(seen, 1, "$.profileId");
- profileId = readBoundedString(parser, "$.profileId", MAXIMUM_PROFILE_ID_UTF8_BYTES);
- }
- case "profileVersion" -> {
- seen = mark(seen, 2, "$.profileVersion");
- profileVersion = readLong(parser, "$.profileVersion");
- }
- case "formatId" -> {
- seen = mark(seen, 3, "$.formatId");
- formatId = readBoundedString(parser, "$.formatId", MAXIMUM_FORMAT_ID_UTF8_BYTES);
- }
- case "displayName" -> {
- seen = mark(seen, 4, "$.displayName");
- displayName = readBoundedString(parser, "$.displayName", MAXIMUM_DISPLAY_NAME_UTF8_BYTES);
- }
- case "maxValidity" -> {
- seen = mark(seen, 5, "$.maxValidity");
- maximumValidity = readDuration(parser, "$.maxValidity");
- }
- case "subject" -> {
- seen = mark(seen, 6, "$.subject");
- subject = readSubject(parser, "$.subject");
- }
- case "subjectAlternativeNames" -> {
- seen = mark(seen, 7, "$.subjectAlternativeNames");
- san = readSan(parser, "$.subjectAlternativeNames");
- }
- case "leafCertificate" -> {
- seen = mark(seen, 8, "$.leafCertificate");
- leaf = readLeaf(parser, "$.leafCertificate");
- }
- default -> throw failure("UNKNOWN_FIELD", "$.?");
- }
+ readDocumentField(parser, field, document);
}
- requireAll(seen, 9, "$");
- if (schemaVersion != CertificateProfileDefinition.SCHEMA_VERSION) {
+ requireAll(document.seen, 8, "$");
+ if (document.schemaVersion != CertificateProfileDefinition.SCHEMA_VERSION) {
throw failure("SCHEMA_VERSION_UNSUPPORTED", "$.schemaVersion");
}
- if (profileVersion <= 0) {
+ if (document.profileVersion <= 0) {
throw failure("PROFILE_VERSION_INVALID", "$.profileVersion");
}
- validateProfileString(profileId, MAXIMUM_PROFILE_ID_UTF8_BYTES, "$.profileId",
+ validateProfileString(document.profileId, MAXIMUM_PROFILE_ID_UTF8_BYTES, "$.profileId",
"TOKEN_INVALID");
- validateProfileString(formatId, MAXIMUM_FORMAT_ID_UTF8_BYTES, "$.formatId",
+ validateProfileString(document.formatId, MAXIMUM_FORMAT_ID_UTF8_BYTES, "$.formatId",
"TOKEN_INVALID");
- validateProfileString(displayName, MAXIMUM_DISPLAY_NAME_UTF8_BYTES, "$.displayName",
+ validateProfileString(document.displayName, MAXIMUM_DISPLAY_NAME_UTF8_BYTES, "$.displayName",
"TOKEN_INVALID");
- return constructDefinition(profileId, profileVersion, formatId, displayName, maximumValidity,
- subject, san, leaf);
+ requirePolicyFields(document.certificateType, document.seen);
+ return constructDefinition(document);
}
- private static CertificateProfileDefinition constructDefinition(String profileId, long profileVersion,
- String formatId, String displayName, Duration maximumValidity, SubjectSection subject,
- SanSection san, LeafSection leaf) {
+ private static void readDocumentField(JsonParser parser, String field, DocumentFields document)
+ throws JacksonException {
+ switch (field) {
+ case "schemaVersion" -> {
+ document.seen = mark(document.seen, 0, "$.schemaVersion");
+ document.schemaVersion = readInt(parser, "$.schemaVersion");
+ }
+ case "certificateType" -> {
+ document.seen = mark(document.seen, 1, "$.certificateType");
+ document.certificateType = readCertificateType(parser, "$.certificateType");
+ }
+ case "profileId" -> {
+ document.seen = mark(document.seen, 2, "$.profileId");
+ document.profileId = readBoundedString(parser, "$.profileId", MAXIMUM_PROFILE_ID_UTF8_BYTES);
+ }
+ case "profileVersion" -> {
+ document.seen = mark(document.seen, 3, "$.profileVersion");
+ document.profileVersion = readLong(parser, "$.profileVersion");
+ }
+ case "formatId" -> {
+ document.seen = mark(document.seen, 4, "$.formatId");
+ document.formatId = readBoundedString(parser, "$.formatId", MAXIMUM_FORMAT_ID_UTF8_BYTES);
+ }
+ case "displayName" -> {
+ document.seen = mark(document.seen, 5, "$.displayName");
+ document.displayName =
+ readBoundedString(parser, "$.displayName", MAXIMUM_DISPLAY_NAME_UTF8_BYTES);
+ }
+ case "maxValidity" -> {
+ document.seen = mark(document.seen, 6, "$.maxValidity");
+ document.maximumValidity = readDuration(parser, "$.maxValidity");
+ }
+ case "subject" -> {
+ document.seen = mark(document.seen, 7, "$.subject");
+ document.subject = readSubject(parser, "$.subject");
+ }
+ case "subjectAlternativeNames" -> readDocumentSan(parser, document);
+ case "leafCertificate" -> readDocumentLeaf(parser, document);
+ case "caCertificate" -> readDocumentCa(parser, document);
+ default -> throw failure("UNKNOWN_FIELD", "$.?");
+ }
+ }
+
+ private static void readDocumentSan(JsonParser parser, DocumentFields document)
+ throws JacksonException {
+ if (document.certificateType != null
+ && document.certificateType != CertificateProfileKind.END_ENTITY) {
+ throw failure("FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE", "$.subjectAlternativeNames");
+ }
+ document.seen = mark(document.seen, 8, "$.subjectAlternativeNames");
+ document.san = readSan(parser, "$.subjectAlternativeNames");
+ }
+
+ private static void readDocumentLeaf(JsonParser parser, DocumentFields document)
+ throws JacksonException {
+ if (document.certificateType != null
+ && document.certificateType != CertificateProfileKind.END_ENTITY) {
+ throw failure("FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE", "$.leafCertificate");
+ }
+ document.seen = mark(document.seen, 9, "$.leafCertificate");
+ document.leaf = readLeaf(parser, "$.leafCertificate");
+ }
+
+ private static void readDocumentCa(JsonParser parser, DocumentFields document)
+ throws JacksonException {
+ if (document.certificateType == CertificateProfileKind.END_ENTITY) {
+ throw failure("FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE", "$.caCertificate");
+ }
+ document.seen = mark(document.seen, 10, "$.caCertificate");
+ document.ca = readCa(parser, "$.caCertificate");
+ }
+
+ private static void requirePolicyFields(CertificateProfileKind certificateType, long seen) {
+ boolean hasSan = isSeen(seen, 8);
+ boolean hasLeaf = isSeen(seen, 9);
+ boolean hasCa = isSeen(seen, 10);
+ if (certificateType == CertificateProfileKind.END_ENTITY) {
+ if (hasCa) {
+ throw failure("FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE", "$.caCertificate");
+ }
+ if (!hasSan || !hasLeaf) {
+ throw failure("MISSING_FIELD", "$");
+ }
+ } else {
+ if (hasSan || hasLeaf) {
+ throw failure("FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE", "$");
+ }
+ if (!hasCa) {
+ throw failure("MISSING_FIELD", "$.caCertificate");
+ }
+ }
+ }
+
+ /*
+ * Typed policy constructors are the authoritative semantic validators, so
+ * their validation exceptions are deliberately normalized at this boundary.
+ */
+ @SuppressWarnings("PMD.ExceptionAsFlowControl")
+ private static CertificateProfileDefinition constructDefinition(DocumentFields document) {
try {
- SubjectPolicy subjectPolicy = new SubjectPolicy(subject.rules());
- SubjectAlternativeNamePolicy sanPolicy = new SubjectAlternativeNamePolicy(subject.allowEmpty(),
- san.minimumTotal(), san.maximumTotal(), san.rules(), san.wildcardAllowed(),
- san.allowedSchemes(), san.criticalWhenSubjectNonEmpty(), san.serviceIdentityRequired(),
- san.emailIdentityRequired());
- LeafCertificatePolicy leafPolicy = new LeafCertificatePolicy(subjectPolicy, sanPolicy,
- leaf.keyUsage(), leaf.extendedKeyUsage(), leaf.keyUsageCritical(),
- leaf.extendedKeyUsageCritical(), leaf.basicConstraintsCritical(),
- leaf.allowedKeyAlgorithms(), maximumValidity);
- return new CertificateProfileDefinition(profileId, profileVersion, new FormatId(formatId),
- displayName, leafPolicy);
+ SubjectPolicy subjectPolicy = new SubjectPolicy(document.subject.allowEmpty(),
+ document.subject.rules());
+ CertificatePolicy policy;
+ if (document.certificateType == CertificateProfileKind.END_ENTITY) {
+ SubjectAlternativeNamePolicy sanPolicy = new SubjectAlternativeNamePolicy(
+ document.san.minimumTotal(), document.san.maximumTotal(), document.san.rules(),
+ document.san.wildcardAllowed(), document.san.allowedSchemes(),
+ document.san.criticalWhenSubjectNonEmpty(),
+ document.san.serviceIdentityRequired(), document.san.emailIdentityRequired());
+ if (document.subject.allowEmpty() && document.san.minimumTotal() < 1) {
+ throw new IllegalArgumentException("An empty subject requires at least one SAN");
+ }
+ policy = new LeafCertificatePolicy(sanPolicy, document.leaf.keyUsage(),
+ document.leaf.extendedKeyUsage(), document.leaf.keyUsageCritical(),
+ document.leaf.extendedKeyUsageCritical(),
+ document.leaf.basicConstraintsCritical(),
+ document.leaf.allowedKeyAlgorithms());
+ } else {
+ policy = new CaCertificatePolicy(document.ca.basicConstraintsCritical(),
+ document.ca.pathLengthConstraint(), document.ca.keyUsageCritical(),
+ document.ca.keyUsages(), document.ca.allowedKeyAlgorithms());
+ }
+ return new CertificateProfileDefinition(document.certificateType, document.profileId,
+ document.profileVersion, new FormatId(document.formatId), document.displayName,
+ document.maximumValidity, subjectPolicy, policy);
} catch (IllegalArgumentException | ArithmeticException ex) {
throw failure("SEMANTIC_INVALID", "$");
}
@@ -612,6 +676,66 @@ public final class CertificateProfileDocumentCodec {
return Set.copyOf(values);
}
+ private static CaSection readCa(JsonParser parser, String path) throws JacksonException {
+ requireToken(parser.currentToken(), JsonToken.START_OBJECT, path);
+ long seen = 0;
+ boolean basicCritical = false;
+ int pathLength = -1;
+ boolean keyCritical = false;
+ Set keyUsages = null;
+ Set algorithms = null;
+ while (parser.nextToken() != JsonToken.END_OBJECT) {
+ requireToken(parser.currentToken(), JsonToken.PROPERTY_NAME, path);
+ String field = parser.currentName();
+ requireValue(parser, path + "." + field);
+ switch (field) {
+ case "basicConstraintsCritical" -> {
+ seen = mark(seen, 0, path + ".basicConstraintsCritical");
+ basicCritical = readBoolean(parser, path + ".basicConstraintsCritical");
+ }
+ case "pathLengthConstraint" -> {
+ seen = mark(seen, 1, path + ".pathLengthConstraint");
+ pathLength = readInt(parser, path + ".pathLengthConstraint");
+ }
+ case "keyUsageCritical" -> {
+ seen = mark(seen, 2, path + ".keyUsageCritical");
+ keyCritical = readBoolean(parser, path + ".keyUsageCritical");
+ }
+ case "keyUsages" -> {
+ seen = mark(seen, 3, path + ".keyUsages");
+ keyUsages = readCaKeyUsages(parser, path + ".keyUsages");
+ }
+ case "allowedSubjectKeyAlgorithms" -> {
+ seen = mark(seen, 4, path + ".allowedSubjectKeyAlgorithms");
+ algorithms = readAlgorithms(parser, path + ".allowedSubjectKeyAlgorithms");
+ }
+ default -> throw failure("UNKNOWN_FIELD", path + ".?");
+ }
+ }
+ requireAll(seen, 5, path);
+ return new CaSection(basicCritical, pathLength, keyCritical, keyUsages, algorithms);
+ }
+
+ private static Set readCaKeyUsages(JsonParser parser, String path)
+ throws JacksonException {
+ requireToken(parser.currentToken(), JsonToken.START_ARRAY, path);
+ Set values = EnumSet.noneOf(CaKeyUsage.class);
+ while (parser.nextToken() != JsonToken.END_ARRAY) {
+ checkArrayBound(values.size(), path);
+ String token = readString(parser, path + "[" + values.size() + "]");
+ CaKeyUsage value;
+ try {
+ value = CaKeyUsage.valueOf(token);
+ } catch (IllegalArgumentException ex) {
+ throw failure("TOKEN_INVALID", path);
+ }
+ if (!values.add(value)) {
+ throw failure("SEMANTIC_INVALID", path);
+ }
+ }
+ return Set.copyOf(values);
+ }
+
private static Set readExtendedKeyUsages(JsonParser parser, String path)
throws JacksonException {
requireToken(parser.currentToken(), JsonToken.START_ARRAY, path);
@@ -650,27 +774,31 @@ public final class CertificateProfileDocumentCodec {
private static void writeDocument(JsonGenerator generator, CertificateProfileDefinition definition)
throws JacksonException {
- LeafCertificatePolicy policy = definition.leafPolicy();
generator.writeStartObject();
generator.writeNumberProperty("schemaVersion", CertificateProfileDefinition.SCHEMA_VERSION);
+ generator.writeStringProperty("certificateType", definition.certificateType().name());
generator.writeStringProperty("profileId", definition.profileId());
generator.writeNumberProperty("profileVersion", definition.profileVersion());
generator.writeStringProperty("formatId", definition.formatId().value());
generator.writeStringProperty("displayName", definition.displayName());
- generator.writeStringProperty("maxValidity", policy.maximumValidity().toString());
- writeSubject(generator, policy);
- writeSan(generator, policy.subjectAlternativeNamePolicy());
- writeLeaf(generator, policy);
+ generator.writeStringProperty("maxValidity", definition.maximumValidity().toString());
+ writeSubject(generator, definition.subjectPolicy());
+ if (definition.certificateType() == CertificateProfileKind.END_ENTITY) {
+ LeafCertificatePolicy leaf = definition.leafPolicy();
+ writeSan(generator, leaf.subjectAlternativeNamePolicy());
+ writeLeaf(generator, leaf);
+ } else {
+ writeCa(generator, definition.caPolicy());
+ }
generator.writeEndObject();
}
- private static void writeSubject(JsonGenerator generator, LeafCertificatePolicy policy)
+ private static void writeSubject(JsonGenerator generator, SubjectPolicy policy)
throws JacksonException {
generator.writeObjectPropertyStart("subject");
- generator.writeBooleanProperty("allowEmpty",
- policy.subjectAlternativeNamePolicy().allowEmptySubject());
+ generator.writeBooleanProperty("allowEmpty", policy.allowEmpty());
generator.writeArrayPropertyStart("rules");
- for (SubjectRdnRule rule : policy.subjectPolicy().rules()) {
+ for (SubjectRdnRule rule : policy.rules()) {
generator.writeStartObject();
generator.writeStringProperty("oid", rule.type().oid());
generator.writeStringProperty("source",
@@ -740,6 +868,20 @@ public final class CertificateProfileDocumentCodec {
generator.writeEndObject();
}
+ private static void writeCa(JsonGenerator generator, CaCertificatePolicy policy)
+ throws JacksonException {
+ generator.writeObjectPropertyStart("caCertificate");
+ generator.writeBooleanProperty("basicConstraintsCritical",
+ policy.basicConstraintsCritical());
+ generator.writeNumberProperty("pathLengthConstraint", policy.pathLengthConstraint());
+ generator.writeBooleanProperty("keyUsageCritical", policy.keyUsageCritical());
+ writeSortedStrings(generator, "keyUsages",
+ policy.keyUsages().stream().map(Enum::name).toList());
+ writeSortedStrings(generator, "allowedSubjectKeyAlgorithms",
+ policy.allowedSubjectKeyAlgorithmIds());
+ generator.writeEndObject();
+ }
+
private static void writeSortedStrings(JsonGenerator generator, String field,
java.util.Collection values) throws JacksonException {
generator.writeArrayPropertyStart(field);
@@ -759,11 +901,25 @@ public final class CertificateProfileDocumentCodec {
"$.formatId", "CANONICALIZATION_FAILED");
validateProfileString(definition.displayName(), MAXIMUM_DISPLAY_NAME_UTF8_BYTES,
"$.displayName", "CANONICALIZATION_FAILED");
- LeafCertificatePolicy leaf = definition.leafPolicy();
- validateWritableString(leaf.maximumValidity().toString(), MAXIMUM_STRING_UTF8_BYTES,
+ validateWritableString(definition.maximumValidity().toString(), MAXIMUM_STRING_UTF8_BYTES,
"$.maxValidity");
- if (leaf.subjectPolicy().rules().size() > MAXIMUM_ARRAY_ELEMENTS
- || leaf.subjectAlternativeNamePolicy().rules().size() > MAXIMUM_ARRAY_ELEMENTS
+ if (definition.subjectPolicy().rules().size() > MAXIMUM_ARRAY_ELEMENTS) {
+ throw failure("CANONICALIZATION_FAILED", "$");
+ }
+ for (SubjectRdnRule rule : definition.subjectPolicy().rules()) {
+ rule.fixedValue().ifPresent(value -> validateWritableString(value,
+ MAXIMUM_STRING_UTF8_BYTES, "$.subject.rules.fixedValue"));
+ }
+ if (definition.certificateType() != CertificateProfileKind.END_ENTITY) {
+ CaCertificatePolicy ca = definition.caPolicy();
+ if (ca.keyUsages().size() > MAXIMUM_ARRAY_ELEMENTS
+ || ca.allowedSubjectKeyAlgorithmIds().size() > MAXIMUM_ARRAY_ELEMENTS) {
+ throw failure("CANONICALIZATION_FAILED", "$");
+ }
+ return;
+ }
+ LeafCertificatePolicy leaf = definition.leafPolicy();
+ if (leaf.subjectAlternativeNamePolicy().rules().size() > MAXIMUM_ARRAY_ELEMENTS
|| leaf.subjectAlternativeNamePolicy().allowedUriSchemes().size()
> MAXIMUM_ARRAY_ELEMENTS
|| leaf.keyUsages().size() > MAXIMUM_ARRAY_ELEMENTS
@@ -771,10 +927,6 @@ public final class CertificateProfileDocumentCodec {
|| leaf.allowedSubjectKeyAlgorithmIds().size() > MAXIMUM_ARRAY_ELEMENTS) {
throw failure("CANONICALIZATION_FAILED", "$");
}
- for (SubjectRdnRule rule : leaf.subjectPolicy().rules()) {
- rule.fixedValue().ifPresent(value -> validateWritableString(value,
- MAXIMUM_STRING_UTF8_BYTES, "$.subject.rules.fixedValue"));
- }
for (String scheme : leaf.subjectAlternativeNamePolicy().allowedUriSchemes()) {
validateWritableString(scheme, MAXIMUM_URI_SCHEME_ASCII_BYTES,
"$.subjectAlternativeNames.rules.allowedSchemes");
@@ -869,6 +1021,16 @@ public final class CertificateProfileDocumentCodec {
}
}
+ private static CertificateProfileKind readCertificateType(JsonParser parser, String path)
+ throws JacksonException {
+ String token = readString(parser, path);
+ try {
+ return CertificateProfileKind.valueOf(token);
+ } catch (IllegalArgumentException ex) {
+ throw failure("CERTIFICATE_TYPE_UNSUPPORTED", path);
+ }
+ }
+
private static boolean parseSubjectSource(String source, String path) {
if (REQUESTER_SOURCE.equals(source)) {
return false;
@@ -972,6 +1134,22 @@ public final class CertificateProfileDocumentCodec {
return new PkiException(PREFIX + code + " path=" + path);
}
+ /** Mutable state confined to one streaming top-level document parse. */
+ private static final class DocumentFields {
+ private long seen;
+ private int schemaVersion;
+ private CertificateProfileKind certificateType;
+ private String profileId;
+ private long profileVersion;
+ private String formatId;
+ private String displayName;
+ private Duration maximumValidity;
+ private SubjectSection subject;
+ private SanSection san;
+ private LeafSection leaf;
+ private CaSection ca;
+ }
+
private record SubjectSection(boolean allowEmpty, List rules) {
}
@@ -993,4 +1171,9 @@ public final class CertificateProfileDocumentCodec {
Set keyUsage, boolean extendedKeyUsageCritical,
Set extendedKeyUsage, Set allowedKeyAlgorithms) {
}
+
+ private record CaSection(boolean basicConstraintsCritical, int pathLengthConstraint,
+ boolean keyUsageCritical, Set keyUsages,
+ Set allowedKeyAlgorithms) {
+ }
}
diff --git a/pki/src/main/java/zeroecho/pki/api/profile/CertificateProfileKind.java b/pki/src/main/java/zeroecho/pki/api/profile/CertificateProfileKind.java
new file mode 100644
index 0000000..06c63ac
--- /dev/null
+++ b/pki/src/main/java/zeroecho/pki/api/profile/CertificateProfileKind.java
@@ -0,0 +1,17 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Leo Galambos
+ * All rights reserved.
+ ******************************************************************************/
+package zeroecho.pki.api.profile;
+
+/**
+ * Closed certificate category governed by a profile definition.
+ */
+public enum CertificateProfileKind {
+ /** End-entity certificate. */
+ END_ENTITY,
+ /** Self-signed root certification-authority certificate. */
+ ROOT_CA,
+ /** Issuer-signed intermediate certification-authority certificate. */
+ INTERMEDIATE_CA
+}
diff --git a/pki/src/main/java/zeroecho/pki/api/profile/LeafCertificatePolicy.java b/pki/src/main/java/zeroecho/pki/api/profile/LeafCertificatePolicy.java
index f88878f..0e5021f 100644
--- a/pki/src/main/java/zeroecho/pki/api/profile/LeafCertificatePolicy.java
+++ b/pki/src/main/java/zeroecho/pki/api/profile/LeafCertificatePolicy.java
@@ -4,13 +4,11 @@
******************************************************************************/
package zeroecho.pki.api.profile;
-import java.time.Duration;
import java.util.Set;
/**
* Complete issuer-controlled leaf certificate extension and identity policy.
*
- * @param subjectPolicy subject policy
* @param subjectAlternativeNamePolicy SAN policy
* @param keyUsages exact key-usage set
* @param extendedKeyUsages exact extended-key-usage set
@@ -18,12 +16,12 @@ import java.util.Set;
* @param extendedKeyUsageCritical extended-key-usage criticality
* @param basicConstraintsCritical BasicConstraints criticality
* @param allowedSubjectKeyAlgorithmIds exact canonical ZeroEcho key algorithm identifiers
- * @param maximumValidity positive maximum validity
*/
-public record LeafCertificatePolicy(SubjectPolicy subjectPolicy,
- SubjectAlternativeNamePolicy subjectAlternativeNamePolicy, Set keyUsages,
+public record LeafCertificatePolicy(SubjectAlternativeNamePolicy subjectAlternativeNamePolicy,
+ Set keyUsages,
Set extendedKeyUsages, boolean keyUsageCritical, boolean extendedKeyUsageCritical,
- boolean basicConstraintsCritical, Set allowedSubjectKeyAlgorithmIds, Duration maximumValidity) {
+ boolean basicConstraintsCritical,
+ Set allowedSubjectKeyAlgorithmIds) implements CertificatePolicy {
private static final Set SUPPORTED_SUBJECT_KEY_ALGORITHMS =
Set.of("RSA", "ECDSA", "Ed25519", "Ed448");
@@ -32,8 +30,8 @@ public record LeafCertificatePolicy(SubjectPolicy subjectPolicy,
* Validates and constructs the policy.
*/
public LeafCertificatePolicy {
- if (subjectPolicy == null || subjectAlternativeNamePolicy == null || keyUsages == null
- || extendedKeyUsages == null || allowedSubjectKeyAlgorithmIds == null || maximumValidity == null) {
+ if (subjectAlternativeNamePolicy == null || keyUsages == null
+ || extendedKeyUsages == null || allowedSubjectKeyAlgorithmIds == null) {
throw new IllegalArgumentException("Leaf certificate policy values must not be null");
}
keyUsages = Set.copyOf(keyUsages);
@@ -47,8 +45,5 @@ public record LeafCertificatePolicy(SubjectPolicy subjectPolicy,
&& !keyUsages.contains(LeafKeyUsage.KEY_AGREEMENT)) {
throw new IllegalArgumentException("encipherOnly and decipherOnly require keyAgreement");
}
- if (maximumValidity.isZero() || maximumValidity.isNegative()) {
- throw new IllegalArgumentException("Maximum validity must be positive");
- }
}
}
diff --git a/pki/src/main/java/zeroecho/pki/api/profile/SubjectAlternativeNamePolicy.java b/pki/src/main/java/zeroecho/pki/api/profile/SubjectAlternativeNamePolicy.java
index 0d5b685..db78980 100644
--- a/pki/src/main/java/zeroecho/pki/api/profile/SubjectAlternativeNamePolicy.java
+++ b/pki/src/main/java/zeroecho/pki/api/profile/SubjectAlternativeNamePolicy.java
@@ -11,7 +11,6 @@ import java.util.Set;
/**
* Deny-by-default Subject Alternative Name policy.
*
- * @param allowEmptySubject whether the subject DN may be empty
* @param minimumTotal minimum total SAN count
* @param maximumTotal maximum total SAN count
* @param rules permitted SAN type rules
@@ -21,7 +20,7 @@ import java.util.Set;
* @param requireServiceIdentity whether DNS, IP, or URI identity is required
* @param requireEmailIdentity whether an RFC822 identity is required
*/
-public record SubjectAlternativeNamePolicy(boolean allowEmptySubject, int minimumTotal, int maximumTotal,
+public record SubjectAlternativeNamePolicy(int minimumTotal, int maximumTotal,
List rules, boolean allowDnsWildcard, Set allowedUriSchemes,
boolean criticalWithNonemptySubject, boolean requireServiceIdentity, boolean requireEmailIdentity) {
@@ -75,8 +74,5 @@ public record SubjectAlternativeNamePolicy(boolean allowEmptySubject, int minimu
|| uriPossible != !allowedUriSchemes.isEmpty()) {
throw new IllegalArgumentException("SAN policy requirements are not satisfiable");
}
- if (allowEmptySubject && minimumTotal < 1) {
- throw new IllegalArgumentException("An empty subject requires at least one SAN");
- }
}
}
diff --git a/pki/src/main/java/zeroecho/pki/api/profile/SubjectPolicy.java b/pki/src/main/java/zeroecho/pki/api/profile/SubjectPolicy.java
index 914ab40..12a4a50 100644
--- a/pki/src/main/java/zeroecho/pki/api/profile/SubjectPolicy.java
+++ b/pki/src/main/java/zeroecho/pki/api/profile/SubjectPolicy.java
@@ -10,9 +10,10 @@ import java.util.Set;
/**
* Deny-by-default subject distinguished-name policy.
*
- * @param rules ordered immutable supported RDN rules
+ * @param allowEmpty whether an empty subject is permitted
+ * @param rules ordered immutable supported RDN rules
*/
-public record SubjectPolicy(List rules) {
+public record SubjectPolicy(boolean allowEmpty, List rules) {
/** Maximum number of subject RDNs. */
public static final int HARD_MAXIMUM_RDN_COUNT = 32;
diff --git a/pki/src/main/java/zeroecho/pki/api/profile/package-info.java b/pki/src/main/java/zeroecho/pki/api/profile/package-info.java
index 4357a7b..baf00b7 100644
--- a/pki/src/main/java/zeroecho/pki/api/profile/package-info.java
+++ b/pki/src/main/java/zeroecho/pki/api/profile/package-info.java
@@ -46,13 +46,13 @@
*
*
*
- * Built-in end-entity profile resources are deterministic baseline
- * provisioning templates. Loading the catalogue neither persists nor activates
- * a profile, and the resources are never implicit issuance defaults.
- * Administrator-supplied profiles will use the same strict document schema.
- * Canonical JSON and SHA-256 hashes support deterministic provisioning and
- * audit. CA profile resources are intentionally absent until the CA profile
- * policy model is defined.
+ * Document schema version 2 distinguishes end-entity, root-CA, and
+ * intermediate-CA profiles through a closed certificate-policy variant.
+ * Built-in resources are deterministic provisioning templates. Loading the
+ * catalogue neither persists nor activates a profile, and resources are never
+ * implicit issuance defaults. A logical profile ID cannot change certificate
+ * kind across versions. Active definitions of every supported certificate kind
+ * are authoritative issuance inputs.
*
*
* @since 1.0
diff --git a/pki/src/main/java/zeroecho/pki/impl/ProfileLifecycleFailure.java b/pki/src/main/java/zeroecho/pki/impl/ProfileLifecycleFailure.java
index 0d066a7..196be45 100644
--- a/pki/src/main/java/zeroecho/pki/impl/ProfileLifecycleFailure.java
+++ b/pki/src/main/java/zeroecho/pki/impl/ProfileLifecycleFailure.java
@@ -18,6 +18,7 @@ public final class ProfileLifecycleFailure extends PkiException {
PROFILE_IMPORT_VALIDATION_FAILED,
BUILT_IN_PROFILE_INVALID,
PROFILE_VERSION_CONFLICT,
+ PROFILE_KIND_CONFLICT,
PROFILE_VERSION_CORRUPT,
PROFILE_ACTIVE_POINTER_CORRUPT,
PROFILE_HASH_MISMATCH,
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/CaCertificateProfileValidator.java b/pki/src/main/java/zeroecho/pki/impl/core/CaCertificateProfileValidator.java
new file mode 100644
index 0000000..34b668a
--- /dev/null
+++ b/pki/src/main/java/zeroecho/pki/impl/core/CaCertificateProfileValidator.java
@@ -0,0 +1,261 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Leo Galambos
+ * All rights reserved.
+ ******************************************************************************/
+package zeroecho.pki.impl.core;
+
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.time.DateTimeException;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.bouncycastle.asn1.x500.AttributeTypeAndValue;
+import org.bouncycastle.asn1.x500.RDN;
+import org.bouncycastle.asn1.x500.X500Name;
+
+import zeroecho.pki.api.EncodedObject;
+import zeroecho.pki.api.FormatId;
+import zeroecho.pki.api.PkiException;
+import zeroecho.pki.api.PkiId;
+import zeroecho.pki.api.SubjectRef;
+import zeroecho.pki.api.Validity;
+import zeroecho.pki.api.profile.ActiveCertificateProfile;
+import zeroecho.pki.api.profile.CertificateProfileDefinition;
+import zeroecho.pki.api.profile.CertificateProfileKind;
+import zeroecho.pki.api.profile.SubjectRdnRule;
+import zeroecho.pki.api.profile.SubjectRdnType;
+import zeroecho.pki.api.request.SubjectRdn;
+import zeroecho.pki.impl.framework.x509.bc.BcX509ProfileSupport;
+
+/**
+ * Authoritative active-profile gate for root and intermediate certificate
+ * requests.
+ */
+// The closed gate deliberately keeps cohesive profile inputs together and
+// redacts attacker-controlled parser failures rather than retaining their causes.
+@SuppressWarnings({ "PMD.CyclomaticComplexity", "PMD.ExcessiveParameterList",
+ "PMD.PreserveStackTrace", "PMD.AvoidRethrowingException" })
+final class CaCertificateProfileValidator {
+ private static final int MAXIMUM_SUBJECT_DER_BYTES = 16 * 1024;
+ private static final int SINGLE_VALUE = 1;
+
+ private CaCertificateProfileValidator() {
+ }
+
+ /* package */ static ValidatedCaCertificateRequest validate(
+ ValidatedCaCertificateRequest.Operation operation, ActiveCertificateProfile activeProfile,
+ CertificateProfileKind expectedKind, FormatId formatId, PkiId issuerCaId, PkiId subjectCaId,
+ SubjectRef requestedSubject, EncodedObject exactPublicKey, Optional requestedValidity,
+ Instant evaluationTime, Optional issuerNotAfter, BigInteger serial) {
+ CertificateProfileDefinition definition = activeProfile.definition();
+ requireProfileShape(definition, expectedKind, formatId);
+ List approvedSubject = validateSubject(requestedSubject, definition);
+ return validateApprovedSubject(operation, activeProfile, expectedKind, formatId, issuerCaId, subjectCaId,
+ approvedSubject, exactPublicKey, requestedValidity, evaluationTime, issuerNotAfter, serial);
+ }
+
+ /*
+ * Package-private continuation used only after validateSubject has approved the
+ * requester input. It preserves profile-fixed RDNs without treating them as a
+ * second requester submission.
+ */
+ /* package */ static ValidatedCaCertificateRequest validateApprovedSubject(
+ ValidatedCaCertificateRequest.Operation operation, ActiveCertificateProfile activeProfile,
+ CertificateProfileKind expectedKind, FormatId formatId, PkiId issuerCaId, PkiId subjectCaId,
+ List approvedSubject, EncodedObject exactPublicKey, Optional requestedValidity,
+ Instant evaluationTime, Optional issuerNotAfter, BigInteger serial) {
+ CertificateProfileDefinition definition = activeProfile.definition();
+ requireProfileShape(definition, expectedKind, formatId);
+ List subjectSnapshot = requireApprovedSubject(approvedSubject);
+ CertificateProfileValidator.requireSubjectKeyAllowed(exactPublicKey,
+ definition.caPolicy().allowedSubjectKeyAlgorithmIds());
+ Validity validity = approvedValidity(requestedValidity, definition, evaluationTime, issuerNotAfter,
+ operation == ValidatedCaCertificateRequest.Operation.IMPORT_ROOT);
+ SubjectRef canonicalSubject = new SubjectRef(BcX509ProfileSupport.subject(subjectSnapshot).toString());
+ return new ValidatedCaCertificateRequest(operation, formatId, issuerCaId, subjectCaId,
+ activeProfile.reference(), expectedKind, canonicalSubject, subjectSnapshot, exactPublicKey,
+ validity, serial, definition.caPolicy());
+ }
+
+ /* package */ static void requireProfileShape(ActiveCertificateProfile activeProfile,
+ CertificateProfileKind expectedKind, FormatId formatId) {
+ requireProfileShape(activeProfile.definition(), expectedKind, formatId);
+ }
+
+ private static void requireProfileShape(CertificateProfileDefinition definition,
+ CertificateProfileKind expectedKind, FormatId formatId) {
+ if (definition.certificateType() != expectedKind) {
+ throw reject("CA_PROFILE_KIND_MISMATCH");
+ }
+ if (!definition.formatId().equals(formatId)) {
+ throw reject("CA_PROFILE_FORMAT_MISMATCH");
+ }
+ }
+
+ /* package */ static List validateSubject(SubjectRef requestedSubject,
+ CertificateProfileDefinition definition) {
+ List requested = parseSubject(requestedSubject);
+ Map rules = new EnumMap<>(SubjectRdnType.class);
+ for (SubjectRdnRule rule : definition.subjectPolicy().rules()) {
+ rules.put(rule.type(), rule);
+ }
+ Map counts = new EnumMap<>(SubjectRdnType.class);
+ List approved = new ArrayList<>(requested.size() + rules.size());
+ for (SubjectRdn rdn : requested) {
+ SubjectRdnRule rule = rules.get(rdn.type());
+ if (rule == null || !rule.requesterSupplied()) {
+ throw reject("CA_SUBJECT_RDN_FORBIDDEN");
+ }
+ String canonical = SubjectRdnRule.canonicalValue(rdn.type(), rdn.value());
+ if (canonical.getBytes(StandardCharsets.UTF_8).length > rule.maximumUtf8Bytes()) {
+ throw reject("CA_SUBJECT_RDN_TOO_LARGE");
+ }
+ int count = Math.addExact(counts.getOrDefault(rdn.type(), 0), 1);
+ if (count > rule.maximumOccurrences()) {
+ throw reject("CA_SUBJECT_RDN_CARDINALITY");
+ }
+ counts.put(rdn.type(), count);
+ approved.add(new SubjectRdn(rdn.type(), canonical));
+ }
+ for (SubjectRdnRule rule : definition.subjectPolicy().rules()) {
+ if (rule.fixedValue().isPresent()) {
+ approved.add(new SubjectRdn(rule.type(), rule.fixedValue().orElseThrow()));
+ counts.put(rule.type(), 1);
+ }
+ int count = counts.getOrDefault(rule.type(), 0);
+ if (count < rule.minimumOccurrences() || count > rule.maximumOccurrences()) {
+ throw reject("CA_SUBJECT_RDN_REQUIRED");
+ }
+ }
+ if (approved.isEmpty() || approved.size() > zeroecho.pki.api.profile.SubjectPolicy.HARD_MAXIMUM_RDN_COUNT) {
+ throw reject("CA_SUBJECT_INVALID");
+ }
+ try {
+ if (BcX509ProfileSupport.subject(approved).getEncoded().length > MAXIMUM_SUBJECT_DER_BYTES) {
+ throw reject("CA_SUBJECT_TOO_LARGE");
+ }
+ } catch (java.io.IOException exception) {
+ throw reject("CA_SUBJECT_INVALID");
+ }
+ return List.copyOf(approved);
+ }
+
+ /*
+ * Validates a previously approved canonical subject without reclassifying
+ * profile-fixed RDNs as requester input.
+ */
+ /* package */ static List validateTrustedSubject(SubjectRef trustedSubject,
+ CertificateProfileDefinition definition) {
+ List existing = parseSubject(trustedSubject);
+ Map rules = new EnumMap<>(SubjectRdnType.class);
+ for (SubjectRdnRule rule : definition.subjectPolicy().rules()) {
+ rules.put(rule.type(), rule);
+ }
+ Map counts = new EnumMap<>(SubjectRdnType.class);
+ List requesterValues = new ArrayList<>(existing.size());
+ for (SubjectRdn rdn : existing) {
+ SubjectRdnRule rule = rules.get(rdn.type());
+ if (rule == null) {
+ throw reject("CA_SUBJECT_RDN_FORBIDDEN");
+ }
+ String canonical = SubjectRdnRule.canonicalValue(rdn.type(), rdn.value());
+ if (!canonical.equals(rdn.value())
+ || canonical.getBytes(StandardCharsets.UTF_8).length > rule.maximumUtf8Bytes()) {
+ throw reject("CA_SUBJECT_RDN_INVALID");
+ }
+ int count = Math.addExact(counts.getOrDefault(rdn.type(), 0), 1);
+ if (count > rule.maximumOccurrences()) {
+ throw reject("CA_SUBJECT_RDN_CARDINALITY");
+ }
+ counts.put(rdn.type(), count);
+ if (rule.requesterSupplied()) {
+ requesterValues.add(rdn);
+ } else if (rule.fixedValue().isEmpty()
+ || !rule.fixedValue().orElseThrow().equals(rdn.value())) {
+ throw reject("CA_SUBJECT_FIXED_VALUE_MISMATCH");
+ }
+ }
+ List expected = new ArrayList<>(existing.size());
+ expected.addAll(requesterValues);
+ for (SubjectRdnRule rule : definition.subjectPolicy().rules()) {
+ int count = counts.getOrDefault(rule.type(), 0);
+ if (count < rule.minimumOccurrences() || count > rule.maximumOccurrences()) {
+ throw reject("CA_SUBJECT_RDN_REQUIRED");
+ }
+ rule.fixedValue().ifPresent(value -> expected.add(new SubjectRdn(rule.type(), value)));
+ }
+ if (!existing.equals(expected)) {
+ throw reject("CA_SUBJECT_ORDER_INVALID");
+ }
+ return requireApprovedSubject(expected);
+ }
+
+ private static List requireApprovedSubject(List approvedSubject) {
+ List snapshot = List.copyOf(approvedSubject);
+ if (snapshot.isEmpty() || snapshot.size() > zeroecho.pki.api.profile.SubjectPolicy.HARD_MAXIMUM_RDN_COUNT) {
+ throw reject("CA_SUBJECT_INVALID");
+ }
+ try {
+ if (BcX509ProfileSupport.subject(snapshot).getEncoded().length > MAXIMUM_SUBJECT_DER_BYTES) {
+ throw reject("CA_SUBJECT_TOO_LARGE");
+ }
+ } catch (java.io.IOException exception) {
+ throw reject("CA_SUBJECT_INVALID");
+ }
+ return snapshot;
+ }
+
+ private static List parseSubject(SubjectRef subjectRef) {
+ try {
+ X500Name name = new X500Name(subjectRef.value());
+ List result = new ArrayList<>();
+ for (RDN rdn : name.getRDNs()) {
+ AttributeTypeAndValue[] values = rdn.getTypesAndValues();
+ if (values.length != SINGLE_VALUE) {
+ throw reject("CA_SUBJECT_MULTIVALUED_RDN");
+ }
+ result.add(new SubjectRdn(SubjectRdnType.fromOid(values[0].getType().getId()),
+ values[0].getValue().toString()));
+ }
+ return List.copyOf(result);
+ } catch (PkiException exception) {
+ throw exception;
+ } catch (IllegalArgumentException exception) {
+ throw reject("CA_SUBJECT_INVALID");
+ }
+ }
+
+ private static Validity approvedValidity(Optional requested,
+ CertificateProfileDefinition definition, Instant evaluationTime, Optional issuerNotAfter,
+ boolean imported) {
+ try {
+ Instant ceiling = evaluationTime.plus(definition.maximumValidity());
+ if (issuerNotAfter.isPresent() && issuerNotAfter.orElseThrow().isBefore(ceiling)) {
+ ceiling = issuerNotAfter.orElseThrow();
+ }
+ Validity validity = requested.isPresent() ? requested.orElseThrow()
+ : new Validity(evaluationTime, ceiling);
+ boolean invalidStart = imported ? validity.notBefore().isAfter(evaluationTime)
+ : !validity.notBefore().equals(evaluationTime);
+ if (invalidStart || !validity.notAfter().isAfter(evaluationTime)
+ || java.time.Duration.between(validity.notBefore(), validity.notAfter())
+ .compareTo(definition.maximumValidity()) > 0
+ || !imported && validity.notAfter().isAfter(ceiling)
+ || !validity.notAfter().isAfter(validity.notBefore())) {
+ throw reject("CA_VALIDITY_REJECTED");
+ }
+ return validity;
+ } catch (DateTimeException | ArithmeticException exception) {
+ throw reject("CA_VALIDITY_REJECTED");
+ }
+ }
+
+ private static PkiException reject(String code) {
+ return new PkiException("CA certificate profile rejected: code=" + code);
+ }
+}
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/CaProofGate.java b/pki/src/main/java/zeroecho/pki/impl/core/CaProofGate.java
index 137a2e7..47856f4 100644
--- a/pki/src/main/java/zeroecho/pki/impl/core/CaProofGate.java
+++ b/pki/src/main/java/zeroecho/pki/impl/core/CaProofGate.java
@@ -59,9 +59,6 @@ import zeroecho.pki.api.FormatId;
import zeroecho.pki.api.KeyRef;
import zeroecho.pki.api.PkiException;
import zeroecho.pki.api.PkiId;
-import zeroecho.pki.api.SubjectRef;
-import zeroecho.pki.api.Validity;
-import zeroecho.pki.api.attr.AttributeSet;
import zeroecho.pki.api.audit.AccessContext;
import zeroecho.pki.api.audit.AuditEvent;
import zeroecho.pki.api.audit.Principal;
@@ -127,16 +124,7 @@ final class CaProofGate {
/* default */ ManagedKeyProof proveManagedKey(KeyRef keyRef, FormatId formatId, String auditAction,
Optional subjectCaId) {
- EncodedObject resolved;
- try {
- resolved = publicKeyResolver.resolveSpkiDer(keyRef);
- } catch (RuntimeException ex) { // NOPMD - managed-key resolution boundary fails closed
- throw rejection(auditAction, formatId, subjectCaId, "MANAGED_KEY_UNAVAILABLE");
- }
- if (resolved == null || resolved.encoding() != Encoding.DER) {
- throw rejection(auditAction, formatId, subjectCaId, "MANAGED_KEY_INVALID");
- }
-
+ EncodedObject resolved = resolveManagedKey(keyRef, formatId, auditAction, subjectCaId);
byte[] exactSpki = resolved.bytes().clone();
byte[] challenge = new byte[MANAGED_KEY_CHALLENGE_DOMAIN.length + CHALLENGE_NONCE_BYTES];
CHALLENGE_RANDOM.nextBytes(challenge);
@@ -160,12 +148,18 @@ final class CaProofGate {
}
}
- /* default */ ManagedCaIssuance authorizeIntermediate(ManagedKeyProof proof,
- ManagedCaIssuance.Operation operation, PkiId issuerCaId, PkiId subjectCaId, String profileId,
- Optional requestedValidity, AttributeSet attributes, SubjectRef subjectRef) {
- Objects.requireNonNull(proof, "proof");
- return new ManagedCaIssuance(proof, operation, issuerCaId, subjectCaId, profileId, requestedValidity,
- attributes, subjectRef);
+ /* default */ EncodedObject resolveManagedKey(KeyRef keyRef, FormatId formatId, String auditAction,
+ Optional subjectCaId) {
+ EncodedObject resolved;
+ try {
+ resolved = publicKeyResolver.resolveSpkiDer(keyRef);
+ } catch (RuntimeException ex) { // NOPMD - managed-key resolution boundary fails closed
+ throw rejection(auditAction, formatId, subjectCaId, "MANAGED_KEY_UNAVAILABLE");
+ }
+ if (resolved == null || resolved.encoding() != Encoding.DER) {
+ throw rejection(auditAction, formatId, subjectCaId, "MANAGED_KEY_INVALID");
+ }
+ return new EncodedObject(Encoding.DER, resolved.bytes());
}
/* default */ PkiException rejection(String action, FormatId formatId, Optional objectId, String code) {
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/CertificateProfileValidator.java b/pki/src/main/java/zeroecho/pki/impl/core/CertificateProfileValidator.java
index fef95e0..154928d 100644
--- a/pki/src/main/java/zeroecho/pki/impl/core/CertificateProfileValidator.java
+++ b/pki/src/main/java/zeroecho/pki/impl/core/CertificateProfileValidator.java
@@ -64,10 +64,10 @@ final class CertificateProfileValidator {
ParsedCertificationRequest request = candidate.request();
LeafCertificatePolicy policy = profile.leafPolicy();
requireCanonicalRequestAttributes(request);
- List approvedSubject = validateSubject(request, policy);
+ List approvedSubject = validateSubject(request, profile);
List approvedSans = validateSans(request, policy, approvedSubject.isEmpty());
- requireSubjectKeyAllowed(candidate, policy);
- Validity validity = approvedValidity(candidate, request, policy, issuerCredential, evaluationTime);
+ requireSubjectKeyAllowed(candidate.exactPublicKey(), policy.allowedSubjectKeyAlgorithmIds());
+ Validity validity = approvedValidity(candidate, request, profile, issuerCredential, evaluationTime);
boolean sanCritical = approvedSubject.isEmpty()
|| policy.subjectAlternativeNamePolicy().criticalWithNonemptySubject();
SubjectRef approvedSubjectRef = new SubjectRef(approvedSubject.isEmpty()
@@ -90,9 +90,9 @@ final class CertificateProfileValidator {
// The branches preserve the deny-by-default RDN ownership and cardinality rules.
@SuppressWarnings("PMD.CyclomaticComplexity")
- private static List validateSubject(ParsedCertificationRequest request, LeafCertificatePolicy policy) {
+ private static List validateSubject(ParsedCertificationRequest request, CertificateProfile profile) {
Map rules = new EnumMap<>(SubjectRdnType.class);
- for (SubjectRdnRule rule : policy.subjectPolicy().rules()) {
+ for (SubjectRdnRule rule : profile.subjectPolicy().rules()) {
rules.put(rule.type(), rule);
}
Map counts = new EnumMap<>(SubjectRdnType.class);
@@ -113,7 +113,7 @@ final class CertificateProfileValidator {
counts.put(rdn.type(), count);
approved.add(new SubjectRdn(rdn.type(), canonical));
}
- for (SubjectRdnRule rule : policy.subjectPolicy().rules()) {
+ for (SubjectRdnRule rule : profile.subjectPolicy().rules()) {
if (rule.fixedValue().isPresent()) {
approved.add(new SubjectRdn(rule.type(), rule.fixedValue().orElseThrow()));
counts.put(rule.type(), 1);
@@ -123,7 +123,7 @@ final class CertificateProfileValidator {
throw reject("SUBJECT_RDN_REQUIRED");
}
}
- if (approved.isEmpty() && !policy.subjectAlternativeNamePolicy().allowEmptySubject()) {
+ if (approved.isEmpty() && !profile.subjectPolicy().allowEmpty()) {
throw reject("SUBJECT_EMPTY");
}
if (approved.size() > zeroecho.pki.api.profile.SubjectPolicy.HARD_MAXIMUM_RDN_COUNT) {
@@ -197,16 +197,17 @@ final class CertificateProfileValidator {
// The public exception deliberately redacts ASN.1 parser details.
@SuppressWarnings({ "PMD.PreserveStackTrace", "PMD.AvoidRethrowingException" })
- private static void requireSubjectKeyAllowed(VerifiedIssuanceCandidate candidate, LeafCertificatePolicy policy) {
- if (candidate.exactPublicKey().encoding() != Encoding.DER) {
+ /* package */ static void requireSubjectKeyAllowed(zeroecho.pki.api.EncodedObject exactPublicKey,
+ Set allowedAlgorithms) {
+ if (exactPublicKey.encoding() != Encoding.DER) {
throw reject("SUBJECT_KEY_UNSUPPORTED");
}
- byte[] encoded = candidate.exactPublicKey().bytes();
+ byte[] encoded = exactPublicKey.bytes();
try {
SubjectPublicKeyInfo spki = SubjectPublicKeyInfo.getInstance(encoded);
SubjectKeyAlgorithm algorithm = subjectKeyAlgorithm(spki.getAlgorithm().getAlgorithm());
requireSupportedParameters(spki, algorithm);
- if (!policy.allowedSubjectKeyAlgorithmIds().contains(algorithm.profileId())) {
+ if (!allowedAlgorithms.contains(algorithm.profileId())) {
throw reject("SUBJECT_KEY_ALGORITHM_FORBIDDEN");
}
PublicKey reconstructed = KeyFactory.getInstance(algorithm.jcaName())
@@ -262,12 +263,12 @@ final class CertificateProfileValidator {
// The public exception deliberately redacts temporal arithmetic details.
@SuppressWarnings("PMD.PreserveStackTrace")
private static Validity approvedValidity(VerifiedIssuanceCandidate candidate, ParsedCertificationRequest request,
- LeafCertificatePolicy policy, Credential issuerCredential, Instant evaluationTime) {
+ CertificateProfile profile, Credential issuerCredential, Instant evaluationTime) {
Optional supplied = candidate.validityOverride().isPresent()
? candidate.validityOverride() : request.requestedValidity();
Duration duration = supplied.map(value -> Duration.between(value.notBefore(), value.notAfter()))
- .orElse(policy.maximumValidity());
- if (duration.isZero() || duration.isNegative() || duration.compareTo(policy.maximumValidity()) > 0) {
+ .orElse(profile.maximumValidity());
+ if (duration.isZero() || duration.isNegative() || duration.compareTo(profile.maximumValidity()) > 0) {
throw reject("VALIDITY_EXCEEDS_PROFILE");
}
Instant notAfter;
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/CredentialProfileBindings.java b/pki/src/main/java/zeroecho/pki/impl/core/CredentialProfileBindings.java
index 581bffb..e9d3478 100644
--- a/pki/src/main/java/zeroecho/pki/impl/core/CredentialProfileBindings.java
+++ b/pki/src/main/java/zeroecho/pki/impl/core/CredentialProfileBindings.java
@@ -29,9 +29,9 @@ final class CredentialProfileBindings {
}
}
- /* default */ static void requireCaBinding(CredentialProfileBinding binding, String expectedCaProfileId) {
+ /* default */ static void requireCaBinding(CredentialProfileBinding binding, CertificateProfileRef expected) {
if (!(binding instanceof CaProfileBinding ca)
- || !ca.profileId().equals(expectedCaProfileId)) {
+ || !ca.reference().equals(expected)) {
throw mismatch();
}
}
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/DefaultCaService.java b/pki/src/main/java/zeroecho/pki/impl/core/DefaultCaService.java
index 9f386b1..d06c1c5 100644
--- a/pki/src/main/java/zeroecho/pki/impl/core/DefaultCaService.java
+++ b/pki/src/main/java/zeroecho/pki/impl/core/DefaultCaService.java
@@ -36,6 +36,7 @@ package zeroecho.pki.impl.core;
import java.io.IOException;
import java.math.BigInteger;
import java.security.MessageDigest;
+import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
@@ -52,9 +53,11 @@ import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.cert.CertException;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
import zeroecho.pki.api.CaService;
@@ -65,10 +68,10 @@ import zeroecho.pki.api.IssuerRef;
import zeroecho.pki.api.KeyRef;
import zeroecho.pki.api.PkiException;
import zeroecho.pki.api.PkiId;
+import zeroecho.pki.api.ProfileService;
import zeroecho.pki.api.SubjectRef;
import zeroecho.pki.api.Validity;
import zeroecho.pki.api.attr.AttributeSet;
-import zeroecho.pki.api.attr.AttributeValue;
import zeroecho.pki.api.ca.CaCreateCommand;
import zeroecho.pki.api.ca.CaImportCommand;
import zeroecho.pki.api.ca.CaKeyRotationCommand;
@@ -85,9 +88,10 @@ import zeroecho.pki.api.credential.CredentialStatus;
import zeroecho.pki.api.credential.CredentialUse;
import zeroecho.pki.api.credential.EffectiveCredentialStatus;
import zeroecho.pki.api.credential.EffectiveCredentialStatusResolver;
+import zeroecho.pki.api.profile.ActiveCertificateProfile;
+import zeroecho.pki.api.profile.CertificateProfileKind;
import zeroecho.pki.impl.core.async.PkiSigningBus;
import zeroecho.pki.impl.core.attr.SimpleAttributeSet;
-import zeroecho.pki.impl.framework.x509.bc.BcX509Attributes;
import zeroecho.pki.spi.audit.AuditSink;
import zeroecho.pki.spi.framework.CredentialFramework;
import zeroecho.pki.spi.framework.CredentialIssuerBackend;
@@ -147,13 +151,17 @@ import zeroecho.pki.spi.store.PkiStore;
*
*/
// PMD cannot infer that retaining boundary causes would violate the redaction contract.
-@SuppressWarnings({ "PMD.CyclomaticComplexity", "PMD.PreserveStackTrace" })
+@SuppressWarnings({ "PMD.CouplingBetweenObjects", "PMD.CyclomaticComplexity",
+ "PMD.ExcessiveParameterList", "PMD.PreserveStackTrace" })
public final class DefaultCaService implements CaService {
private static final Logger LOG = Logger.getLogger(DefaultCaService.class.getName());
+ private static final String CREATE_ROOT_REJECTED = "CREATE_ROOT_REJECTED";
private static final String CREATE_INT_REJECTED = "CREATE_INTERMEDIATE_REJECTED";
private static final String ISSUE_INT_REJECTED = "ISSUE_INTERMEDIATE_REJECTED";
+ private static final String IMPORT_ROOT_REJECTED = "IMPORT_ROOT_REJECTED";
private static final String BACKEND_CRED_MISMATCH = "BACKEND_CREDENTIAL_MISMATCH";
+ private static final String ROOT_CREDENTIAL_INVALID = "ROOT_CREDENTIAL_INVALID";
private final PkiStore store;
private final CredentialFramework framework;
@@ -161,6 +169,8 @@ public final class DefaultCaService implements CaService {
private final CaProofGate proofGate;
private final AuditSink auditSink;
private final EffectiveCredentialStatusResolver statusResolver;
+ private final ProfileService profileService;
+ private final Clock clock;
/**
* Creates a CA service bound to a specific store, credential framework, and
@@ -214,7 +224,8 @@ public final class DefaultCaService implements CaService {
*/
public DefaultCaService(PkiStore store, CredentialFramework framework, CredentialIssuerBackend issuerBackend,
PublicKeyInfoResolver publicKeyResolver, PkiSigningBus signingBus, AuditSink auditSink,
- EffectiveCredentialStatusResolver statusResolver, String signatureAlgorithmId, Duration signingTtl) {
+ EffectiveCredentialStatusResolver statusResolver, ProfileService profileService, Clock clock,
+ String signatureAlgorithmId, Duration signingTtl) {
this.store = Objects.requireNonNull(store, "store");
this.framework = Objects.requireNonNull(framework, "framework");
@@ -223,6 +234,8 @@ public final class DefaultCaService implements CaService {
Objects.requireNonNull(signingBus, "signingBus");
this.auditSink = Objects.requireNonNull(auditSink, "auditSink");
this.statusResolver = Objects.requireNonNull(statusResolver, "statusResolver");
+ this.profileService = Objects.requireNonNull(profileService, "profileService");
+ this.clock = Objects.requireNonNull(clock, "clock");
if (signatureAlgorithmId == null || signatureAlgorithmId.isBlank()) {
throw new IllegalArgumentException("signatureAlgorithmId must not be null/blank");
}
@@ -263,30 +276,42 @@ public final class DefaultCaService implements CaService {
@Override
public PkiId createRoot(CaCreateCommand command) {
Objects.requireNonNull(command, "command"); // NOPMD
+ ActiveCertificateProfile activeProfile = profileService.requireActiveProfile(command.profileId());
+ CaCertificateProfileValidator.requireProfileShape(activeProfile, CertificateProfileKind.ROOT_CA,
+ command.formatId());
+ Instant evaluationTime = clock.instant();
if (command.keyRef().isEmpty()) {
throw new PkiException("Root CA creation requires keyRef (key generation not wired)");
}
+ requireNoCaOverrides(command.attributes());
if (!framework.formatId().equals(command.formatId())) {
throw new PkiException("Unsupported formatId for this runtime");
}
KeyRef keyRef = command.keyRef().get();
-
- SubjectRef subjectRef = command.subjectRef();
- CaProofGate.ManagedKeyProof proof = proofGate.proveManagedKey(keyRef, command.formatId(),
- "CREATE_ROOT_REJECTED", Optional.empty());
- EncodedObject spki = proof.exactPublicKey();
+ EncodedObject spki = proofGate.resolveManagedKey(keyRef, command.formatId(),
+ CREATE_ROOT_REJECTED, Optional.empty());
+ BigInteger serial = CertificateSerialAllocator.allocate();
+ ValidatedCaCertificateRequest request = CaCertificateProfileValidator.validate(
+ ValidatedCaCertificateRequest.Operation.CREATE_ROOT, activeProfile,
+ CertificateProfileKind.ROOT_CA,
+ command.formatId(), new PkiId("ca:pending-root"), new PkiId("ca:pending-root"),
+ command.subjectRef(), spki, Optional.empty(), evaluationTime, Optional.empty(), serial);
SubjectPublicKeyInfo rootPublicKeyInfo = proofGate.parseRootSpki(spki, command.formatId());
+ CaProofGate.ManagedKeyProof proof = proofGate.proveManagedKey(keyRef, command.formatId(),
+ CREATE_ROOT_REJECTED, Optional.empty());
+ requireSameManagedKey(spki, proof.exactPublicKey(), CREATE_ROOT_REJECTED,
+ command.formatId(), Optional.empty());
- Instant now = Instant.now();
- Validity validity = new Validity(now.minus(Duration.ofMinutes(1)), now.plus(Duration.ofDays(3650)));
- X500Name dn = new X500Name(subjectRef.value());
+ Validity validity = request.validity();
+ X500Name dn = zeroecho.pki.impl.framework.x509.bc.BcX509ProfileSupport.subject(request.subjectRdns());
- BigInteger serial = BigInteger.valueOf(Math.abs(now.toEpochMilli()) + 1L);
X509v3CertificateBuilder b = new X509v3CertificateBuilder(dn, serial, Date.from(validity.notBefore()),
Date.from(validity.notAfter()), dn, rootPublicKeyInfo);
try {
- b.addExtension(Extension.basicConstraints, true, new BasicConstraints(true));
- b.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign));
+ b.addExtension(Extension.basicConstraints, request.policy().basicConstraintsCritical(),
+ new BasicConstraints(request.policy().pathLengthConstraint()));
+ b.addExtension(Extension.keyUsage, request.policy().keyUsageCritical(),
+ new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign));
} catch (Exception ex) {
throw new PkiException("Root extension construction failed: code=ROOT_EXTENSION_BUILD_FAILED");
}
@@ -296,12 +321,12 @@ public final class DefaultCaService implements CaService {
try {
cert = b.build(signer);
} catch (RuntimeException ex) { // NOPMD
- throw proofGate.rejection("CREATE_ROOT_REJECTED", command.formatId(), Optional.empty(),
+ throw proofGate.rejection(CREATE_ROOT_REJECTED, command.formatId(), Optional.empty(),
"ROOT_SIGNING_FAILED");
}
if (!proofGate.rootProofIsValid(cert, spki)) {
- throw proofGate.rejection("CREATE_ROOT_REJECTED", command.formatId(), Optional.empty(),
+ throw proofGate.rejection(CREATE_ROOT_REJECTED, command.formatId(), Optional.empty(),
"ROOT_SELF_SIGNATURE_INVALID");
}
@@ -317,12 +342,16 @@ public final class DefaultCaService implements CaService {
PkiId publicKeyId = new PkiId("spki:" + sha256Hex(spki.bytes()));
- Credential credential = new Credential(credId, command.formatId(), new IssuerRef(caId), subjectRef, validity,
- serial.toString(), publicKeyId, new CaProfileBinding(command.profileId()), CredentialStatus.ISSUED,
- new EncodedObject(Encoding.DER, certDer), command.attributes());
- CredentialProfileBindings.requireCaBinding(credential.profileBinding(), command.profileId());
+ Credential credential = new Credential(credId, command.formatId(), new IssuerRef(caId), request.subjectRef(),
+ validity, serial.toString(), publicKeyId, new CaProfileBinding(request.profileReference()),
+ CredentialStatus.ISSUED, new EncodedObject(Encoding.DER, certDer),
+ SimpleAttributeSet.builder().build());
+ CredentialProfileBindings.requireCaBinding(credential.profileBinding(), request.profileReference());
- CaRecord ca = new CaRecord(caId, CaKind.ROOT, CaState.ACTIVE, keyRef, subjectRef, List.of(credential));
+ requireCaCertificateMatches(credential, credential, request, caId, CREATE_ROOT_REJECTED,
+ BACKEND_CRED_MISMATCH);
+ CaRecord ca = new CaRecord(caId, CaKind.ROOT, CaState.ACTIVE, keyRef, request.subjectRef(),
+ List.of(credential));
store.putCa(ca);
store.putCredential(credential);
return caId;
@@ -356,6 +385,11 @@ public final class DefaultCaService implements CaService {
@Override
public PkiId importRoot(CaImportCommand command) {
Objects.requireNonNull(command, "command");
+ ActiveCertificateProfile activeProfile = profileService.requireActiveProfile(command.profileId());
+ CaCertificateProfileValidator.requireProfileShape(activeProfile, CertificateProfileKind.ROOT_CA,
+ command.formatId());
+ Instant evaluationTime = clock.instant();
+ requireNoCaOverrides(command.attributes());
if (!framework.formatId().equals(command.formatId())) {
throw new PkiException("Unsupported formatId for this runtime");
}
@@ -371,10 +405,7 @@ public final class DefaultCaService implements CaService {
throw new PkiException("Invalid X.509 credential: code=CREDENTIAL_INVALID");
}
- requireValidImportedRoot(command, holder);
-
Instant notBefore = holder.getNotBefore().toInstant();
-
Instant notAfter = holder.getNotAfter().toInstant();
Validity validity = new Validity(notBefore, notAfter);
@@ -391,13 +422,22 @@ public final class DefaultCaService implements CaService {
PkiId publicKeyId = new PkiId("spki:" + sha256Hex(spkiDer));
BigInteger serial = holder.getSerialNumber();
- Credential credential = new Credential(credId, command.formatId(), new IssuerRef(caId), command.subjectRef(),
- validity, serial.toString(), publicKeyId, new CaProfileBinding(command.profileId()),
+ EncodedObject spki = new EncodedObject(Encoding.DER, spkiDer);
+ ValidatedCaCertificateRequest request = CaCertificateProfileValidator.validate(
+ ValidatedCaCertificateRequest.Operation.IMPORT_ROOT, activeProfile,
+ CertificateProfileKind.ROOT_CA,
+ command.formatId(), caId, caId, command.subjectRef(), spki, Optional.of(validity),
+ evaluationTime, Optional.empty(), serial);
+ Credential credential = new Credential(credId, command.formatId(), new IssuerRef(caId), request.subjectRef(),
+ validity, serial.toString(), publicKeyId, new CaProfileBinding(request.profileReference()),
CredentialStatus.ISSUED,
- new EncodedObject(Encoding.DER, certDer), command.attributes());
- CredentialProfileBindings.requireCaBinding(credential.profileBinding(), command.profileId());
+ new EncodedObject(Encoding.DER, certDer), SimpleAttributeSet.builder().build());
+ CredentialProfileBindings.requireCaBinding(credential.profileBinding(), request.profileReference());
+ requireCaCertificateMatches(credential, credential, request, caId, IMPORT_ROOT_REJECTED,
+ ROOT_CREDENTIAL_INVALID);
+ requireValidImportedRoot(command, holder);
store.putCredential(credential);
- CaRecord ca = new CaRecord(caId, CaKind.ROOT, CaState.ACTIVE, command.keyRef(), command.subjectRef(),
+ CaRecord ca = new CaRecord(caId, CaKind.ROOT, CaState.ACTIVE, command.keyRef(), request.subjectRef(),
List.of(credential));
store.putCa(ca);
return caId;
@@ -439,9 +479,14 @@ public final class DefaultCaService implements CaService {
@Override
public PkiId createIntermediate(IntermediateCreateCommand command) {
Objects.requireNonNull(command, "command");
+ ActiveCertificateProfile activeProfile = profileService.requireActiveProfile(command.profileId());
+ CaCertificateProfileValidator.requireProfileShape(activeProfile, CertificateProfileKind.INTERMEDIATE_CA,
+ command.formatId());
+ Instant evaluationTime = clock.instant();
if (command.keyRef().isEmpty()) {
throw new PkiException("Intermediate creation requires keyRef (key generation not wired)");
}
+ requireNoCaOverrides(command.attributes());
CaRecord issuer = getCa(command.issuerCaId());
ensureActive(issuer, "issuer");
@@ -455,29 +500,40 @@ public final class DefaultCaService implements CaService {
EffectiveCredentialStatusResolver.Evaluation statusEvaluation = statusResolver.beginEvaluation();
Credential issuerCredential = CredentialSnapshots.copy(selectIssuerCredential(issuer, command.formatId(),
CredentialUse.INTERMEDIATE_ISSUER, statusEvaluation));
+ requireHistoricalCaProfile(issuerCredential,
+ issuer.kind() == CaKind.ROOT ? CertificateProfileKind.ROOT_CA
+ : CertificateProfileKind.INTERMEDIATE_CA);
- PkiId caId = new PkiId("ca:" + sha256Hex((issuer.caId().value() + "\n" + command.subjectRef().value())
+ List approvedSubject =
+ CaCertificateProfileValidator.validateSubject(command.subjectRef(), activeProfile.definition());
+ SubjectRef canonicalSubject = new SubjectRef(
+ zeroecho.pki.impl.framework.x509.bc.BcX509ProfileSupport.subject(approvedSubject).toString());
+ PkiId caId = new PkiId("ca:" + sha256Hex((issuer.caId().value() + "\n" + canonicalSubject.value())
.getBytes(java.nio.charset.StandardCharsets.UTF_8)).substring(0, 16));
+ EncodedObject subjectSpki = proofGate.resolveManagedKey(command.keyRef().get(),
+ command.formatId(), CREATE_INT_REJECTED, Optional.of(caId));
+ ValidatedCaCertificateRequest issue = CaCertificateProfileValidator.validateApprovedSubject(
+ ValidatedCaCertificateRequest.Operation.CREATE_INTERMEDIATE, activeProfile,
+ CertificateProfileKind.INTERMEDIATE_CA, command.formatId(), command.issuerCaId(), caId,
+ approvedSubject, subjectSpki, Optional.empty(), evaluationTime,
+ Optional.of(issuerCredential.validity().notAfter()), CertificateSerialAllocator.allocate());
CaProofGate.ManagedKeyProof subjectProof = proofGate.proveManagedKey(command.keyRef().get(),
command.formatId(), CREATE_INT_REJECTED, Optional.of(caId));
- EncodedObject subjectSpki = subjectProof.exactPublicKey();
+ requireSameManagedKey(subjectSpki, subjectProof.exactPublicKey(), CREATE_INT_REJECTED,
+ command.formatId(), Optional.of(caId));
requireIssuerKeyBinding(issuer, issuerCredential, command.formatId(), CREATE_INT_REJECTED,
Optional.of(caId));
- AttributeSet authoritative = authoritativeIntermediateAttributes(command.attributes(), issuer,
- issuerCredential, subjectSpki, command.subjectRef());
- ManagedCaIssuance issue = proofGate.authorizeIntermediate(subjectProof,
- ManagedCaIssuance.Operation.CREATE_INTERMEDIATE, command.issuerCaId(), caId, command.profileId(),
- Optional.empty(), authoritative, command.subjectRef());
Credential backendCredential;
try {
- backendCredential = issuerBackend.issueIntermediateCertificate(issue);
+ backendCredential = issuerBackend.issueIntermediateCertificate(issue, issuerCredential.encoded(),
+ issuer.issuerKeyRef());
} catch (RuntimeException ex) { // NOPMD - reject malformed or mutable framework output
throw proofGate.rejection(CREATE_INT_REJECTED, command.formatId(), Optional.of(caId),
BACKEND_CRED_MISMATCH);
}
- requireCaBinding(backendCredential, command.profileId(), CREATE_INT_REJECTED,
+ requireCaBinding(backendCredential, issue.profileReference(), CREATE_INT_REJECTED,
command.formatId(), Optional.of(caId));
Credential cred;
try {
@@ -486,12 +542,12 @@ public final class DefaultCaService implements CaService {
throw proofGate.rejection(CREATE_INT_REJECTED, command.formatId(), Optional.of(caId),
BACKEND_CRED_MISMATCH);
}
- requireIntermediateCredentialMatches(cred, issuerCredential, subjectSpki, command.subjectRef(),
- command.issuerCaId(), caId, CREATE_INT_REJECTED);
+ requireCaCertificateMatches(cred, issuerCredential, issue, caId, CREATE_INT_REJECTED,
+ BACKEND_CRED_MISMATCH);
store.putCredential(cred);
CaRecord subject = new CaRecord(caId, CaKind.INTERMEDIATE, CaState.ACTIVE, command.keyRef().get(),
- command.subjectRef(), List.of(cred));
+ issue.subjectRef(), List.of(cred));
store.putCa(subject);
return caId;
}
@@ -523,10 +579,19 @@ public final class DefaultCaService implements CaService {
@Override
public Credential issueIntermediateCertificate(IntermediateCertIssueCommand command) {
Objects.requireNonNull(command, "command");
+ ActiveCertificateProfile activeProfile = profileService.requireActiveProfile(command.profileId());
+ CaCertificateProfileValidator.requireProfileShape(activeProfile, CertificateProfileKind.INTERMEDIATE_CA,
+ command.formatId());
+ Instant evaluationTime = clock.instant();
+ requireNoCaOverrides(command.attributes());
CaRecord issuer = getCa(command.issuerCaId());
ensureActive(issuer, "issuer");
CaRecord subject = getCa(command.subjectCaId());
ensureActive(subject, "subject");
+ if (subject.kind() != CaKind.INTERMEDIATE) {
+ throw proofGate.rejection(ISSUE_INT_REJECTED, command.formatId(),
+ Optional.of(subject.caId()), "CA_SUBJECT_KIND_INVALID");
+ }
if (!framework.formatId().equals(command.formatId())) {
throw proofGate.rejection(ISSUE_INT_REJECTED, command.formatId(),
Optional.of(subject.caId()),
@@ -535,26 +600,37 @@ public final class DefaultCaService implements CaService {
EffectiveCredentialStatusResolver.Evaluation statusEvaluation = statusResolver.beginEvaluation();
Credential issuerCredential = CredentialSnapshots.copy(selectIssuerCredential(issuer, command.formatId(),
CredentialUse.INTERMEDIATE_ISSUER, statusEvaluation));
+ requireHistoricalCaProfile(issuerCredential,
+ issuer.kind() == CaKind.ROOT ? CertificateProfileKind.ROOT_CA
+ : CertificateProfileKind.INTERMEDIATE_CA);
+ List approvedSubject =
+ CaCertificateProfileValidator.validateTrustedSubject(subject.subjectRef(),
+ activeProfile.definition());
+ EncodedObject subjectSpki = proofGate.resolveManagedKey(subject.issuerKeyRef(),
+ command.formatId(), ISSUE_INT_REJECTED, Optional.of(subject.caId()));
+ ValidatedCaCertificateRequest gated = CaCertificateProfileValidator.validateApprovedSubject(
+ ValidatedCaCertificateRequest.Operation.ISSUE_INTERMEDIATE, activeProfile,
+ CertificateProfileKind.INTERMEDIATE_CA, command.formatId(), command.issuerCaId(),
+ command.subjectCaId(), approvedSubject, subjectSpki, command.requestedValidity(),
+ evaluationTime, Optional.of(issuerCredential.validity().notAfter()),
+ CertificateSerialAllocator.allocate());
CaProofGate.ManagedKeyProof subjectProof = proofGate.proveManagedKey(subject.issuerKeyRef(),
command.formatId(), ISSUE_INT_REJECTED, Optional.of(subject.caId()));
- EncodedObject subjectSpki = subjectProof.exactPublicKey();
+ requireSameManagedKey(subjectSpki, subjectProof.exactPublicKey(), ISSUE_INT_REJECTED,
+ command.formatId(), Optional.of(subject.caId()));
requireIssuerKeyBinding(issuer, issuerCredential, command.formatId(), ISSUE_INT_REJECTED,
Optional.of(subject.caId()));
- AttributeSet authoritative = authoritativeIntermediateAttributes(command.attributes(), issuer,
- issuerCredential, subjectSpki, subject.subjectRef());
- ManagedCaIssuance gated = proofGate.authorizeIntermediate(subjectProof,
- ManagedCaIssuance.Operation.ISSUE_INTERMEDIATE, command.issuerCaId(), command.subjectCaId(),
- command.profileId(), command.requestedValidity(), authoritative, subject.subjectRef());
Credential backendCredential;
try {
- backendCredential = issuerBackend.issueIntermediateCertificate(gated);
+ backendCredential = issuerBackend.issueIntermediateCertificate(gated, issuerCredential.encoded(),
+ issuer.issuerKeyRef());
} catch (RuntimeException ex) { // NOPMD - reject malformed or mutable framework output
throw proofGate.rejection(ISSUE_INT_REJECTED, command.formatId(),
Optional.of(subject.caId()), BACKEND_CRED_MISMATCH);
}
- requireCaBinding(backendCredential, command.profileId(), ISSUE_INT_REJECTED,
+ requireCaBinding(backendCredential, gated.profileReference(), ISSUE_INT_REJECTED,
command.formatId(), Optional.of(subject.caId()));
Credential cred;
try {
@@ -563,8 +639,8 @@ public final class DefaultCaService implements CaService {
throw proofGate.rejection(ISSUE_INT_REJECTED, command.formatId(),
Optional.of(subject.caId()), BACKEND_CRED_MISMATCH);
}
- requireIntermediateCredentialMatches(cred, issuerCredential, subjectSpki, subject.subjectRef(),
- command.issuerCaId(), subject.caId(), ISSUE_INT_REJECTED);
+ requireCaCertificateMatches(cred, issuerCredential, gated, subject.caId(), ISSUE_INT_REJECTED,
+ BACKEND_CRED_MISMATCH);
store.putCredential(cred);
List updated = new ArrayList<>(subject.caCredentials());
@@ -752,26 +828,17 @@ public final class DefaultCaService implements CaService {
private void requireValidImportedRoot(CaImportCommand command, X509CertificateHolder holder) {
try {
- BasicConstraints constraints = BasicConstraints
- .getInstance(holder.getExtension(Extension.basicConstraints).getParsedValue());
- if (!holder.getSubject().equals(holder.getIssuer()) || !constraints.isCA()
- || !holder.getSubject().equals(new X500Name(command.subjectRef().value()))
- || !holder.isSignatureValid(
- new JcaContentVerifierProviderBuilder().build(holder.getSubjectPublicKeyInfo()))) {
- throw proofGate.rejection("IMPORT_ROOT_REJECTED", command.formatId(), Optional.empty(),
- "ROOT_CREDENTIAL_INVALID");
- }
CaProofGate.ManagedKeyProof proof = proofGate.proveManagedKey(command.keyRef(), command.formatId(),
- "IMPORT_ROOT_REJECTED", Optional.empty());
+ IMPORT_ROOT_REJECTED, Optional.empty());
if (!MessageDigest.isEqual(proof.exactPublicKey().bytes(), holder.getSubjectPublicKeyInfo().getEncoded())) {
- throw proofGate.rejection("IMPORT_ROOT_REJECTED", command.formatId(), Optional.empty(),
+ throw proofGate.rejection(IMPORT_ROOT_REJECTED, command.formatId(), Optional.empty(),
"ROOT_MANAGED_KEY_MISMATCH");
}
} catch (PkiException ex) {
throw ex;
} catch (Exception ex) {
- throw proofGate.rejection("IMPORT_ROOT_REJECTED", command.formatId(), Optional.empty(),
- "ROOT_CREDENTIAL_INVALID");
+ throw proofGate.rejection(IMPORT_ROOT_REJECTED, command.formatId(), Optional.empty(),
+ ROOT_CREDENTIAL_INVALID);
}
}
@@ -795,17 +862,12 @@ public final class DefaultCaService implements CaService {
}
}
- private void requireIntermediateCredentialMatches(Credential credential, Credential issuerCredential,
- EncodedObject exactSubjectSpki, SubjectRef subjectRef, PkiId issuerCaId, PkiId subjectCaId,
- String action) {
+ private void requireCaCertificateMatches(Credential credential, Credential issuerCredential,
+ ValidatedCaCertificateRequest request, PkiId subjectCaId, String action, String mismatchCode) {
try {
- if (!framework.formatId().equals(credential.formatId())
- || credential.encoded().encoding() != Encoding.DER
- || credential.status() != CredentialStatus.ISSUED
- || !credential.subjectRef().equals(subjectRef)
- || !credential.issuerRef().equals(new IssuerRef(issuerCaId))) {
+ if (!matchesCaCredentialEnvelope(credential, request, subjectCaId)) {
throw proofGate.rejection(action, framework.formatId(), Optional.of(subjectCaId),
- BACKEND_CRED_MISMATCH);
+ mismatchCode);
}
X509CertificateHolder holder = new X509CertificateHolder(credential.encoded().bytes());
X509CertificateHolder issuerHolder = new X509CertificateHolder(issuerCredential.encoded().bytes());
@@ -816,40 +878,82 @@ public final class DefaultCaService implements CaService {
Extension keyUsageExtension = holder.getExtension(Extension.keyUsage);
KeyUsage keyUsage = keyUsageExtension == null ? null
: KeyUsage.getInstance(keyUsageExtension.getParsedValue());
- if (!MessageDigest.isEqual(exactSubjectSpki.bytes(), actualSpki)
- || !holder.getSubject().equals(new X500Name(subjectRef.value()))
- || !holder.getIssuer().equals(issuerHolder.getSubject())
- || !holder.isSignatureValid(
- new JcaContentVerifierProviderBuilder().build(issuerHolder.getSubjectPublicKeyInfo()))
- || constraintsExtension == null || !constraintsExtension.isCritical()
- || constraints == null || !constraints.isCA()
- || !BigInteger.ZERO.equals(constraints.getPathLenConstraint())
- || keyUsageExtension == null || !keyUsageExtension.isCritical()
- || !hasIntermediateKeyUsage(keyUsage)
- || !credential.publicKeyId().equals(new PkiId("spki:" + sha256Hex(actualSpki)))
- || !credential.credentialId()
- .equals(new PkiId("x509:" + sha256Hex(credential.encoded().bytes())))
- || !credential.serialOrUniqueId().equals(holder.getSerialNumber().toString())
- || credential.validity().notBefore().getEpochSecond() != holder.getNotBefore().toInstant()
- .getEpochSecond()
- || credential.validity().notAfter().getEpochSecond() != holder.getNotAfter().toInstant()
- .getEpochSecond()) {
+ X500Name expectedSubject =
+ zeroecho.pki.impl.framework.x509.bc.BcX509ProfileSupport.subject(request.subjectRdns());
+ X500Name expectedIssuer = request.certificateType() == CertificateProfileKind.ROOT_CA
+ ? expectedSubject : issuerHolder.getSubject();
+ if (!matchesCaCertificateIdentity(holder, issuerHolder, request, expectedSubject, expectedIssuer,
+ actualSpki)
+ || !matchesCaCertificatePolicy(holder, request, constraintsExtension, constraints,
+ keyUsageExtension, keyUsage)
+ || !matchesCaCredentialMetadata(credential, holder, request, actualSpki)) {
throw proofGate.rejection(action, framework.formatId(), Optional.of(subjectCaId),
- BACKEND_CRED_MISMATCH);
+ mismatchCode);
}
+ CredentialProfileBindings.requireCaBinding(credential.profileBinding(), request.profileReference());
} catch (PkiException ex) {
throw ex;
} catch (Exception ex) {
throw proofGate.rejection(action, framework.formatId(), Optional.of(subjectCaId),
- BACKEND_CRED_MISMATCH);
+ mismatchCode);
}
}
- private void requireCaBinding(Credential credential, String expectedCaProfileId, String action,
+ private boolean matchesCaCredentialEnvelope(Credential credential, ValidatedCaCertificateRequest request,
+ PkiId subjectCaId) {
+ return framework.formatId().equals(credential.formatId())
+ && credential.encoded().encoding() == Encoding.DER
+ && credential.status() == CredentialStatus.ISSUED
+ && credential.subjectRef().equals(request.subjectRef())
+ && credential.issuerRef().equals(new IssuerRef(
+ request.certificateType() == CertificateProfileKind.ROOT_CA
+ ? subjectCaId : request.issuerCaId()));
+ }
+
+ private static boolean matchesCaCertificateIdentity(X509CertificateHolder holder,
+ X509CertificateHolder issuerHolder, ValidatedCaCertificateRequest request, X500Name expectedSubject,
+ X500Name expectedIssuer, byte[] actualSpki)
+ throws IOException, OperatorCreationException, CertException {
+ return MessageDigest.isEqual(request.exactPublicKey().bytes(), actualSpki)
+ && MessageDigest.isEqual(holder.getSubject().getEncoded(), expectedSubject.getEncoded())
+ && MessageDigest.isEqual(holder.getIssuer().getEncoded(), expectedIssuer.getEncoded())
+ && holder.isSignatureValid(
+ new JcaContentVerifierProviderBuilder().build(issuerHolder.getSubjectPublicKeyInfo()));
+ }
+
+ private static boolean matchesCaCertificatePolicy(X509CertificateHolder holder,
+ ValidatedCaCertificateRequest request, Extension constraintsExtension, BasicConstraints constraints,
+ Extension keyUsageExtension, KeyUsage keyUsage) {
+ return constraintsExtension != null && constraintsExtension.isCritical()
+ && constraints != null && constraints.isCA()
+ && BigInteger.valueOf(request.policy().pathLengthConstraint())
+ .equals(constraints.getPathLenConstraint())
+ && keyUsageExtension != null && keyUsageExtension.isCritical()
+ && hasIntermediateKeyUsage(keyUsage)
+ && holder.getExtensions().getExtensionOIDs().length == 2;
+ }
+
+ private static boolean matchesCaCredentialMetadata(Credential credential, X509CertificateHolder holder,
+ ValidatedCaCertificateRequest request, byte[] actualSpki) {
+ return credential.publicKeyId().equals(new PkiId("spki:" + sha256Hex(actualSpki)))
+ && credential.credentialId()
+ .equals(new PkiId("x509:" + sha256Hex(credential.encoded().bytes())))
+ && credential.serialOrUniqueId().equals(holder.getSerialNumber().toString())
+ && credential.validity().notBefore().getEpochSecond() == holder.getNotBefore().toInstant()
+ .getEpochSecond()
+ && credential.validity().notAfter().getEpochSecond() == holder.getNotAfter().toInstant()
+ .getEpochSecond()
+ && credential.validity().equals(request.validity())
+ && credential.serialOrUniqueId().equals(request.serial().toString())
+ && credential.attributes().ids().isEmpty();
+ }
+
+ private void requireCaBinding(Credential credential,
+ zeroecho.pki.api.profile.CertificateProfileRef expectedCaProfile, String action,
FormatId formatId, Optional objectId) {
try {
CredentialProfileBindings.requireCaBinding(
- credential == null ? null : credential.profileBinding(), expectedCaProfileId);
+ credential == null ? null : credential.profileBinding(), expectedCaProfile);
} catch (PkiException mismatch) {
throw proofGate.rejection(action, formatId, objectId, CredentialProfileBindings.MISMATCH_CODE);
}
@@ -868,16 +972,33 @@ public final class DefaultCaService implements CaService {
&& !keyUsage.hasUsages(KeyUsage.decipherOnly);
}
- private static AttributeSet authoritativeIntermediateAttributes(AttributeSet callerAttributes, CaRecord issuer,
- Credential issuerCredential, EncodedObject subjectSpki, SubjectRef subjectRef) {
- SimpleAttributeSet.Builder builder = SimpleAttributeSet.builder().putAll(callerAttributes);
- builder.put(BcX509Attributes.ISSUER_CERT_DER,
- new AttributeValue.BytesValue(issuerCredential.encoded().bytes().clone()));
- builder.put(BcX509Attributes.ISSUER_KEYREF, new AttributeValue.StringValue(issuer.issuerKeyRef().value()));
- builder.put(BcX509Attributes.SUBJECT_SPKI_DER,
- new AttributeValue.BytesValue(subjectSpki.bytes().clone()));
- builder.put(BcX509Attributes.SUBJECT_DN, new AttributeValue.StringValue(subjectRef.value()));
- return builder.build();
+ private static void requireNoCaOverrides(AttributeSet attributes) {
+ if (!attributes.ids().isEmpty()) {
+ throw new PkiException("CA certificate profile rejected: code=CA_REQUEST_ATTRIBUTE_UNSUPPORTED");
+ }
+ }
+
+ private void requireHistoricalCaProfile(Credential credential, CertificateProfileKind expectedKind) {
+ if (!(credential.profileBinding() instanceof CaProfileBinding binding)) {
+ throw new PkiException("CA issuer profile rejected: code=CREDENTIAL_PROFILE_BINDING_MISMATCH");
+ }
+ zeroecho.pki.api.profile.CertificateProfileRef reference = binding.reference();
+ zeroecho.pki.api.profile.ImportedCertificateProfileVersion version = profileService
+ .getImportedVersion(reference.profileId(), reference.profileVersion())
+ .orElseThrow(() -> new PkiException("CA issuer profile rejected: code=CA_PROFILE_VERSION_MISSING"));
+ if (!version.reference().equals(reference) || version.definition().certificateType() != expectedKind
+ || !version.definition().formatId().equals(credential.formatId())
+ || !credential.formatId().equals(framework.formatId())) {
+ throw new PkiException("CA issuer profile rejected: code=CA_PROFILE_REFERENCE_MISMATCH");
+ }
+ }
+
+ private void requireSameManagedKey(EncodedObject expected, EncodedObject actual, String action,
+ FormatId formatId, Optional objectId) {
+ if (expected.encoding() != actual.encoding()
+ || !MessageDigest.isEqual(expected.bytes(), actual.bytes())) {
+ throw proofGate.rejection(action, formatId, objectId, "MANAGED_KEY_CHANGED");
+ }
}
private static String sha256Hex(byte[] in) {
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/ManagedCaIssuance.java b/pki/src/main/java/zeroecho/pki/impl/core/ManagedCaIssuance.java
deleted file mode 100644
index a39f96b..0000000
--- a/pki/src/main/java/zeroecho/pki/impl/core/ManagedCaIssuance.java
+++ /dev/null
@@ -1,194 +0,0 @@
-/*******************************************************************************
- * Copyright (C) 2026, Leo Galambos
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * 3. All advertising materials mentioning features or use of this software must
- * display the following acknowledgement:
- * This product includes software developed by the Egothor project.
- *
- * 4. Neither the name of the copyright holder nor the names of its contributors
- * may be used to endorse or promote products derived from this software without
- * specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- ******************************************************************************/
-package zeroecho.pki.impl.core;
-
-import java.util.Objects;
-import java.util.Optional;
-
-import zeroecho.pki.api.EncodedObject;
-import zeroecho.pki.api.FormatId;
-import zeroecho.pki.api.KeyRef;
-import zeroecho.pki.api.PkiId;
-import zeroecho.pki.api.SubjectRef;
-import zeroecho.pki.api.Validity;
-import zeroecho.pki.api.attr.AttributeSet;
-
-/**
- * Immutable opaque authority for an intermediate CA issuance operation.
- *
- *
- * Instances are constructed only inside the core CA proof gate after a successful
- * managed-key possession challenge. The class has no public constructor or
- * factory, so callers outside the core implementation cannot convert raw
- * attributes or public-key bytes into issuance authority.
- *
- *
- *
- * All mutable values are defensively snapshotted. Accessors return defensive
- * copies where necessary, allowing a backend to consume the object concurrently
- * without observing caller mutation.
- *
- */
-@SuppressWarnings("PMD.DataClass")
-public final class ManagedCaIssuance {
-
- /**
- * Supported proof-bound CA issuance operations.
- */
- public enum Operation {
- /** Creates the initial credential for a newly defined intermediate CA. */
- CREATE_INTERMEDIATE,
- /** Issues an additional credential for an existing intermediate CA. */
- ISSUE_INTERMEDIATE
- }
-
- private final Operation operation;
- private final FormatId formatId;
- private final PkiId issuerCaId;
- private final PkiId subjectCaId;
- private final String profileId;
- private final Optional requestedValidity;
- private final AttributeSet attributes;
- private final SubjectRef subjectRef;
- private final KeyRef subjectKeyRef;
- private final EncodedObject exactPublicKey;
-
- /* default */ ManagedCaIssuance(CaProofGate.ManagedKeyProof proof, Operation operation, PkiId issuerCaId,
- PkiId subjectCaId, String profileId, Optional requestedValidity, AttributeSet attributes,
- SubjectRef subjectRef) {
- CaProofGate.ManagedKeyProof managedKeyProof = Objects.requireNonNull(proof, "proof");
- this.operation = Objects.requireNonNull(operation, "operation");
- this.formatId = managedKeyProof.formatId();
- this.issuerCaId = Objects.requireNonNull(issuerCaId, "issuerCaId");
- this.subjectCaId = Objects.requireNonNull(subjectCaId, "subjectCaId");
- this.profileId = Objects.requireNonNull(profileId, "profileId");
- this.requestedValidity = Objects.requireNonNull(requestedValidity, "requestedValidity");
- this.attributes = VerifiedIssuanceCandidate.snapshotAttributes(Objects.requireNonNull(attributes,
- "attributes"));
- this.subjectRef = Objects.requireNonNull(subjectRef, "subjectRef");
- this.subjectKeyRef = managedKeyProof.keyRef();
- EncodedObject publicKey = managedKeyProof.exactPublicKey();
- this.exactPublicKey = new EncodedObject(publicKey.encoding(), publicKey.bytes());
- }
-
- /**
- * Returns the authorized operation.
- *
- * @return authorized operation, never {@code null}
- */
- public Operation operation() {
- return operation;
- }
-
- /**
- * Returns the credential format.
- *
- * @return format identifier, never {@code null}
- */
- public FormatId formatId() {
- return formatId;
- }
-
- /**
- * Returns the issuing CA identifier.
- *
- * @return issuing CA identifier, never {@code null}
- */
- public PkiId issuerCaId() {
- return issuerCaId;
- }
-
- /**
- * Returns the subject CA identifier.
- *
- * @return subject CA identifier, never {@code null}
- */
- public PkiId subjectCaId() {
- return subjectCaId;
- }
-
- /**
- * Returns the validated profile identifier.
- *
- * @return profile identifier, never blank
- */
- public String profileId() {
- return profileId;
- }
-
- /**
- * Returns the optional validated validity request.
- *
- * @return optional validity, never {@code null}
- */
- public Optional requestedValidity() {
- return requestedValidity;
- }
-
- /**
- * Returns a defensive snapshot of authoritative CA attributes.
- *
- * @return immutable attribute snapshot, never {@code null}
- */
- public AttributeSet attributes() {
- return VerifiedIssuanceCandidate.snapshotAttributes(attributes);
- }
-
- /**
- * Returns the subject bound to the possession proof.
- *
- * @return exact subject, never {@code null}
- */
- public SubjectRef subjectRef() {
- return subjectRef;
- }
-
- /**
- * Returns the managed key reference that completed the challenge.
- *
- * @return proven managed key reference, never {@code null}
- */
- public KeyRef subjectKeyRef() {
- return subjectKeyRef;
- }
-
- /**
- * Returns a defensive copy of the exact public key bound to the proof.
- *
- * @return DER-encoded public key, never {@code null}
- */
- public EncodedObject exactPublicKey() {
- return new EncodedObject(exactPublicKey.encoding(), exactPublicKey.bytes());
- }
-}
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/ValidatedCaCertificateRequest.java b/pki/src/main/java/zeroecho/pki/impl/core/ValidatedCaCertificateRequest.java
new file mode 100644
index 0000000..c14a9a5
--- /dev/null
+++ b/pki/src/main/java/zeroecho/pki/impl/core/ValidatedCaCertificateRequest.java
@@ -0,0 +1,138 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Leo Galambos
+ * All rights reserved.
+ ******************************************************************************/
+package zeroecho.pki.impl.core;
+
+import java.math.BigInteger;
+import java.util.List;
+import java.util.Objects;
+
+import zeroecho.pki.api.EncodedObject;
+import zeroecho.pki.api.FormatId;
+import zeroecho.pki.api.PkiId;
+import zeroecho.pki.api.SubjectRef;
+import zeroecho.pki.api.Validity;
+import zeroecho.pki.api.profile.CaCertificatePolicy;
+import zeroecho.pki.api.profile.CertificateProfileKind;
+import zeroecho.pki.api.profile.CertificateProfileRef;
+import zeroecho.pki.api.request.SubjectRdn;
+
+/**
+ * Immutable gate-produced CA certificate request accepted by the issuer backend.
+ *
+ * The request contains only values approved against one exact active profile
+ * version. It carries no generic attributes, raw profile document, or caller
+ * selected extension material.
+ */
+@SuppressWarnings("PMD.DataClass")
+public final class ValidatedCaCertificateRequest {
+ /** Closed CA certificate operations authorized by this gate. */
+ public enum Operation {
+ /** Creates a self-signed root credential. */
+ CREATE_ROOT,
+ /** Imports an existing root credential after full profile validation. */
+ IMPORT_ROOT,
+ /** Creates an intermediate CA's initial credential. */
+ CREATE_INTERMEDIATE,
+ /** Issues an additional intermediate CA credential. */
+ ISSUE_INTERMEDIATE
+ }
+
+ private final Operation operation;
+ private final FormatId formatId;
+ private final PkiId issuerCaId;
+ private final PkiId subjectCaId;
+ private final CertificateProfileRef profileReference;
+ private final CertificateProfileKind certificateType;
+ private final SubjectRef subjectRef;
+ private final List subjectRdns;
+ private final EncodedObject exactPublicKey;
+ private final Validity validity;
+ private final BigInteger serial;
+ private final CaCertificatePolicy policy;
+
+ // The constructor is the single cohesive gate output boundary.
+ @SuppressWarnings("PMD.ExcessiveParameterList")
+ /* package */ ValidatedCaCertificateRequest(Operation operation, FormatId formatId,
+ PkiId issuerCaId, PkiId subjectCaId, CertificateProfileRef profileReference,
+ CertificateProfileKind certificateType, SubjectRef subjectRef, List subjectRdns,
+ EncodedObject exactPublicKey, Validity validity, BigInteger serial, CaCertificatePolicy policy) {
+ this.operation = Objects.requireNonNull(operation, "operation");
+ this.formatId = Objects.requireNonNull(formatId, "formatId");
+ this.issuerCaId = Objects.requireNonNull(issuerCaId, "issuerCaId");
+ this.subjectCaId = Objects.requireNonNull(subjectCaId, "subjectCaId");
+ this.profileReference = Objects.requireNonNull(profileReference, "profileReference");
+ this.certificateType = Objects.requireNonNull(certificateType, "certificateType");
+ this.subjectRef = Objects.requireNonNull(subjectRef, "subjectRef");
+ this.subjectRdns = List.copyOf(Objects.requireNonNull(subjectRdns, "subjectRdns"));
+ EncodedObject publicKey = Objects.requireNonNull(exactPublicKey, "exactPublicKey");
+ this.exactPublicKey = new EncodedObject(publicKey.encoding(), publicKey.bytes());
+ this.validity = Objects.requireNonNull(validity, "validity");
+ this.serial = Objects.requireNonNull(serial, "serial");
+ this.policy = Objects.requireNonNull(policy, "policy");
+ if (serial.signum() <= 0 || serial.toByteArray().length > 20) {
+ throw new IllegalArgumentException("serial must be positive and at most 20 bytes");
+ }
+ }
+
+ /** Returns the authorized operation. */
+ public Operation operation() {
+ return operation;
+ }
+
+ /** Returns the credential format. */
+ public FormatId formatId() {
+ return formatId;
+ }
+
+ /** Returns the issuing CA identifier. */
+ public PkiId issuerCaId() {
+ return issuerCaId;
+ }
+
+ /** Returns the subject CA identifier. */
+ public PkiId subjectCaId() {
+ return subjectCaId;
+ }
+
+ /** Returns the exact active profile reference. */
+ public CertificateProfileRef profileReference() {
+ return profileReference;
+ }
+
+ /** Returns the required certificate kind. */
+ public CertificateProfileKind certificateType() {
+ return certificateType;
+ }
+
+ /** Returns the canonical subject reference. */
+ public SubjectRef subjectRef() {
+ return subjectRef;
+ }
+
+ /** Returns the ordered canonical subject components. */
+ public List subjectRdns() {
+ return subjectRdns;
+ }
+
+ /** Returns a defensive copy of the proof-bound public key. */
+ public EncodedObject exactPublicKey() {
+ return new EncodedObject(exactPublicKey.encoding(), exactPublicKey.bytes());
+ }
+
+ /** Returns the approved exact validity. */
+ public Validity validity() {
+ return validity;
+ }
+
+ /** Returns the issuer-controlled serial. */
+ public BigInteger serial() {
+ return serial;
+ }
+
+ /** Returns the exact profile-derived CA policy. */
+ public CaCertificatePolicy policy() {
+ return policy;
+ }
+}
diff --git a/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509CredentialIssuerBackend.java b/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509CredentialIssuerBackend.java
index bbe4c45..4cdfe6a 100644
--- a/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509CredentialIssuerBackend.java
+++ b/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509CredentialIssuerBackend.java
@@ -36,10 +36,8 @@ package zeroecho.pki.impl.framework.x509.bc;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.time.Duration;
-import java.time.Instant;
import java.util.Date;
import java.util.HexFormat;
-import java.util.Optional;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.DEROctetString;
@@ -64,9 +62,6 @@ import zeroecho.pki.api.PkiException;
import zeroecho.pki.api.PkiId;
import zeroecho.pki.api.SubjectRef;
import zeroecho.pki.api.Validity;
-import zeroecho.pki.api.attr.AttributeId;
-import zeroecho.pki.api.attr.AttributeSet;
-import zeroecho.pki.api.attr.AttributeValue;
import zeroecho.pki.api.credential.Credential;
import zeroecho.pki.api.credential.CaProfileBinding;
import zeroecho.pki.api.credential.EndEntityProfileBinding;
@@ -74,7 +69,7 @@ import zeroecho.pki.api.credential.CredentialBundle;
import zeroecho.pki.api.credential.CredentialStatus;
import zeroecho.pki.api.profile.LeafKeyUsage;
import zeroecho.pki.api.request.SubjectAlternativeName;
-import zeroecho.pki.impl.core.ManagedCaIssuance;
+import zeroecho.pki.impl.core.ValidatedCaCertificateRequest;
import zeroecho.pki.impl.core.ValidatedCertificateRequest;
import zeroecho.pki.impl.core.async.PkiSigningBus;
import zeroecho.pki.impl.core.attr.SimpleAttributeSet;
@@ -94,9 +89,9 @@ import zeroecho.pki.spi.framework.CredentialIssuerBackend;
*
* End-entity issuance derives all requester-influenced certificate material
* exclusively from a profile-gated {@link ValidatedCertificateRequest}.
- * Intermediate CA issuance retains its proof-gated managed-CA input and
- * framework attributes because it operates on an existing CA subject entity
- * rather than the end-entity CSR flow.
+ * CA issuance derives certificate content exclusively from a proof-bound,
+ * active-profile-validated request and separately supplied trusted issuer
+ * material.
*
*
* Signing model
@@ -188,7 +183,7 @@ public final class BcX509CredentialIssuerBackend implements CredentialIssuerBack
byte[] issuerDer = issuerCertificate.bytes();
X509CertificateHolder issuer;
try {
- issuer = IssuanceContext.parseIssuerCertificateOrThrow(issuerDer);
+ issuer = parseIssuerCertificateOrThrow(issuerDer);
} finally {
java.util.Arrays.fill(issuerDer, (byte) 0);
}
@@ -288,16 +283,19 @@ public final class BcX509CredentialIssuerBackend implements CredentialIssuerBack
* Issues an intermediate CA X.509 certificate.
*
*
- * The method derives issuer and subject wiring from framework attributes,
- * constructs an intermediate CA certificate with CA-oriented extensions,
- * delegates signing through {@link PkiSigningBus}, and returns the resulting
- * credential.
+ * The method consumes only the gate-produced validated CA request and trusted
+ * issuer inputs, constructs an intermediate CA certificate with CA-oriented
+ * extensions, delegates signing through {@link PkiSigningBus}, and returns the
+ * resulting credential.
*
*
- * @param issuance gate-produced managed CA issuance authority; must not be
- * {@code null}
+ * @param request gate-produced validated CA certificate request;
+ * must not be {@code null}
+ * @param issuerCertificate trusted issuer certificate; must not be {@code null}
+ * @param issuerKeyRef trusted issuer signing-key reference; must not be
+ * {@code null}
* @return issued intermediate CA credential
- * @throws IllegalArgumentException if {@code command} is {@code null} or uses
+ * @throws IllegalArgumentException if {@code request} is {@code null} or uses
* an unsupported format identifier
* @throws PkiException if issuer wiring or subject wiring is
* missing or invalid, certificate construction
@@ -305,17 +303,23 @@ public final class BcX509CredentialIssuerBackend implements CredentialIssuerBack
* encoding fails
*/
@Override
- public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) {
- if (issuance == null) {
- throw new IllegalArgumentException("issuance must not be null");
+ public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest request,
+ EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
+ if (request == null || issuerCertificate == null || issuerKeyRef == null) {
+ throw new IllegalArgumentException("validated CA issuance inputs must not be null");
}
- if (!BcX509CredentialFramework.FORMAT_ID.equals(issuance.formatId())) {
+ if (!BcX509CredentialFramework.FORMAT_ID.equals(request.formatId())) {
throw new IllegalArgumentException("Unsupported formatId");
}
- IssuanceContext ctx = IssuanceContext.from(issuance.attributes());
- X509CertificateHolder issuer = ctx.issuerCertHolder;
- byte[] subjectSpki = issuance.exactPublicKey().bytes();
+ byte[] issuerDer = issuerCertificate.bytes();
+ X509CertificateHolder issuer;
+ try {
+ issuer = parseIssuerCertificateOrThrow(issuerDer);
+ } finally {
+ java.util.Arrays.fill(issuerDer, (byte) 0);
+ }
+ byte[] subjectSpki = request.exactPublicKey().bytes();
SubjectPublicKeyInfo spki;
PkiId publicKeyId;
try {
@@ -324,24 +328,24 @@ public final class BcX509CredentialIssuerBackend implements CredentialIssuerBack
} finally {
java.util.Arrays.fill(subjectSpki, (byte) 0);
}
- SubjectRef subjectRef = issuance.subjectRef();
-
- Instant now = Instant.now();
- Validity validity = issuance.requestedValidity().orElse(new Validity(now, now.plus(Duration.ofDays(3650))));
- BigInteger serial = ctx.serial.orElse(BigInteger.valueOf(Math.abs(System.nanoTime())));
+ SubjectRef subjectRef = request.subjectRef();
+ Validity validity = request.validity();
+ BigInteger serial = request.serial();
X500Name issuerDn = issuer.getSubject();
- X500Name subjectDn = new X500Name(subjectRef.value());
+ X500Name subjectDn = BcX509ProfileSupport.subject(request.subjectRdns());
X509v3CertificateBuilder builder = new X509v3CertificateBuilder(issuerDn, serial,
Date.from(validity.notBefore()), Date.from(validity.notAfter()), subjectDn, spki);
try {
- builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(0));
- builder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign));
+ builder.addExtension(Extension.basicConstraints, request.policy().basicConstraintsCritical(),
+ new BasicConstraints(request.policy().pathLengthConstraint()));
+ builder.addExtension(Extension.keyUsage, request.policy().keyUsageCritical(),
+ new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign));
} catch (Exception ex) {
throw new PkiException("X.509 extension construction failed: code=EXTENSION_BUILD_FAILED");
}
- ContentSigner signer = new PkiBusContentSigner(signingBus, ctx.issuerKeyRef, signatureAlgorithmId, signingTtl);
+ ContentSigner signer = new PkiBusContentSigner(signingBus, issuerKeyRef, signatureAlgorithmId, signingTtl);
X509CertificateHolder certificate;
try {
certificate = builder.build(signer);
@@ -359,170 +363,20 @@ public final class BcX509CredentialIssuerBackend implements CredentialIssuerBack
PkiId credId = new PkiId("x509:" + sha256Hex(certDer));
try {
- return new Credential(credId, issuance.formatId(), new IssuerRef(issuance.issuerCaId()), subjectRef,
- validity, serial.toString(), publicKeyId, new CaProfileBinding(issuance.profileId()),
+ return new Credential(credId, request.formatId(), new IssuerRef(request.issuerCaId()), subjectRef,
+ validity, serial.toString(), publicKeyId, new CaProfileBinding(request.profileReference()),
CredentialStatus.ISSUED,
- new EncodedObject(Encoding.DER, certDer), issuance.attributes());
+ new EncodedObject(Encoding.DER, certDer), SimpleAttributeSet.builder().build());
} finally {
java.util.Arrays.fill(certDer, (byte) 0);
}
}
- /**
- * Immutable issuance context derived from framework-specific adapter
- * attributes.
- *
- *
- * This helper encapsulates the issuer-side material and optional subject-side
- * overrides required by the X.509 issuance backend. It keeps the main issuance
- * methods focused on certificate construction while centralizing attribute
- * validation and conversion.
- *
- */
- private static final class IssuanceContext {
-
- private final X509CertificateHolder issuerCertHolder;
- private final KeyRef issuerKeyRef;
- private final Optional serial;
-
- /**
- * Creates the issuance context.
- *
- * @param issuerCertHolder parsed issuer certificate holder; must not be
- * {@code null}
- * @param issuerKeyRef issuer signing key reference; must not be
- * {@code null}
- * @param serial optional serial override; must not be {@code null}
- */
- private IssuanceContext(X509CertificateHolder issuerCertHolder, KeyRef issuerKeyRef,
- Optional serial) {
- this.issuerCertHolder = issuerCertHolder;
- this.issuerKeyRef = issuerKeyRef;
- this.serial = serial;
- }
-
- /**
- * Builds an issuance context from framework-specific attributes.
- *
- *
- * Required attributes:
- *
- *
- * - {@link BcX509Attributes#ISSUER_CERT_DER} as
- * {@link AttributeValue.BytesValue}
- * - {@link BcX509Attributes#ISSUER_KEYREF} as
- * {@link AttributeValue.StringValue}
- *
- *
- *
- * Optional attributes:
- *
- *
- * - {@link BcX509Attributes#SERIAL} as
- * {@link AttributeValue.IntegerValue}
- *
- *
- * @param attrs source attribute set; must not be {@code null}
- * @return validated issuance context
- * @throws IllegalArgumentException if {@code attrs} is {@code null}
- * @throws PkiException if required attributes are missing,
- * attributes use an unexpected value type, or
- * the issuer certificate DER cannot be parsed
- */
- private static IssuanceContext from(AttributeSet attrs) {
- if (attrs == null) {
- throw new IllegalArgumentException("attrs must not be null");
- }
-
- byte[] issuerCertDer = requiredBytes(attrs, BcX509Attributes.ISSUER_CERT_DER,
- "Missing issuer certificate DER attribute", "Issuer certificate DER must be BytesValue");
- KeyRef issuerKeyRef = new KeyRef(requiredString(attrs, BcX509Attributes.ISSUER_KEYREF,
- "Missing issuer keyref attribute", "Issuer keyref must be StringValue"));
-
- Optional serial = optionalInteger(attrs, BcX509Attributes.SERIAL, "Serial must be IntegerValue")
- .map(BigInteger::valueOf);
-
- X509CertificateHolder issuerHolder = parseIssuerCertificateOrThrow(issuerCertDer);
- return new IssuanceContext(issuerHolder, issuerKeyRef, serial);
- }
-
- /**
- * Resolves a required bytes-valued attribute.
- *
- * @param attrs source attributes; must not be {@code null}
- * @param id attribute identifier; must not be {@code null}
- * @param missingMessage exception message used when the attribute is absent
- * @param typeMessage exception message used when the attribute has an
- * unexpected type
- * @return bytes stored in the attribute
- * @throws PkiException if the attribute is missing or has an unexpected value
- * type
- */
- private static byte[] requiredBytes(AttributeSet attrs, AttributeId id, String missingMessage,
- String typeMessage) {
- AttributeValue value = attrs.get(id).orElseThrow(() -> new PkiException(missingMessage));
- if (!(value instanceof AttributeValue.BytesValue)) {
- throw new PkiException(typeMessage);
- }
- return ((AttributeValue.BytesValue) value).value();
- }
-
- /**
- * Resolves a required string-valued attribute.
- *
- * @param attrs source attributes; must not be {@code null}
- * @param id attribute identifier; must not be {@code null}
- * @param missingMessage exception message used when the attribute is absent
- * @param typeMessage exception message used when the attribute has an
- * unexpected type
- * @return string stored in the attribute
- * @throws PkiException if the attribute is missing or has an unexpected value
- * type
- */
- private static String requiredString(AttributeSet attrs, AttributeId id, String missingMessage,
- String typeMessage) {
- AttributeValue value = attrs.get(id).orElseThrow(() -> new PkiException(missingMessage));
- if (!(value instanceof AttributeValue.StringValue)) {
- throw new PkiException(typeMessage);
- }
- return ((AttributeValue.StringValue) value).value();
- }
-
- /**
- * Resolves an optional integer-valued attribute.
- *
- * @param attrs source attributes; must not be {@code null}
- * @param id attribute identifier; must not be {@code null}
- * @param typeMessage exception message used when the attribute has an
- * unexpected type
- * @return optional integer value
- * @throws PkiException if the attribute is present but has an unexpected value
- * type
- */
- private static Optional optionalInteger(AttributeSet attrs, AttributeId id, String typeMessage) {
- Optional value = attrs.get(id);
- if (value.isEmpty()) {
- return Optional.empty();
- }
- if (!(value.get() instanceof AttributeValue.IntegerValue)) {
- throw new PkiException(typeMessage);
- }
- return Optional.of(((AttributeValue.IntegerValue) value.get()).value());
- }
-
- /**
- * Parses the issuer certificate DER into an X.509 certificate holder.
- *
- * @param issuerCertDer DER-encoded issuer certificate; must not be {@code null}
- * @return parsed issuer certificate holder
- * @throws PkiException if the issuer certificate DER is invalid
- */
- private static X509CertificateHolder parseIssuerCertificateOrThrow(byte[] issuerCertDer) {
- try {
- return new X509CertificateHolder(issuerCertDer);
- } catch (Exception ex) {
- throw new PkiException("Invalid issuer certificate: code=ISSUER_CERTIFICATE_INVALID");
- }
+ private static X509CertificateHolder parseIssuerCertificateOrThrow(byte[] issuerCertDer) {
+ try {
+ return new X509CertificateHolder(issuerCertDer);
+ } catch (Exception ex) {
+ throw new PkiException("Invalid issuer certificate: code=ISSUER_CERTIFICATE_INVALID");
}
}
diff --git a/pki/src/main/java/zeroecho/pki/impl/fs/FilesystemPkiStore.java b/pki/src/main/java/zeroecho/pki/impl/fs/FilesystemPkiStore.java
index 68271a1..ff4bdfc 100644
--- a/pki/src/main/java/zeroecho/pki/impl/fs/FilesystemPkiStore.java
+++ b/pki/src/main/java/zeroecho/pki/impl/fs/FilesystemPkiStore.java
@@ -466,6 +466,7 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
}
return existing;
}
+ requireConsistentProfileKind(version);
try {
FsOperations.writeNewAtomicStrict(target, FsCodec.encode(FsCodec.PROFILE_VERSION, version));
return version;
@@ -487,6 +488,16 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
}
}
+ private void requireConsistentProfileKind(ImportedCertificateProfileVersion candidate) {
+ for (ImportedCertificateProfileVersion existing :
+ listProfileVersions(candidate.reference().profileId())) {
+ if (existing.definition().certificateType()
+ != candidate.definition().certificateType()) {
+ throw new ProfileLifecycleFailure(Code.PROFILE_KIND_CONFLICT);
+ }
+ }
+ }
+
@Override
public Optional getProfileVersion(final String profileId,
final long profileVersion) {
diff --git a/pki/src/main/java/zeroecho/pki/impl/fs/FsCodec.java b/pki/src/main/java/zeroecho/pki/impl/fs/FsCodec.java
index c50fdde..1657d9b 100644
--- a/pki/src/main/java/zeroecho/pki/impl/fs/FsCodec.java
+++ b/pki/src/main/java/zeroecho/pki/impl/fs/FsCodec.java
@@ -688,7 +688,7 @@ final class FsCodec {
}
case CaProfileBinding ca -> {
writer.writeUnsignedByte(2);
- writer.writeValue(STRING, ca.profileId());
+ writer.writeValue(PROFILE_REF, ca.reference());
}
}
}
@@ -696,7 +696,7 @@ final class FsCodec {
private static CredentialProfileBinding readProfileBinding(Reader reader) throws IOException {
return switch (reader.readUnsignedByte()) {
case 1 -> new EndEntityProfileBinding(reader.readValue(PROFILE_REF));
- case 2 -> new CaProfileBinding(reader.readValue(STRING));
+ case 2 -> new CaProfileBinding(reader.readValue(PROFILE_REF));
default -> throw new IOException("unknown credential profile binding");
};
}
diff --git a/pki/src/main/java/zeroecho/pki/spi/framework/CredentialIssuerBackend.java b/pki/src/main/java/zeroecho/pki/spi/framework/CredentialIssuerBackend.java
index b51097f..59d0129 100644
--- a/pki/src/main/java/zeroecho/pki/spi/framework/CredentialIssuerBackend.java
+++ b/pki/src/main/java/zeroecho/pki/spi/framework/CredentialIssuerBackend.java
@@ -39,7 +39,7 @@ import zeroecho.pki.api.EncodedObject;
import zeroecho.pki.api.KeyRef;
import zeroecho.pki.api.credential.Credential;
import zeroecho.pki.api.credential.CredentialBundle;
-import zeroecho.pki.impl.core.ManagedCaIssuance;
+import zeroecho.pki.impl.core.ValidatedCaCertificateRequest;
import zeroecho.pki.impl.core.ValidatedCertificateRequest;
/**
@@ -153,13 +153,14 @@ public interface CredentialIssuerBackend {
*
*
*
- * The supplied {@link ManagedCaIssuance} can be constructed only after the core
- * CA proof gate has completed a managed-key possession challenge and bound the
- * exact public key, subject, operation, and authoritative attributes.
+ * The supplied {@link ValidatedCaCertificateRequest} can be constructed only
+ * after the core CA proof and active-profile gates have bound the exact public
+ * key, subject, profile version, policy, validity, and serial.
*
*
- * @param issuance gate-produced managed CA issuance authority; must not be
- * {@code null}
+ * @param request gate-produced validated CA request; must not be {@code null}
+ * @param issuerCertificate trusted encoded issuer certificate
+ * @param issuerKeyRef trusted issuer signing-key reference
* @return issued CA credential, never {@code null}
* @throws IllegalArgumentException if {@code command} is {@code null} or
* structurally invalid for the concrete
@@ -168,5 +169,6 @@ public interface CredentialIssuerBackend {
* or other framework-specific issuance
* processing fails
*/
- Credential issueIntermediateCertificate(ManagedCaIssuance issuance);
+ Credential issueIntermediateCertificate(ValidatedCaCertificateRequest request,
+ EncodedObject issuerCertificate, KeyRef issuerKeyRef);
}
diff --git a/pki/src/main/java/zeroecho/pki/spi/store/PkiStore.java b/pki/src/main/java/zeroecho/pki/spi/store/PkiStore.java
index e94d178..7e4681c 100644
--- a/pki/src/main/java/zeroecho/pki/spi/store/PkiStore.java
+++ b/pki/src/main/java/zeroecho/pki/spi/store/PkiStore.java
@@ -245,7 +245,24 @@ public interface PkiStore extends SignWorkflowStore {
*/
List listPublicationRecords();
- /** Atomically imports one immutable validated profile version. */
+ /**
+ * Atomically imports one immutable validated profile version.
+ *
+ *
+ * Every version stored under one logical profile ID must have the same
+ * certificate kind. An implementation must reject an import whose kind
+ * differs from any already imported version for that ID. For an existing
+ * identical ID and version, exact canonical-reference equality is
+ * idempotent and returns the committed version; a different reference for
+ * that same version is a version conflict. This same-version decision takes
+ * precedence over the cross-version kind check.
+ *
+ *
+ * @param version validated immutable version to import
+ * @return the newly committed version or the identical existing version
+ * @throws RuntimeException if validation, kind consistency, conflict
+ * handling, or durable persistence fails
+ */
ImportedCertificateProfileVersion importProfileVersion(ImportedCertificateProfileVersion version);
/** Retrieves one imported version. */
diff --git a/pki/src/main/resources/zeroecho/pki/profiles/v1/catalog.json b/pki/src/main/resources/zeroecho/pki/profiles/v1/catalog.json
index fa21c67..d723fd1 100644
--- a/pki/src/main/resources/zeroecho/pki/profiles/v1/catalog.json
+++ b/pki/src/main/resources/zeroecho/pki/profiles/v1/catalog.json
@@ -1 +1 @@
-{"schemaVersion":1,"profiles":[{"resource":"zeroecho/pki/profiles/v1/server-tls.json"},{"resource":"zeroecho/pki/profiles/v1/vpn-server.json"},{"resource":"zeroecho/pki/profiles/v1/vpn-client.json"},{"resource":"zeroecho/pki/profiles/v1/email-signing.json"}]}
\ No newline at end of file
+{"schemaVersion":1,"profiles":[{"resource":"zeroecho/pki/profiles/v1/server-tls.json"},{"resource":"zeroecho/pki/profiles/v1/vpn-server.json"},{"resource":"zeroecho/pki/profiles/v1/vpn-client.json"},{"resource":"zeroecho/pki/profiles/v1/email-signing.json"},{"resource":"zeroecho/pki/profiles/v1/root-ca.json"},{"resource":"zeroecho/pki/profiles/v1/intermediate-ca.json"}]}
\ No newline at end of file
diff --git a/pki/src/main/resources/zeroecho/pki/profiles/v1/email-signing.json b/pki/src/main/resources/zeroecho/pki/profiles/v1/email-signing.json
index 89c9146..d14a2e8 100644
--- a/pki/src/main/resources/zeroecho/pki/profiles/v1/email-signing.json
+++ b/pki/src/main/resources/zeroecho/pki/profiles/v1/email-signing.json
@@ -1 +1 @@
-{"schemaVersion":1,"profileId":"email-signing","profileVersion":1,"formatId":"x509","displayName":"Email Signing","maxValidity":"PT8760H","subject":{"allowEmpty":true,"rules":[{"oid":"2.5.4.3","source":"REQUESTER","minimumOccurrences":0,"maximumOccurrences":1,"maximumUtf8Bytes":253}]},"subjectAlternativeNames":{"minimumTotal":1,"maximumTotal":16,"serviceIdentityRequired":false,"emailIdentityRequired":true,"criticalWhenSubjectNonEmpty":false,"rules":[{"type":"RFC822_NAME","minimumOccurrences":1,"maximumOccurrences":16}]},"leafCertificate":{"basicConstraintsCritical":true,"keyUsageCritical":true,"keyUsage":["DIGITAL_SIGNATURE"],"extendedKeyUsageCritical":false,"extendedKeyUsage":["1.3.6.1.5.5.7.3.4"],"allowedKeyAlgorithms":["ECDSA","Ed25519","RSA"]}}
\ No newline at end of file
+{"schemaVersion":2,"certificateType":"END_ENTITY","profileId":"email-signing","profileVersion":1,"formatId":"x509","displayName":"Email Signing","maxValidity":"PT8760H","subject":{"allowEmpty":true,"rules":[{"oid":"2.5.4.3","source":"REQUESTER","minimumOccurrences":0,"maximumOccurrences":1,"maximumUtf8Bytes":253}]},"subjectAlternativeNames":{"minimumTotal":1,"maximumTotal":16,"serviceIdentityRequired":false,"emailIdentityRequired":true,"criticalWhenSubjectNonEmpty":false,"rules":[{"type":"RFC822_NAME","minimumOccurrences":1,"maximumOccurrences":16}]},"leafCertificate":{"basicConstraintsCritical":true,"keyUsageCritical":true,"keyUsage":["DIGITAL_SIGNATURE"],"extendedKeyUsageCritical":false,"extendedKeyUsage":["1.3.6.1.5.5.7.3.4"],"allowedKeyAlgorithms":["ECDSA","Ed25519","RSA"]}}
\ No newline at end of file
diff --git a/pki/src/main/resources/zeroecho/pki/profiles/v1/intermediate-ca.json b/pki/src/main/resources/zeroecho/pki/profiles/v1/intermediate-ca.json
new file mode 100644
index 0000000..18110e6
--- /dev/null
+++ b/pki/src/main/resources/zeroecho/pki/profiles/v1/intermediate-ca.json
@@ -0,0 +1 @@
+{"schemaVersion":2,"certificateType":"INTERMEDIATE_CA","profileId":"intermediate-ca","profileVersion":1,"formatId":"x509","displayName":"Intermediate CA","maxValidity":"PT43800H","subject":{"allowEmpty":false,"rules":[{"oid":"2.5.4.3","source":"REQUESTER","minimumOccurrences":1,"maximumOccurrences":1,"maximumUtf8Bytes":253}]},"caCertificate":{"basicConstraintsCritical":true,"pathLengthConstraint":0,"keyUsageCritical":true,"keyUsages":["CRL_SIGN","KEY_CERT_SIGN"],"allowedSubjectKeyAlgorithms":["ECDSA","Ed25519","RSA"]}}
\ No newline at end of file
diff --git a/pki/src/main/resources/zeroecho/pki/profiles/v1/root-ca.json b/pki/src/main/resources/zeroecho/pki/profiles/v1/root-ca.json
new file mode 100644
index 0000000..388a908
--- /dev/null
+++ b/pki/src/main/resources/zeroecho/pki/profiles/v1/root-ca.json
@@ -0,0 +1 @@
+{"schemaVersion":2,"certificateType":"ROOT_CA","profileId":"root-ca","profileVersion":1,"formatId":"x509","displayName":"Root CA","maxValidity":"PT87600H","subject":{"allowEmpty":false,"rules":[{"oid":"2.5.4.3","source":"REQUESTER","minimumOccurrences":1,"maximumOccurrences":1,"maximumUtf8Bytes":253}]},"caCertificate":{"basicConstraintsCritical":true,"pathLengthConstraint":1,"keyUsageCritical":true,"keyUsages":["CRL_SIGN","KEY_CERT_SIGN"],"allowedSubjectKeyAlgorithms":["ECDSA","Ed25519","RSA"]}}
\ No newline at end of file
diff --git a/pki/src/main/resources/zeroecho/pki/profiles/v1/server-tls.json b/pki/src/main/resources/zeroecho/pki/profiles/v1/server-tls.json
index b147661..0bfb195 100644
--- a/pki/src/main/resources/zeroecho/pki/profiles/v1/server-tls.json
+++ b/pki/src/main/resources/zeroecho/pki/profiles/v1/server-tls.json
@@ -1 +1 @@
-{"schemaVersion":1,"profileId":"server-tls","profileVersion":1,"formatId":"x509","displayName":"Server TLS","maxValidity":"PT8760H","subject":{"allowEmpty":true,"rules":[{"oid":"2.5.4.3","source":"REQUESTER","minimumOccurrences":0,"maximumOccurrences":1,"maximumUtf8Bytes":253}]},"subjectAlternativeNames":{"minimumTotal":1,"maximumTotal":64,"serviceIdentityRequired":true,"emailIdentityRequired":false,"criticalWhenSubjectNonEmpty":false,"rules":[{"type":"DNS_NAME","minimumOccurrences":0,"maximumOccurrences":64,"wildcardAllowed":false},{"type":"IP_ADDRESS","minimumOccurrences":0,"maximumOccurrences":16,"ipv4Allowed":true,"ipv6Allowed":true}]},"leafCertificate":{"basicConstraintsCritical":true,"keyUsageCritical":true,"keyUsage":["DIGITAL_SIGNATURE"],"extendedKeyUsageCritical":false,"extendedKeyUsage":["1.3.6.1.5.5.7.3.1"],"allowedKeyAlgorithms":["ECDSA","Ed25519","RSA"]}}
\ No newline at end of file
+{"schemaVersion":2,"certificateType":"END_ENTITY","profileId":"server-tls","profileVersion":1,"formatId":"x509","displayName":"Server TLS","maxValidity":"PT8760H","subject":{"allowEmpty":true,"rules":[{"oid":"2.5.4.3","source":"REQUESTER","minimumOccurrences":0,"maximumOccurrences":1,"maximumUtf8Bytes":253}]},"subjectAlternativeNames":{"minimumTotal":1,"maximumTotal":64,"serviceIdentityRequired":true,"emailIdentityRequired":false,"criticalWhenSubjectNonEmpty":false,"rules":[{"type":"DNS_NAME","minimumOccurrences":0,"maximumOccurrences":64,"wildcardAllowed":false},{"type":"IP_ADDRESS","minimumOccurrences":0,"maximumOccurrences":16,"ipv4Allowed":true,"ipv6Allowed":true}]},"leafCertificate":{"basicConstraintsCritical":true,"keyUsageCritical":true,"keyUsage":["DIGITAL_SIGNATURE"],"extendedKeyUsageCritical":false,"extendedKeyUsage":["1.3.6.1.5.5.7.3.1"],"allowedKeyAlgorithms":["ECDSA","Ed25519","RSA"]}}
\ No newline at end of file
diff --git a/pki/src/main/resources/zeroecho/pki/profiles/v1/vpn-client.json b/pki/src/main/resources/zeroecho/pki/profiles/v1/vpn-client.json
index e831916..aded624 100644
--- a/pki/src/main/resources/zeroecho/pki/profiles/v1/vpn-client.json
+++ b/pki/src/main/resources/zeroecho/pki/profiles/v1/vpn-client.json
@@ -1 +1 @@
-{"schemaVersion":1,"profileId":"vpn-client","profileVersion":1,"formatId":"x509","displayName":"VPN Client","maxValidity":"PT8760H","subject":{"allowEmpty":true,"rules":[{"oid":"2.5.4.3","source":"REQUESTER","minimumOccurrences":0,"maximumOccurrences":1,"maximumUtf8Bytes":253}]},"subjectAlternativeNames":{"minimumTotal":1,"maximumTotal":16,"serviceIdentityRequired":false,"emailIdentityRequired":false,"criticalWhenSubjectNonEmpty":false,"rules":[{"type":"RFC822_NAME","minimumOccurrences":0,"maximumOccurrences":16},{"type":"URI","minimumOccurrences":0,"maximumOccurrences":16,"allowedSchemes":["spiffe"]}]},"leafCertificate":{"basicConstraintsCritical":true,"keyUsageCritical":true,"keyUsage":["DIGITAL_SIGNATURE"],"extendedKeyUsageCritical":false,"extendedKeyUsage":["1.3.6.1.5.5.7.3.2"],"allowedKeyAlgorithms":["ECDSA","Ed25519","RSA"]}}
\ No newline at end of file
+{"schemaVersion":2,"certificateType":"END_ENTITY","profileId":"vpn-client","profileVersion":1,"formatId":"x509","displayName":"VPN Client","maxValidity":"PT8760H","subject":{"allowEmpty":true,"rules":[{"oid":"2.5.4.3","source":"REQUESTER","minimumOccurrences":0,"maximumOccurrences":1,"maximumUtf8Bytes":253}]},"subjectAlternativeNames":{"minimumTotal":1,"maximumTotal":16,"serviceIdentityRequired":false,"emailIdentityRequired":false,"criticalWhenSubjectNonEmpty":false,"rules":[{"type":"RFC822_NAME","minimumOccurrences":0,"maximumOccurrences":16},{"type":"URI","minimumOccurrences":0,"maximumOccurrences":16,"allowedSchemes":["spiffe"]}]},"leafCertificate":{"basicConstraintsCritical":true,"keyUsageCritical":true,"keyUsage":["DIGITAL_SIGNATURE"],"extendedKeyUsageCritical":false,"extendedKeyUsage":["1.3.6.1.5.5.7.3.2"],"allowedKeyAlgorithms":["ECDSA","Ed25519","RSA"]}}
\ No newline at end of file
diff --git a/pki/src/main/resources/zeroecho/pki/profiles/v1/vpn-server.json b/pki/src/main/resources/zeroecho/pki/profiles/v1/vpn-server.json
index 350d43f..1c9a714 100644
--- a/pki/src/main/resources/zeroecho/pki/profiles/v1/vpn-server.json
+++ b/pki/src/main/resources/zeroecho/pki/profiles/v1/vpn-server.json
@@ -1 +1 @@
-{"schemaVersion":1,"profileId":"vpn-server","profileVersion":1,"formatId":"x509","displayName":"VPN Server","maxValidity":"PT8760H","subject":{"allowEmpty":true,"rules":[{"oid":"2.5.4.3","source":"REQUESTER","minimumOccurrences":0,"maximumOccurrences":1,"maximumUtf8Bytes":253}]},"subjectAlternativeNames":{"minimumTotal":1,"maximumTotal":64,"serviceIdentityRequired":true,"emailIdentityRequired":false,"criticalWhenSubjectNonEmpty":false,"rules":[{"type":"DNS_NAME","minimumOccurrences":0,"maximumOccurrences":64,"wildcardAllowed":false},{"type":"IP_ADDRESS","minimumOccurrences":0,"maximumOccurrences":16,"ipv4Allowed":true,"ipv6Allowed":true}]},"leafCertificate":{"basicConstraintsCritical":true,"keyUsageCritical":true,"keyUsage":["DIGITAL_SIGNATURE"],"extendedKeyUsageCritical":false,"extendedKeyUsage":["1.3.6.1.5.5.7.3.1"],"allowedKeyAlgorithms":["ECDSA","Ed25519","RSA"]}}
\ No newline at end of file
+{"schemaVersion":2,"certificateType":"END_ENTITY","profileId":"vpn-server","profileVersion":1,"formatId":"x509","displayName":"VPN Server","maxValidity":"PT8760H","subject":{"allowEmpty":true,"rules":[{"oid":"2.5.4.3","source":"REQUESTER","minimumOccurrences":0,"maximumOccurrences":1,"maximumUtf8Bytes":253}]},"subjectAlternativeNames":{"minimumTotal":1,"maximumTotal":64,"serviceIdentityRequired":true,"emailIdentityRequired":false,"criticalWhenSubjectNonEmpty":false,"rules":[{"type":"DNS_NAME","minimumOccurrences":0,"maximumOccurrences":64,"wildcardAllowed":false},{"type":"IP_ADDRESS","minimumOccurrences":0,"maximumOccurrences":16,"ipv4Allowed":true,"ipv6Allowed":true}]},"leafCertificate":{"basicConstraintsCritical":true,"keyUsageCritical":true,"keyUsage":["DIGITAL_SIGNATURE"],"extendedKeyUsageCritical":false,"extendedKeyUsage":["1.3.6.1.5.5.7.3.1"],"allowedKeyAlgorithms":["ECDSA","Ed25519","RSA"]}}
\ No newline at end of file
diff --git a/pki/src/test/java/zeroecho/pki/api/profile/BuiltInCertificateProfileCatalogTest.java b/pki/src/test/java/zeroecho/pki/api/profile/BuiltInCertificateProfileCatalogTest.java
index c82c1b7..3b65f64 100644
--- a/pki/src/test/java/zeroecho/pki/api/profile/BuiltInCertificateProfileCatalogTest.java
+++ b/pki/src/test/java/zeroecho/pki/api/profile/BuiltInCertificateProfileCatalogTest.java
@@ -42,10 +42,13 @@ final class BuiltInCertificateProfileCatalogTest {
private static final String VPN_SERVER = ROOT + "vpn-server.json";
private static final String VPN_CLIENT = ROOT + "vpn-client.json";
private static final String EMAIL = ROOT + "email-signing.json";
+ private static final String ROOT_CA = ROOT + "root-ca.json";
+ private static final String INTERMEDIATE_CA = ROOT + "intermediate-ca.json";
private static final List PROFILE_RESOURCES =
- List.of(SERVER, VPN_SERVER, VPN_CLIENT, EMAIL);
+ List.of(SERVER, VPN_SERVER, VPN_CLIENT, EMAIL, ROOT_CA, INTERMEDIATE_CA);
private static final List EXPECTED_ORDER =
- List.of("server-tls", "vpn-server", "vpn-client", "email-signing");
+ List.of("server-tls", "vpn-server", "vpn-client", "email-signing",
+ "root-ca", "intermediate-ca");
private static final Set EXPECTED_ALGORITHMS =
Set.of("RSA", "ECDSA", "Ed25519");
private static final ExtendedKeyUsageId SERVER_AUTH =
@@ -65,7 +68,7 @@ final class BuiltInCertificateProfileCatalogTest {
List second =
BuiltInCertificateProfileCatalog.load(getClass().getClassLoader());
- assertEquals(4, first.size());
+ assertEquals(6, first.size());
assertEquals(EXPECTED_ORDER,
first.stream().map(template -> template.definition().profileId()).toList());
assertEquals(first, second);
@@ -76,7 +79,6 @@ final class BuiltInCertificateProfileCatalogTest {
byte[] hash = template.canonicalSha256();
assertEquals(1, definition.profileVersion());
assertEquals("x509", definition.formatId().value());
- assertEquals(Duration.ofDays(365), definition.leafPolicy().maximumValidity());
assertArrayEquals(canonical,
CertificateProfileDocumentCodec.writeCanonical(definition));
assertEquals(definition, CertificateProfileDocumentCodec.parse(canonical));
@@ -103,8 +105,8 @@ final class BuiltInCertificateProfileCatalogTest {
LeafCertificatePolicy leaf = profiles.get(profileId).leafPolicy();
SubjectAlternativeNamePolicy san = leaf.subjectAlternativeNamePolicy();
- assertOptionalCommonName(leaf);
- assertTrue(san.allowEmptySubject());
+ assertOptionalCommonName(profiles.get(profileId));
+ assertTrue(profiles.get(profileId).subjectPolicy().allowEmpty());
assertEquals(1, san.minimumTotal());
assertEquals(64, san.maximumTotal());
assertTrue(san.requireServiceIdentity());
@@ -123,11 +125,12 @@ final class BuiltInCertificateProfileCatalogTest {
@Test
void vpnClientTemplateHasOnlySpiffeUriAndRfc822Identity() {
- LeafCertificatePolicy leaf = productionDefinitions().get("vpn-client").leafPolicy();
+ CertificateProfileDefinition definition = productionDefinitions().get("vpn-client");
+ LeafCertificatePolicy leaf = definition.leafPolicy();
SubjectAlternativeNamePolicy san = leaf.subjectAlternativeNamePolicy();
- assertOptionalCommonName(leaf);
- assertTrue(san.allowEmptySubject());
+ assertOptionalCommonName(definition);
+ assertTrue(definition.subjectPolicy().allowEmpty());
assertEquals(1, san.minimumTotal());
assertEquals(16, san.maximumTotal());
assertFalse(san.requireServiceIdentity());
@@ -142,13 +145,14 @@ final class BuiltInCertificateProfileCatalogTest {
@Test
void emailTemplateRequiresRfc822SanAndDoesNotEnableSubjectEmail() {
- LeafCertificatePolicy leaf = productionDefinitions().get("email-signing").leafPolicy();
+ CertificateProfileDefinition definition = productionDefinitions().get("email-signing");
+ LeafCertificatePolicy leaf = definition.leafPolicy();
SubjectAlternativeNamePolicy san = leaf.subjectAlternativeNamePolicy();
- assertOptionalCommonName(leaf);
+ assertOptionalCommonName(definition);
assertEquals(List.of(SubjectRdnType.COMMON_NAME),
- leaf.subjectPolicy().rules().stream().map(SubjectRdnRule::type).toList());
- assertTrue(san.allowEmptySubject());
+ definition.subjectPolicy().rules().stream().map(SubjectRdnRule::type).toList());
+ assertTrue(definition.subjectPolicy().allowEmpty());
assertEquals(1, san.minimumTotal());
assertEquals(16, san.maximumTotal());
assertFalse(san.requireServiceIdentity());
@@ -159,6 +163,15 @@ final class BuiltInCertificateProfileCatalogTest {
assertLeafPolicy(leaf, Set.of(EMAIL_PROTECTION));
}
+ @Test
+ void caTemplatesHaveExactKindsSubjectAndCertificatePolicies() {
+ Map profiles = productionDefinitions();
+ assertCaProfile(profiles.get("root-ca"), CertificateProfileKind.ROOT_CA,
+ Duration.ofHours(87_600), 1);
+ assertCaProfile(profiles.get("intermediate-ca"),
+ CertificateProfileKind.INTERMEDIATE_CA, Duration.ofHours(43_800), 0);
+ }
+
@Test
void manifestRejectsUnknownDuplicateMissingInvalidPathsLimitsAndTrailingTokens() {
List invalidManifests = List.of(
@@ -226,12 +239,12 @@ final class BuiltInCertificateProfileCatalogTest {
@Test
void profileDocumentsFailClosedForInvalidSchemaFieldsEncodingAndClassMetadata() {
List invalidDocuments = List.of(
- replace(mainResource(SERVER), "\"schemaVersion\":1",
- "\"schemaVersion\":2"),
- replace(mainResource(SERVER), "\"schemaVersion\":1,",
- "\"schemaVersion\":1,\"active\":true,"),
- replace(mainResource(SERVER), "\"schemaVersion\":1,",
- "\"schemaVersion\":1,\"@class\":\""
+ replace(mainResource(SERVER), "\"schemaVersion\":2",
+ "\"schemaVersion\":1"),
+ replace(mainResource(SERVER), "\"schemaVersion\":2,",
+ "\"schemaVersion\":2,\"active\":true,"),
+ replace(mainResource(SERVER), "\"schemaVersion\":2,",
+ "\"schemaVersion\":2,\"@class\":\""
+ InitializationSentinel.CLASS_NAME + "\","),
malformedUtf8(mainResource(SERVER)));
for (byte[] invalid : invalidDocuments) {
@@ -251,8 +264,9 @@ final class BuiltInCertificateProfileCatalogTest {
CertificateProfileDefinition server =
CertificateProfileDocumentCodec.parse(mainResource(SERVER));
CertificateProfileDefinition changed = new CertificateProfileDefinition(
- server.profileId(), server.profileVersion(), server.formatId(),
- "Changed display", server.leafPolicy());
+ server.certificateType(), server.profileId(), server.profileVersion(),
+ server.formatId(), "Changed display", server.maximumValidity(),
+ server.subjectPolicy(), server.certificatePolicy());
Map> duplicateIdentity = baseResources();
duplicateIdentity.put(VPN_SERVER, List.of(
CertificateProfileDocumentCodec.writeCanonical(changed)));
@@ -265,9 +279,9 @@ final class BuiltInCertificateProfileCatalogTest {
assertCode(wrongSet, "BUILT_IN_PROFILE_SET_INVALID");
}
- private static void assertOptionalCommonName(LeafCertificatePolicy leaf) {
- assertEquals(1, leaf.subjectPolicy().rules().size());
- SubjectRdnRule commonName = leaf.subjectPolicy().rules().get(0);
+ private static void assertOptionalCommonName(CertificateProfileDefinition definition) {
+ assertEquals(1, definition.subjectPolicy().rules().size());
+ SubjectRdnRule commonName = definition.subjectPolicy().rules().get(0);
assertEquals(SubjectRdnType.COMMON_NAME, commonName.type());
assertEquals(0, commonName.minimumOccurrences());
assertEquals(1, commonName.maximumOccurrences());
@@ -284,7 +298,25 @@ final class BuiltInCertificateProfileCatalogTest {
assertFalse(leaf.extendedKeyUsageCritical());
assertTrue(leaf.basicConstraintsCritical());
assertEquals(EXPECTED_ALGORITHMS, leaf.allowedSubjectKeyAlgorithmIds());
- assertEquals(Duration.ofDays(365), leaf.maximumValidity());
+ }
+
+ private static void assertCaProfile(CertificateProfileDefinition definition,
+ CertificateProfileKind kind, Duration maximumValidity, int pathLength) {
+ assertEquals(kind, definition.certificateType());
+ assertEquals(maximumValidity, definition.maximumValidity());
+ assertFalse(definition.subjectPolicy().allowEmpty());
+ assertEquals(1, definition.subjectPolicy().rules().size());
+ SubjectRdnRule commonName = definition.subjectPolicy().rules().get(0);
+ assertEquals(SubjectRdnType.COMMON_NAME, commonName.type());
+ assertEquals(1, commonName.minimumOccurrences());
+ assertEquals(1, commonName.maximumOccurrences());
+ assertEquals(253, commonName.maximumUtf8Bytes());
+ CaCertificatePolicy ca = definition.caPolicy();
+ assertTrue(ca.basicConstraintsCritical());
+ assertEquals(pathLength, ca.pathLengthConstraint());
+ assertTrue(ca.keyUsageCritical());
+ assertEquals(Set.of(CaKeyUsage.KEY_CERT_SIGN, CaKeyUsage.CRL_SIGN), ca.keyUsages());
+ assertEquals(EXPECTED_ALGORITHMS, ca.allowedSubjectKeyAlgorithmIds());
}
private static SubjectAlternativeNameRule rule(SubjectAlternativeNamePolicy policy,
diff --git a/pki/src/test/java/zeroecho/pki/api/profile/CertificateProfileDocumentCodecTest.java b/pki/src/test/java/zeroecho/pki/api/profile/CertificateProfileDocumentCodecTest.java
index 567f521..e4b3b90 100644
--- a/pki/src/test/java/zeroecho/pki/api/profile/CertificateProfileDocumentCodecTest.java
+++ b/pki/src/test/java/zeroecho/pki/api/profile/CertificateProfileDocumentCodecTest.java
@@ -34,7 +34,8 @@ final class CertificateProfileDocumentCodecTest {
private static final String VALID_DOCUMENT = """
{
- "schemaVersion": 1,
+ "schemaVersion": 2,
+ "certificateType": "END_ENTITY",
"profileId": "tls-service",
"profileVersion": 7,
"formatId": "x509",
@@ -108,17 +109,18 @@ final class CertificateProfileDocumentCodecTest {
void parsesEverySupportedRuleShapeIntoAuthoritativeTypedPolicies() {
CertificateProfileDefinition definition = parse(VALID_DOCUMENT);
- assertEquals(CertificateProfileDefinition.SCHEMA_VERSION, 1);
+ assertEquals(2, CertificateProfileDefinition.SCHEMA_VERSION);
+ assertEquals(CertificateProfileKind.END_ENTITY, definition.certificateType());
assertEquals("tls-service", definition.profileId());
assertEquals(7, definition.profileVersion());
assertEquals("x509", definition.formatId().value());
assertEquals("TLS service", definition.displayName());
LeafCertificatePolicy leaf = definition.leafPolicy();
- assertEquals(Duration.ofDays(1), leaf.maximumValidity());
+ assertEquals(Duration.ofDays(1), definition.maximumValidity());
assertEquals(List.of(SubjectRdnType.COMMON_NAME, SubjectRdnType.ORGANIZATION_NAME),
- leaf.subjectPolicy().rules().stream().map(SubjectRdnRule::type).toList());
- assertEquals("ZeroEcho", leaf.subjectPolicy().rules().get(1).fixedValue().orElseThrow());
- assertFalse(leaf.subjectAlternativeNamePolicy().allowEmptySubject());
+ definition.subjectPolicy().rules().stream().map(SubjectRdnRule::type).toList());
+ assertEquals("ZeroEcho", definition.subjectPolicy().rules().get(1).fixedValue().orElseThrow());
+ assertFalse(definition.subjectPolicy().allowEmpty());
assertTrue(leaf.subjectAlternativeNamePolicy().allowDnsWildcard());
assertEquals(Set.of("https", "spiffe"),
leaf.subjectAlternativeNamePolicy().allowedUriSchemes());
@@ -141,7 +143,7 @@ final class CertificateProfileDocumentCodecTest {
definition("uri", requesterCn(), uriSan(), eku(), Set.of("Ed448")),
definition("email", requesterCn(), emailSan(), eku(), Set.of("RSA")),
definition("mixed", requesterCn(), mixedSan(), eku(), Set.of("RSA", "ECDSA")),
- definition("empty-subject", new SubjectPolicy(List.of()), dnsSan(false, true),
+ definition("empty-subject", new SubjectPolicy(true, List.of()), dnsSan(false, true),
eku(), Set.of("RSA")),
definition("fixed-rdn", fixedOrganization(), noSan(), eku(), Set.of("RSA")),
definition("multiple-algorithms", requesterCn(), noSan(), Set.of(),
@@ -161,7 +163,7 @@ final class CertificateProfileDocumentCodecTest {
CertificateProfileDefinition immutable = CertificateProfileDocumentCodec.parse(
CertificateProfileDocumentCodec.writeCanonical(definitions.get(7)));
assertThrows(UnsupportedOperationException.class,
- () -> immutable.leafPolicy().subjectPolicy().rules().add(
+ () -> immutable.subjectPolicy().rules().add(
new SubjectRdnRule(SubjectRdnType.PSEUDONYM, 0, 0, 32,
Optional.empty(), true)));
assertThrows(UnsupportedOperationException.class,
@@ -171,6 +173,55 @@ final class CertificateProfileDocumentCodecTest {
.allowedUriSchemes().add("ssh"));
}
+ @Test
+ void roundTripsRootAndIntermediateCaPoliciesAndRejectsWrongKindShapes() {
+ CertificateProfileDefinition root = caDefinition("root-test",
+ CertificateProfileKind.ROOT_CA, 1);
+ CertificateProfileDefinition intermediate = caDefinition("intermediate-test",
+ CertificateProfileKind.INTERMEDIATE_CA, 0);
+ CertificateProfileDefinition delegatedIntermediate = caDefinition(
+ "delegated-intermediate-test", CertificateProfileKind.INTERMEDIATE_CA, 1);
+ for (CertificateProfileDefinition expected :
+ List.of(root, intermediate, delegatedIntermediate)) {
+ byte[] canonical = CertificateProfileDocumentCodec.writeCanonical(expected);
+ CertificateProfileDefinition actual = CertificateProfileDocumentCodec.parse(canonical);
+ assertEquals(expected, actual);
+ assertArrayEquals(canonical, CertificateProfileDocumentCodec.writeCanonical(actual));
+ }
+
+ String rootJson = canonical(root);
+ assertCode(rootJson.replace("\"certificateType\":\"ROOT_CA\"",
+ "\"certificateType\":\"END_ENTITY\""),
+ "FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE");
+ assertCode(rootJson.replace("\"caCertificate\":", "\"leafCertificate\":"),
+ "FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE");
+ assertCode(rootJson.replace("\"caCertificate\":", "\"subjectAlternativeNames\":"),
+ "FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE");
+ assertCode(rootJson.replace(",\"caCertificate\":{", ",\"leafCertificate\":{}"
+ + ",\"caCertificate\":{"), "FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE");
+ assertCode(rootJson.replace("\"allowEmpty\":false", "\"allowEmpty\":true"),
+ "SEMANTIC_INVALID");
+ assertCode(rootJson.replace("\"pathLengthConstraint\":1",
+ "\"pathLengthConstraint\":-1"), "SEMANTIC_INVALID");
+ assertCode(rootJson.replace("\"pathLengthConstraint\":1",
+ "\"pathLengthConstraint\":33"), "SEMANTIC_INVALID");
+ assertCode(rootJson.replace("\"basicConstraintsCritical\":true",
+ "\"basicConstraintsCritical\":false"), "SEMANTIC_INVALID");
+ assertCode(rootJson.replace("\"keyUsageCritical\":true",
+ "\"keyUsageCritical\":false"), "SEMANTIC_INVALID");
+ assertCode(rootJson.replace("[\"CRL_SIGN\",\"KEY_CERT_SIGN\"]",
+ "[\"KEY_CERT_SIGN\"]"), "SEMANTIC_INVALID");
+ assertCode(rootJson.replace("[\"CRL_SIGN\",\"KEY_CERT_SIGN\"]",
+ "[\"CRL_SIGN\"]"), "SEMANTIC_INVALID");
+ assertCode(rootJson.replace("[\"CRL_SIGN\",\"KEY_CERT_SIGN\"]",
+ "[\"CRL_SIGN\",\"CRL_SIGN\",\"KEY_CERT_SIGN\"]"), "SEMANTIC_INVALID");
+ assertCode(rootJson.replace("\"allowedSubjectKeyAlgorithms\":[\"RSA\"]",
+ "\"allowedSubjectKeyAlgorithms\":[]"), "SEMANTIC_INVALID");
+ assertCode(rootJson.replace("\"allowedSubjectKeyAlgorithms\":",
+ "\"extendedKeyUsage\":[],\"allowedSubjectKeyAlgorithms\":"),
+ "UNKNOWN_FIELD");
+ }
+
@Test
void canonicalOutputHasFixedOrderSortedSetsAndIsIdempotent() {
byte[] first = CertificateProfileDocumentCodec.writeCanonical(parse(VALID_DOCUMENT));
@@ -184,7 +235,8 @@ final class CertificateProfileDocumentCodecTest {
assertFalse(json.contains("\n"));
assertFalse(json.contains(": "));
assertFalse(json.contains(", "));
- assertTrue(json.indexOf("\"schemaVersion\"") < json.indexOf("\"profileId\""));
+ assertTrue(json.indexOf("\"schemaVersion\"") < json.indexOf("\"certificateType\""));
+ assertTrue(json.indexOf("\"certificateType\"") < json.indexOf("\"profileId\""));
assertTrue(json.indexOf("\"DNS_NAME\"") < json.indexOf("\"IP_ADDRESS\""));
assertTrue(json.indexOf("\"IP_ADDRESS\"") < json.indexOf("\"RFC822_NAME\""));
assertTrue(json.indexOf("\"RFC822_NAME\"") < json.indexOf("\"URI\""));
@@ -201,12 +253,15 @@ final class CertificateProfileDocumentCodecTest {
CertificateProfileDefinition valid = definition("valid", requesterCn(), noSan(),
Set.of(), Set.of("RSA"));
List invalid = List.of(
- new CertificateProfileDefinition(" padded", valid.profileVersion(),
- valid.formatId(), valid.displayName(), valid.leafPolicy()),
- new CertificateProfileDefinition(valid.profileId(), valid.profileVersion(),
- new FormatId("x509 "), valid.displayName(), valid.leafPolicy()),
- new CertificateProfileDefinition(valid.profileId(), valid.profileVersion(),
- valid.formatId(), " padded ", valid.leafPolicy()));
+ new CertificateProfileDefinition(valid.certificateType(), " padded", valid.profileVersion(),
+ valid.formatId(), valid.displayName(), valid.maximumValidity(),
+ valid.subjectPolicy(), valid.certificatePolicy()),
+ new CertificateProfileDefinition(valid.certificateType(), valid.profileId(),
+ valid.profileVersion(), new FormatId("x509 "), valid.displayName(),
+ valid.maximumValidity(), valid.subjectPolicy(), valid.certificatePolicy()),
+ new CertificateProfileDefinition(valid.certificateType(), valid.profileId(),
+ valid.profileVersion(), valid.formatId(), " padded ", valid.maximumValidity(),
+ valid.subjectPolicy(), valid.certificatePolicy()));
for (CertificateProfileDefinition definition : invalid) {
assertWriteCode(definition, "CANONICALIZATION_FAILED");
@@ -245,11 +300,11 @@ final class CertificateProfileDocumentCodecTest {
@Test
void rejectsDuplicateUnknownMissingNullWrongAndNonintegralFields() {
- assertCode(VALID_DOCUMENT.replace("\"schemaVersion\": 1,",
- "\"schemaVersion\": 1,\"schemaVersion\": 1,"), "DUPLICATE_FIELD");
+ assertCode(VALID_DOCUMENT.replace("\"schemaVersion\": 2,",
+ "\"schemaVersion\": 2,\"schemaVersion\": 2,"), "DUPLICATE_FIELD");
for (String document : List.of(
- VALID_DOCUMENT.replace("\"schemaVersion\": 1,",
- "\"secret-field\": true,\"schemaVersion\": 1,"),
+ VALID_DOCUMENT.replace("\"schemaVersion\": 2,",
+ "\"secret-field\": true,\"schemaVersion\": 2,"),
VALID_DOCUMENT.replace("\"allowEmpty\": false,",
"\"unknown\": true,\"allowEmpty\": false,"),
VALID_DOCUMENT.replace("\"oid\": \"2.5.4.3\",",
@@ -272,7 +327,7 @@ final class CertificateProfileDocumentCodecTest {
assertCode(document, "DUPLICATE_FIELD");
}
for (String document : List.of(
- VALID_DOCUMENT.replace("\"schemaVersion\": 1,\n", ""),
+ VALID_DOCUMENT.replace("\"schemaVersion\": 2,\n", ""),
VALID_DOCUMENT.replace("\"allowEmpty\": false,\n", ""),
VALID_DOCUMENT.replace("\"oid\": \"2.5.4.3\",\n", ""),
VALID_DOCUMENT.replace("\"minimumTotal\": 1,\n", ""),
@@ -303,8 +358,10 @@ final class CertificateProfileDocumentCodecTest {
@Test
void rejectsUnsupportedVersionsTokensCaseWhitespaceAndNoncanonicalDuration() {
- assertCode(VALID_DOCUMENT.replace("\"schemaVersion\": 1",
- "\"schemaVersion\": 2"), "SCHEMA_VERSION_UNSUPPORTED");
+ assertCode(VALID_DOCUMENT.replace("\"schemaVersion\": 2",
+ "\"schemaVersion\": 1"), "SCHEMA_VERSION_UNSUPPORTED");
+ assertCode(VALID_DOCUMENT.replace("\"certificateType\": \"END_ENTITY\"",
+ "\"certificateType\": \"end_entity\""), "CERTIFICATE_TYPE_UNSUPPORTED");
assertCode(VALID_DOCUMENT.replace("\"profileVersion\": 7",
"\"profileVersion\": 0"), "PROFILE_VERSION_INVALID");
assertCode(VALID_DOCUMENT.replace("\"type\": \"DNS_NAME\"",
@@ -386,7 +443,7 @@ final class CertificateProfileDocumentCodecTest {
eku(), Set.of("RSA")));
assertCode(email.replace("\"serviceIdentityRequired\":false",
"\"serviceIdentityRequired\":true"), "SEMANTIC_INVALID");
- String empty = canonical(definition("empty-invalid", new SubjectPolicy(List.of()),
+ String empty = canonical(definition("empty-invalid", new SubjectPolicy(true, List.of()),
dnsSan(false, true), eku(), Set.of("RSA")));
assertCode(empty.replace("\"minimumTotal\":1", "\"minimumTotal\":0"),
"SEMANTIC_INVALID");
@@ -449,8 +506,8 @@ final class CertificateProfileDocumentCodecTest {
@Test
void redactsHostileInputAndParserDetailsFromFailures() {
String secret = "do-not-disclose-credential";
- String hostile = VALID_DOCUMENT.replace("\"schemaVersion\": 1,",
- "\"" + secret + "\": true,\"schemaVersion\": 1,");
+ String hostile = VALID_DOCUMENT.replace("\"schemaVersion\": 2,",
+ "\"" + secret + "\": true,\"schemaVersion\": 2,");
PkiException exception = assertThrows(PkiException.class, () -> parse(hostile));
@@ -485,8 +542,8 @@ final class CertificateProfileDocumentCodecTest {
logger.addHandler(handler);
try {
for (String field : hostileFields) {
- String document = VALID_DOCUMENT.replace("\"schemaVersion\": 1,",
- "\"" + field + "\":\"" + probeName + "\",\"schemaVersion\": 1,");
+ String document = VALID_DOCUMENT.replace("\"schemaVersion\": 2,",
+ "\"" + field + "\":\"" + probeName + "\",\"schemaVersion\": 2,");
PkiException exception = assertThrows(PkiException.class, () -> parse(document));
assertTrue(exception.getMessage().contains("code=UNKNOWN_FIELD "));
assertFalse(exception.getMessage().contains(field));
@@ -512,57 +569,69 @@ final class CertificateProfileDocumentCodecTest {
private static CertificateProfileDefinition definition(String id, SubjectPolicy subject,
SubjectAlternativeNamePolicy san, Set extendedKeyUsages,
Set algorithms) {
- LeafCertificatePolicy leaf = new LeafCertificatePolicy(subject, san,
+ LeafCertificatePolicy leaf = new LeafCertificatePolicy(san,
Set.of(LeafKeyUsage.DIGITAL_SIGNATURE), extendedKeyUsages, true, false, true,
- algorithms, Duration.ofDays(1));
- return new CertificateProfileDefinition(id, 1, new FormatId("x509"), id, leaf);
+ algorithms);
+ return new CertificateProfileDefinition(CertificateProfileKind.END_ENTITY, id, 1,
+ new FormatId("x509"), id, Duration.ofDays(1), subject, leaf);
+ }
+
+ private static CertificateProfileDefinition caDefinition(String id,
+ CertificateProfileKind kind, int pathLength) {
+ SubjectPolicy subject = new SubjectPolicy(false,
+ List.of(new SubjectRdnRule(SubjectRdnType.COMMON_NAME, 1, 1, 253,
+ Optional.empty(), true)));
+ CaCertificatePolicy ca = new CaCertificatePolicy(true, pathLength, true,
+ Set.of(CaKeyUsage.KEY_CERT_SIGN, CaKeyUsage.CRL_SIGN), Set.of("RSA"));
+ return new CertificateProfileDefinition(kind, id, 1, new FormatId("x509"), id,
+ Duration.ofDays(365), subject, ca);
}
private static SubjectPolicy requesterCn() {
- return new SubjectPolicy(List.of(new SubjectRdnRule(SubjectRdnType.COMMON_NAME,
+ return new SubjectPolicy(false, List.of(new SubjectRdnRule(SubjectRdnType.COMMON_NAME,
1, 1, 128, Optional.empty(), true)));
}
private static SubjectPolicy fixedOrganization() {
- return new SubjectPolicy(List.of(new SubjectRdnRule(SubjectRdnType.ORGANIZATION_NAME,
+ return new SubjectPolicy(false, List.of(new SubjectRdnRule(SubjectRdnType.ORGANIZATION_NAME,
1, 1, 64, Optional.of("ZeroEcho"), false)));
}
private static SubjectAlternativeNamePolicy noSan() {
- return new SubjectAlternativeNamePolicy(false, 0, 0, List.of(), false, Set.of(),
+ return new SubjectAlternativeNamePolicy(0, 0, List.of(), false, Set.of(),
false, false, false);
}
private static SubjectAlternativeNamePolicy dnsSan(boolean wildcard, boolean emptySubject) {
- return new SubjectAlternativeNamePolicy(emptySubject, 1, 1,
+ return new SubjectAlternativeNamePolicy(1, 1,
List.of(new SubjectAlternativeNameRule(SubjectAlternativeNameType.DNS_NAME,
1, 1, false, false)),
wildcard, Set.of(), emptySubject, true, false);
}
private static SubjectAlternativeNamePolicy ipSan(boolean ipv4, boolean ipv6) {
- return new SubjectAlternativeNamePolicy(false, 1, 1,
+ return new SubjectAlternativeNamePolicy(1, 1,
List.of(new SubjectAlternativeNameRule(SubjectAlternativeNameType.IP_ADDRESS,
1, 1, ipv4, ipv6)),
false, Set.of(), false, true, false);
}
private static SubjectAlternativeNamePolicy uriSan() {
- return new SubjectAlternativeNamePolicy(false, 1, 1,
+ return new SubjectAlternativeNamePolicy(1, 1,
List.of(new SubjectAlternativeNameRule(SubjectAlternativeNameType.URI,
1, 1, false, false)),
false, Set.of("https"), false, true, false);
}
private static SubjectAlternativeNamePolicy emailSan() {
- return new SubjectAlternativeNamePolicy(false, 1, 1,
+ return new SubjectAlternativeNamePolicy(1, 1,
List.of(new SubjectAlternativeNameRule(SubjectAlternativeNameType.RFC822_NAME,
1, 1, false, false)),
false, Set.of(), false, false, true);
}
private static SubjectAlternativeNamePolicy mixedSan() {
- return new SubjectAlternativeNamePolicy(false, 0, 4, List.of(
+ return new SubjectAlternativeNamePolicy(0, 4, List.of(
new SubjectAlternativeNameRule(SubjectAlternativeNameType.DNS_NAME,
0, 1, false, false),
new SubjectAlternativeNameRule(SubjectAlternativeNameType.IP_ADDRESS,
diff --git a/pki/src/test/java/zeroecho/pki/e2e/CaProfileIssuanceEnforcementTest.java b/pki/src/test/java/zeroecho/pki/e2e/CaProfileIssuanceEnforcementTest.java
new file mode 100644
index 0000000..b542a6d
--- /dev/null
+++ b/pki/src/test/java/zeroecho/pki/e2e/CaProfileIssuanceEnforcementTest.java
@@ -0,0 +1,919 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Leo Galambos
+ * All rights reserved.
+ ******************************************************************************/
+package zeroecho.pki.e2e;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.MessageDigest;
+import java.time.Instant;
+import java.util.Date;
+import java.util.HexFormat;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Stream;
+
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.DEROctetString;
+import org.bouncycastle.asn1.DERPrintableString;
+import org.bouncycastle.asn1.x500.RDN;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x500.style.BCStyle;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.KeyUsage;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+
+import zeroecho.pki.api.EncodedObject;
+import zeroecho.pki.api.Encoding;
+import zeroecho.pki.api.KeyRef;
+import zeroecho.pki.api.CaService;
+import zeroecho.pki.api.PkiException;
+import zeroecho.pki.api.PkiId;
+import zeroecho.pki.api.ProfileService;
+import zeroecho.pki.api.SubjectRef;
+import zeroecho.pki.api.Validity;
+import zeroecho.pki.api.ca.CaCreateCommand;
+import zeroecho.pki.api.ca.CaImportCommand;
+import zeroecho.pki.api.ca.CaRecord;
+import zeroecho.pki.api.ca.IntermediateCertIssueCommand;
+import zeroecho.pki.api.ca.IntermediateCreateCommand;
+import zeroecho.pki.api.credential.CaProfileBinding;
+import zeroecho.pki.api.credential.Credential;
+import zeroecho.pki.api.credential.CredentialBundle;
+import zeroecho.pki.api.profile.BuiltInCertificateProfileCatalog;
+import zeroecho.pki.api.profile.ActiveCertificateProfile;
+import zeroecho.pki.api.profile.BuiltInCertificateProfileTemplate;
+import zeroecho.pki.api.profile.CertificateProfileRef;
+import zeroecho.pki.api.profile.ImportedCertificateProfileVersion;
+import zeroecho.pki.impl.core.ValidatedCaCertificateRequest;
+import zeroecho.pki.impl.core.ValidatedCertificateRequest;
+import zeroecho.pki.impl.core.attr.SimpleAttributeSet;
+import zeroecho.pki.spi.framework.CredentialIssuerBackend;
+import zeroecho.pki.testkit.PkiTestRuntime;
+
+/**
+ * Focused real-certificate acceptance tests for active CA profile enforcement.
+ */
+final class CaProfileIssuanceEnforcementTest {
+
+ @Test
+ void fixedSubjectIntermediateIsValidatedOnceAndPersistsCanonicalSubject(@TempDir Path directory)
+ throws Exception {
+ KeyPair rootKey = rsa();
+ KeyPair intermediateKey = rsa();
+ KeyRef rootRef = new KeyRef("kref:v1:keyring:ca-profile:fixed-root");
+ KeyRef intermediateRef = new KeyRef("kref:v1:keyring:ca-profile:fixed-intermediate");
+ try (PkiTestRuntime runtime = PkiTestRuntime.create(directory, directory.resolve("bus.log"),
+ Map.of(rootRef, rootKey, intermediateRef, intermediateKey))) {
+ byte[] fixedProfile = builtIn("intermediate-ca")
+ .replace("\"profileId\":\"intermediate-ca\"",
+ "\"profileId\":\"fixed-intermediate-ca\"")
+ .replace("\"maximumUtf8Bytes\":253}]}",
+ "\"maximumUtf8Bytes\":253},{\"oid\":\"2.5.4.10\",\"source\":\"PROFILE_FIXED\","
+ + "\"minimumOccurrences\":1,\"maximumOccurrences\":1,"
+ + "\"maximumUtf8Bytes\":256,\"fixedValue\":\"Fixed Organization\"}]}")
+ .getBytes(StandardCharsets.UTF_8);
+ CertificateProfileRef reference = runtime.profileService().importProfile(fixedProfile);
+ runtime.profileService().activateProfile(reference.profileId(), reference.profileVersion());
+
+ PkiId rootId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
+ new SubjectRef("CN=Fixed Subject Root"), "root-ca", Optional.of(rootRef),
+ new SimpleAttributeSet()));
+ PkiId intermediateId = runtime.caService().createIntermediate(new IntermediateCreateCommand(
+ runtime.framework().formatId(), rootId, new SubjectRef("CN=Fixed Subject Intermediate"),
+ "fixed-intermediate-ca", Optional.of(intermediateRef), new SimpleAttributeSet()));
+ Credential additional = runtime.caService().issueIntermediateCertificate(
+ new IntermediateCertIssueCommand(runtime.framework().formatId(), rootId, intermediateId,
+ "fixed-intermediate-ca", Optional.empty(), new SimpleAttributeSet()));
+
+ CaRecord intermediate = runtime.caService().getCa(intermediateId);
+ Credential credential = intermediate.caCredentials().get(0);
+ X509CertificateHolder holder = new X509CertificateHolder(credential.encoded().bytes());
+ X509CertificateHolder additionalHolder = new X509CertificateHolder(additional.encoded().bytes());
+ assertEquals("Fixed Organization",
+ holder.getSubject().getRDNs(BCStyle.O)[0].getFirst().getValue().toString());
+ assertEquals("Fixed Organization",
+ additionalHolder.getSubject().getRDNs(BCStyle.O)[0].getFirst().getValue().toString());
+ assertEquals(holder.getSubject().toString(), intermediate.subjectRef().value());
+ assertEquals(intermediate.subjectRef(), credential.subjectRef());
+ assertEquals(intermediate.subjectRef(), additional.subjectRef());
+ assertEquals(2, intermediate.caCredentials().size());
+ }
+ }
+
+ @Test
+ void rootIntermediateAdditionalAndRestartRetainExactReferences(@TempDir Path directory) throws Exception {
+ KeyPair rootKey = rsa();
+ KeyPair intermediateKey = rsa();
+ KeyRef rootRef = new KeyRef("kref:v1:keyring:ca-profile:root");
+ KeyRef intermediateRef = new KeyRef("kref:v1:keyring:ca-profile:intermediate");
+ Path store = directory.resolve("store");
+ Path bus = directory.resolve("bus.log");
+ PkiId rootId;
+ PkiId intermediateId;
+ CertificateProfileRef rootProfile;
+ CertificateProfileRef intermediateProfile;
+ try (PkiTestRuntime runtime = PkiTestRuntime.create(store, bus,
+ Map.of(rootRef, rootKey, intermediateRef, intermediateKey))) {
+ rootId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
+ new SubjectRef("CN=Profile Root"), "root-ca", Optional.of(rootRef),
+ new SimpleAttributeSet()));
+ intermediateId = runtime.caService().createIntermediate(new IntermediateCreateCommand(
+ runtime.framework().formatId(), rootId, new SubjectRef("CN=Profile Intermediate"),
+ "intermediate-ca", Optional.of(intermediateRef), new SimpleAttributeSet()));
+ Credential additional = runtime.caService().issueIntermediateCertificate(
+ new IntermediateCertIssueCommand(runtime.framework().formatId(), rootId, intermediateId,
+ "intermediate-ca", Optional.empty(), new SimpleAttributeSet()));
+ rootProfile = ((CaProfileBinding) runtime.caService().getCa(rootId).caCredentials().get(0)
+ .profileBinding()).reference();
+ intermediateProfile = ((CaProfileBinding) additional.profileBinding()).reference();
+ assertCaCertificate(runtime.caService().getCa(rootId).caCredentials().get(0), 1);
+ assertCaCertificate(additional, 0);
+ }
+ try (PkiTestRuntime reopened = PkiTestRuntime.create(store, directory.resolve("reopened-bus.log"),
+ Map.of(rootRef, rootKey, intermediateRef, intermediateKey))) {
+ assertEquals(rootProfile, ((CaProfileBinding) reopened.caService().getCa(rootId)
+ .caCredentials().get(0).profileBinding()).reference());
+ assertEquals(intermediateProfile, ((CaProfileBinding) reopened.caService().getCa(intermediateId)
+ .caCredentials().get(1).profileBinding()).reference());
+ }
+ }
+
+ @Test
+ void missingAndWrongKindActiveProfilesFailBeforeProof(@TempDir Path directory) throws Exception {
+ KeyPair rootKey = rsa();
+ KeyRef rootRef = new KeyRef("kref:v1:keyring:ca-profile:missing");
+ try (PkiTestRuntime runtime = PkiTestRuntime.create(directory, directory.resolve("bus.log"),
+ Map.of(rootRef, rootKey))) {
+ assertThrows(PkiException.class, () -> runtime.caService().createRoot(new CaCreateCommand(
+ runtime.framework().formatId(), new SubjectRef("CN=Missing"), "missing-ca-profile",
+ Optional.of(rootRef), new SimpleAttributeSet())));
+ assertThrows(PkiException.class, () -> runtime.caService().createRoot(new CaCreateCommand(
+ runtime.framework().formatId(), new SubjectRef("CN=Wrong Kind"), "default",
+ Optional.of(rootRef), new SimpleAttributeSet())));
+ assertEquals(0, runtime.submittedSignCount());
+ assertTrue(runtime.store().listCas().isEmpty());
+ }
+ }
+
+ @Test
+ void historicalIssuerVersionRemainsValidAfterActiveSwitch(@TempDir Path directory) throws Exception {
+ KeyPair rootKey = rsa();
+ KeyPair intermediateKey = rsa();
+ KeyRef rootRef = new KeyRef("kref:v1:keyring:ca-profile:history-root");
+ KeyRef intermediateRef = new KeyRef("kref:v1:keyring:ca-profile:history-intermediate");
+ try (PkiTestRuntime runtime = PkiTestRuntime.create(directory, directory.resolve("bus.log"),
+ Map.of(rootRef, rootKey, intermediateRef, intermediateKey))) {
+ PkiId rootId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
+ new SubjectRef("CN=Historical Root"), "root-ca", Optional.of(rootRef),
+ new SimpleAttributeSet()));
+ byte[] versionTwo = builtIn("root-ca").replace("\"profileVersion\":1",
+ "\"profileVersion\":2").getBytes(StandardCharsets.UTF_8);
+ CertificateProfileRef imported = runtime.profileService().importProfile(versionTwo);
+ runtime.profileService().activateProfile(imported.profileId(), imported.profileVersion());
+
+ PkiId intermediateId = runtime.caService().createIntermediate(new IntermediateCreateCommand(
+ runtime.framework().formatId(), rootId, new SubjectRef("CN=Historical Intermediate"),
+ "intermediate-ca", Optional.of(intermediateRef), new SimpleAttributeSet()));
+ CaProfileBinding issuerBinding = (CaProfileBinding) runtime.caService().getCa(rootId)
+ .caCredentials().get(0).profileBinding();
+ assertEquals(1, issuerBinding.reference().profileVersion());
+ assertEquals(1, runtime.caService().getCa(intermediateId).caCredentials().size());
+ }
+ }
+
+ @Test
+ void rootCaProfileVersionSwitchA_B_C(@TempDir Path directory) throws Exception {
+ System.out.println("rootCaProfileVersionSwitchA_B_C");
+ String profileId = "ca-version-switch-root";
+ byte[] versionOneDocument = caProfileDocument("root-ca", profileId, 1, 1, 1);
+ byte[] versionTwoDocument = caProfileDocument("root-ca", profileId, 2, 1, 2);
+ KeyPair keyA = rsa();
+ KeyPair keyB = rsa();
+ KeyPair keyC = rsa();
+ KeyRef keyRefA = new KeyRef("kref:v1:keyring:ca-version-switch:root-a");
+ KeyRef keyRefB = new KeyRef("kref:v1:keyring:ca-version-switch:root-b");
+ KeyRef keyRefC = new KeyRef("kref:v1:keyring:ca-version-switch:root-c");
+ Map keys = Map.of(keyRefA, keyA, keyRefB, keyB, keyRefC, keyC);
+ Path store = directory.resolve("store");
+ CertificateProfileRef versionOne;
+ CertificateProfileRef versionTwo;
+ byte[] persistedVersionOne;
+ byte[] persistedVersionOneHash;
+ PkiId caA;
+ PkiId caB;
+ PkiId caC;
+ PkiId credentialA;
+ PkiId credentialB;
+ PkiId credentialC;
+
+ try (PkiTestRuntime runtime = PkiTestRuntime.create(store, directory.resolve("bus.log"), keys)) {
+ versionOne = runtime.profileService().importProfile(versionOneDocument);
+ assertEquals(profileId, versionOne.profileId());
+ assertEquals(1, versionOne.profileVersion());
+ runtime.profileService().activateProfile(profileId, 1);
+
+ caA = createRoot(runtime, keyRefA, "CN=Version Switch Root A", profileId);
+ Credential issuedA = onlyCredential(runtime.caService(), caA);
+ assertCaProfileCredential("A", issuedA, versionOne, 1);
+ credentialA = issuedA.credentialId();
+
+ ImportedCertificateProfileVersion storedOne = runtime.profileService()
+ .getImportedVersion(profileId, 1).orElseThrow();
+ persistedVersionOne = storedOne.canonicalJson();
+ persistedVersionOneHash = storedOne.reference().canonicalSha256();
+
+ versionTwo = runtime.profileService().importProfile(versionTwoDocument);
+ assertEquals(profileId, versionTwo.profileId());
+ assertEquals(2, versionTwo.profileVersion());
+ assertTrue(!MessageDigest.isEqual(versionOne.canonicalSha256(), versionTwo.canonicalSha256()));
+ assertEquals(versionOne, runtime.profileService().getActiveReference(profileId).orElseThrow());
+
+ caB = createRoot(runtime, keyRefB, "CN=Version Switch Root B", profileId);
+ Credential issuedB = onlyCredential(runtime.caService(), caB);
+ assertCaProfileCredential("B", issuedB, versionOne, 1);
+ credentialB = issuedB.credentialId();
+
+ assertEquals(versionTwo, runtime.profileService().activateProfile(profileId, 2));
+ caC = createRoot(runtime, keyRefC, "CN=Version Switch Root C", profileId);
+ Credential issuedC = onlyCredential(runtime.caService(), caC);
+ assertCaProfileCredential("C", issuedC, versionTwo, 2);
+ credentialC = issuedC.credentialId();
+
+ assertCaProfileCredential("A-reread", runtime.store().getCredential(credentialA).orElseThrow(),
+ versionOne, 1);
+ assertCaProfileCredential("B-reread", runtime.store().getCredential(credentialB).orElseThrow(),
+ versionOne, 1);
+ ImportedCertificateProfileVersion unchanged = runtime.profileService()
+ .getImportedVersion(profileId, 1).orElseThrow();
+ assertArrayEquals(persistedVersionOne, unchanged.canonicalJson());
+ assertArrayEquals(persistedVersionOneHash, unchanged.reference().canonicalSha256());
+ }
+
+ try (PkiTestRuntime reopened = PkiTestRuntime.create(store, directory.resolve("reopened-bus.log"), keys)) {
+ assertEquals(versionTwo, reopened.profileService().getActiveReference(profileId).orElseThrow());
+ assertCaProfileCredential("A-restart", reopened.store().getCredential(credentialA).orElseThrow(),
+ versionOne, 1);
+ assertCaProfileCredential("B-restart", reopened.store().getCredential(credentialB).orElseThrow(),
+ versionOne, 1);
+ assertCaProfileCredential("C-restart", reopened.store().getCredential(credentialC).orElseThrow(),
+ versionTwo, 2);
+ ImportedCertificateProfileVersion unchanged = reopened.profileService()
+ .getImportedVersion(profileId, 1).orElseThrow();
+ assertArrayEquals(persistedVersionOne, unchanged.canonicalJson());
+ assertArrayEquals(persistedVersionOneHash, unchanged.reference().canonicalSha256());
+ }
+ System.out.println("...ok");
+ }
+
+ @Test
+ void intermediateCaProfileVersionSwitchA_B_C(@TempDir Path directory) throws Exception {
+ System.out.println("intermediateCaProfileVersionSwitchA_B_C");
+ String rootProfileId = "ca-version-switch-issuer-root";
+ String intermediateProfileId = "ca-version-switch-intermediate";
+ byte[] rootProfileDocument = caProfileDocument("root-ca", rootProfileId, 1, 1, 2);
+ byte[] versionOneDocument = caProfileDocument("intermediate-ca", intermediateProfileId, 1, 0, 0);
+ byte[] versionTwoDocument = caProfileDocument("intermediate-ca", intermediateProfileId, 2, 0, 1);
+ KeyPair rootKey = rsa();
+ KeyPair keyA = rsa();
+ KeyPair keyB = rsa();
+ KeyPair keyC = rsa();
+ KeyRef rootKeyRef = new KeyRef("kref:v1:keyring:ca-version-switch:issuer-root");
+ KeyRef keyRefA = new KeyRef("kref:v1:keyring:ca-version-switch:intermediate-a");
+ KeyRef keyRefB = new KeyRef("kref:v1:keyring:ca-version-switch:intermediate-b");
+ KeyRef keyRefC = new KeyRef("kref:v1:keyring:ca-version-switch:intermediate-c");
+ Map keys = Map.of(rootKeyRef, rootKey, keyRefA, keyA, keyRefB, keyB, keyRefC, keyC);
+ Path store = directory.resolve("store");
+ CertificateProfileRef rootProfile;
+ CertificateProfileRef versionOne;
+ CertificateProfileRef versionTwo;
+ byte[] persistedVersionOne;
+ byte[] persistedVersionOneHash;
+ PkiId rootId;
+ PkiId caA;
+ PkiId caB;
+ PkiId caC;
+ PkiId credentialA;
+ PkiId credentialB;
+ PkiId credentialC;
+
+ try (PkiTestRuntime runtime = PkiTestRuntime.create(store, directory.resolve("bus.log"), keys)) {
+ rootProfile = runtime.profileService().importProfile(rootProfileDocument);
+ runtime.profileService().activateProfile(rootProfileId, 1);
+ rootId = createRoot(runtime, rootKeyRef, "CN=Version Switch Issuer Root", rootProfileId);
+ Credential rootCredential = onlyCredential(runtime.caService(), rootId);
+ assertCaProfileCredential("issuer", rootCredential, rootProfile, 2);
+
+ versionOne = runtime.profileService().importProfile(versionOneDocument);
+ assertEquals(intermediateProfileId, versionOne.profileId());
+ assertEquals(1, versionOne.profileVersion());
+ runtime.profileService().activateProfile(intermediateProfileId, 1);
+
+ caA = createIntermediate(runtime, rootId, keyRefA, "CN=Version Switch Intermediate A",
+ intermediateProfileId);
+ Credential issuedA = onlyCredential(runtime.caService(), caA);
+ assertCaProfileCredential("A", issuedA, versionOne, 0);
+ credentialA = issuedA.credentialId();
+ assertCaProfileCredential("issuer-after-A", onlyCredential(runtime.caService(), rootId),
+ rootProfile, 2);
+
+ ImportedCertificateProfileVersion storedOne = runtime.profileService()
+ .getImportedVersion(intermediateProfileId, 1).orElseThrow();
+ persistedVersionOne = storedOne.canonicalJson();
+ persistedVersionOneHash = storedOne.reference().canonicalSha256();
+
+ versionTwo = runtime.profileService().importProfile(versionTwoDocument);
+ assertEquals(intermediateProfileId, versionTwo.profileId());
+ assertEquals(2, versionTwo.profileVersion());
+ assertTrue(!MessageDigest.isEqual(versionOne.canonicalSha256(), versionTwo.canonicalSha256()));
+ assertEquals(versionOne,
+ runtime.profileService().getActiveReference(intermediateProfileId).orElseThrow());
+
+ caB = createIntermediate(runtime, rootId, keyRefB, "CN=Version Switch Intermediate B",
+ intermediateProfileId);
+ Credential issuedB = onlyCredential(runtime.caService(), caB);
+ assertCaProfileCredential("B", issuedB, versionOne, 0);
+ credentialB = issuedB.credentialId();
+
+ assertEquals(versionTwo, runtime.profileService().activateProfile(intermediateProfileId, 2));
+ caC = createIntermediate(runtime, rootId, keyRefC, "CN=Version Switch Intermediate C",
+ intermediateProfileId);
+ Credential issuedC = onlyCredential(runtime.caService(), caC);
+ assertCaProfileCredential("C", issuedC, versionTwo, 1);
+ credentialC = issuedC.credentialId();
+
+ assertCaProfileCredential("A-reread", runtime.store().getCredential(credentialA).orElseThrow(),
+ versionOne, 0);
+ assertCaProfileCredential("B-reread", runtime.store().getCredential(credentialB).orElseThrow(),
+ versionOne, 0);
+ assertCaProfileCredential("issuer-reread", onlyCredential(runtime.caService(), rootId),
+ rootProfile, 2);
+ ImportedCertificateProfileVersion unchanged = runtime.profileService()
+ .getImportedVersion(intermediateProfileId, 1).orElseThrow();
+ assertArrayEquals(persistedVersionOne, unchanged.canonicalJson());
+ assertArrayEquals(persistedVersionOneHash, unchanged.reference().canonicalSha256());
+ }
+
+ try (PkiTestRuntime reopened = PkiTestRuntime.create(store, directory.resolve("reopened-bus.log"), keys)) {
+ assertEquals(versionTwo,
+ reopened.profileService().getActiveReference(intermediateProfileId).orElseThrow());
+ assertCaProfileCredential("A-restart", reopened.store().getCredential(credentialA).orElseThrow(),
+ versionOne, 0);
+ assertCaProfileCredential("B-restart", reopened.store().getCredential(credentialB).orElseThrow(),
+ versionOne, 0);
+ assertCaProfileCredential("C-restart", reopened.store().getCredential(credentialC).orElseThrow(),
+ versionTwo, 1);
+ assertCaProfileCredential("issuer-restart", onlyCredential(reopened.caService(), rootId),
+ rootProfile, 2);
+ ImportedCertificateProfileVersion unchanged = reopened.profileService()
+ .getImportedVersion(intermediateProfileId, 1).orElseThrow();
+ assertArrayEquals(persistedVersionOne, unchanged.canonicalJson());
+ assertArrayEquals(persistedVersionOneHash, unchanged.reference().canonicalSha256());
+ }
+ System.out.println("...ok");
+ }
+
+ @Test
+ void historicalIssuerFormatMismatchFailsBeforeProof(@TempDir Path directory) throws Exception {
+ KeyPair rootKey = rsa();
+ KeyPair intermediateKey = rsa();
+ KeyRef rootRef = new KeyRef("kref:v1:keyring:ca-profile:format-root");
+ KeyRef intermediateRef = new KeyRef("kref:v1:keyring:ca-profile:format-intermediate");
+ try (PkiTestRuntime runtime = PkiTestRuntime.create(directory, directory.resolve("bus.log"),
+ Map.of(rootRef, rootKey, intermediateRef, intermediateKey))) {
+ PkiId rootId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
+ new SubjectRef("CN=Format Root"), "root-ca", Optional.of(rootRef),
+ new SimpleAttributeSet()));
+ CertificateProfileRef wrongFormat = runtime.profileService().importProfile(builtIn("root-ca")
+ .replace("\"profileId\":\"root-ca\"", "\"profileId\":\"other-format-root\"")
+ .replace("\"formatId\":\"x509\"", "\"formatId\":\"other\"")
+ .getBytes(StandardCharsets.UTF_8));
+ CaRecord root = runtime.caService().getCa(rootId);
+ Credential original = root.caCredentials().get(0);
+ Credential mutated = new Credential(original.credentialId(), original.formatId(), original.issuerRef(),
+ original.subjectRef(), original.validity(), original.serialOrUniqueId(), original.publicKeyId(),
+ new CaProfileBinding(wrongFormat), original.status(), original.encoded(), original.attributes());
+ runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(),
+ root.subjectRef(), List.of(mutated)));
+
+ int signCount = runtime.submittedSignCount();
+ assertThrows(PkiException.class, () -> runtime.caService().createIntermediate(
+ new IntermediateCreateCommand(runtime.framework().formatId(), rootId,
+ new SubjectRef("CN=Format Intermediate"), "intermediate-ca",
+ Optional.of(intermediateRef), new SimpleAttributeSet())));
+ assertEquals(signCount, runtime.submittedSignCount());
+ assertEquals(1, runtime.store().listCas().size());
+ }
+ }
+
+ @Test
+ void invalidCaSubjectAndKeyAlgorithmFailBeforeProof(@TempDir Path directory) throws Exception {
+ KeyPair rootKey = rsa();
+ KeyPair dsaKey = keyPair("DSA");
+ KeyRef rootRef = new KeyRef("kref:v1:keyring:ca-profile:invalid-root");
+ KeyRef dsaRef = new KeyRef("kref:v1:keyring:ca-profile:invalid-dsa");
+ try (PkiTestRuntime runtime = PkiTestRuntime.create(directory, directory.resolve("bus.log"),
+ Map.of(rootRef, rootKey, dsaRef, dsaKey))) {
+ assertThrows(PkiException.class, () -> runtime.caService().createRoot(new CaCreateCommand(
+ runtime.framework().formatId(), new SubjectRef("O=Forbidden"), "root-ca",
+ Optional.of(rootRef), new SimpleAttributeSet())));
+ assertThrows(PkiException.class, () -> runtime.caService().createRoot(new CaCreateCommand(
+ runtime.framework().formatId(), new SubjectRef("CN=Unsupported Algorithm"), "root-ca",
+ Optional.of(dsaRef), new SimpleAttributeSet())));
+ assertEquals(0, runtime.submittedSignCount());
+ assertTrue(runtime.store().listCas().isEmpty());
+ }
+ }
+
+ @Test
+ void eachCaOperationResolvesItsActiveProfileExactlyOnce(@TempDir Path directory) throws Exception {
+ KeyPair rootKey = rsa();
+ KeyPair intermediateKey = rsa();
+ KeyRef rootRef = new KeyRef("kref:v1:keyring:ca-profile:lookup-root");
+ KeyRef intermediateRef = new KeyRef("kref:v1:keyring:ca-profile:lookup-intermediate");
+ byte[] rootCertificate;
+ try (PkiTestRuntime runtime = PkiTestRuntime.create(directory.resolve("main"),
+ directory.resolve("main-bus.log"), Map.of(rootRef, rootKey, intermediateRef, intermediateKey))) {
+ runtime.caService();
+ CountingProfileService profiles = new CountingProfileService(runtime.profileService());
+ CaService service = runtime.caService(profiles);
+
+ PkiId rootId = service.createRoot(new CaCreateCommand(runtime.framework().formatId(),
+ new SubjectRef("CN=Lookup Root"), "root-ca", Optional.of(rootRef),
+ new SimpleAttributeSet()));
+ profiles.assertAndReset("root-ca");
+ PkiId intermediateId = service.createIntermediate(new IntermediateCreateCommand(
+ runtime.framework().formatId(), rootId, new SubjectRef("CN=Lookup Intermediate"),
+ "intermediate-ca", Optional.of(intermediateRef), new SimpleAttributeSet()));
+ profiles.assertAndReset("intermediate-ca");
+ service.issueIntermediateCertificate(new IntermediateCertIssueCommand(runtime.framework().formatId(),
+ rootId, intermediateId, "intermediate-ca", Optional.empty(), new SimpleAttributeSet()));
+ profiles.assertAndReset("intermediate-ca");
+ rootCertificate = service.getCa(rootId).caCredentials().get(0).encoded().bytes();
+ }
+ try (PkiTestRuntime target = PkiTestRuntime.create(directory.resolve("import"),
+ directory.resolve("import-bus.log"), Map.of(rootRef, rootKey))) {
+ target.caService();
+ CountingProfileService profiles = new CountingProfileService(target.profileService());
+ CaService service = target.caService(profiles);
+ service.importRoot(new CaImportCommand(target.framework().formatId(), new SubjectRef("CN=Lookup Root"),
+ "root-ca", rootRef, new EncodedObject(Encoding.DER, rootCertificate),
+ new SimpleAttributeSet()));
+ profiles.assertAndReset("root-ca");
+ }
+ }
+
+
+ @Test
+ void rootCannotBeUsedAsAdditionalIntermediateSubject(@TempDir Path directory) throws Exception {
+ KeyPair rootKey = rsa();
+ KeyRef rootRef = new KeyRef("kref:v1:keyring:ca-profile:root-subject");
+ try (PkiTestRuntime runtime = PkiTestRuntime.create(directory, directory.resolve("bus.log"),
+ Map.of(rootRef, rootKey))) {
+ PkiId rootId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
+ new SubjectRef("CN=Root Subject"), "root-ca", Optional.of(rootRef),
+ new SimpleAttributeSet()));
+ int signCount = runtime.submittedSignCount();
+ assertThrows(PkiException.class, () -> runtime.caService().issueIntermediateCertificate(
+ new IntermediateCertIssueCommand(runtime.framework().formatId(), rootId, rootId,
+ "intermediate-ca", Optional.empty(), new SimpleAttributeSet())));
+ assertEquals(signCount, runtime.submittedSignCount());
+ assertEquals(1, runtime.caService().getCa(rootId).caCredentials().size());
+ }
+ }
+
+ @Test
+ void invalidRequestedIntermediateValidityFailsBeforeProof(@TempDir Path directory) throws Exception {
+ KeyPair rootKey = rsa();
+ KeyPair intermediateKey = rsa();
+ KeyRef rootRef = new KeyRef("kref:v1:keyring:ca-profile:validity-root");
+ KeyRef intermediateRef = new KeyRef("kref:v1:keyring:ca-profile:validity-intermediate");
+ try (PkiTestRuntime runtime = PkiTestRuntime.create(directory, directory.resolve("bus.log"),
+ Map.of(rootRef, rootKey, intermediateRef, intermediateKey))) {
+ PkiId rootId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
+ new SubjectRef("CN=Validity Root"), "root-ca", Optional.of(rootRef),
+ new SimpleAttributeSet()));
+ PkiId intermediateId = runtime.caService().createIntermediate(new IntermediateCreateCommand(
+ runtime.framework().formatId(), rootId, new SubjectRef("CN=Validity Intermediate"),
+ "intermediate-ca", Optional.of(intermediateRef), new SimpleAttributeSet()));
+ int signCount = runtime.submittedSignCount();
+ Validity invalid = new Validity(Instant.EPOCH, Instant.EPOCH.plusSeconds(3_153_600_000L));
+
+ assertThrows(PkiException.class, () -> runtime.caService().issueIntermediateCertificate(
+ new IntermediateCertIssueCommand(runtime.framework().formatId(), rootId, intermediateId,
+ "intermediate-ca", Optional.of(invalid), new SimpleAttributeSet())));
+ assertEquals(signCount, runtime.submittedSignCount());
+ assertEquals(1, runtime.caService().getCa(intermediateId).caCredentials().size());
+ }
+ }
+
+ @Test
+ void generatedRootCanBeImportedAgainstTheSameActivatedProfile(@TempDir Path directory) throws Exception {
+ KeyPair rootKey = rsa();
+ KeyRef rootRef = new KeyRef("kref:v1:keyring:ca-profile:import");
+ byte[] encoded;
+ try (PkiTestRuntime source = PkiTestRuntime.create(directory.resolve("source"),
+ directory.resolve("source-bus.log"), Map.of(rootRef, rootKey))) {
+ PkiId rootId = source.caService().createRoot(new CaCreateCommand(source.framework().formatId(),
+ new SubjectRef("CN=Imported Root"), "root-ca", Optional.of(rootRef),
+ new SimpleAttributeSet()));
+ encoded = source.caService().getCa(rootId).caCredentials().get(0).encoded().bytes();
+ }
+ try (PkiTestRuntime target = PkiTestRuntime.create(directory.resolve("target"),
+ directory.resolve("target-bus.log"), Map.of(rootRef, rootKey))) {
+ PkiId imported = target.caService().importRoot(new CaImportCommand(target.framework().formatId(),
+ new SubjectRef("CN=Imported Root"), "root-ca", rootRef,
+ new EncodedObject(Encoding.DER, encoded), new SimpleAttributeSet()));
+ Credential credential = target.caService().getCa(imported).caCredentials().get(0);
+ assertEquals(target.profileService().getActiveReference("root-ca").orElseThrow(),
+ ((CaProfileBinding) credential.profileBinding()).reference());
+ }
+ }
+
+ @ParameterizedTest
+ @EnumSource(ImportMutation.class)
+ void importedRootMutationFailsBeforePersistence(ImportMutation mutation, @TempDir Path directory)
+ throws Exception {
+ KeyPair rootKey = rsa();
+ KeyRef rootRef = new KeyRef("kref:v1:keyring:ca-profile:import-mutation");
+ byte[] encoded;
+ try (PkiTestRuntime source = PkiTestRuntime.create(directory.resolve("source"),
+ directory.resolve("source-bus.log"), Map.of(rootRef, rootKey))) {
+ PkiId rootId = source.caService().createRoot(new CaCreateCommand(source.framework().formatId(),
+ new SubjectRef("CN=Import Mutation Root"), "root-ca", Optional.of(rootRef),
+ new SimpleAttributeSet()));
+ Credential sourceCredential = source.caService().getCa(rootId).caCredentials().get(0);
+ encoded = mutation.mutate(sourceCredential, rootKey);
+ }
+ try (PkiTestRuntime target = PkiTestRuntime.create(directory.resolve("target"),
+ directory.resolve("target-bus.log"), Map.of(rootRef, rootKey))) {
+ CaImportCommand command = mutation.command(target, rootRef, encoded);
+ assertThrows(PkiException.class, () -> target.caService().importRoot(command));
+ assertTrue(target.store().listCas().isEmpty());
+ assertEquals(0, target.submittedSignCount());
+ }
+ }
+
+ @ParameterizedTest
+ @EnumSource(MetadataMutation.class)
+ void intermediateBackendMetadataMutationFailsBeforePersistence(MetadataMutation mutation,
+ @TempDir Path directory) throws Exception {
+ KeyPair rootKey = rsa();
+ KeyPair intermediateKey = rsa();
+ KeyRef rootRef = new KeyRef("kref:v1:keyring:ca-profile:mutation-root");
+ KeyRef intermediateRef = new KeyRef("kref:v1:keyring:ca-profile:mutation-intermediate");
+ try (PkiTestRuntime runtime = PkiTestRuntime.create(directory, directory.resolve("bus.log"),
+ Map.of(rootRef, rootKey, intermediateRef, intermediateKey))) {
+ PkiId rootId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
+ new SubjectRef("CN=Mutation Root"), "root-ca", Optional.of(rootRef),
+ new SimpleAttributeSet()));
+ CredentialIssuerBackend delegate = runtime.issuerBackend();
+ CredentialIssuerBackend backend = new CredentialIssuerBackend() {
+ @Override
+ public CredentialBundle issueEndEntity(ValidatedCertificateRequest request,
+ EncodedObject issuerCertificate, KeyRef issuerKeyRef, java.math.BigInteger serial) {
+ return delegate.issueEndEntity(request, issuerCertificate, issuerKeyRef, serial);
+ }
+
+ @Override
+ public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest request,
+ EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
+ return mutation.apply(delegate.issueIntermediateCertificate(request, issuerCertificate,
+ issuerKeyRef), rootKey);
+ }
+ };
+ assertThrows(PkiException.class, () -> runtime.caService(backend).createIntermediate(
+ new IntermediateCreateCommand(runtime.framework().formatId(), rootId,
+ new SubjectRef("CN=Mutation Intermediate"), "intermediate-ca",
+ Optional.of(intermediateRef), new SimpleAttributeSet())));
+ assertEquals(1, runtime.store().listCas().size());
+ }
+ }
+
+ private static void assertCaCertificate(Credential credential, int pathLength) throws Exception {
+ X509CertificateHolder holder = new X509CertificateHolder(credential.encoded().bytes());
+ BasicConstraints constraints = BasicConstraints.getInstance(
+ holder.getExtension(Extension.basicConstraints).getParsedValue());
+ KeyUsage usage = KeyUsage.getInstance(holder.getExtension(Extension.keyUsage).getParsedValue());
+ assertTrue(holder.getExtension(Extension.basicConstraints).isCritical());
+ assertTrue(holder.getExtension(Extension.keyUsage).isCritical());
+ assertEquals(pathLength, constraints.getPathLenConstraint().intValueExact());
+ assertTrue(usage.hasUsages(KeyUsage.keyCertSign | KeyUsage.cRLSign));
+ assertEquals(2, holder.getExtensions().getExtensionOIDs().length);
+ }
+
+ private static void assertCaProfileCredential(String label, Credential credential,
+ CertificateProfileRef expectedProfile, int pathLength) throws Exception {
+ assertTrue(credential.profileBinding() instanceof CaProfileBinding);
+ assertEquals(expectedProfile, ((CaProfileBinding) credential.profileBinding()).reference());
+ assertCaCertificate(credential, pathLength);
+ System.out.println("..." + label + " profileVersion=" + expectedProfile.profileVersion()
+ + " hash=" + abbreviatedHash(expectedProfile)
+ + " credentialId=" + credential.credentialId().value()
+ + " pathLength=" + pathLength);
+ }
+
+ private static PkiId createRoot(PkiTestRuntime runtime, KeyRef keyRef, String subject, String profileId) {
+ return runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
+ new SubjectRef(subject), profileId, Optional.of(keyRef), new SimpleAttributeSet()));
+ }
+
+ private static PkiId createIntermediate(PkiTestRuntime runtime, PkiId issuerId, KeyRef keyRef,
+ String subject, String profileId) {
+ return runtime.caService().createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(),
+ issuerId, new SubjectRef(subject), profileId, Optional.of(keyRef), new SimpleAttributeSet()));
+ }
+
+ private static Credential onlyCredential(CaService service, PkiId caId) {
+ CaRecord ca = service.getCa(caId);
+ assertEquals(1, ca.caCredentials().size());
+ return ca.caCredentials().get(0);
+ }
+
+ private static byte[] caProfileDocument(String builtInProfileId, String profileId,
+ long profileVersion, int originalPathLength, int pathLength) {
+ return builtIn(builtInProfileId)
+ .replace("\"profileId\":\"" + builtInProfileId + "\"",
+ "\"profileId\":\"" + profileId + "\"")
+ .replace("\"profileVersion\":1", "\"profileVersion\":" + profileVersion)
+ .replace("\"pathLengthConstraint\":" + originalPathLength,
+ "\"pathLengthConstraint\":" + pathLength)
+ .getBytes(StandardCharsets.UTF_8);
+ }
+
+ private static String abbreviatedHash(CertificateProfileRef reference) {
+ byte[] hash = reference.canonicalSha256();
+ return HexFormat.of().formatHex(hash, 0, 6) + "...";
+ }
+
+ private static String builtIn(String profileId) {
+ return BuiltInCertificateProfileCatalog.load(CaProfileIssuanceEnforcementTest.class.getClassLoader())
+ .stream().filter(template -> template.definition().profileId().equals(profileId))
+ .map(template -> new String(template.canonicalJson(), StandardCharsets.UTF_8))
+ .findFirst().orElseThrow();
+ }
+
+ private static KeyPair rsa() throws Exception {
+ return keyPair("RSA");
+ }
+
+ private static KeyPair keyPair(String algorithm) throws Exception {
+ KeyPairGenerator generator = KeyPairGenerator.getInstance(algorithm);
+ generator.initialize(2048);
+ return generator.generateKeyPair();
+ }
+
+ private static final class CountingProfileService implements ProfileService {
+ private final ProfileService delegate;
+ private final AtomicInteger activeLookups = new AtomicInteger();
+ private String lastProfileId;
+
+ private CountingProfileService(ProfileService delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public CertificateProfileRef importProfile(byte[] jsonDocument) {
+ return delegate.importProfile(jsonDocument);
+ }
+
+ @Override
+ public CertificateProfileRef importProfile(java.io.InputStream jsonDocument) {
+ return delegate.importProfile(jsonDocument);
+ }
+
+ @Override
+ public CertificateProfileRef importBuiltIn(BuiltInCertificateProfileTemplate template) {
+ return delegate.importBuiltIn(template);
+ }
+
+ @Override
+ public CertificateProfileRef activateProfile(String profileId, long profileVersion) {
+ return delegate.activateProfile(profileId, profileVersion);
+ }
+
+ @Override
+ public ActiveCertificateProfile requireActiveProfile(String profileId) {
+ lastProfileId = profileId;
+ activeLookups.incrementAndGet();
+ return delegate.requireActiveProfile(profileId);
+ }
+
+ @Override
+ public Optional getImportedVersion(String profileId,
+ long profileVersion) {
+ return delegate.getImportedVersion(profileId, profileVersion);
+ }
+
+ @Override
+ public List listImportedVersions(String profileId) {
+ return delegate.listImportedVersions(profileId);
+ }
+
+ @Override
+ public Optional getActiveReference(String profileId) {
+ return delegate.getActiveReference(profileId);
+ }
+
+ private void assertAndReset(String profileId) {
+ assertEquals(1, activeLookups.getAndSet(0));
+ assertEquals(profileId, lastProfileId);
+ lastProfileId = null;
+ }
+ }
+
+ private enum MetadataMutation {
+ PROFILE {
+ @Override Credential apply(Credential value, KeyPair issuerKey) {
+ return copy(value, value.serialOrUniqueId(), value.validity(),
+ new CaProfileBinding(new CertificateProfileRef("other", 1, new byte[32])));
+ }
+ },
+ SERIAL {
+ @Override Credential apply(Credential value, KeyPair issuerKey) {
+ return copy(value, value.serialOrUniqueId() + "1", value.validity(), value.profileBinding());
+ }
+ },
+ VALIDITY {
+ @Override Credential apply(Credential value, KeyPair issuerKey) {
+ return copy(value, value.serialOrUniqueId(),
+ new zeroecho.pki.api.Validity(value.validity().notBefore(),
+ value.validity().notAfter().minusSeconds(1)),
+ value.profileBinding());
+ }
+ },
+ SUBJECT {
+ @Override Credential apply(Credential value, KeyPair issuerKey) {
+ return new Credential(value.credentialId(), value.formatId(), value.issuerRef(),
+ new SubjectRef("CN=Substituted"), value.validity(), value.serialOrUniqueId(),
+ value.publicKeyId(), value.profileBinding(), value.status(), value.encoded(),
+ value.attributes());
+ }
+ },
+ ATTRIBUTES {
+ @Override Credential apply(Credential value, KeyPair issuerKey) {
+ zeroecho.pki.api.attr.AttributeSet attributes = SimpleAttributeSet.builder()
+ .put(new zeroecho.pki.api.attr.AttributeId("test.unexpected"),
+ new zeroecho.pki.api.attr.AttributeValue.StringValue("unexpected"))
+ .build();
+ return new Credential(value.credentialId(), value.formatId(), value.issuerRef(),
+ value.subjectRef(), value.validity(), value.serialOrUniqueId(), value.publicKeyId(),
+ value.profileBinding(), value.status(), value.encoded(), attributes);
+ }
+ },
+ EXTRA_EXTENSION {
+ @Override Credential apply(Credential value, KeyPair issuerKey) {
+ return rebuild(value, issuerKey, null, true);
+ }
+ },
+ SUBJECT_DER {
+ @Override Credential apply(Credential value, KeyPair issuerKey) {
+ X500Name alternate = new X500Name(new RDN[] {
+ new RDN(BCStyle.CN, new DERPrintableString("Mutation Intermediate"))
+ });
+ return rebuild(value, issuerKey, alternate, false);
+ }
+ };
+
+ abstract Credential apply(Credential value, KeyPair issuerKey);
+
+ private static Credential copy(Credential value, String serial, zeroecho.pki.api.Validity validity,
+ zeroecho.pki.api.credential.CredentialProfileBinding binding) {
+ return new Credential(value.credentialId(), value.formatId(), value.issuerRef(), value.subjectRef(),
+ validity, serial, value.publicKeyId(), binding, value.status(), value.encoded(),
+ value.attributes());
+ }
+
+ private static Credential rebuild(Credential value, KeyPair issuerKey, X500Name alternateSubject,
+ boolean extraExtension) {
+ return rebuild(value, issuerKey, alternateSubject, extraExtension, null, null, null);
+ }
+
+ private static Credential rebuild(Credential value, KeyPair issuerKey, X500Name alternateSubject,
+ boolean extraExtension, BasicConstraints alternateConstraints, KeyUsage alternateKeyUsage,
+ Date alternateNotAfter) {
+ try {
+ X509CertificateHolder original = new X509CertificateHolder(value.encoded().bytes());
+ X500Name subject = alternateSubject == null ? original.getSubject() : alternateSubject;
+ X509v3CertificateBuilder builder = new X509v3CertificateBuilder(original.getIssuer(),
+ original.getSerialNumber(), Date.from(original.getNotBefore().toInstant()),
+ alternateNotAfter == null ? Date.from(original.getNotAfter().toInstant()) : alternateNotAfter,
+ subject, original.getSubjectPublicKeyInfo());
+ if (alternateConstraints == null) {
+ builder.addExtension(original.getExtension(Extension.basicConstraints));
+ } else {
+ builder.addExtension(Extension.basicConstraints, true, alternateConstraints);
+ }
+ if (alternateKeyUsage == null) {
+ builder.addExtension(original.getExtension(Extension.keyUsage));
+ } else {
+ builder.addExtension(Extension.keyUsage, true, alternateKeyUsage);
+ }
+ if (extraExtension) {
+ builder.addExtension(Extension.subjectKeyIdentifier, false,
+ new DEROctetString(new byte[] { 1 }));
+ }
+ byte[] encoded = builder.build(new JcaContentSignerBuilder("SHA256withRSA")
+ .build(issuerKey.getPrivate())).getEncoded();
+ return new Credential(new PkiId("x509:" + sha256(encoded)), value.formatId(), value.issuerRef(),
+ value.subjectRef(), value.validity(), value.serialOrUniqueId(), value.publicKeyId(),
+ value.profileBinding(), value.status(), new EncodedObject(Encoding.DER, encoded),
+ value.attributes());
+ } catch (Exception exception) {
+ throw new IllegalStateException("test certificate mutation failed", exception);
+ }
+ }
+
+ private static String sha256(byte[] value) throws Exception {
+ return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value));
+ }
+ }
+
+ private enum ImportMutation {
+ SUBJECT {
+ @Override CaImportCommand command(PkiTestRuntime runtime, KeyRef keyRef, byte[] encoded) {
+ return importCommand(runtime, keyRef, encoded, new SubjectRef("CN=Substituted"), "root-ca");
+ }
+ },
+ KIND {
+ @Override CaImportCommand command(PkiTestRuntime runtime, KeyRef keyRef, byte[] encoded) {
+ return importCommand(runtime, keyRef, encoded, new SubjectRef("CN=Import Mutation Root"),
+ "intermediate-ca");
+ }
+ },
+ MALFORMED_DER {
+ @Override CaImportCommand command(PkiTestRuntime runtime, KeyRef keyRef, byte[] encoded) {
+ return importCommand(runtime, keyRef, new byte[] { 0x30, 0x01, 0x00 },
+ new SubjectRef("CN=Import Mutation Root"), "root-ca");
+ }
+ },
+ EXTRA_EXTENSION {
+ @Override CaImportCommand command(PkiTestRuntime runtime, KeyRef keyRef, byte[] encoded) {
+ return importCommand(runtime, keyRef, encoded, new SubjectRef("CN=Import Mutation Root"),
+ "root-ca");
+ }
+ @Override byte[] mutate(Credential credential, KeyPair issuerKey) {
+ return MetadataMutation.rebuild(credential, issuerKey, null, true).encoded().bytes();
+ }
+ },
+ BASIC_CONSTRAINTS {
+ @Override CaImportCommand command(PkiTestRuntime runtime, KeyRef keyRef, byte[] encoded) {
+ return importCommand(runtime, keyRef, encoded, new SubjectRef("CN=Import Mutation Root"),
+ "root-ca");
+ }
+ @Override byte[] mutate(Credential credential, KeyPair issuerKey) {
+ return MetadataMutation.rebuild(credential, issuerKey, null, false,
+ new BasicConstraints(false), null, null).encoded().bytes();
+ }
+ },
+ KEY_USAGE {
+ @Override CaImportCommand command(PkiTestRuntime runtime, KeyRef keyRef, byte[] encoded) {
+ return importCommand(runtime, keyRef, encoded, new SubjectRef("CN=Import Mutation Root"),
+ "root-ca");
+ }
+ @Override byte[] mutate(Credential credential, KeyPair issuerKey) {
+ return MetadataMutation.rebuild(credential, issuerKey, null, false, null,
+ new KeyUsage(KeyUsage.digitalSignature), null).encoded().bytes();
+ }
+ },
+ VALIDITY {
+ @Override CaImportCommand command(PkiTestRuntime runtime, KeyRef keyRef, byte[] encoded) {
+ return importCommand(runtime, keyRef, encoded, new SubjectRef("CN=Import Mutation Root"),
+ "root-ca");
+ }
+ @Override byte[] mutate(Credential credential, KeyPair issuerKey) {
+ Date extended = Date.from(credential.validity().notAfter().plusSeconds(1));
+ return MetadataMutation.rebuild(credential, issuerKey, null, false, null, null, extended)
+ .encoded().bytes();
+ }
+ };
+
+ abstract CaImportCommand command(PkiTestRuntime runtime, KeyRef keyRef, byte[] encoded);
+
+ byte[] mutate(Credential credential, KeyPair issuerKey) {
+ return credential.encoded().bytes();
+ }
+
+ private static CaImportCommand importCommand(PkiTestRuntime runtime, KeyRef keyRef, byte[] encoded,
+ SubjectRef subject, String profileId) {
+ return new CaImportCommand(runtime.framework().formatId(), subject, profileId, keyRef,
+ new EncodedObject(Encoding.DER, encoded), new SimpleAttributeSet());
+ }
+ }
+}
diff --git a/pki/src/test/java/zeroecho/pki/e2e/H7EndEntityAcceptanceE2eTest.java b/pki/src/test/java/zeroecho/pki/e2e/H7EndEntityAcceptanceE2eTest.java
index fdeb4fa..f375497 100644
--- a/pki/src/test/java/zeroecho/pki/e2e/H7EndEntityAcceptanceE2eTest.java
+++ b/pki/src/test/java/zeroecho/pki/e2e/H7EndEntityAcceptanceE2eTest.java
@@ -84,7 +84,7 @@ import zeroecho.pki.api.credential.CredentialStatus;
import zeroecho.pki.api.issuance.IssueEndEntityCommand;
import zeroecho.pki.api.request.CertificationRequest;
import zeroecho.pki.api.request.ParsedCertificationRequest;
-import zeroecho.pki.impl.core.ManagedCaIssuance;
+import zeroecho.pki.impl.core.ValidatedCaCertificateRequest;
import zeroecho.pki.impl.core.ValidatedCertificateRequest;
import zeroecho.pki.impl.core.attr.SimpleAttributeSet;
import zeroecho.pki.spi.framework.CredentialIssuerBackend;
@@ -114,15 +114,18 @@ final class H7EndEntityAcceptanceE2eTest {
Map.of(rootKeyRef, rootKey))) {
List templates = BuiltInCertificateProfileCatalog.load(
H7EndEntityAcceptanceE2eTest.class.getClassLoader());
- assertEquals(4, templates.size());
+ assertEquals(6, templates.size());
for (BuiltInCertificateProfileTemplate template : templates) {
String profileId = template.definition().profileId();
assertTrue(runtime.profileService().getImportedVersion(profileId, 1).isEmpty());
}
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
- new SubjectRef("CN=H7 Built-in Root"), "default", Optional.of(rootKeyRef),
+ new SubjectRef("CN=H7 Built-in Root"), "root-ca", Optional.of(rootKeyRef),
new SimpleAttributeSet()));
- for (BuiltInCertificateProfileTemplate template : templates) {
+ for (BuiltInCertificateProfileTemplate template : templates.stream()
+ .filter(template -> template.definition().certificateType()
+ == zeroecho.pki.api.profile.CertificateProfileKind.END_ENTITY)
+ .toList()) {
String profileId = template.definition().profileId();
CertificateProfileRef imported = runtime.profileService().importBuiltIn(template);
assertTrue(runtime.profileService().getActiveReference(profileId).isEmpty());
@@ -170,7 +173,7 @@ final class H7EndEntityAcceptanceE2eTest {
List profiles = acceptanceProfileDocuments();
profiles.forEach(runtime::importAndActivate);
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
- new SubjectRef("CN=H7 Acceptance Root"), "default", Optional.of(rootKeyRef),
+ new SubjectRef("CN=H7 Acceptance Root"), "root-ca", Optional.of(rootKeyRef),
new SimpleAttributeSet()));
CredentialIssuerBackend serialCapturingBackend = serialCapturingBackend(runtime.issuerBackend(),
@@ -278,7 +281,7 @@ final class H7EndEntityAcceptanceE2eTest {
Map.of(rootKeyRef, rootKey))) {
acceptanceProfileDocuments().forEach(runtime::importAndActivate);
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
- new SubjectRef("CN=H7 Rejection Root"), "default", Optional.of(rootKeyRef),
+ new SubjectRef("CN=H7 Rejection Root"), "root-ca", Optional.of(rootKeyRef),
new SimpleAttributeSet()));
assertProfileRejected(runtime, rootCaId, leafKey, "h7-dns",
@@ -323,7 +326,7 @@ final class H7EndEntityAcceptanceE2eTest {
runtime.importAndActivate(H7ProfileDocuments.backendMutationProfile());
runtime.importAndActivate(H7ProfileDocuments.noSanOrEkuProfile());
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
- new SubjectRef("CN=H7 Mutation Root"), "default", Optional.of(rootKeyRef),
+ new SubjectRef("CN=H7 Mutation Root"), "root-ca", Optional.of(rootKeyRef),
new SimpleAttributeSet()));
ParsedCertificationRequest request = parse(runtime, leafKey,
new X500Name("CN=Mutation Leaf,O=Example"),
@@ -444,8 +447,8 @@ final class H7EndEntityAcceptanceE2eTest {
}
@Override
- public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) {
- return delegate.issueIntermediateCertificate(issuance);
+ public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
+ return delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
}
};
}
@@ -542,8 +545,8 @@ final class H7EndEntityAcceptanceE2eTest {
}
@Override
- public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) {
- return delegate.issueIntermediateCertificate(issuance);
+ public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
+ return delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
}
};
}
@@ -598,8 +601,8 @@ final class H7EndEntityAcceptanceE2eTest {
}
@Override
- public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) {
- return delegate.issueIntermediateCertificate(issuance);
+ public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
+ return delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
}
};
}
diff --git a/pki/src/test/java/zeroecho/pki/e2e/H7EndEntityCsrRejectionE2eTest.java b/pki/src/test/java/zeroecho/pki/e2e/H7EndEntityCsrRejectionE2eTest.java
index 4b371f0..801f23b 100644
--- a/pki/src/test/java/zeroecho/pki/e2e/H7EndEntityCsrRejectionE2eTest.java
+++ b/pki/src/test/java/zeroecho/pki/e2e/H7EndEntityCsrRejectionE2eTest.java
@@ -60,7 +60,7 @@ import zeroecho.pki.api.issuance.IssueEndEntityCommand;
import zeroecho.pki.api.request.CertificationRequest;
import zeroecho.pki.api.request.ParsedCertificationRequest;
import zeroecho.pki.impl.core.DefaultIssuanceService;
-import zeroecho.pki.impl.core.ManagedCaIssuance;
+import zeroecho.pki.impl.core.ValidatedCaCertificateRequest;
import zeroecho.pki.impl.core.ValidatedCertificateRequest;
import zeroecho.pki.impl.core.attr.SimpleAttributeSet;
import zeroecho.pki.impl.framework.x509.bc.BcX509Attributes;
@@ -294,7 +294,7 @@ final class H7EndEntityCsrRejectionE2eTest {
runtime.importAndActivate(H7ProfileDocuments.uriProfile());
runtime.importAndActivate(H7ProfileDocuments.subjectEmailAndRfc822Profile());
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
- new SubjectRef("CN=H7 CSR Rejection Root"), "default", Optional.of(rootRef),
+ new SubjectRef("CN=H7 CSR Rejection Root"), "root-ca", Optional.of(rootRef),
new SimpleAttributeSet()));
PKCS10CertificationRequest valid = signedSanCsr(leafKey, new X500Name("CN=Template"),
List.of(dns("template.example.com")));
@@ -398,8 +398,8 @@ final class H7EndEntityCsrRejectionE2eTest {
}
@Override
- public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) {
- return delegate.issueIntermediateCertificate(issuance);
+ public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
+ return delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
}
};
}
diff --git a/pki/src/test/java/zeroecho/pki/e2e/PkiCoreE2eTest.java b/pki/src/test/java/zeroecho/pki/e2e/PkiCoreE2eTest.java
index 655ff57..2504f50 100644
--- a/pki/src/test/java/zeroecho/pki/e2e/PkiCoreE2eTest.java
+++ b/pki/src/test/java/zeroecho/pki/e2e/PkiCoreE2eTest.java
@@ -101,7 +101,7 @@ import zeroecho.pki.api.status.StatusObject;
import zeroecho.pki.api.status.StatusObjectGenerateCommand;
import zeroecho.pki.api.status.StatusObjectType;
import zeroecho.pki.impl.core.attr.SimpleAttributeSet;
-import zeroecho.pki.impl.core.ManagedCaIssuance;
+import zeroecho.pki.impl.core.ValidatedCaCertificateRequest;
import zeroecho.pki.impl.core.ValidatedCertificateRequest;
import zeroecho.pki.spi.framework.CredentialIssuerBackend;
import zeroecho.pki.testkit.PkiTestRuntime;
@@ -140,7 +140,7 @@ public final class PkiCoreE2eTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"), keys)) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
- new SubjectRef("CN=Matrix Root"), "default", Optional.of(rootKeyRef), emptyAttributes()));
+ new SubjectRef("CN=Matrix Root"), "root-ca", Optional.of(rootKeyRef), emptyAttributes()));
Credential usable = runtime.caService().getCa(rootCaId).caCredentials().get(0);
Credential unusable = copyWithId(usable, new PkiId("credential:matrix-unusable"));
CaRecord root = runtime.caService().getCa(rootCaId);
@@ -166,13 +166,13 @@ public final class PkiCoreE2eTest {
resolved.clear();
PkiId intermediateCaId = caService.createIntermediate(new IntermediateCreateCommand(
- runtime.framework().formatId(), rootCaId, new SubjectRef("CN=Matrix Intermediate"), "default",
+ runtime.framework().formatId(), rootCaId, new SubjectRef("CN=Matrix Intermediate"), "intermediate-ca",
Optional.of(intermediateKeyRef), emptyAttributes()));
assertEquals(List.of(unusable.credentialId(), usable.credentialId()), List.copyOf(resolved));
resolved.clear();
caService.issueIntermediateCertificate(new IntermediateCertIssueCommand(runtime.framework().formatId(),
- rootCaId, intermediateCaId, "default", Optional.empty(), emptyAttributes()));
+ rootCaId, intermediateCaId, "intermediate-ca", Optional.empty(), emptyAttributes()));
assertEquals(List.of(unusable.credentialId(), usable.credentialId()), List.copyOf(resolved));
resolved.clear();
@@ -200,10 +200,10 @@ public final class PkiCoreE2eTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"), keys)) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
- new SubjectRef("CN=H6 Root"), "default", Optional.of(rootKeyRef), emptyAttributes()));
+ new SubjectRef("CN=H6 Root"), "root-ca", Optional.of(rootKeyRef), emptyAttributes()));
PkiId intermediateCaId = runtime.caService()
.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootCaId,
- new SubjectRef("CN=H6 Intermediate"), "default", Optional.of(intermediateKeyRef),
+ new SubjectRef("CN=H6 Intermediate"), "intermediate-ca", Optional.of(intermediateKeyRef),
emptyAttributes()));
PkiId rootCredentialId = runtime.caService().getCa(rootCaId).caCredentials().get(0).credentialId();
runtime.revocationService().revokePermanently(new RevocationCommand.RevokePermanently(rootCredentialId,
@@ -221,12 +221,12 @@ public final class PkiCoreE2eTest {
assertThrows(PkiException.class,
() -> runtime.caService()
.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootCaId,
- new SubjectRef("CN=H6 Rejected"), "default",
+ new SubjectRef("CN=H6 Rejected"), "intermediate-ca",
Optional.of(nextIntermediateKeyRef), emptyAttributes())));
assertThrows(PkiException.class,
() -> runtime.caService().issueIntermediateCertificate(
new IntermediateCertIssueCommand(runtime.framework().formatId(), rootCaId,
- intermediateCaId, "default", Optional.empty(), emptyAttributes())));
+ intermediateCaId, "intermediate-ca", Optional.empty(), emptyAttributes())));
assertThrows(PkiException.class,
() -> runtime.statusObjectService().generate(new StatusObjectGenerateCommand(rootCaId,
StatusObjectType.CRL, runtime.framework().formatId(), emptyAttributes())));
@@ -265,7 +265,7 @@ public final class PkiCoreE2eTest {
// Create ROOT CA (self-signed).
PkiId rootCaId = caSvc.createRoot(new CaCreateCommand(runtime.framework().formatId(),
- new SubjectRef("CN=Root"), "default", Optional.of(rootKeyRef), emptyAttributes()));
+ new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef), emptyAttributes()));
System.out.println("...rootCaId=" + rootCaId.value());
// CSR for end entity.
@@ -320,9 +320,9 @@ public final class PkiCoreE2eTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"), keys)) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
- new SubjectRef("CN=Rejected Root"), "default", Optional.of(rootKeyRef), emptyAttributes()));
+ new SubjectRef("CN=Rejected Root"), "root-ca", Optional.of(rootKeyRef), emptyAttributes()));
PkiId intermediateCaId = runtime.caService().createIntermediate(new IntermediateCreateCommand(
- runtime.framework().formatId(), rootCaId, new SubjectRef("CN=Rejected Intermediate"), "default",
+ runtime.framework().formatId(), rootCaId, new SubjectRef("CN=Rejected Intermediate"), "intermediate-ca",
Optional.of(intermediateKeyRef), emptyAttributes()));
Credential rootCredential = runtime.caService().getCa(rootCaId).caCredentials().get(0);
EffectiveCredentialStatusResolver resolver = scriptedResolver(credential -> status, resolutionFailure);
@@ -343,11 +343,11 @@ public final class PkiCoreE2eTest {
Optional.empty())));
assertThrows(PkiException.class,
() -> caService.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(),
- rootCaId, new SubjectRef("CN=Rejected Next"), "default",
+ rootCaId, new SubjectRef("CN=Rejected Next"), "intermediate-ca",
Optional.of(nextIntermediateKeyRef), emptyAttributes())));
assertThrows(PkiException.class,
() -> caService.issueIntermediateCertificate(new IntermediateCertIssueCommand(
- runtime.framework().formatId(), rootCaId, intermediateCaId, "default", Optional.empty(),
+ runtime.framework().formatId(), rootCaId, intermediateCaId, "intermediate-ca", Optional.empty(),
emptyAttributes())));
assertThrows(PkiException.class,
() -> statusService.generate(new StatusObjectGenerateCommand(rootCaId, StatusObjectType.CRL,
@@ -420,9 +420,9 @@ public final class PkiCoreE2eTest {
}
@Override
- public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) {
+ public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
intermediateCalls.incrementAndGet();
- return delegate.issueIntermediateCertificate(issuance);
+ return delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
}
}
diff --git a/pki/src/test/java/zeroecho/pki/e2e/PkiProofGateE2eTest.java b/pki/src/test/java/zeroecho/pki/e2e/PkiProofGateE2eTest.java
index 1d72175..14b2cc8 100644
--- a/pki/src/test/java/zeroecho/pki/e2e/PkiProofGateE2eTest.java
+++ b/pki/src/test/java/zeroecho/pki/e2e/PkiProofGateE2eTest.java
@@ -114,7 +114,7 @@ import zeroecho.pki.api.request.ParsedCertificationRequest;
import zeroecho.pki.api.request.ProofOfPossessionResult;
import zeroecho.pki.api.request.ProofOfPossessionStatus;
import zeroecho.pki.impl.core.DefaultIssuanceService;
-import zeroecho.pki.impl.core.ManagedCaIssuance;
+import zeroecho.pki.impl.core.ValidatedCaCertificateRequest;
import zeroecho.pki.impl.core.ValidatedCertificateRequest;
import zeroecho.pki.impl.core.attr.SimpleAttributeSet;
import zeroecho.pki.impl.framework.x509.bc.BcX509Attributes;
@@ -134,10 +134,10 @@ final class PkiProofGateE2eTest {
System.out.println("issuerBackendApiRequiresOpaqueGateProducedInputs");
assertTrue(Modifier.isFinal(ValidatedCertificateRequest.class.getModifiers()));
- assertTrue(Modifier.isFinal(ManagedCaIssuance.class.getModifiers()));
+ assertTrue(Modifier.isFinal(ValidatedCaCertificateRequest.class.getModifiers()));
assertTrue(java.util.Arrays.stream(ValidatedCertificateRequest.class.getDeclaredConstructors())
.noneMatch(constructor -> Modifier.isPublic(constructor.getModifiers())));
- assertTrue(java.util.Arrays.stream(ManagedCaIssuance.class.getDeclaredConstructors())
+ assertTrue(java.util.Arrays.stream(ValidatedCaCertificateRequest.class.getDeclaredConstructors())
.noneMatch(constructor -> Modifier.isPublic(constructor.getModifiers())));
Class> managedKeyProof = Class.forName("zeroecho.pki.impl.core.CaProofGate$ManagedKeyProof");
assertTrue(java.util.Arrays.stream(managedKeyProof.getDeclaredConstructors())
@@ -153,7 +153,8 @@ final class PkiProofGateE2eTest {
.findFirst().orElseThrow();
assertArrayEquals(new Class>[] { ValidatedCertificateRequest.class, EncodedObject.class, KeyRef.class,
BigInteger.class }, endEntity.getParameterTypes());
- assertArrayEquals(new Class>[] { ManagedCaIssuance.class }, intermediate.getParameterTypes());
+ assertArrayEquals(new Class>[] { ValidatedCaCertificateRequest.class, EncodedObject.class, KeyRef.class },
+ intermediate.getParameterTypes());
assertTrue(java.util.Arrays.stream(BcX509CredentialIssuerBackend.class.getMethods())
.filter(method -> method.getName().startsWith("issue"))
.noneMatch(method -> java.util.Arrays.asList(method.getParameterTypes())
@@ -189,7 +190,7 @@ final class PkiProofGateE2eTest {
assertEquals(0, counting.endEntityCalls.get());
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
- new SubjectRef("CN=Root"), "default", Optional.of(rootKeyRef), new SimpleAttributeSet()));
+ new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef), new SimpleAttributeSet()));
CredentialBundle issued = issuance.issueEndEntity(new IssueEndEntityCommand(rootCaId, valid, "default",
Optional.empty()));
assertEquals(1, counting.endEntityCalls.get());
@@ -199,14 +200,14 @@ final class PkiProofGateE2eTest {
CaService caService = runtime.caService(counting);
caService.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootCaId,
- new SubjectRef("CN=Intermediate"), "default", Optional.of(subjectKeyRef),
+ new SubjectRef("CN=Intermediate"), "intermediate-ca", Optional.of(subjectKeyRef),
new SimpleAttributeSet()));
assertEquals(1, counting.intermediateCalls.get());
runtime.replaceResolvedKey(subjectKeyRef, wrongKey.getPublic());
assertThrows(PkiException.class,
() -> caService.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(),
- rootCaId, new SubjectRef("CN=Rejected"), "default", Optional.of(subjectKeyRef),
+ rootCaId, new SubjectRef("CN=Rejected"), "intermediate-ca", Optional.of(subjectKeyRef),
new SimpleAttributeSet())));
assertEquals(1, counting.intermediateCalls.get());
@@ -222,12 +223,12 @@ final class PkiProofGateE2eTest {
System.out.println("unsupportedIssuanceVariantsFailWithoutSideEffects");
try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"), Map.of())) {
- int auditCount = runtime.auditSink().snapshot().size();
CountingIssuerBackend counting = new CountingIssuerBackend(runtime.issuerBackend());
DefaultIssuanceService issuance = new DefaultIssuanceService(runtime.store(), runtime.framework(),
counting, runtime.auditSink(), runtime.statusResolver(), runtime.profileService(),
Clock.systemUTC());
CaService caService = runtime.caService(counting);
+ int auditCount = runtime.auditSink().snapshot().size();
ParsedCertificationRequest request = new ParsedCertificationRequest(new PkiId("csr:unsupported"),
runtime.framework().formatId(), new SubjectRef("CN=Unsupported"),
new EncodedObject(Encoding.DER, new byte[] { 1 }), Optional.empty(), Optional.empty(),
@@ -282,7 +283,7 @@ final class PkiProofGateE2eTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"),
Map.of(rootKeyRef, rootKey, subjectKeyRef, subjectKey))) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
- new SubjectRef("CN=Root"), "default", Optional.of(rootKeyRef), new SimpleAttributeSet()));
+ new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef), new SimpleAttributeSet()));
PKCS10CertificationRequest validCsr = makeCsr(subjectKey, subjectKey, "CN=Subject");
ParsedCertificationRequest valid = parse(runtime, validCsr);
ParsedCertificationRequest pss = parse(runtime,
@@ -376,7 +377,7 @@ final class PkiProofGateE2eTest {
return new ProofOfPossessionResult(status, Optional.empty());
})) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
- new SubjectRef("CN=Root"), "default", Optional.of(rootKeyRef), new SimpleAttributeSet()));
+ new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef), new SimpleAttributeSet()));
ParsedCertificationRequest parsed = parse(runtime, makeCsr(subjectKey, subjectKey, "CN=Subject"));
assertThrows(PkiException.class, () -> issue(runtime, rootCaId, parsed));
assertTrue(required.get());
@@ -411,7 +412,7 @@ final class PkiProofGateE2eTest {
return result;
})) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
- new SubjectRef("CN=Root"), "default", Optional.of(rootKeyRef), new SimpleAttributeSet()));
+ new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef), new SimpleAttributeSet()));
ParsedCertificationRequest parsed = parse(runtime, makeCsr(subjectKey, subjectKey, "CN=Subject"));
byte[] callerCsr = csrDer(parsed);
byte[] callerSpki = parsed.publicKeyInfo().bytes();
@@ -449,7 +450,7 @@ final class PkiProofGateE2eTest {
new BcX509ProofOfPossessionVerifier())) {
assertThrows(PkiException.class,
() -> runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
- new SubjectRef("CN=Root"), "default", Optional.of(rootKeyRef),
+ new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef),
new SimpleAttributeSet())));
assertTrue(runtime.store().listCas().isEmpty());
assertTrue(runtime.store().listWorkflowStates().isEmpty());
@@ -462,7 +463,7 @@ final class PkiProofGateE2eTest {
Map.of(rootKeyRef, expectedRoot.getPublic()), new BcX509ProofOfPossessionVerifier())) {
assertThrows(PkiException.class,
() -> runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
- new SubjectRef("CN=Root"), "default", Optional.of(rootKeyRef),
+ new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef),
new SimpleAttributeSet())));
assertTrue(runtime.store().listWorkflowStates().isEmpty());
assertEquals(1, runtime.submittedSignCount());
@@ -477,11 +478,12 @@ final class PkiProofGateE2eTest {
keys.put(intermediateKeyRef, intermediateKey);
try (PkiTestRuntime runtime = PkiTestRuntime.create(validDir, validDir.resolve("bus.log"), keys)) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
- new SubjectRef("CN=Root"), "default", Optional.of(rootKeyRef), new SimpleAttributeSet()));
- AttributeSet hostile = hostileIntermediateAttributes(expectedRoot.getPublic());
+ new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef), new SimpleAttributeSet()));
+ AttributeSet approved = new SimpleAttributeSet();
PkiId intermediateCaId = runtime.caService()
.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootCaId,
- new SubjectRef("CN=Intermediate"), "default", Optional.of(intermediateKeyRef), hostile));
+ new SubjectRef("CN=Intermediate"), "intermediate-ca",
+ Optional.of(intermediateKeyRef), approved));
Credential first = runtime.caService().getCa(intermediateCaId).caCredentials().get(0);
X509CertificateHolder firstHolder = new X509CertificateHolder(first.encoded().bytes());
@@ -492,7 +494,7 @@ final class PkiProofGateE2eTest {
Credential additional = runtime.caService()
.issueIntermediateCertificate(new IntermediateCertIssueCommand(runtime.framework().formatId(),
- rootCaId, intermediateCaId, "default", Optional.empty(), hostile));
+ rootCaId, intermediateCaId, "intermediate-ca", Optional.empty(), approved));
X509CertificateHolder additionalHolder = new X509CertificateHolder(additional.encoded().bytes());
assertEquals("CN=Intermediate", additionalHolder.getSubject().toString());
assertArrayEquals(intermediateKey.getPublic().getEncoded(),
@@ -517,13 +519,13 @@ final class PkiProofGateE2eTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(issuerDir, issuerDir.resolve("bus.log"),
Map.of(rootKeyRef, rootKey, intermediateKeyRef, intermediateKey))) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
- new SubjectRef("CN=Root"), "default", Optional.of(rootKeyRef), new SimpleAttributeSet()));
+ new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef), new SimpleAttributeSet()));
assertEquals(2, runtime.submittedSignCount());
runtime.replaceManagedKey(rootKeyRef, replacementRootKey);
assertThrows(PkiException.class,
() -> runtime.caService()
.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(),
- rootCaId, new SubjectRef("CN=Intermediate"), "default",
+ rootCaId, new SubjectRef("CN=Intermediate"), "intermediate-ca",
Optional.of(intermediateKeyRef), new SimpleAttributeSet())));
assertEquals(4, runtime.submittedSignCount());
assertEquals(1, runtime.store().listCas().size());
@@ -537,17 +539,17 @@ final class PkiProofGateE2eTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(additionalDir, additionalDir.resolve("bus.log"),
Map.of(rootKeyRef, rootKey, intermediateKeyRef, intermediateKey))) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
- new SubjectRef("CN=Root"), "default", Optional.of(rootKeyRef), new SimpleAttributeSet()));
+ new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef), new SimpleAttributeSet()));
PkiId intermediateCaId = runtime.caService()
.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootCaId,
- new SubjectRef("CN=Intermediate"), "default", Optional.of(intermediateKeyRef),
+ new SubjectRef("CN=Intermediate"), "intermediate-ca", Optional.of(intermediateKeyRef),
new SimpleAttributeSet()));
assertEquals(5, runtime.submittedSignCount());
runtime.replaceManagedKey(rootKeyRef, replacementRootKey);
assertThrows(PkiException.class,
() -> runtime.caService().issueIntermediateCertificate(
new IntermediateCertIssueCommand(runtime.framework().formatId(), rootCaId,
- intermediateCaId, "default", Optional.empty(), new SimpleAttributeSet())));
+ intermediateCaId, "intermediate-ca", Optional.empty(), new SimpleAttributeSet())));
assertEquals(7, runtime.submittedSignCount());
assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size());
assertTrue(runtime.store().listWorkflowStates().isEmpty());
@@ -560,11 +562,11 @@ final class PkiProofGateE2eTest {
try (PkiTestRuntime source = PkiTestRuntime.create(sourceDir, sourceDir.resolve("bus.log"),
Map.of(rootKeyRef, rootKey, intermediateKeyRef, intermediateKey))) {
PkiId rootCaId = source.caService().createRoot(new CaCreateCommand(source.framework().formatId(),
- new SubjectRef("CN=Root"), "default", Optional.of(rootKeyRef), new SimpleAttributeSet()));
+ new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef), new SimpleAttributeSet()));
rootCertificate = source.caService().getCa(rootCaId).caCredentials().get(0).encoded().bytes().clone();
PkiId intermediateCaId = source.caService()
.createIntermediate(new IntermediateCreateCommand(source.framework().formatId(), rootCaId,
- new SubjectRef("CN=Intermediate"), "default", Optional.of(intermediateKeyRef),
+ new SubjectRef("CN=Intermediate"), "intermediate-ca", Optional.of(intermediateKeyRef),
new SimpleAttributeSet()));
intermediateCertificate = source.caService().getCa(intermediateCaId).caCredentials().get(0).encoded()
.bytes().clone();
@@ -577,7 +579,7 @@ final class PkiProofGateE2eTest {
try (PkiTestRuntime target = PkiTestRuntime.create(importDir, importDir.resolve("bus.log"),
Map.of(rootKeyRef, replacementRootKey))) {
CaImportCommand command = new CaImportCommand(target.framework().formatId(), new SubjectRef("CN=Root"),
- "default", rootKeyRef, new EncodedObject(Encoding.DER, rootCertificate),
+ "root-ca", rootKeyRef, new EncodedObject(Encoding.DER, rootCertificate),
new SimpleAttributeSet());
assertThrows(PkiException.class, () -> target.caService().importRoot(command));
assertTrue(target.store().listCas().isEmpty());
@@ -593,7 +595,7 @@ final class PkiProofGateE2eTest {
Map.of(rootKeyRef, rootKey))) {
target.onPublicKeyResolve(() -> callerOwnedCertificate[callerOwnedCertificate.length - 1] ^= 0x01);
PkiId importedCaId = target.caService().importRoot(new CaImportCommand(target.framework().formatId(),
- new SubjectRef("CN=Root"), "default", rootKeyRef,
+ new SubjectRef("CN=Root"), "root-ca", rootKeyRef,
new EncodedObject(Encoding.DER, callerOwnedCertificate), new SimpleAttributeSet()));
assertTrue(target.caService().getCa(importedCaId).caCredentials().get(0)
.profileBinding() instanceof CaProfileBinding);
@@ -616,7 +618,7 @@ final class PkiProofGateE2eTest {
assertThrows(PkiException.class,
() -> signingFailure.caService().createRoot(new CaCreateCommand(
signingFailure.framework().formatId(),
- new SubjectRef("CN=Root"), "default", Optional.of(rootKeyRef),
+ new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef),
new SimpleAttributeSet())));
assertTrue(signingFailure.store().listCas().isEmpty());
assertTrue(signingFailure.store().listWorkflowStates().isEmpty());
@@ -637,7 +639,7 @@ final class PkiProofGateE2eTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(rootDir, rootDir.resolve("bus.log"),
Map.of(keyRef, keyPair))) {
CaImportCommand command = new CaImportCommand(runtime.framework().formatId(), new SubjectRef(subject),
- "default", keyRef, new EncodedObject(Encoding.DER, certificate), new SimpleAttributeSet());
+ "root-ca", keyRef, new EncodedObject(Encoding.DER, certificate), new SimpleAttributeSet());
assertThrows(PkiException.class, () -> runtime.caService().importRoot(command));
assertTrue(runtime.store().listCas().isEmpty());
assertEquals(0, runtime.submittedSignCount());
@@ -657,7 +659,7 @@ final class PkiProofGateE2eTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(runtimeDir, runtimeDir.resolve("bus.log"),
Map.of(rootKeyRef, rootKey))) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
- new SubjectRef("CN=Root"), "default", Optional.of(rootKeyRef), new SimpleAttributeSet()));
+ new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef), new SimpleAttributeSet()));
ParsedCertificationRequest subject = parse(runtime, makeCsr(subjectKey, subjectKey, "CN=Subject"));
ParsedCertificationRequest substitute = parse(runtime,
makeCsr(subjectKey, subjectKey, "CN=Substitute"));
@@ -670,7 +672,7 @@ final class PkiProofGateE2eTest {
}
@Override
- public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) {
+ public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
throw new IllegalStateException("DO_NOT_LOG_SIGNATURE_SENTINEL");
}
};
@@ -696,8 +698,8 @@ final class PkiProofGateE2eTest {
}
@Override
- public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) {
- return delegateBackend.issueIntermediateCertificate(issuance);
+ public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
+ return delegateBackend.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
}
};
DefaultIssuanceService service = new DefaultIssuanceService(runtime.store(), runtime.framework(),
@@ -719,15 +721,15 @@ final class PkiProofGateE2eTest {
CredentialBundle rawBundle = delegateBackend.issueEndEntity(candidate, issuerCertificate,
issuerKeyRef, serial);
Credential raw = rawBundle.credential();
- Credential wrongBinding = copyWithBinding(raw, new CaProfileBinding(candidate.profileReference()
- .profileId()));
+ Credential wrongBinding = copyWithBinding(raw,
+ new CaProfileBinding(candidate.profileReference()));
wrongBindingCredential.set(wrongBinding);
return new CredentialBundle(wrongBinding, rawBundle.supportingObjects());
}
@Override
- public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) {
- return delegateBackend.issueIntermediateCertificate(issuance);
+ public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
+ return delegateBackend.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
}
};
DefaultIssuanceService wrongBindingService = new DefaultIssuanceService(runtime.store(),
@@ -754,8 +756,8 @@ final class PkiProofGateE2eTest {
}
@Override
- public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) {
- return delegateBackend.issueIntermediateCertificate(issuance);
+ public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
+ return delegateBackend.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
}
};
DefaultIssuanceService invalidSignatureService = new DefaultIssuanceService(runtime.store(),
@@ -775,8 +777,8 @@ final class PkiProofGateE2eTest {
}
@Override
- public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) {
- return delegateBackend.issueIntermediateCertificate(issuance);
+ public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
+ return delegateBackend.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
}
};
DefaultIssuanceService snapshotService = new DefaultIssuanceService(runtime.store(),
@@ -865,7 +867,7 @@ final class PkiProofGateE2eTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"),
Map.of(rootKeyRef, rootKey, intermediateKeyRef, intermediateKey))) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
- new SubjectRef("CN=Root"), "default", Optional.of(rootKeyRef), new SimpleAttributeSet()));
+ new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef), new SimpleAttributeSet()));
assertTrue(runtime.caService().getCa(rootCaId).caCredentials().get(0)
.profileBinding() instanceof CaProfileBinding);
CredentialIssuerBackend delegate = runtime.issuerBackend();
@@ -876,7 +878,7 @@ final class PkiProofGateE2eTest {
PkiException rejected = assertThrows(PkiException.class,
() -> wrongBindingService.createIntermediate(new IntermediateCreateCommand(
runtime.framework().formatId(), rootCaId,
- new SubjectRef("CN=BindingRejectedIntermediate"), "default",
+ new SubjectRef("CN=BindingRejectedIntermediate"), "intermediate-ca",
Optional.of(intermediateKeyRef), new SimpleAttributeSet())), mutation.name());
assertTrue(rejected.getMessage().contains("CREDENTIAL_PROFILE_BINDING_MISMATCH"), mutation.name());
assertEquals(1, runtime.store().listCas().size(), mutation.name());
@@ -892,8 +894,8 @@ final class PkiProofGateE2eTest {
}
@Override
- public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) {
- Credential raw = delegate.issueIntermediateCertificate(issuance);
+ public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
+ Credential raw = delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
return rebuildIntermediateIdentity(raw, rootKey, Optional.of(wrongKey.getPublic()),
Optional.empty());
}
@@ -901,13 +903,13 @@ final class PkiProofGateE2eTest {
CaService wrongKeyService = runtime.caService(wrongKeyBackend);
assertThrows(PkiException.class,
() -> wrongKeyService.createIntermediate(new IntermediateCreateCommand(runtime.framework()
- .formatId(), rootCaId, new SubjectRef("CN=Intermediate"), "default",
+ .formatId(), rootCaId, new SubjectRef("CN=Intermediate"), "intermediate-ca",
Optional.of(intermediateKeyRef), new SimpleAttributeSet())));
assertEquals(1, runtime.store().listCas().size());
PkiId intermediateCaId = runtime.caService()
.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootCaId,
- new SubjectRef("CN=Intermediate"), "default", Optional.of(intermediateKeyRef),
+ new SubjectRef("CN=Intermediate"), "intermediate-ca", Optional.of(intermediateKeyRef),
new SimpleAttributeSet()));
for (BindingVariantMutation mutation : BindingVariantMutation.values()) {
AtomicReference produced = new AtomicReference<>();
@@ -915,7 +917,7 @@ final class PkiProofGateE2eTest {
produced));
PkiException rejected = assertThrows(PkiException.class,
() -> wrongBindingService.issueIntermediateCertificate(new IntermediateCertIssueCommand(
- runtime.framework().formatId(), rootCaId, intermediateCaId, "default",
+ runtime.framework().formatId(), rootCaId, intermediateCaId, "intermediate-ca",
Optional.empty(), new SimpleAttributeSet())), mutation.name());
assertTrue(rejected.getMessage().contains("CREDENTIAL_PROFILE_BINDING_MISMATCH"), mutation.name());
assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size(),
@@ -932,8 +934,8 @@ final class PkiProofGateE2eTest {
}
@Override
- public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) {
- Credential raw = delegate.issueIntermediateCertificate(issuance);
+ public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
+ Credential raw = delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
return rebuildIntermediateIdentity(raw, rootKey, Optional.empty(),
Optional.of("CN=WrongIntermediate"));
}
@@ -941,7 +943,7 @@ final class PkiProofGateE2eTest {
CaService wrongSubjectService = runtime.caService(wrongSubjectBackend);
assertThrows(PkiException.class,
() -> wrongSubjectService.issueIntermediateCertificate(new IntermediateCertIssueCommand(
- runtime.framework().formatId(), rootCaId, intermediateCaId, "default", Optional.empty(),
+ runtime.framework().formatId(), rootCaId, intermediateCaId, "intermediate-ca", Optional.empty(),
new SimpleAttributeSet())));
assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size());
@@ -952,8 +954,8 @@ final class PkiProofGateE2eTest {
}
@Override
- public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) {
- Credential raw = delegate.issueIntermediateCertificate(issuance);
+ public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
+ Credential raw = delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
byte[] invalid = raw.encoded().bytes().clone();
invalid[invalid.length - 1] ^= 0x01;
return new Credential(raw.credentialId(), raw.formatId(), raw.issuerRef(),
@@ -965,7 +967,7 @@ final class PkiProofGateE2eTest {
CaService invalidSignatureService = runtime.caService(invalidSignatureBackend);
assertThrows(PkiException.class,
() -> invalidSignatureService.issueIntermediateCertificate(new IntermediateCertIssueCommand(
- runtime.framework().formatId(), rootCaId, intermediateCaId, "default", Optional.empty(),
+ runtime.framework().formatId(), rootCaId, intermediateCaId, "intermediate-ca", Optional.empty(),
new SimpleAttributeSet())));
assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size());
@@ -975,7 +977,7 @@ final class PkiProofGateE2eTest {
assertThrows(PkiException.class,
() -> maliciousExtensionService.issueIntermediateCertificate(
new IntermediateCertIssueCommand(runtime.framework().formatId(), rootCaId,
- intermediateCaId, "default", Optional.empty(), new SimpleAttributeSet())),
+ intermediateCaId, "intermediate-ca", Optional.empty(), new SimpleAttributeSet())),
variant.name());
assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size(), variant.name());
}
@@ -988,15 +990,15 @@ final class PkiProofGateE2eTest {
}
@Override
- public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) {
- Credential raw = delegate.issueIntermediateCertificate(issuance);
+ public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
+ Credential raw = delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
rawCredential.set(raw);
return raw;
}
};
CaService snapshotService = runtime.caService(mutableBackend);
Credential returned = snapshotService.issueIntermediateCertificate(new IntermediateCertIssueCommand(
- runtime.framework().formatId(), rootCaId, intermediateCaId, "default", Optional.empty(),
+ runtime.framework().formatId(), rootCaId, intermediateCaId, "intermediate-ca", Optional.empty(),
new SimpleAttributeSet()));
byte[] expected = returned.encoded().bytes().clone();
rawCredential.get().encoded().bytes()[0] ^= 0x01;
@@ -1041,9 +1043,9 @@ final class PkiProofGateE2eTest {
}
@Override
- public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) {
+ public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
intermediateCalls.incrementAndGet();
- return delegate.issueIntermediateCertificate(issuance);
+ return delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
}
}
@@ -1057,8 +1059,8 @@ final class PkiProofGateE2eTest {
}
@Override
- public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) {
- Credential credential = delegate.issueIntermediateCertificate(issuance);
+ public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
+ Credential credential = delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
return rebuildIntermediateExtensions(credential, issuerKey, variant);
}
};
@@ -1074,24 +1076,27 @@ final class PkiProofGateE2eTest {
}
@Override
- public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) {
- Credential raw = delegate.issueIntermediateCertificate(issuance);
+ public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
+ Credential raw = delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
produced.set(raw);
if (mutation == BindingVariantMutation.NULL_CREDENTIAL) {
return null;
}
- return copyWithBinding(raw, bindingFor(mutation, issuance.profileId()));
+ return copyWithBinding(raw, bindingFor(mutation, issuance.profileReference()));
}
};
}
- private static CredentialProfileBinding bindingFor(BindingVariantMutation mutation, String profileId) {
+ private static CredentialProfileBinding bindingFor(BindingVariantMutation mutation,
+ CertificateProfileRef profileReference) {
return switch (mutation) {
- case END_ENTITY_SAME_ID -> new EndEntityProfileBinding(new CertificateProfileRef(profileId, 1,
+ case END_ENTITY_SAME_ID -> new EndEntityProfileBinding(new CertificateProfileRef(
+ profileReference.profileId(), 1,
new byte[CertificateProfileRef.HASH_BYTES]));
case END_ENTITY_OTHER_ID -> new EndEntityProfileBinding(new CertificateProfileRef("other", 1,
new byte[CertificateProfileRef.HASH_BYTES]));
- case CA_OTHER_ID -> new CaProfileBinding("other");
+ case CA_OTHER_ID -> new CaProfileBinding(new CertificateProfileRef("other", 1,
+ new byte[CertificateProfileRef.HASH_BYTES]));
case NULL_CREDENTIAL -> throw new IllegalStateException("null credential has no binding");
};
}
diff --git a/pki/src/test/java/zeroecho/pki/impl/core/DefaultStatusObjectServiceCrlTest.java b/pki/src/test/java/zeroecho/pki/impl/core/DefaultStatusObjectServiceCrlTest.java
index e4bb677..d968f5b 100644
--- a/pki/src/test/java/zeroecho/pki/impl/core/DefaultStatusObjectServiceCrlTest.java
+++ b/pki/src/test/java/zeroecho/pki/impl/core/DefaultStatusObjectServiceCrlTest.java
@@ -372,7 +372,7 @@ final class DefaultStatusObjectServiceCrlTest {
CaProfileBinding binding = assertInstanceOf(CaProfileBinding.class, template.profileBinding());
return new Credential(new PkiId("credential:" + suffix), formatId, template.issuerRef(),
template.subjectRef(), template.validity(), template.serialOrUniqueId(),
- template.publicKeyId(), new CaProfileBinding(binding.profileId()), CredentialStatus.ISSUED, encoded,
+ template.publicKeyId(), new CaProfileBinding(binding.reference()), CredentialStatus.ISSUED, encoded,
template.attributes());
}
@@ -410,7 +410,7 @@ final class DefaultStatusObjectServiceCrlTest {
private static PkiId createRoot(PkiTestRuntime runtime, KeyRef keyRef, String commonName) {
return runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
- new SubjectRef("CN=" + commonName), "default", Optional.of(keyRef), emptyAttributes()));
+ new SubjectRef("CN=" + commonName), "root-ca", Optional.of(keyRef), emptyAttributes()));
}
private static KeyPair generateRsa() throws Exception {
diff --git a/pki/src/test/java/zeroecho/pki/impl/core/H7ProfileEnforcementTest.java b/pki/src/test/java/zeroecho/pki/impl/core/H7ProfileEnforcementTest.java
index 8dbf49a..3f1034d 100644
--- a/pki/src/test/java/zeroecho/pki/impl/core/H7ProfileEnforcementTest.java
+++ b/pki/src/test/java/zeroecho/pki/impl/core/H7ProfileEnforcementTest.java
@@ -139,13 +139,13 @@ final class H7ProfileEnforcementTest {
SubjectAlternativeNameRule dns =
new SubjectAlternativeNameRule(SubjectAlternativeNameType.DNS_NAME, 0, 1, false, false);
assertThrows(IllegalArgumentException.class,
- () -> new SubjectAlternativeNamePolicy(false, 2, 2, List.of(dns), false, Set.of(), false,
+ () -> new SubjectAlternativeNamePolicy(2, 2, List.of(dns), false, Set.of(), false,
false, false));
assertThrows(IllegalArgumentException.class,
- () -> new SubjectAlternativeNamePolicy(false, 0, 1, List.of(), false, Set.of(), false,
+ () -> new SubjectAlternativeNamePolicy(0, 1, List.of(), false, Set.of(), false,
true, false));
assertThrows(IllegalArgumentException.class,
- () -> new SubjectAlternativeNamePolicy(false, 0, 1, List.of(dns), false, Set.of("https"), false,
+ () -> new SubjectAlternativeNamePolicy(0, 1, List.of(dns), false, Set.of("https"), false,
false, false));
assertThrows(IllegalArgumentException.class, () -> leaf(Set.of(LeafKeyUsage.ENCIPHER_ONLY),
Set.of("RSA"), Set.of()));
@@ -262,7 +262,7 @@ final class H7ProfileEnforcementTest {
CertificateProfileRef reference = new CertificateProfileRef("shared-profile", 1,
new byte[CertificateProfileRef.HASH_BYTES]);
EndEntityProfileBinding endEntity = new EndEntityProfileBinding(reference);
- CaProfileBinding ca = new CaProfileBinding(reference.profileId());
+ CaProfileBinding ca = new CaProfileBinding(reference);
assertTrue(CredentialProfileBinding.class.isSealed());
assertEquals(Set.of(EndEntityProfileBinding.class, CaProfileBinding.class),
@@ -273,16 +273,17 @@ final class H7ProfileEnforcementTest {
.noneMatch(method -> method.getName().equals("profileId")));
CredentialProfileBindings.requireEndEntityBinding(endEntity, reference);
- CredentialProfileBindings.requireCaBinding(ca, reference.profileId());
+ CredentialProfileBindings.requireCaBinding(ca, reference);
assertCode(CredentialProfileBindings.MISMATCH_CODE,
() -> CredentialProfileBindings.requireEndEntityBinding(ca, reference));
assertCode(CredentialProfileBindings.MISMATCH_CODE,
- () -> CredentialProfileBindings.requireCaBinding(endEntity, reference.profileId()));
+ () -> CredentialProfileBindings.requireCaBinding(endEntity, reference));
assertCode(CredentialProfileBindings.MISMATCH_CODE,
- () -> CredentialProfileBindings.requireCaBinding(new CaProfileBinding("other"),
- reference.profileId()));
+ () -> CredentialProfileBindings.requireCaBinding(
+ new CaProfileBinding(new CertificateProfileRef("other", 1, new byte[32])),
+ reference));
assertCode(CredentialProfileBindings.MISMATCH_CODE,
- () -> CredentialProfileBindings.requireCaBinding(null, reference.profileId()));
+ () -> CredentialProfileBindings.requireCaBinding(null, reference));
}
@Test
@@ -293,7 +294,7 @@ final class H7ProfileEnforcementTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"),
Map.of(rootRef, root))) {
PkiId caId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
- new SubjectRef("CN=Root"), "default", Optional.of(rootRef), new SimpleAttributeSet()));
+ new SubjectRef("CN=Root"), "root-ca", Optional.of(rootRef), new SimpleAttributeSet()));
ParsedCertificationRequest request = runtime.certificationRequestService().parse(
new CertificationRequest(runtime.framework().formatId(),
new EncodedObject(Encoding.DER,
@@ -385,31 +386,34 @@ final class H7ProfileEnforcementTest {
return new Credential(new PkiId("credential:issuer"), BcX509CredentialFramework.FORMAT_ID,
new IssuerRef(new PkiId("ca:issuer")), new SubjectRef("CN=Issuer"),
new Validity(NOW.minus(Duration.ofDays(1)), NOW.plus(Duration.ofDays(1000))), "1",
- new PkiId("spki:issuer"), new CaProfileBinding("root"), CredentialStatus.ISSUED,
+ new PkiId("spki:issuer"),
+ new CaProfileBinding(new CertificateProfileRef("root", 1, new byte[32])),
+ CredentialStatus.ISSUED,
new EncodedObject(Encoding.DER, new byte[] { 1 }), new SimpleAttributeSet());
}
private static CertificateProfile policyWithFixedOrganization(String algorithm) {
- SubjectPolicy subject = new SubjectPolicy(List.of(
+ SubjectPolicy subject = new SubjectPolicy(false, List.of(
new SubjectRdnRule(SubjectRdnType.COMMON_NAME, 1, 1, 256, Optional.empty(), true),
new SubjectRdnRule(SubjectRdnType.ORGANIZATION_NAME, 1, 1, 256,
Optional.of("Profile Fixed"), false)));
SubjectAlternativeNamePolicy sans =
- new SubjectAlternativeNamePolicy(false, 0, 0, List.of(), false, Set.of(), false, false, false);
- LeafCertificatePolicy leaf = new LeafCertificatePolicy(subject, sans,
- Set.of(LeafKeyUsage.DIGITAL_SIGNATURE), Set.of(), true, false, true, Set.of(algorithm),
- Duration.ofDays(365));
- return new CertificateProfile("h7", BcX509CredentialFramework.FORMAT_ID, "H7", leaf);
+ new SubjectAlternativeNamePolicy(0, 0, List.of(), false, Set.of(), false, false, false);
+ LeafCertificatePolicy leaf = new LeafCertificatePolicy(sans,
+ Set.of(LeafKeyUsage.DIGITAL_SIGNATURE), Set.of(), true, false, true,
+ Set.of(algorithm));
+ return new CertificateProfile("h7", BcX509CredentialFramework.FORMAT_ID, "H7",
+ Duration.ofDays(365), subject, leaf);
}
private static LeafCertificatePolicy leaf(Set usages, Set algorithms,
Set ekus) {
- return new LeafCertificatePolicy(new SubjectPolicy(List.of()),
- new SubjectAlternativeNamePolicy(true, 1, 1,
+ return new LeafCertificatePolicy(
+ new SubjectAlternativeNamePolicy(1, 1,
List.of(new SubjectAlternativeNameRule(SubjectAlternativeNameType.DNS_NAME, 1, 1,
false, false)),
false, Set.of(), false, true, false),
- usages, ekus, true, false, true, algorithms, Duration.ofDays(1));
+ usages, ekus, true, false, true, algorithms);
}
private static ParsedCertificationRequest parse(PKCS10CertificationRequest request) throws Exception {
diff --git a/pki/src/test/java/zeroecho/pki/impl/core/StoreBackedEffectiveCredentialStatusResolverTest.java b/pki/src/test/java/zeroecho/pki/impl/core/StoreBackedEffectiveCredentialStatusResolverTest.java
index f561c22..124ed6a 100644
--- a/pki/src/test/java/zeroecho/pki/impl/core/StoreBackedEffectiveCredentialStatusResolverTest.java
+++ b/pki/src/test/java/zeroecho/pki/impl/core/StoreBackedEffectiveCredentialStatusResolverTest.java
@@ -176,7 +176,7 @@ final class StoreBackedEffectiveCredentialStatusResolverTest {
Credential credential = new Credential(new PkiId("credential:audit"), new FormatId(sentinel),
new IssuerRef(new PkiId("ca:issuer")), new SubjectRef("CN=Audit"),
new Validity(NOW.minusSeconds(60), NOW.plusSeconds(60)), "audit", new PkiId("key:audit"),
- new CaProfileBinding("default"), CredentialStatus.ISSUED,
+ new CaProfileBinding(new zeroecho.pki.api.profile.CertificateProfileRef("default", 1, new byte[32])), CredentialStatus.ISSUED,
new EncodedObject(Encoding.DER, new byte[] { 1 }),
new SimpleAttributeSet());
AtomicReference recorded = new AtomicReference<>();
@@ -239,7 +239,7 @@ final class StoreBackedEffectiveCredentialStatusResolverTest {
return new Credential(new PkiId("credential:" + suffix), new FormatId("x509"),
new IssuerRef(new PkiId("ca:issuer")), new SubjectRef("CN=" + suffix),
new Validity(notBefore, notAfter), suffix, new PkiId("key:" + suffix),
- new CaProfileBinding("default"), status,
+ new CaProfileBinding(new zeroecho.pki.api.profile.CertificateProfileRef("default", 1, new byte[32])), status,
new EncodedObject(Encoding.DER, new byte[] { 1, 2, 3 }), new SimpleAttributeSet());
}
diff --git a/pki/src/test/java/zeroecho/pki/impl/fs/FilesystemPkiStoreTest.java b/pki/src/test/java/zeroecho/pki/impl/fs/FilesystemPkiStoreTest.java
index 2588290..451c42c 100644
--- a/pki/src/test/java/zeroecho/pki/impl/fs/FilesystemPkiStoreTest.java
+++ b/pki/src/test/java/zeroecho/pki/impl/fs/FilesystemPkiStoreTest.java
@@ -76,6 +76,7 @@ import zeroecho.pki.api.credential.CredentialStatus;
import zeroecho.pki.api.profile.CertificateProfile;
import zeroecho.pki.api.profile.CertificateProfileDefinition;
import zeroecho.pki.api.profile.CertificateProfileDocumentCodec;
+import zeroecho.pki.api.profile.CertificateProfileKind;
import zeroecho.pki.api.profile.CertificateProfileRef;
import zeroecho.pki.api.profile.ExtendedKeyUsageId;
import zeroecho.pki.api.profile.ImportedCertificateProfileVersion;
@@ -527,12 +528,15 @@ public final class FilesystemPkiStoreTest {
*/
static void importProfile(FilesystemPkiStore store, CertificateProfile profile, Instant importedAt)
throws Exception {
- CertificateProfileDefinition definition = new CertificateProfileDefinition(profile.profileId(), 1,
- profile.formatId(), profile.displayName(), profile.leafPolicy());
+ CertificateProfileDefinition definition = new CertificateProfileDefinition(
+ CertificateProfileKind.END_ENTITY, profile.profileId(), 1, profile.formatId(),
+ profile.displayName(), profile.maximumValidity(), profile.subjectPolicy(),
+ profile.leafPolicy());
byte[] canonical = CertificateProfileDocumentCodec.writeCanonical(definition);
byte[] hash = MessageDigest.getInstance("SHA-256").digest(canonical);
CertificateProfileRef reference = new CertificateProfileRef(profile.profileId(), 1, hash);
- store.importProfileVersion(new ImportedCertificateProfileVersion(reference, 1, definition, canonical,
+ store.importProfileVersion(new ImportedCertificateProfileVersion(reference,
+ CertificateProfileDefinition.SCHEMA_VERSION, definition, canonical,
importedAt));
}
@@ -554,15 +558,16 @@ public final class FilesystemPkiStoreTest {
static CertificateProfile minimalProfile(String profileId) {
FormatId formatId = new FormatId("fmt-x509");
- SubjectPolicy subject = new SubjectPolicy(List.of(new SubjectRdnRule(
+ SubjectPolicy subject = new SubjectPolicy(false, List.of(new SubjectRdnRule(
SubjectRdnType.COMMON_NAME, 1, 1, 256, Optional.empty(), true)));
- SubjectAlternativeNamePolicy sans = new SubjectAlternativeNamePolicy(false, 0, 0,
+ SubjectAlternativeNamePolicy sans = new SubjectAlternativeNamePolicy(0, 0,
List.of(), false, Set.of(), false, false, false);
- LeafCertificatePolicy leaf = new LeafCertificatePolicy(subject, sans,
+ LeafCertificatePolicy leaf = new LeafCertificatePolicy(sans,
Set.of(LeafKeyUsage.DIGITAL_SIGNATURE, LeafKeyUsage.KEY_ENCIPHERMENT),
Set.of(new ExtendedKeyUsageId("1.3.6.1.5.5.7.3.1")), true, false, true,
- Set.of("RSA"), Duration.ofDays(365));
- return new CertificateProfile(profileId, formatId, "Test leaf profile", leaf);
+ Set.of("RSA"));
+ return new CertificateProfile(profileId, formatId, "Test leaf profile",
+ Duration.ofDays(365), subject, leaf);
}
static Credential minimalCredential(String serial, String profileId) {
@@ -584,7 +589,7 @@ public final class FilesystemPkiStoreTest {
AttributeSet attrs = emptyAttributes();
return new Credential(credentialId, formatId, issuerRef, subjectRef, validity, serial, publicKeyId,
- new CaProfileBinding(profileId), status, encoded, attrs);
+ new CaProfileBinding(new zeroecho.pki.api.profile.CertificateProfileRef(profileId, 1, new byte[32])), status, encoded, attrs);
}
static AttributeSet emptyAttributes() {
diff --git a/pki/src/test/java/zeroecho/pki/impl/fs/FilesystemProfileLifecycleTest.java b/pki/src/test/java/zeroecho/pki/impl/fs/FilesystemProfileLifecycleTest.java
index 5a08869..faedfff 100644
--- a/pki/src/test/java/zeroecho/pki/impl/fs/FilesystemProfileLifecycleTest.java
+++ b/pki/src/test/java/zeroecho/pki/impl/fs/FilesystemProfileLifecycleTest.java
@@ -30,7 +30,9 @@ import org.junit.jupiter.api.io.TempDir;
import zeroecho.pki.api.PkiException;
import zeroecho.pki.api.profile.BuiltInCertificateProfileCatalog;
import zeroecho.pki.api.profile.BuiltInCertificateProfileTemplate;
+import zeroecho.pki.api.profile.CertificateProfileDefinition;
import zeroecho.pki.api.profile.CertificateProfileRef;
+import zeroecho.pki.api.profile.CertificateProfileKind;
import zeroecho.pki.api.profile.ImportedCertificateProfileVersion;
import zeroecho.pki.impl.audit.InMemoryAuditSink;
import zeroecho.pki.impl.core.DefaultProfileService;
@@ -100,7 +102,8 @@ final class FilesystemProfileLifecycleTest {
ImportedCertificateProfileVersion invalid = new ImportedCertificateProfileVersion(
new CertificateProfileRef(template.definition().profileId(),
template.definition().profileVersion(), template.canonicalSha256()),
- 1, template.definition(), noncanonical, CLOCK.instant());
+ CertificateProfileDefinition.SCHEMA_VERSION, template.definition(),
+ noncanonical, CLOCK.instant());
Path path = new FsPaths(noncanonicalRoot).profileVersion("server-tls", 1);
FsOperations.ensureDir(path.getParent());
Files.write(path, FsCodec.encode(FsCodec.PROFILE_VERSION, invalid));
@@ -115,7 +118,8 @@ final class FilesystemProfileLifecycleTest {
new CertificateProfileRef(template.definition().profileId(),
template.definition().profileVersion(),
new byte[CertificateProfileRef.HASH_BYTES]),
- 1, template.definition(), template.canonicalJson(), CLOCK.instant());
+ CertificateProfileDefinition.SCHEMA_VERSION, template.definition(),
+ template.canonicalJson(), CLOCK.instant());
Path path = new FsPaths(hashMismatchRoot).profileVersion("server-tls", 1);
FsOperations.ensureDir(path.getParent());
Files.write(path, FsCodec.encode(FsCodec.PROFILE_VERSION, invalid));
@@ -168,6 +172,51 @@ final class FilesystemProfileLifecycleTest {
}
}
+ @Test
+ void caKindsUseLifecycleAndOneLogicalIdCannotChangeKindAcrossVersions(
+ @TempDir Path directory) throws Exception {
+ Path root = directory.resolve("store");
+ CertificateProfileRef rootRef;
+ CertificateProfileRef intermediateRef;
+ try (FilesystemPkiStore store = store(root)) {
+ DefaultProfileService service = service(store);
+ rootRef = service.importBuiltIn(builtIn("root-ca"));
+ intermediateRef = service.importBuiltIn(builtIn("intermediate-ca"));
+ assertTrue(service.getActiveReference("root-ca").isEmpty());
+ assertTrue(service.getActiveReference("intermediate-ca").isEmpty());
+ assertEquals(rootRef, service.activateProfile("root-ca", 1));
+ assertEquals(intermediateRef, service.activateProfile("intermediate-ca", 1));
+ assertEquals(CertificateProfileKind.ROOT_CA,
+ service.requireActiveProfile("root-ca").definition().certificateType());
+ assertEquals(CertificateProfileKind.INTERMEDIATE_CA,
+ service.requireActiveProfile("intermediate-ca").definition().certificateType());
+
+ byte[] sameKindVersion = version(builtIn("root-ca"), 2, "Root CA v2");
+ CertificateProfileRef versionTwo = service.importProfile(sameKindVersion);
+ assertEquals(2, versionTwo.profileVersion());
+
+ byte[] conflictingKind = new String(builtIn("intermediate-ca").canonicalJson(),
+ StandardCharsets.UTF_8)
+ .replace("\"profileId\":\"intermediate-ca\"", "\"profileId\":\"root-ca\"")
+ .replace("\"profileVersion\":1", "\"profileVersion\":3")
+ .getBytes(StandardCharsets.UTF_8);
+ assertCode(() -> service.importProfile(conflictingKind), "PROFILE_KIND_CONFLICT");
+ assertTrue(service.getImportedVersion("root-ca", 3).isEmpty());
+ assertEquals(rootRef, service.getActiveReference("root-ca").orElseThrow());
+ }
+
+ try (FilesystemPkiStore reopened = store(root)) {
+ DefaultProfileService service = service(reopened);
+ assertEquals(rootRef, service.getActiveReference("root-ca").orElseThrow());
+ assertEquals(intermediateRef,
+ service.getActiveReference("intermediate-ca").orElseThrow());
+ assertEquals(CertificateProfileKind.ROOT_CA,
+ service.requireActiveProfile("root-ca").definition().certificateType());
+ assertEquals(CertificateProfileKind.INTERMEDIATE_CA,
+ service.requireActiveProfile("intermediate-ca").definition().certificateType());
+ }
+ }
+
@Test
void missingCorruptAndHashMismatchedPointersNeverFallback(@TempDir Path directory) throws Exception {
BuiltInCertificateProfileTemplate template = builtIn("vpn-client");
diff --git a/pki/src/test/java/zeroecho/pki/impl/fs/FilesystemRevocationJournalTest.java b/pki/src/test/java/zeroecho/pki/impl/fs/FilesystemRevocationJournalTest.java
index 31ab5aa..2a48937 100644
--- a/pki/src/test/java/zeroecho/pki/impl/fs/FilesystemRevocationJournalTest.java
+++ b/pki/src/test/java/zeroecho/pki/impl/fs/FilesystemRevocationJournalTest.java
@@ -499,7 +499,7 @@ final class FilesystemRevocationJournalTest {
return new Credential(new PkiId("credential:" + suffix), new FormatId("x509"),
new IssuerRef(new PkiId("ca:issuer")), new SubjectRef("CN=" + suffix),
new Validity(TIME.minusSeconds(60), TIME.plusSeconds(60)), suffix,
- new PkiId("key:" + suffix), new CaProfileBinding("default"), CredentialStatus.ISSUED,
+ new PkiId("key:" + suffix), new CaProfileBinding(new zeroecho.pki.api.profile.CertificateProfileRef("default", 1, new byte[32])), CredentialStatus.ISSUED,
new EncodedObject(Encoding.DER, new byte[] { 1, 2, 3 }), new SimpleAttributeSet());
}
diff --git a/pki/src/test/java/zeroecho/pki/impl/fs/FsCodecTest.java b/pki/src/test/java/zeroecho/pki/impl/fs/FsCodecTest.java
index 00a5636..6949056 100644
--- a/pki/src/test/java/zeroecho/pki/impl/fs/FsCodecTest.java
+++ b/pki/src/test/java/zeroecho/pki/impl/fs/FsCodecTest.java
@@ -71,6 +71,7 @@ import zeroecho.pki.api.credential.EndEntityProfileBinding;
import zeroecho.pki.api.profile.CertificateProfile;
import zeroecho.pki.api.profile.CertificateProfileDefinition;
import zeroecho.pki.api.profile.CertificateProfileDocumentCodec;
+import zeroecho.pki.api.profile.CertificateProfileKind;
import zeroecho.pki.api.profile.CertificateProfileRef;
import zeroecho.pki.api.profile.ExtendedKeyUsageId;
import zeroecho.pki.api.profile.ImportedCertificateProfileVersion;
@@ -215,7 +216,7 @@ final class FsCodecTest {
CertificateProfileRef reference = new CertificateProfileRef("profile-a", 1,
new byte[CertificateProfileRef.HASH_BYTES]);
List bindings = List.of(new EndEntityProfileBinding(reference),
- new CaProfileBinding(reference.profileId()));
+ new CaProfileBinding(reference));
for (CredentialProfileBinding binding : bindings) {
Credential original = credential(binding);
@@ -225,6 +226,33 @@ final class FsCodecTest {
}
}
+ @Test
+ void obsoleteBareStringCaBindingFailsStrictDecode() {
+ byte[] encoded = FsCodec.encode(FsCodec.CREDENTIAL,
+ credential(new CaProfileBinding(new CertificateProfileRef("profile-a", 1,
+ new byte[CertificateProfileRef.HASH_BYTES]))));
+ int binding = indexOf(encoded, new byte[] { 73, 2, 72 });
+ assertTrue(binding >= 0);
+ encoded[binding + 2] = 1;
+ assertInvalid(encoded);
+ }
+
+ private static int indexOf(byte[] source, byte[] target) {
+ for (int index = 0; index <= source.length - target.length; index++) {
+ boolean matches = true;
+ for (int offset = 0; offset < target.length; offset++) {
+ if (source[index + offset] != target[offset]) {
+ matches = false;
+ break;
+ }
+ }
+ if (matches) {
+ return index;
+ }
+ }
+ return -1;
+ }
+
private static ParsedCertificationRequest roundTripRequest(AttributeSet attributes) {
byte[] encoded = FsCodec.encode(FsCodec.PARSED_REQUEST, request(attributes));
return FsCodec.decode(FsCodec.PARSED_REQUEST, encoded);
@@ -239,15 +267,16 @@ final class FsCodecTest {
}
private static CertificateProfile profile() {
- SubjectPolicy subject = new SubjectPolicy(List.of(new SubjectRdnRule(
+ SubjectPolicy subject = new SubjectPolicy(false, List.of(new SubjectRdnRule(
SubjectRdnType.COMMON_NAME, 1, 1, 256, Optional.empty(), true)));
- SubjectAlternativeNamePolicy sans = new SubjectAlternativeNamePolicy(false, 0, 0,
+ SubjectAlternativeNamePolicy sans = new SubjectAlternativeNamePolicy(0, 0,
List.of(), false, java.util.Set.of(), false, false, false);
- LeafCertificatePolicy leaf = new LeafCertificatePolicy(subject, sans,
+ LeafCertificatePolicy leaf = new LeafCertificatePolicy(sans,
java.util.Set.of(LeafKeyUsage.DIGITAL_SIGNATURE, LeafKeyUsage.KEY_ENCIPHERMENT),
java.util.Set.of(new ExtendedKeyUsageId("1.3.6.1.5.5.7.3.1")), true, false, true,
- java.util.Set.of("RSA"), Duration.ofDays(365));
- return new CertificateProfile("profile-a", new FormatId("x509"), "Test leaf profile", leaf);
+ java.util.Set.of("RSA"));
+ return new CertificateProfile("profile-a", new FormatId("x509"), "Test leaf profile",
+ Duration.ofDays(365), subject, leaf);
}
private static Credential credential(CredentialProfileBinding binding) {
@@ -261,12 +290,15 @@ final class FsCodecTest {
private static ImportedCertificateProfileVersion profileVersion() {
try {
CertificateProfile profile = profile();
- CertificateProfileDefinition definition = new CertificateProfileDefinition(profile.profileId(), 1,
- profile.formatId(), profile.displayName(), profile.leafPolicy());
+ CertificateProfileDefinition definition = new CertificateProfileDefinition(
+ CertificateProfileKind.END_ENTITY, profile.profileId(), 1, profile.formatId(),
+ profile.displayName(), profile.maximumValidity(), profile.subjectPolicy(),
+ profile.leafPolicy());
byte[] canonical = CertificateProfileDocumentCodec.writeCanonical(definition);
CertificateProfileRef reference = new CertificateProfileRef(profile.profileId(), 1,
MessageDigest.getInstance("SHA-256").digest(canonical));
- return new ImportedCertificateProfileVersion(reference, 1, definition, canonical, Instant.EPOCH);
+ return new ImportedCertificateProfileVersion(reference,
+ CertificateProfileDefinition.SCHEMA_VERSION, definition, canonical, Instant.EPOCH);
} catch (java.security.NoSuchAlgorithmException impossible) {
throw new IllegalStateException(impossible);
}
diff --git a/pki/src/test/java/zeroecho/pki/testkit/H7ProfileDocuments.java b/pki/src/test/java/zeroecho/pki/testkit/H7ProfileDocuments.java
index 59fb709..27e9e39 100644
--- a/pki/src/test/java/zeroecho/pki/testkit/H7ProfileDocuments.java
+++ b/pki/src/test/java/zeroecho/pki/testkit/H7ProfileDocuments.java
@@ -212,7 +212,7 @@ public final class H7ProfileDocuments {
boolean critical, List sanRules, boolean noKeyEncipherment, List eku) {
String keyUsage = noKeyEncipherment
? "[\"DIGITAL_SIGNATURE\"]" : "[\"DIGITAL_SIGNATURE\",\"KEY_ENCIPHERMENT\"]";
- String json = "{\"schemaVersion\":1,\"profileId\":\"" + id
+ String json = "{\"schemaVersion\":2,\"certificateType\":\"END_ENTITY\",\"profileId\":\"" + id
+ "\",\"profileVersion\":1,\"formatId\":\"" + formatId
+ "\",\"displayName\":\"H7 Test Profile\","
+ "\"maxValidity\":\"PT8760H\",\"subject\":{\"allowEmpty\":" + allowEmpty
diff --git a/pki/src/test/java/zeroecho/pki/testkit/PkiTestRuntime.java b/pki/src/test/java/zeroecho/pki/testkit/PkiTestRuntime.java
index db3805c..2c9d838 100644
--- a/pki/src/test/java/zeroecho/pki/testkit/PkiTestRuntime.java
+++ b/pki/src/test/java/zeroecho/pki/testkit/PkiTestRuntime.java
@@ -54,6 +54,7 @@ import zeroecho.pki.api.RevocationService;
import zeroecho.pki.api.ProfileService;
import zeroecho.pki.api.StatusObjectService;
import zeroecho.pki.api.credential.EffectiveCredentialStatusResolver;
+import zeroecho.pki.api.profile.BuiltInCertificateProfileCatalog;
import zeroecho.pki.impl.core.DefaultCaService;
import zeroecho.pki.impl.core.DefaultCertificationRequestService;
import zeroecho.pki.impl.core.DefaultIssuanceService;
@@ -104,6 +105,7 @@ public final class PkiTestRuntime implements AutoCloseable {
private final Map publicKeysByKeyRef;
private Runnable publicKeyResolveHook;
+ private boolean caProfilesProvisioned;
private PkiTestRuntime(FilesystemPkiStore store, PkiSigningBus signingBus, SignatureWorkflow signatureWorkflow,
CredentialFramework framework, CredentialIssuerBackend issuerBackend,
@@ -129,7 +131,7 @@ public final class PkiTestRuntime implements AutoCloseable {
this.statusObjectService = new DefaultStatusObjectService(store, framework, auditSink, statusResolver);
this.caService = new DefaultCaService(store, framework, issuerBackend, this::resolvePublicKeyInfo, signingBus,
- auditSink, statusResolver, "SHA256withRSA", signingTtl);
+ auditSink, statusResolver, profileService, clock, "SHA256withRSA", signingTtl);
}
/**
@@ -306,27 +308,53 @@ public final class PkiTestRuntime implements AutoCloseable {
}
public CaService caService() {
+ provisionCaProfiles();
return caService;
}
public CaService caService(CredentialFramework credentialFramework) {
+ provisionCaProfiles();
return new DefaultCaService(store, Objects.requireNonNull(credentialFramework, "credentialFramework"),
- issuerBackend, this::resolvePublicKeyInfo, signingBus, auditSink, statusResolver, "SHA256withRSA",
- Duration.ofSeconds(2));
+ issuerBackend, this::resolvePublicKeyInfo, signingBus, auditSink, statusResolver, profileService,
+ Clock.systemUTC(), "SHA256withRSA", Duration.ofSeconds(2));
}
public CaService caService(CredentialIssuerBackend backend) {
+ provisionCaProfiles();
return new DefaultCaService(store, framework, Objects.requireNonNull(backend, "backend"),
- this::resolvePublicKeyInfo, signingBus, auditSink, statusResolver, "SHA256withRSA",
- Duration.ofSeconds(2));
+ this::resolvePublicKeyInfo, signingBus, auditSink, statusResolver, profileService, Clock.systemUTC(),
+ "SHA256withRSA", Duration.ofSeconds(2));
}
public CaService caService(CredentialIssuerBackend backend, EffectiveCredentialStatusResolver resolver) {
+ provisionCaProfiles();
return new DefaultCaService(store, framework, Objects.requireNonNull(backend, "backend"),
this::resolvePublicKeyInfo, signingBus, auditSink, Objects.requireNonNull(resolver, "resolver"),
+ profileService, Clock.systemUTC(), "SHA256withRSA", Duration.ofSeconds(2));
+ }
+
+ public CaService caService(ProfileService profiles) {
+ provisionCaProfiles();
+ return new DefaultCaService(store, framework, issuerBackend, this::resolvePublicKeyInfo, signingBus,
+ auditSink, statusResolver, Objects.requireNonNull(profiles, "profiles"), Clock.systemUTC(),
"SHA256withRSA", Duration.ofSeconds(2));
}
+ private synchronized void provisionCaProfiles() {
+ if (caProfilesProvisioned) {
+ return;
+ }
+ BuiltInCertificateProfileCatalog.load(PkiTestRuntime.class.getClassLoader()).stream()
+ .filter(template -> template.definition().profileId().equals("root-ca")
+ || template.definition().profileId().equals("intermediate-ca"))
+ .forEach(template -> {
+ zeroecho.pki.api.profile.CertificateProfileRef reference =
+ profileService.importBuiltIn(template);
+ profileService.activateProfile(reference.profileId(), reference.profileVersion());
+ });
+ caProfilesProvisioned = true;
+ }
+
public CertificationRequestService certificationRequestService() {
return certificationRequestService;
}