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.
This commit is contained in:
2026-07-30 20:14:11 +02:00
parent 9e40e8b5a2
commit 849c8c82cb
50 changed files with 2608 additions and 899 deletions

View File

@@ -16,8 +16,14 @@ import zeroecho.pki.api.profile.ImportedCertificateProfileVersion;
/** /**
* Versioned certificate-profile import, activation, and lookup service. * Versioned certificate-profile import, activation, and lookup service.
* *
* <p>Import never activates a profile. End-entity issuance must resolve only an * <p>
* explicitly activated persisted version through {@link #requireActiveProfile(String)}.</p> * 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.
* </p>
*/ */
public interface ProfileService { public interface ProfileService {
/** Imports a bounded strict JSON profile document. */ /** Imports a bounded strict JSON profile document. */

View File

@@ -4,16 +4,18 @@
******************************************************************************/ ******************************************************************************/
package zeroecho.pki.api.credential; 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. */ /** Creates a CA profile binding. */
public CaProfileBinding { public CaProfileBinding {
if (profileId == null || profileId.isBlank()) { Objects.requireNonNull(reference, "reference");
throw new IllegalArgumentException("profileId must not be null/blank");
}
} }
} }

View File

@@ -32,7 +32,7 @@ import tools.jackson.core.json.JsonReadFeature;
import zeroecho.pki.api.PkiException; 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.
* *
* <p> * <p>
* The catalogue is stateless and thread-safe. Every load reparses the packaged * 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 = private static final String ERROR_PREFIX =
"Built-in certificate profile catalogue rejected: code="; "Built-in certificate profile catalogue rejected: code=";
private static final Set<String> EXPECTED_PROFILE_IDS = private static final Set<String> 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() { private BuiltInCertificateProfileCatalog() {
} }
@@ -222,6 +223,9 @@ public final class BuiltInCertificateProfileCatalog {
private final Set<ProfileIdentity> identities = new HashSet<>(); private final Set<ProfileIdentity> identities = new HashSet<>();
private final Set<ByteBuffer> hashes = new HashSet<>(); private final Set<ByteBuffer> hashes = new HashSet<>();
private final Set<String> profileIds = new HashSet<>(); private final Set<String> profileIds = new HashSet<>();
private int endEntityCount;
private int rootCount;
private int intermediateCount;
private CatalogueAccumulator(int expectedSize) { private CatalogueAccumulator(int expectedSize) {
templates = new ArrayList<>(expectedSize); templates = new ArrayList<>(expectedSize);
@@ -240,12 +244,18 @@ public final class BuiltInCertificateProfileCatalog {
|| !profileIds.add(definition.profileId())) { || !profileIds.add(definition.profileId())) {
throw failure("BUILT_IN_PROFILE_SET_INVALID"); 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); templates.add(template);
} }
private List<BuiltInCertificateProfileTemplate> finish() { private List<BuiltInCertificateProfileTemplate> finish() {
if (templates.size() != EXPECTED_PROFILE_IDS.size() 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"); throw failure("BUILT_IN_PROFILE_SET_INVALID");
} }
return List.copyOf(templates); return List.copyOf(templates);

View File

@@ -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.
*
* <p>
* 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.
* </p>
*
* @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<CaKeyUsage> keyUsages,
Set<String> allowedSubjectKeyAlgorithmIds) implements CertificatePolicy {
/** Maximum supported path-length constraint. */
public static final int MAXIMUM_PATH_LENGTH = 32;
private static final Set<String> 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");
}
}
}

View File

@@ -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
}

View File

@@ -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 {
}

View File

@@ -33,6 +33,8 @@
******************************************************************************/ ******************************************************************************/
package zeroecho.pki.api.profile; package zeroecho.pki.api.profile;
import java.time.Duration;
import zeroecho.pki.api.FormatId; import zeroecho.pki.api.FormatId;
/** /**
@@ -51,10 +53,12 @@ import zeroecho.pki.api.FormatId;
* @param profileId stable profile identifier * @param profileId stable profile identifier
* @param formatId framework/format supported by the profile * @param formatId framework/format supported by the profile
* @param displayName human-readable name * @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, public record CertificateProfile(String profileId, FormatId formatId, String displayName,
LeafCertificatePolicy leafPolicy) { Duration maximumValidity, SubjectPolicy subjectPolicy, LeafCertificatePolicy leafPolicy) {
/** /**
* Creates a certificate profile. * Creates a certificate profile.
@@ -72,8 +76,9 @@ public record CertificateProfile(String profileId, FormatId formatId, String dis
if (displayName == null || displayName.isBlank()) { if (displayName == null || displayName.isBlank()) {
throw new IllegalArgumentException("displayName must not be null/blank"); throw new IllegalArgumentException("displayName must not be null/blank");
} }
if (leafPolicy == null) { if (maximumValidity == null || maximumValidity.isZero() || maximumValidity.isNegative()
throw new IllegalArgumentException("leafPolicy must not be null"); || 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) { if (definition == null) {
throw new IllegalArgumentException("definition must not be 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(), return new CertificateProfile(definition.profileId(), definition.formatId(), definition.displayName(),
definition.leafPolicy()); definition.maximumValidity(), definition.subjectPolicy(), definition.leafPolicy());
} }
} }

View File

@@ -4,28 +4,35 @@
******************************************************************************/ ******************************************************************************/
package zeroecho.pki.api.profile; package zeroecho.pki.api.profile;
import java.time.Duration;
import zeroecho.pki.api.FormatId; import zeroecho.pki.api.FormatId;
/** /**
* Immutable, versioned certificate-profile configuration. * Immutable, versioned certificate-profile configuration.
* *
* <p> * <p>
* This definition deliberately excludes runtime activation state. Its * Schema version 2 replaces the pre-release version 1 shape. The certificate
* {@link LeafCertificatePolicy} is the authoritative typed policy used by the * kind and its closed policy variant are immutable across the definition.
* issuance path. * Runtime activation state is deliberately excluded.
* </p> * </p>
* *
* @param certificateType kind of certificate governed by the profile
* @param profileId stable profile identifier * @param profileId stable profile identifier
* @param profileVersion positive configuration version * @param profileVersion positive configuration version
* @param formatId framework/format identifier * @param formatId framework/format identifier
* @param displayName human-readable profile name * @param displayName human-readable profile name
* @param leafPolicy complete leaf certificate policy * @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, public record CertificateProfileDefinition(CertificateProfileKind certificateType,
String displayName, LeafCertificatePolicy leafPolicy) { String profileId, long profileVersion, FormatId formatId, String displayName,
Duration maximumValidity, SubjectPolicy subjectPolicy,
CertificatePolicy certificatePolicy) {
/** Current certificate-profile document schema version. */ /** 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. * Creates a certificate-profile definition.
@@ -34,6 +41,9 @@ public record CertificateProfileDefinition(String profileId, long profileVersion
* profile version is not positive * profile version is not positive
*/ */
public CertificateProfileDefinition { public CertificateProfileDefinition {
if (certificateType == null) {
throw new IllegalArgumentException("certificateType must not be null");
}
if (profileId == null || profileId.isBlank()) { if (profileId == null || profileId.isBlank()) {
throw new IllegalArgumentException("profileId must not be null/blank"); 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()) { if (displayName == null || displayName.isBlank()) {
throw new IllegalArgumentException("displayName must not be null/blank"); throw new IllegalArgumentException("displayName must not be null/blank");
} }
if (leafPolicy == null) { if (maximumValidity == null || maximumValidity.isZero() || maximumValidity.isNegative()) {
throw new IllegalArgumentException("leafPolicy must not be null"); 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");
} }
} }

View File

@@ -34,7 +34,7 @@ import zeroecho.pki.api.FormatId;
import zeroecho.pki.api.PkiException; 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. * documents.
* *
* <p> * <p>
@@ -42,9 +42,10 @@ import zeroecho.pki.api.PkiException;
* tokens and creates the existing typed policy model directly. * tokens and creates the existing typed policy model directly.
* </p> * </p>
*/ */
// The closed streaming grammar intentionally keeps all bounded field handlers in one codec.
@SuppressWarnings({ "PMD.AvoidDuplicateLiterals", "PMD.AvoidInstantiatingObjectsInLoops", @SuppressWarnings({ "PMD.AvoidDuplicateLiterals", "PMD.AvoidInstantiatingObjectsInLoops",
"PMD.AvoidUncheckedExceptionsInSignatures", "PMD.CyclomaticComplexity", "PMD.AvoidUncheckedExceptionsInSignatures", "PMD.CyclomaticComplexity",
"PMD.PreserveStackTrace" }) "PMD.PreserveStackTrace", "PMD.TooManyMethods" })
public final class CertificateProfileDocumentCodec { public final class CertificateProfileDocumentCodec {
/** Maximum accepted encoded document size. */ /** Maximum accepted encoded document size. */
@@ -157,7 +158,7 @@ public final class CertificateProfileDocumentCodec {
* @return newly allocated canonical JSON bytes without a BOM or trailing * @return newly allocated canonical JSON bytes without a BOM or trailing
* whitespace * whitespace
* @throws PkiException if the definition cannot be represented by schema * @throws PkiException if the definition cannot be represented by schema
* version 1 * version 2
*/ */
public static byte[] writeCanonical(CertificateProfileDefinition definition) { public static byte[] writeCanonical(CertificateProfileDefinition definition) {
validateDefinition(definition); validateDefinition(definition);
@@ -209,92 +210,155 @@ public final class CertificateProfileDocumentCodec {
private static CertificateProfileDefinition parseDocument(JsonParser parser) throws JacksonException { private static CertificateProfileDefinition parseDocument(JsonParser parser) throws JacksonException {
requireToken(parser.nextToken(), JsonToken.START_OBJECT, "$"); requireToken(parser.nextToken(), JsonToken.START_OBJECT, "$");
long seen = 0; DocumentFields document = new DocumentFields();
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;
while (parser.nextToken() != JsonToken.END_OBJECT) { while (parser.nextToken() != JsonToken.END_OBJECT) {
requireToken(parser.currentToken(), JsonToken.PROPERTY_NAME, "$"); requireToken(parser.currentToken(), JsonToken.PROPERTY_NAME, "$");
String field = parser.currentName(); String field = parser.currentName();
requireValue(parser, "$." + field); requireValue(parser, "$." + field);
readDocumentField(parser, field, document);
}
requireAll(document.seen, 8, "$");
if (document.schemaVersion != CertificateProfileDefinition.SCHEMA_VERSION) {
throw failure("SCHEMA_VERSION_UNSUPPORTED", "$.schemaVersion");
}
if (document.profileVersion <= 0) {
throw failure("PROFILE_VERSION_INVALID", "$.profileVersion");
}
validateProfileString(document.profileId, MAXIMUM_PROFILE_ID_UTF8_BYTES, "$.profileId",
"TOKEN_INVALID");
validateProfileString(document.formatId, MAXIMUM_FORMAT_ID_UTF8_BYTES, "$.formatId",
"TOKEN_INVALID");
validateProfileString(document.displayName, MAXIMUM_DISPLAY_NAME_UTF8_BYTES, "$.displayName",
"TOKEN_INVALID");
requirePolicyFields(document.certificateType, document.seen);
return constructDefinition(document);
}
private static void readDocumentField(JsonParser parser, String field, DocumentFields document)
throws JacksonException {
switch (field) { switch (field) {
case "schemaVersion" -> { case "schemaVersion" -> {
seen = mark(seen, 0, "$.schemaVersion"); document.seen = mark(document.seen, 0, "$.schemaVersion");
schemaVersion = readInt(parser, "$.schemaVersion"); document.schemaVersion = readInt(parser, "$.schemaVersion");
}
case "certificateType" -> {
document.seen = mark(document.seen, 1, "$.certificateType");
document.certificateType = readCertificateType(parser, "$.certificateType");
} }
case "profileId" -> { case "profileId" -> {
seen = mark(seen, 1, "$.profileId"); document.seen = mark(document.seen, 2, "$.profileId");
profileId = readBoundedString(parser, "$.profileId", MAXIMUM_PROFILE_ID_UTF8_BYTES); document.profileId = readBoundedString(parser, "$.profileId", MAXIMUM_PROFILE_ID_UTF8_BYTES);
} }
case "profileVersion" -> { case "profileVersion" -> {
seen = mark(seen, 2, "$.profileVersion"); document.seen = mark(document.seen, 3, "$.profileVersion");
profileVersion = readLong(parser, "$.profileVersion"); document.profileVersion = readLong(parser, "$.profileVersion");
} }
case "formatId" -> { case "formatId" -> {
seen = mark(seen, 3, "$.formatId"); document.seen = mark(document.seen, 4, "$.formatId");
formatId = readBoundedString(parser, "$.formatId", MAXIMUM_FORMAT_ID_UTF8_BYTES); document.formatId = readBoundedString(parser, "$.formatId", MAXIMUM_FORMAT_ID_UTF8_BYTES);
} }
case "displayName" -> { case "displayName" -> {
seen = mark(seen, 4, "$.displayName"); document.seen = mark(document.seen, 5, "$.displayName");
displayName = readBoundedString(parser, "$.displayName", MAXIMUM_DISPLAY_NAME_UTF8_BYTES); document.displayName =
readBoundedString(parser, "$.displayName", MAXIMUM_DISPLAY_NAME_UTF8_BYTES);
} }
case "maxValidity" -> { case "maxValidity" -> {
seen = mark(seen, 5, "$.maxValidity"); document.seen = mark(document.seen, 6, "$.maxValidity");
maximumValidity = readDuration(parser, "$.maxValidity"); document.maximumValidity = readDuration(parser, "$.maxValidity");
} }
case "subject" -> { case "subject" -> {
seen = mark(seen, 6, "$.subject"); document.seen = mark(document.seen, 7, "$.subject");
subject = readSubject(parser, "$.subject"); document.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");
} }
case "subjectAlternativeNames" -> readDocumentSan(parser, document);
case "leafCertificate" -> readDocumentLeaf(parser, document);
case "caCertificate" -> readDocumentCa(parser, document);
default -> throw failure("UNKNOWN_FIELD", "$.?"); default -> throw failure("UNKNOWN_FIELD", "$.?");
} }
} }
requireAll(seen, 9, "$");
if (schemaVersion != CertificateProfileDefinition.SCHEMA_VERSION) { private static void readDocumentSan(JsonParser parser, DocumentFields document)
throw failure("SCHEMA_VERSION_UNSUPPORTED", "$.schemaVersion"); throws JacksonException {
if (document.certificateType != null
&& document.certificateType != CertificateProfileKind.END_ENTITY) {
throw failure("FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE", "$.subjectAlternativeNames");
} }
if (profileVersion <= 0) { document.seen = mark(document.seen, 8, "$.subjectAlternativeNames");
throw failure("PROFILE_VERSION_INVALID", "$.profileVersion"); document.san = readSan(parser, "$.subjectAlternativeNames");
}
validateProfileString(profileId, MAXIMUM_PROFILE_ID_UTF8_BYTES, "$.profileId",
"TOKEN_INVALID");
validateProfileString(formatId, MAXIMUM_FORMAT_ID_UTF8_BYTES, "$.formatId",
"TOKEN_INVALID");
validateProfileString(displayName, MAXIMUM_DISPLAY_NAME_UTF8_BYTES, "$.displayName",
"TOKEN_INVALID");
return constructDefinition(profileId, profileVersion, formatId, displayName, maximumValidity,
subject, san, leaf);
} }
private static CertificateProfileDefinition constructDefinition(String profileId, long profileVersion, private static void readDocumentLeaf(JsonParser parser, DocumentFields document)
String formatId, String displayName, Duration maximumValidity, SubjectSection subject, throws JacksonException {
SanSection san, LeafSection leaf) { 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 { try {
SubjectPolicy subjectPolicy = new SubjectPolicy(subject.rules()); SubjectPolicy subjectPolicy = new SubjectPolicy(document.subject.allowEmpty(),
SubjectAlternativeNamePolicy sanPolicy = new SubjectAlternativeNamePolicy(subject.allowEmpty(), document.subject.rules());
san.minimumTotal(), san.maximumTotal(), san.rules(), san.wildcardAllowed(), CertificatePolicy policy;
san.allowedSchemes(), san.criticalWhenSubjectNonEmpty(), san.serviceIdentityRequired(), if (document.certificateType == CertificateProfileKind.END_ENTITY) {
san.emailIdentityRequired()); SubjectAlternativeNamePolicy sanPolicy = new SubjectAlternativeNamePolicy(
LeafCertificatePolicy leafPolicy = new LeafCertificatePolicy(subjectPolicy, sanPolicy, document.san.minimumTotal(), document.san.maximumTotal(), document.san.rules(),
leaf.keyUsage(), leaf.extendedKeyUsage(), leaf.keyUsageCritical(), document.san.wildcardAllowed(), document.san.allowedSchemes(),
leaf.extendedKeyUsageCritical(), leaf.basicConstraintsCritical(), document.san.criticalWhenSubjectNonEmpty(),
leaf.allowedKeyAlgorithms(), maximumValidity); document.san.serviceIdentityRequired(), document.san.emailIdentityRequired());
return new CertificateProfileDefinition(profileId, profileVersion, new FormatId(formatId), if (document.subject.allowEmpty() && document.san.minimumTotal() < 1) {
displayName, leafPolicy); 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) { } catch (IllegalArgumentException | ArithmeticException ex) {
throw failure("SEMANTIC_INVALID", "$"); throw failure("SEMANTIC_INVALID", "$");
} }
@@ -612,6 +676,66 @@ public final class CertificateProfileDocumentCodec {
return Set.copyOf(values); 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<CaKeyUsage> keyUsages = null;
Set<String> 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<CaKeyUsage> readCaKeyUsages(JsonParser parser, String path)
throws JacksonException {
requireToken(parser.currentToken(), JsonToken.START_ARRAY, path);
Set<CaKeyUsage> 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<ExtendedKeyUsageId> readExtendedKeyUsages(JsonParser parser, String path) private static Set<ExtendedKeyUsageId> readExtendedKeyUsages(JsonParser parser, String path)
throws JacksonException { throws JacksonException {
requireToken(parser.currentToken(), JsonToken.START_ARRAY, path); requireToken(parser.currentToken(), JsonToken.START_ARRAY, path);
@@ -650,27 +774,31 @@ public final class CertificateProfileDocumentCodec {
private static void writeDocument(JsonGenerator generator, CertificateProfileDefinition definition) private static void writeDocument(JsonGenerator generator, CertificateProfileDefinition definition)
throws JacksonException { throws JacksonException {
LeafCertificatePolicy policy = definition.leafPolicy();
generator.writeStartObject(); generator.writeStartObject();
generator.writeNumberProperty("schemaVersion", CertificateProfileDefinition.SCHEMA_VERSION); generator.writeNumberProperty("schemaVersion", CertificateProfileDefinition.SCHEMA_VERSION);
generator.writeStringProperty("certificateType", definition.certificateType().name());
generator.writeStringProperty("profileId", definition.profileId()); generator.writeStringProperty("profileId", definition.profileId());
generator.writeNumberProperty("profileVersion", definition.profileVersion()); generator.writeNumberProperty("profileVersion", definition.profileVersion());
generator.writeStringProperty("formatId", definition.formatId().value()); generator.writeStringProperty("formatId", definition.formatId().value());
generator.writeStringProperty("displayName", definition.displayName()); generator.writeStringProperty("displayName", definition.displayName());
generator.writeStringProperty("maxValidity", policy.maximumValidity().toString()); generator.writeStringProperty("maxValidity", definition.maximumValidity().toString());
writeSubject(generator, policy); writeSubject(generator, definition.subjectPolicy());
writeSan(generator, policy.subjectAlternativeNamePolicy()); if (definition.certificateType() == CertificateProfileKind.END_ENTITY) {
writeLeaf(generator, policy); LeafCertificatePolicy leaf = definition.leafPolicy();
writeSan(generator, leaf.subjectAlternativeNamePolicy());
writeLeaf(generator, leaf);
} else {
writeCa(generator, definition.caPolicy());
}
generator.writeEndObject(); generator.writeEndObject();
} }
private static void writeSubject(JsonGenerator generator, LeafCertificatePolicy policy) private static void writeSubject(JsonGenerator generator, SubjectPolicy policy)
throws JacksonException { throws JacksonException {
generator.writeObjectPropertyStart("subject"); generator.writeObjectPropertyStart("subject");
generator.writeBooleanProperty("allowEmpty", generator.writeBooleanProperty("allowEmpty", policy.allowEmpty());
policy.subjectAlternativeNamePolicy().allowEmptySubject());
generator.writeArrayPropertyStart("rules"); generator.writeArrayPropertyStart("rules");
for (SubjectRdnRule rule : policy.subjectPolicy().rules()) { for (SubjectRdnRule rule : policy.rules()) {
generator.writeStartObject(); generator.writeStartObject();
generator.writeStringProperty("oid", rule.type().oid()); generator.writeStringProperty("oid", rule.type().oid());
generator.writeStringProperty("source", generator.writeStringProperty("source",
@@ -740,6 +868,20 @@ public final class CertificateProfileDocumentCodec {
generator.writeEndObject(); 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, private static void writeSortedStrings(JsonGenerator generator, String field,
java.util.Collection<String> values) throws JacksonException { java.util.Collection<String> values) throws JacksonException {
generator.writeArrayPropertyStart(field); generator.writeArrayPropertyStart(field);
@@ -759,11 +901,25 @@ public final class CertificateProfileDocumentCodec {
"$.formatId", "CANONICALIZATION_FAILED"); "$.formatId", "CANONICALIZATION_FAILED");
validateProfileString(definition.displayName(), MAXIMUM_DISPLAY_NAME_UTF8_BYTES, validateProfileString(definition.displayName(), MAXIMUM_DISPLAY_NAME_UTF8_BYTES,
"$.displayName", "CANONICALIZATION_FAILED"); "$.displayName", "CANONICALIZATION_FAILED");
LeafCertificatePolicy leaf = definition.leafPolicy(); validateWritableString(definition.maximumValidity().toString(), MAXIMUM_STRING_UTF8_BYTES,
validateWritableString(leaf.maximumValidity().toString(), MAXIMUM_STRING_UTF8_BYTES,
"$.maxValidity"); "$.maxValidity");
if (leaf.subjectPolicy().rules().size() > MAXIMUM_ARRAY_ELEMENTS if (definition.subjectPolicy().rules().size() > MAXIMUM_ARRAY_ELEMENTS) {
|| leaf.subjectAlternativeNamePolicy().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() || leaf.subjectAlternativeNamePolicy().allowedUriSchemes().size()
> MAXIMUM_ARRAY_ELEMENTS > MAXIMUM_ARRAY_ELEMENTS
|| leaf.keyUsages().size() > MAXIMUM_ARRAY_ELEMENTS || leaf.keyUsages().size() > MAXIMUM_ARRAY_ELEMENTS
@@ -771,10 +927,6 @@ public final class CertificateProfileDocumentCodec {
|| leaf.allowedSubjectKeyAlgorithmIds().size() > MAXIMUM_ARRAY_ELEMENTS) { || leaf.allowedSubjectKeyAlgorithmIds().size() > MAXIMUM_ARRAY_ELEMENTS) {
throw failure("CANONICALIZATION_FAILED", "$"); 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()) { for (String scheme : leaf.subjectAlternativeNamePolicy().allowedUriSchemes()) {
validateWritableString(scheme, MAXIMUM_URI_SCHEME_ASCII_BYTES, validateWritableString(scheme, MAXIMUM_URI_SCHEME_ASCII_BYTES,
"$.subjectAlternativeNames.rules.allowedSchemes"); "$.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) { private static boolean parseSubjectSource(String source, String path) {
if (REQUESTER_SOURCE.equals(source)) { if (REQUESTER_SOURCE.equals(source)) {
return false; return false;
@@ -972,6 +1134,22 @@ public final class CertificateProfileDocumentCodec {
return new PkiException(PREFIX + code + " path=" + path); 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<SubjectRdnRule> rules) { private record SubjectSection(boolean allowEmpty, List<SubjectRdnRule> rules) {
} }
@@ -993,4 +1171,9 @@ public final class CertificateProfileDocumentCodec {
Set<LeafKeyUsage> keyUsage, boolean extendedKeyUsageCritical, Set<LeafKeyUsage> keyUsage, boolean extendedKeyUsageCritical,
Set<ExtendedKeyUsageId> extendedKeyUsage, Set<String> allowedKeyAlgorithms) { Set<ExtendedKeyUsageId> extendedKeyUsage, Set<String> allowedKeyAlgorithms) {
} }
private record CaSection(boolean basicConstraintsCritical, int pathLengthConstraint,
boolean keyUsageCritical, Set<CaKeyUsage> keyUsages,
Set<String> allowedKeyAlgorithms) {
}
} }

View File

@@ -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
}

View File

@@ -4,13 +4,11 @@
******************************************************************************/ ******************************************************************************/
package zeroecho.pki.api.profile; package zeroecho.pki.api.profile;
import java.time.Duration;
import java.util.Set; import java.util.Set;
/** /**
* Complete issuer-controlled leaf certificate extension and identity policy. * Complete issuer-controlled leaf certificate extension and identity policy.
* *
* @param subjectPolicy subject policy
* @param subjectAlternativeNamePolicy SAN policy * @param subjectAlternativeNamePolicy SAN policy
* @param keyUsages exact key-usage set * @param keyUsages exact key-usage set
* @param extendedKeyUsages exact extended-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 extendedKeyUsageCritical extended-key-usage criticality
* @param basicConstraintsCritical BasicConstraints criticality * @param basicConstraintsCritical BasicConstraints criticality
* @param allowedSubjectKeyAlgorithmIds exact canonical ZeroEcho key algorithm identifiers * @param allowedSubjectKeyAlgorithmIds exact canonical ZeroEcho key algorithm identifiers
* @param maximumValidity positive maximum validity
*/ */
public record LeafCertificatePolicy(SubjectPolicy subjectPolicy, public record LeafCertificatePolicy(SubjectAlternativeNamePolicy subjectAlternativeNamePolicy,
SubjectAlternativeNamePolicy subjectAlternativeNamePolicy, Set<LeafKeyUsage> keyUsages, Set<LeafKeyUsage> keyUsages,
Set<ExtendedKeyUsageId> extendedKeyUsages, boolean keyUsageCritical, boolean extendedKeyUsageCritical, Set<ExtendedKeyUsageId> extendedKeyUsages, boolean keyUsageCritical, boolean extendedKeyUsageCritical,
boolean basicConstraintsCritical, Set<String> allowedSubjectKeyAlgorithmIds, Duration maximumValidity) { boolean basicConstraintsCritical,
Set<String> allowedSubjectKeyAlgorithmIds) implements CertificatePolicy {
private static final Set<String> SUPPORTED_SUBJECT_KEY_ALGORITHMS = private static final Set<String> SUPPORTED_SUBJECT_KEY_ALGORITHMS =
Set.of("RSA", "ECDSA", "Ed25519", "Ed448"); Set.of("RSA", "ECDSA", "Ed25519", "Ed448");
@@ -32,8 +30,8 @@ public record LeafCertificatePolicy(SubjectPolicy subjectPolicy,
* Validates and constructs the policy. * Validates and constructs the policy.
*/ */
public LeafCertificatePolicy { public LeafCertificatePolicy {
if (subjectPolicy == null || subjectAlternativeNamePolicy == null || keyUsages == null if (subjectAlternativeNamePolicy == null || keyUsages == null
|| extendedKeyUsages == null || allowedSubjectKeyAlgorithmIds == null || maximumValidity == null) { || extendedKeyUsages == null || allowedSubjectKeyAlgorithmIds == null) {
throw new IllegalArgumentException("Leaf certificate policy values must not be null"); throw new IllegalArgumentException("Leaf certificate policy values must not be null");
} }
keyUsages = Set.copyOf(keyUsages); keyUsages = Set.copyOf(keyUsages);
@@ -47,8 +45,5 @@ public record LeafCertificatePolicy(SubjectPolicy subjectPolicy,
&& !keyUsages.contains(LeafKeyUsage.KEY_AGREEMENT)) { && !keyUsages.contains(LeafKeyUsage.KEY_AGREEMENT)) {
throw new IllegalArgumentException("encipherOnly and decipherOnly require keyAgreement"); throw new IllegalArgumentException("encipherOnly and decipherOnly require keyAgreement");
} }
if (maximumValidity.isZero() || maximumValidity.isNegative()) {
throw new IllegalArgumentException("Maximum validity must be positive");
}
} }
} }

View File

@@ -11,7 +11,6 @@ import java.util.Set;
/** /**
* Deny-by-default Subject Alternative Name policy. * Deny-by-default Subject Alternative Name policy.
* *
* @param allowEmptySubject whether the subject DN may be empty
* @param minimumTotal minimum total SAN count * @param minimumTotal minimum total SAN count
* @param maximumTotal maximum total SAN count * @param maximumTotal maximum total SAN count
* @param rules permitted SAN type rules * @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 requireServiceIdentity whether DNS, IP, or URI identity is required
* @param requireEmailIdentity whether an RFC822 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<SubjectAlternativeNameRule> rules, boolean allowDnsWildcard, Set<String> allowedUriSchemes, List<SubjectAlternativeNameRule> rules, boolean allowDnsWildcard, Set<String> allowedUriSchemes,
boolean criticalWithNonemptySubject, boolean requireServiceIdentity, boolean requireEmailIdentity) { boolean criticalWithNonemptySubject, boolean requireServiceIdentity, boolean requireEmailIdentity) {
@@ -75,8 +74,5 @@ public record SubjectAlternativeNamePolicy(boolean allowEmptySubject, int minimu
|| uriPossible != !allowedUriSchemes.isEmpty()) { || uriPossible != !allowedUriSchemes.isEmpty()) {
throw new IllegalArgumentException("SAN policy requirements are not satisfiable"); throw new IllegalArgumentException("SAN policy requirements are not satisfiable");
} }
if (allowEmptySubject && minimumTotal < 1) {
throw new IllegalArgumentException("An empty subject requires at least one SAN");
}
} }
} }

View File

@@ -10,9 +10,10 @@ import java.util.Set;
/** /**
* Deny-by-default subject distinguished-name policy. * Deny-by-default subject distinguished-name policy.
* *
* @param allowEmpty whether an empty subject is permitted
* @param rules ordered immutable supported RDN rules * @param rules ordered immutable supported RDN rules
*/ */
public record SubjectPolicy(List<SubjectRdnRule> rules) { public record SubjectPolicy(boolean allowEmpty, List<SubjectRdnRule> rules) {
/** Maximum number of subject RDNs. */ /** Maximum number of subject RDNs. */
public static final int HARD_MAXIMUM_RDN_COUNT = 32; public static final int HARD_MAXIMUM_RDN_COUNT = 32;

View File

@@ -46,13 +46,13 @@
* </p> * </p>
* *
* <p> * <p>
* Built-in end-entity profile resources are deterministic baseline * Document schema version 2 distinguishes end-entity, root-CA, and
* provisioning templates. Loading the catalogue neither persists nor activates * intermediate-CA profiles through a closed certificate-policy variant.
* a profile, and the resources are never implicit issuance defaults. * Built-in resources are deterministic provisioning templates. Loading the
* Administrator-supplied profiles will use the same strict document schema. * catalogue neither persists nor activates a profile, and resources are never
* Canonical JSON and SHA-256 hashes support deterministic provisioning and * implicit issuance defaults. A logical profile ID cannot change certificate
* audit. CA profile resources are intentionally absent until the CA profile * kind across versions. Active definitions of every supported certificate kind
* policy model is defined. * are authoritative issuance inputs.
* </p> * </p>
* *
* @since 1.0 * @since 1.0

View File

@@ -18,6 +18,7 @@ public final class ProfileLifecycleFailure extends PkiException {
PROFILE_IMPORT_VALIDATION_FAILED, PROFILE_IMPORT_VALIDATION_FAILED,
BUILT_IN_PROFILE_INVALID, BUILT_IN_PROFILE_INVALID,
PROFILE_VERSION_CONFLICT, PROFILE_VERSION_CONFLICT,
PROFILE_KIND_CONFLICT,
PROFILE_VERSION_CORRUPT, PROFILE_VERSION_CORRUPT,
PROFILE_ACTIVE_POINTER_CORRUPT, PROFILE_ACTIVE_POINTER_CORRUPT,
PROFILE_HASH_MISMATCH, PROFILE_HASH_MISMATCH,

View File

@@ -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<Validity> requestedValidity,
Instant evaluationTime, Optional<Instant> issuerNotAfter, BigInteger serial) {
CertificateProfileDefinition definition = activeProfile.definition();
requireProfileShape(definition, expectedKind, formatId);
List<SubjectRdn> 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<SubjectRdn> approvedSubject, EncodedObject exactPublicKey, Optional<Validity> requestedValidity,
Instant evaluationTime, Optional<Instant> issuerNotAfter, BigInteger serial) {
CertificateProfileDefinition definition = activeProfile.definition();
requireProfileShape(definition, expectedKind, formatId);
List<SubjectRdn> 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<SubjectRdn> validateSubject(SubjectRef requestedSubject,
CertificateProfileDefinition definition) {
List<SubjectRdn> requested = parseSubject(requestedSubject);
Map<SubjectRdnType, SubjectRdnRule> rules = new EnumMap<>(SubjectRdnType.class);
for (SubjectRdnRule rule : definition.subjectPolicy().rules()) {
rules.put(rule.type(), rule);
}
Map<SubjectRdnType, Integer> counts = new EnumMap<>(SubjectRdnType.class);
List<SubjectRdn> 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<SubjectRdn> validateTrustedSubject(SubjectRef trustedSubject,
CertificateProfileDefinition definition) {
List<SubjectRdn> existing = parseSubject(trustedSubject);
Map<SubjectRdnType, SubjectRdnRule> rules = new EnumMap<>(SubjectRdnType.class);
for (SubjectRdnRule rule : definition.subjectPolicy().rules()) {
rules.put(rule.type(), rule);
}
Map<SubjectRdnType, Integer> counts = new EnumMap<>(SubjectRdnType.class);
List<SubjectRdn> 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<SubjectRdn> 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<SubjectRdn> requireApprovedSubject(List<SubjectRdn> approvedSubject) {
List<SubjectRdn> 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<SubjectRdn> parseSubject(SubjectRef subjectRef) {
try {
X500Name name = new X500Name(subjectRef.value());
List<SubjectRdn> 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<Validity> requested,
CertificateProfileDefinition definition, Instant evaluationTime, Optional<Instant> 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);
}
}

View File

@@ -59,9 +59,6 @@ import zeroecho.pki.api.FormatId;
import zeroecho.pki.api.KeyRef; import zeroecho.pki.api.KeyRef;
import zeroecho.pki.api.PkiException; import zeroecho.pki.api.PkiException;
import zeroecho.pki.api.PkiId; 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.AccessContext;
import zeroecho.pki.api.audit.AuditEvent; import zeroecho.pki.api.audit.AuditEvent;
import zeroecho.pki.api.audit.Principal; import zeroecho.pki.api.audit.Principal;
@@ -127,16 +124,7 @@ final class CaProofGate {
/* default */ ManagedKeyProof proveManagedKey(KeyRef keyRef, FormatId formatId, String auditAction, /* default */ ManagedKeyProof proveManagedKey(KeyRef keyRef, FormatId formatId, String auditAction,
Optional<PkiId> subjectCaId) { Optional<PkiId> subjectCaId) {
EncodedObject resolved; EncodedObject resolved = resolveManagedKey(keyRef, formatId, auditAction, subjectCaId);
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");
}
byte[] exactSpki = resolved.bytes().clone(); byte[] exactSpki = resolved.bytes().clone();
byte[] challenge = new byte[MANAGED_KEY_CHALLENGE_DOMAIN.length + CHALLENGE_NONCE_BYTES]; byte[] challenge = new byte[MANAGED_KEY_CHALLENGE_DOMAIN.length + CHALLENGE_NONCE_BYTES];
CHALLENGE_RANDOM.nextBytes(challenge); CHALLENGE_RANDOM.nextBytes(challenge);
@@ -160,12 +148,18 @@ final class CaProofGate {
} }
} }
/* default */ ManagedCaIssuance authorizeIntermediate(ManagedKeyProof proof, /* default */ EncodedObject resolveManagedKey(KeyRef keyRef, FormatId formatId, String auditAction,
ManagedCaIssuance.Operation operation, PkiId issuerCaId, PkiId subjectCaId, String profileId, Optional<PkiId> subjectCaId) {
Optional<Validity> requestedValidity, AttributeSet attributes, SubjectRef subjectRef) { EncodedObject resolved;
Objects.requireNonNull(proof, "proof"); try {
return new ManagedCaIssuance(proof, operation, issuerCaId, subjectCaId, profileId, requestedValidity, resolved = publicKeyResolver.resolveSpkiDer(keyRef);
attributes, subjectRef); } 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<PkiId> objectId, String code) { /* default */ PkiException rejection(String action, FormatId formatId, Optional<PkiId> objectId, String code) {

View File

@@ -64,10 +64,10 @@ final class CertificateProfileValidator {
ParsedCertificationRequest request = candidate.request(); ParsedCertificationRequest request = candidate.request();
LeafCertificatePolicy policy = profile.leafPolicy(); LeafCertificatePolicy policy = profile.leafPolicy();
requireCanonicalRequestAttributes(request); requireCanonicalRequestAttributes(request);
List<SubjectRdn> approvedSubject = validateSubject(request, policy); List<SubjectRdn> approvedSubject = validateSubject(request, profile);
List<SubjectAlternativeName> approvedSans = validateSans(request, policy, approvedSubject.isEmpty()); List<SubjectAlternativeName> approvedSans = validateSans(request, policy, approvedSubject.isEmpty());
requireSubjectKeyAllowed(candidate, policy); requireSubjectKeyAllowed(candidate.exactPublicKey(), policy.allowedSubjectKeyAlgorithmIds());
Validity validity = approvedValidity(candidate, request, policy, issuerCredential, evaluationTime); Validity validity = approvedValidity(candidate, request, profile, issuerCredential, evaluationTime);
boolean sanCritical = approvedSubject.isEmpty() boolean sanCritical = approvedSubject.isEmpty()
|| policy.subjectAlternativeNamePolicy().criticalWithNonemptySubject(); || policy.subjectAlternativeNamePolicy().criticalWithNonemptySubject();
SubjectRef approvedSubjectRef = new SubjectRef(approvedSubject.isEmpty() 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. // The branches preserve the deny-by-default RDN ownership and cardinality rules.
@SuppressWarnings("PMD.CyclomaticComplexity") @SuppressWarnings("PMD.CyclomaticComplexity")
private static List<SubjectRdn> validateSubject(ParsedCertificationRequest request, LeafCertificatePolicy policy) { private static List<SubjectRdn> validateSubject(ParsedCertificationRequest request, CertificateProfile profile) {
Map<SubjectRdnType, SubjectRdnRule> rules = new EnumMap<>(SubjectRdnType.class); Map<SubjectRdnType, SubjectRdnRule> rules = new EnumMap<>(SubjectRdnType.class);
for (SubjectRdnRule rule : policy.subjectPolicy().rules()) { for (SubjectRdnRule rule : profile.subjectPolicy().rules()) {
rules.put(rule.type(), rule); rules.put(rule.type(), rule);
} }
Map<SubjectRdnType, Integer> counts = new EnumMap<>(SubjectRdnType.class); Map<SubjectRdnType, Integer> counts = new EnumMap<>(SubjectRdnType.class);
@@ -113,7 +113,7 @@ final class CertificateProfileValidator {
counts.put(rdn.type(), count); counts.put(rdn.type(), count);
approved.add(new SubjectRdn(rdn.type(), canonical)); approved.add(new SubjectRdn(rdn.type(), canonical));
} }
for (SubjectRdnRule rule : policy.subjectPolicy().rules()) { for (SubjectRdnRule rule : profile.subjectPolicy().rules()) {
if (rule.fixedValue().isPresent()) { if (rule.fixedValue().isPresent()) {
approved.add(new SubjectRdn(rule.type(), rule.fixedValue().orElseThrow())); approved.add(new SubjectRdn(rule.type(), rule.fixedValue().orElseThrow()));
counts.put(rule.type(), 1); counts.put(rule.type(), 1);
@@ -123,7 +123,7 @@ final class CertificateProfileValidator {
throw reject("SUBJECT_RDN_REQUIRED"); throw reject("SUBJECT_RDN_REQUIRED");
} }
} }
if (approved.isEmpty() && !policy.subjectAlternativeNamePolicy().allowEmptySubject()) { if (approved.isEmpty() && !profile.subjectPolicy().allowEmpty()) {
throw reject("SUBJECT_EMPTY"); throw reject("SUBJECT_EMPTY");
} }
if (approved.size() > zeroecho.pki.api.profile.SubjectPolicy.HARD_MAXIMUM_RDN_COUNT) { 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. // The public exception deliberately redacts ASN.1 parser details.
@SuppressWarnings({ "PMD.PreserveStackTrace", "PMD.AvoidRethrowingException" }) @SuppressWarnings({ "PMD.PreserveStackTrace", "PMD.AvoidRethrowingException" })
private static void requireSubjectKeyAllowed(VerifiedIssuanceCandidate candidate, LeafCertificatePolicy policy) { /* package */ static void requireSubjectKeyAllowed(zeroecho.pki.api.EncodedObject exactPublicKey,
if (candidate.exactPublicKey().encoding() != Encoding.DER) { Set<String> allowedAlgorithms) {
if (exactPublicKey.encoding() != Encoding.DER) {
throw reject("SUBJECT_KEY_UNSUPPORTED"); throw reject("SUBJECT_KEY_UNSUPPORTED");
} }
byte[] encoded = candidate.exactPublicKey().bytes(); byte[] encoded = exactPublicKey.bytes();
try { try {
SubjectPublicKeyInfo spki = SubjectPublicKeyInfo.getInstance(encoded); SubjectPublicKeyInfo spki = SubjectPublicKeyInfo.getInstance(encoded);
SubjectKeyAlgorithm algorithm = subjectKeyAlgorithm(spki.getAlgorithm().getAlgorithm()); SubjectKeyAlgorithm algorithm = subjectKeyAlgorithm(spki.getAlgorithm().getAlgorithm());
requireSupportedParameters(spki, algorithm); requireSupportedParameters(spki, algorithm);
if (!policy.allowedSubjectKeyAlgorithmIds().contains(algorithm.profileId())) { if (!allowedAlgorithms.contains(algorithm.profileId())) {
throw reject("SUBJECT_KEY_ALGORITHM_FORBIDDEN"); throw reject("SUBJECT_KEY_ALGORITHM_FORBIDDEN");
} }
PublicKey reconstructed = KeyFactory.getInstance(algorithm.jcaName()) PublicKey reconstructed = KeyFactory.getInstance(algorithm.jcaName())
@@ -262,12 +263,12 @@ final class CertificateProfileValidator {
// The public exception deliberately redacts temporal arithmetic details. // The public exception deliberately redacts temporal arithmetic details.
@SuppressWarnings("PMD.PreserveStackTrace") @SuppressWarnings("PMD.PreserveStackTrace")
private static Validity approvedValidity(VerifiedIssuanceCandidate candidate, ParsedCertificationRequest request, private static Validity approvedValidity(VerifiedIssuanceCandidate candidate, ParsedCertificationRequest request,
LeafCertificatePolicy policy, Credential issuerCredential, Instant evaluationTime) { CertificateProfile profile, Credential issuerCredential, Instant evaluationTime) {
Optional<Validity> supplied = candidate.validityOverride().isPresent() Optional<Validity> supplied = candidate.validityOverride().isPresent()
? candidate.validityOverride() : request.requestedValidity(); ? candidate.validityOverride() : request.requestedValidity();
Duration duration = supplied.map(value -> Duration.between(value.notBefore(), value.notAfter())) Duration duration = supplied.map(value -> Duration.between(value.notBefore(), value.notAfter()))
.orElse(policy.maximumValidity()); .orElse(profile.maximumValidity());
if (duration.isZero() || duration.isNegative() || duration.compareTo(policy.maximumValidity()) > 0) { if (duration.isZero() || duration.isNegative() || duration.compareTo(profile.maximumValidity()) > 0) {
throw reject("VALIDITY_EXCEEDS_PROFILE"); throw reject("VALIDITY_EXCEEDS_PROFILE");
} }
Instant notAfter; Instant notAfter;

View File

@@ -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) if (!(binding instanceof CaProfileBinding ca)
|| !ca.profileId().equals(expectedCaProfileId)) { || !ca.reference().equals(expected)) {
throw mismatch(); throw mismatch();
} }
} }

View File

@@ -36,6 +36,7 @@ package zeroecho.pki.impl.core;
import java.io.IOException; import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.time.Clock;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; 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.Extension;
import org.bouncycastle.asn1.x509.KeyUsage; import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.CertException;
import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder; import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder; import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
import zeroecho.pki.api.CaService; import zeroecho.pki.api.CaService;
@@ -65,10 +68,10 @@ import zeroecho.pki.api.IssuerRef;
import zeroecho.pki.api.KeyRef; import zeroecho.pki.api.KeyRef;
import zeroecho.pki.api.PkiException; import zeroecho.pki.api.PkiException;
import zeroecho.pki.api.PkiId; import zeroecho.pki.api.PkiId;
import zeroecho.pki.api.ProfileService;
import zeroecho.pki.api.SubjectRef; import zeroecho.pki.api.SubjectRef;
import zeroecho.pki.api.Validity; import zeroecho.pki.api.Validity;
import zeroecho.pki.api.attr.AttributeSet; import zeroecho.pki.api.attr.AttributeSet;
import zeroecho.pki.api.attr.AttributeValue;
import zeroecho.pki.api.ca.CaCreateCommand; import zeroecho.pki.api.ca.CaCreateCommand;
import zeroecho.pki.api.ca.CaImportCommand; import zeroecho.pki.api.ca.CaImportCommand;
import zeroecho.pki.api.ca.CaKeyRotationCommand; 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.CredentialUse;
import zeroecho.pki.api.credential.EffectiveCredentialStatus; import zeroecho.pki.api.credential.EffectiveCredentialStatus;
import zeroecho.pki.api.credential.EffectiveCredentialStatusResolver; 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.async.PkiSigningBus;
import zeroecho.pki.impl.core.attr.SimpleAttributeSet; 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.audit.AuditSink;
import zeroecho.pki.spi.framework.CredentialFramework; import zeroecho.pki.spi.framework.CredentialFramework;
import zeroecho.pki.spi.framework.CredentialIssuerBackend; import zeroecho.pki.spi.framework.CredentialIssuerBackend;
@@ -147,13 +151,17 @@ import zeroecho.pki.spi.store.PkiStore;
* </p> * </p>
*/ */
// PMD cannot infer that retaining boundary causes would violate the redaction contract. // 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 { public final class DefaultCaService implements CaService {
private static final Logger LOG = Logger.getLogger(DefaultCaService.class.getName()); 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 CREATE_INT_REJECTED = "CREATE_INTERMEDIATE_REJECTED";
private static final String ISSUE_INT_REJECTED = "ISSUE_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 BACKEND_CRED_MISMATCH = "BACKEND_CREDENTIAL_MISMATCH";
private static final String ROOT_CREDENTIAL_INVALID = "ROOT_CREDENTIAL_INVALID";
private final PkiStore store; private final PkiStore store;
private final CredentialFramework framework; private final CredentialFramework framework;
@@ -161,6 +169,8 @@ public final class DefaultCaService implements CaService {
private final CaProofGate proofGate; private final CaProofGate proofGate;
private final AuditSink auditSink; private final AuditSink auditSink;
private final EffectiveCredentialStatusResolver statusResolver; private final EffectiveCredentialStatusResolver statusResolver;
private final ProfileService profileService;
private final Clock clock;
/** /**
* Creates a CA service bound to a specific store, credential framework, and * 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, public DefaultCaService(PkiStore store, CredentialFramework framework, CredentialIssuerBackend issuerBackend,
PublicKeyInfoResolver publicKeyResolver, PkiSigningBus signingBus, AuditSink auditSink, 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.store = Objects.requireNonNull(store, "store");
this.framework = Objects.requireNonNull(framework, "framework"); this.framework = Objects.requireNonNull(framework, "framework");
@@ -223,6 +234,8 @@ public final class DefaultCaService implements CaService {
Objects.requireNonNull(signingBus, "signingBus"); Objects.requireNonNull(signingBus, "signingBus");
this.auditSink = Objects.requireNonNull(auditSink, "auditSink"); this.auditSink = Objects.requireNonNull(auditSink, "auditSink");
this.statusResolver = Objects.requireNonNull(statusResolver, "statusResolver"); this.statusResolver = Objects.requireNonNull(statusResolver, "statusResolver");
this.profileService = Objects.requireNonNull(profileService, "profileService");
this.clock = Objects.requireNonNull(clock, "clock");
if (signatureAlgorithmId == null || signatureAlgorithmId.isBlank()) { if (signatureAlgorithmId == null || signatureAlgorithmId.isBlank()) {
throw new IllegalArgumentException("signatureAlgorithmId must not be null/blank"); throw new IllegalArgumentException("signatureAlgorithmId must not be null/blank");
} }
@@ -263,30 +276,42 @@ public final class DefaultCaService implements CaService {
@Override @Override
public PkiId createRoot(CaCreateCommand command) { public PkiId createRoot(CaCreateCommand command) {
Objects.requireNonNull(command, "command"); // NOPMD 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()) { if (command.keyRef().isEmpty()) {
throw new PkiException("Root CA creation requires keyRef (key generation not wired)"); throw new PkiException("Root CA creation requires keyRef (key generation not wired)");
} }
requireNoCaOverrides(command.attributes());
if (!framework.formatId().equals(command.formatId())) { if (!framework.formatId().equals(command.formatId())) {
throw new PkiException("Unsupported formatId for this runtime"); throw new PkiException("Unsupported formatId for this runtime");
} }
KeyRef keyRef = command.keyRef().get(); KeyRef keyRef = command.keyRef().get();
EncodedObject spki = proofGate.resolveManagedKey(keyRef, command.formatId(),
SubjectRef subjectRef = command.subjectRef(); CREATE_ROOT_REJECTED, Optional.empty());
CaProofGate.ManagedKeyProof proof = proofGate.proveManagedKey(keyRef, command.formatId(), BigInteger serial = CertificateSerialAllocator.allocate();
"CREATE_ROOT_REJECTED", Optional.empty()); ValidatedCaCertificateRequest request = CaCertificateProfileValidator.validate(
EncodedObject spki = proof.exactPublicKey(); 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()); 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 = request.validity();
Validity validity = new Validity(now.minus(Duration.ofMinutes(1)), now.plus(Duration.ofDays(3650))); X500Name dn = zeroecho.pki.impl.framework.x509.bc.BcX509ProfileSupport.subject(request.subjectRdns());
X500Name dn = new X500Name(subjectRef.value());
BigInteger serial = BigInteger.valueOf(Math.abs(now.toEpochMilli()) + 1L);
X509v3CertificateBuilder b = new X509v3CertificateBuilder(dn, serial, Date.from(validity.notBefore()), X509v3CertificateBuilder b = new X509v3CertificateBuilder(dn, serial, Date.from(validity.notBefore()),
Date.from(validity.notAfter()), dn, rootPublicKeyInfo); Date.from(validity.notAfter()), dn, rootPublicKeyInfo);
try { try {
b.addExtension(Extension.basicConstraints, true, new BasicConstraints(true)); b.addExtension(Extension.basicConstraints, request.policy().basicConstraintsCritical(),
b.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign)); new BasicConstraints(request.policy().pathLengthConstraint()));
b.addExtension(Extension.keyUsage, request.policy().keyUsageCritical(),
new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign));
} catch (Exception ex) { } catch (Exception ex) {
throw new PkiException("Root extension construction failed: code=ROOT_EXTENSION_BUILD_FAILED"); throw new PkiException("Root extension construction failed: code=ROOT_EXTENSION_BUILD_FAILED");
} }
@@ -296,12 +321,12 @@ public final class DefaultCaService implements CaService {
try { try {
cert = b.build(signer); cert = b.build(signer);
} catch (RuntimeException ex) { // NOPMD } 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"); "ROOT_SIGNING_FAILED");
} }
if (!proofGate.rootProofIsValid(cert, spki)) { 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"); "ROOT_SELF_SIGNATURE_INVALID");
} }
@@ -317,12 +342,16 @@ public final class DefaultCaService implements CaService {
PkiId publicKeyId = new PkiId("spki:" + sha256Hex(spki.bytes())); PkiId publicKeyId = new PkiId("spki:" + sha256Hex(spki.bytes()));
Credential credential = new Credential(credId, command.formatId(), new IssuerRef(caId), subjectRef, validity, Credential credential = new Credential(credId, command.formatId(), new IssuerRef(caId), request.subjectRef(),
serial.toString(), publicKeyId, new CaProfileBinding(command.profileId()), CredentialStatus.ISSUED, validity, serial.toString(), publicKeyId, new CaProfileBinding(request.profileReference()),
new EncodedObject(Encoding.DER, certDer), command.attributes()); CredentialStatus.ISSUED, new EncodedObject(Encoding.DER, certDer),
CredentialProfileBindings.requireCaBinding(credential.profileBinding(), command.profileId()); 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.putCa(ca);
store.putCredential(credential); store.putCredential(credential);
return caId; return caId;
@@ -356,6 +385,11 @@ public final class DefaultCaService implements CaService {
@Override @Override
public PkiId importRoot(CaImportCommand command) { public PkiId importRoot(CaImportCommand command) {
Objects.requireNonNull(command, "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())) { if (!framework.formatId().equals(command.formatId())) {
throw new PkiException("Unsupported formatId for this runtime"); 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"); throw new PkiException("Invalid X.509 credential: code=CREDENTIAL_INVALID");
} }
requireValidImportedRoot(command, holder);
Instant notBefore = holder.getNotBefore().toInstant(); Instant notBefore = holder.getNotBefore().toInstant();
Instant notAfter = holder.getNotAfter().toInstant(); Instant notAfter = holder.getNotAfter().toInstant();
Validity validity = new Validity(notBefore, notAfter); Validity validity = new Validity(notBefore, notAfter);
@@ -391,13 +422,22 @@ public final class DefaultCaService implements CaService {
PkiId publicKeyId = new PkiId("spki:" + sha256Hex(spkiDer)); PkiId publicKeyId = new PkiId("spki:" + sha256Hex(spkiDer));
BigInteger serial = holder.getSerialNumber(); BigInteger serial = holder.getSerialNumber();
Credential credential = new Credential(credId, command.formatId(), new IssuerRef(caId), command.subjectRef(), EncodedObject spki = new EncodedObject(Encoding.DER, spkiDer);
validity, serial.toString(), publicKeyId, new CaProfileBinding(command.profileId()), 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, CredentialStatus.ISSUED,
new EncodedObject(Encoding.DER, certDer), command.attributes()); new EncodedObject(Encoding.DER, certDer), SimpleAttributeSet.builder().build());
CredentialProfileBindings.requireCaBinding(credential.profileBinding(), command.profileId()); CredentialProfileBindings.requireCaBinding(credential.profileBinding(), request.profileReference());
requireCaCertificateMatches(credential, credential, request, caId, IMPORT_ROOT_REJECTED,
ROOT_CREDENTIAL_INVALID);
requireValidImportedRoot(command, holder);
store.putCredential(credential); 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)); List.of(credential));
store.putCa(ca); store.putCa(ca);
return caId; return caId;
@@ -439,9 +479,14 @@ public final class DefaultCaService implements CaService {
@Override @Override
public PkiId createIntermediate(IntermediateCreateCommand command) { public PkiId createIntermediate(IntermediateCreateCommand command) {
Objects.requireNonNull(command, "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()) { if (command.keyRef().isEmpty()) {
throw new PkiException("Intermediate creation requires keyRef (key generation not wired)"); throw new PkiException("Intermediate creation requires keyRef (key generation not wired)");
} }
requireNoCaOverrides(command.attributes());
CaRecord issuer = getCa(command.issuerCaId()); CaRecord issuer = getCa(command.issuerCaId());
ensureActive(issuer, "issuer"); ensureActive(issuer, "issuer");
@@ -455,29 +500,40 @@ public final class DefaultCaService implements CaService {
EffectiveCredentialStatusResolver.Evaluation statusEvaluation = statusResolver.beginEvaluation(); EffectiveCredentialStatusResolver.Evaluation statusEvaluation = statusResolver.beginEvaluation();
Credential issuerCredential = CredentialSnapshots.copy(selectIssuerCredential(issuer, command.formatId(), Credential issuerCredential = CredentialSnapshots.copy(selectIssuerCredential(issuer, command.formatId(),
CredentialUse.INTERMEDIATE_ISSUER, statusEvaluation)); 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<zeroecho.pki.api.request.SubjectRdn> 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)); .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(), CaProofGate.ManagedKeyProof subjectProof = proofGate.proveManagedKey(command.keyRef().get(),
command.formatId(), CREATE_INT_REJECTED, Optional.of(caId)); 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, requireIssuerKeyBinding(issuer, issuerCredential, command.formatId(), CREATE_INT_REJECTED,
Optional.of(caId)); 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; Credential backendCredential;
try { try {
backendCredential = issuerBackend.issueIntermediateCertificate(issue); backendCredential = issuerBackend.issueIntermediateCertificate(issue, issuerCredential.encoded(),
issuer.issuerKeyRef());
} catch (RuntimeException ex) { // NOPMD - reject malformed or mutable framework output } catch (RuntimeException ex) { // NOPMD - reject malformed or mutable framework output
throw proofGate.rejection(CREATE_INT_REJECTED, command.formatId(), Optional.of(caId), throw proofGate.rejection(CREATE_INT_REJECTED, command.formatId(), Optional.of(caId),
BACKEND_CRED_MISMATCH); BACKEND_CRED_MISMATCH);
} }
requireCaBinding(backendCredential, command.profileId(), CREATE_INT_REJECTED, requireCaBinding(backendCredential, issue.profileReference(), CREATE_INT_REJECTED,
command.formatId(), Optional.of(caId)); command.formatId(), Optional.of(caId));
Credential cred; Credential cred;
try { try {
@@ -486,12 +542,12 @@ public final class DefaultCaService implements CaService {
throw proofGate.rejection(CREATE_INT_REJECTED, command.formatId(), Optional.of(caId), throw proofGate.rejection(CREATE_INT_REJECTED, command.formatId(), Optional.of(caId),
BACKEND_CRED_MISMATCH); BACKEND_CRED_MISMATCH);
} }
requireIntermediateCredentialMatches(cred, issuerCredential, subjectSpki, command.subjectRef(), requireCaCertificateMatches(cred, issuerCredential, issue, caId, CREATE_INT_REJECTED,
command.issuerCaId(), caId, CREATE_INT_REJECTED); BACKEND_CRED_MISMATCH);
store.putCredential(cred); store.putCredential(cred);
CaRecord subject = new CaRecord(caId, CaKind.INTERMEDIATE, CaState.ACTIVE, command.keyRef().get(), 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); store.putCa(subject);
return caId; return caId;
} }
@@ -523,10 +579,19 @@ public final class DefaultCaService implements CaService {
@Override @Override
public Credential issueIntermediateCertificate(IntermediateCertIssueCommand command) { public Credential issueIntermediateCertificate(IntermediateCertIssueCommand command) {
Objects.requireNonNull(command, "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()); CaRecord issuer = getCa(command.issuerCaId());
ensureActive(issuer, "issuer"); ensureActive(issuer, "issuer");
CaRecord subject = getCa(command.subjectCaId()); CaRecord subject = getCa(command.subjectCaId());
ensureActive(subject, "subject"); 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())) { if (!framework.formatId().equals(command.formatId())) {
throw proofGate.rejection(ISSUE_INT_REJECTED, command.formatId(), throw proofGate.rejection(ISSUE_INT_REJECTED, command.formatId(),
Optional.of(subject.caId()), Optional.of(subject.caId()),
@@ -535,26 +600,37 @@ public final class DefaultCaService implements CaService {
EffectiveCredentialStatusResolver.Evaluation statusEvaluation = statusResolver.beginEvaluation(); EffectiveCredentialStatusResolver.Evaluation statusEvaluation = statusResolver.beginEvaluation();
Credential issuerCredential = CredentialSnapshots.copy(selectIssuerCredential(issuer, command.formatId(), Credential issuerCredential = CredentialSnapshots.copy(selectIssuerCredential(issuer, command.formatId(),
CredentialUse.INTERMEDIATE_ISSUER, statusEvaluation)); CredentialUse.INTERMEDIATE_ISSUER, statusEvaluation));
requireHistoricalCaProfile(issuerCredential,
issuer.kind() == CaKind.ROOT ? CertificateProfileKind.ROOT_CA
: CertificateProfileKind.INTERMEDIATE_CA);
List<zeroecho.pki.api.request.SubjectRdn> 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(), CaProofGate.ManagedKeyProof subjectProof = proofGate.proveManagedKey(subject.issuerKeyRef(),
command.formatId(), ISSUE_INT_REJECTED, Optional.of(subject.caId())); 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, requireIssuerKeyBinding(issuer, issuerCredential, command.formatId(), ISSUE_INT_REJECTED,
Optional.of(subject.caId())); 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; Credential backendCredential;
try { try {
backendCredential = issuerBackend.issueIntermediateCertificate(gated); backendCredential = issuerBackend.issueIntermediateCertificate(gated, issuerCredential.encoded(),
issuer.issuerKeyRef());
} catch (RuntimeException ex) { // NOPMD - reject malformed or mutable framework output } catch (RuntimeException ex) { // NOPMD - reject malformed or mutable framework output
throw proofGate.rejection(ISSUE_INT_REJECTED, command.formatId(), throw proofGate.rejection(ISSUE_INT_REJECTED, command.formatId(),
Optional.of(subject.caId()), BACKEND_CRED_MISMATCH); 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())); command.formatId(), Optional.of(subject.caId()));
Credential cred; Credential cred;
try { try {
@@ -563,8 +639,8 @@ public final class DefaultCaService implements CaService {
throw proofGate.rejection(ISSUE_INT_REJECTED, command.formatId(), throw proofGate.rejection(ISSUE_INT_REJECTED, command.formatId(),
Optional.of(subject.caId()), BACKEND_CRED_MISMATCH); Optional.of(subject.caId()), BACKEND_CRED_MISMATCH);
} }
requireIntermediateCredentialMatches(cred, issuerCredential, subjectSpki, subject.subjectRef(), requireCaCertificateMatches(cred, issuerCredential, gated, subject.caId(), ISSUE_INT_REJECTED,
command.issuerCaId(), subject.caId(), ISSUE_INT_REJECTED); BACKEND_CRED_MISMATCH);
store.putCredential(cred); store.putCredential(cred);
List<Credential> updated = new ArrayList<>(subject.caCredentials()); List<Credential> updated = new ArrayList<>(subject.caCredentials());
@@ -752,26 +828,17 @@ public final class DefaultCaService implements CaService {
private void requireValidImportedRoot(CaImportCommand command, X509CertificateHolder holder) { private void requireValidImportedRoot(CaImportCommand command, X509CertificateHolder holder) {
try { 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(), 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())) { 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"); "ROOT_MANAGED_KEY_MISMATCH");
} }
} catch (PkiException ex) { } catch (PkiException ex) {
throw ex; throw ex;
} catch (Exception ex) { } catch (Exception ex) {
throw proofGate.rejection("IMPORT_ROOT_REJECTED", command.formatId(), Optional.empty(), throw proofGate.rejection(IMPORT_ROOT_REJECTED, command.formatId(), Optional.empty(),
"ROOT_CREDENTIAL_INVALID"); ROOT_CREDENTIAL_INVALID);
} }
} }
@@ -795,17 +862,12 @@ public final class DefaultCaService implements CaService {
} }
} }
private void requireIntermediateCredentialMatches(Credential credential, Credential issuerCredential, private void requireCaCertificateMatches(Credential credential, Credential issuerCredential,
EncodedObject exactSubjectSpki, SubjectRef subjectRef, PkiId issuerCaId, PkiId subjectCaId, ValidatedCaCertificateRequest request, PkiId subjectCaId, String action, String mismatchCode) {
String action) {
try { try {
if (!framework.formatId().equals(credential.formatId()) if (!matchesCaCredentialEnvelope(credential, request, subjectCaId)) {
|| credential.encoded().encoding() != Encoding.DER
|| credential.status() != CredentialStatus.ISSUED
|| !credential.subjectRef().equals(subjectRef)
|| !credential.issuerRef().equals(new IssuerRef(issuerCaId))) {
throw proofGate.rejection(action, framework.formatId(), Optional.of(subjectCaId), throw proofGate.rejection(action, framework.formatId(), Optional.of(subjectCaId),
BACKEND_CRED_MISMATCH); mismatchCode);
} }
X509CertificateHolder holder = new X509CertificateHolder(credential.encoded().bytes()); X509CertificateHolder holder = new X509CertificateHolder(credential.encoded().bytes());
X509CertificateHolder issuerHolder = new X509CertificateHolder(issuerCredential.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); Extension keyUsageExtension = holder.getExtension(Extension.keyUsage);
KeyUsage keyUsage = keyUsageExtension == null ? null KeyUsage keyUsage = keyUsageExtension == null ? null
: KeyUsage.getInstance(keyUsageExtension.getParsedValue()); : KeyUsage.getInstance(keyUsageExtension.getParsedValue());
if (!MessageDigest.isEqual(exactSubjectSpki.bytes(), actualSpki) X500Name expectedSubject =
|| !holder.getSubject().equals(new X500Name(subjectRef.value())) zeroecho.pki.impl.framework.x509.bc.BcX509ProfileSupport.subject(request.subjectRdns());
|| !holder.getIssuer().equals(issuerHolder.getSubject()) X500Name expectedIssuer = request.certificateType() == CertificateProfileKind.ROOT_CA
|| !holder.isSignatureValid( ? expectedSubject : issuerHolder.getSubject();
new JcaContentVerifierProviderBuilder().build(issuerHolder.getSubjectPublicKeyInfo())) if (!matchesCaCertificateIdentity(holder, issuerHolder, request, expectedSubject, expectedIssuer,
|| constraintsExtension == null || !constraintsExtension.isCritical() actualSpki)
|| constraints == null || !constraints.isCA() || !matchesCaCertificatePolicy(holder, request, constraintsExtension, constraints,
|| !BigInteger.ZERO.equals(constraints.getPathLenConstraint()) keyUsageExtension, keyUsage)
|| keyUsageExtension == null || !keyUsageExtension.isCritical() || !matchesCaCredentialMetadata(credential, holder, request, actualSpki)) {
|| !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()) {
throw proofGate.rejection(action, framework.formatId(), Optional.of(subjectCaId), throw proofGate.rejection(action, framework.formatId(), Optional.of(subjectCaId),
BACKEND_CRED_MISMATCH); mismatchCode);
} }
CredentialProfileBindings.requireCaBinding(credential.profileBinding(), request.profileReference());
} catch (PkiException ex) { } catch (PkiException ex) {
throw ex; throw ex;
} catch (Exception ex) { } catch (Exception ex) {
throw proofGate.rejection(action, framework.formatId(), Optional.of(subjectCaId), 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<PkiId> objectId) { FormatId formatId, Optional<PkiId> objectId) {
try { try {
CredentialProfileBindings.requireCaBinding( CredentialProfileBindings.requireCaBinding(
credential == null ? null : credential.profileBinding(), expectedCaProfileId); credential == null ? null : credential.profileBinding(), expectedCaProfile);
} catch (PkiException mismatch) { } catch (PkiException mismatch) {
throw proofGate.rejection(action, formatId, objectId, CredentialProfileBindings.MISMATCH_CODE); throw proofGate.rejection(action, formatId, objectId, CredentialProfileBindings.MISMATCH_CODE);
} }
@@ -868,16 +972,33 @@ public final class DefaultCaService implements CaService {
&& !keyUsage.hasUsages(KeyUsage.decipherOnly); && !keyUsage.hasUsages(KeyUsage.decipherOnly);
} }
private static AttributeSet authoritativeIntermediateAttributes(AttributeSet callerAttributes, CaRecord issuer, private static void requireNoCaOverrides(AttributeSet attributes) {
Credential issuerCredential, EncodedObject subjectSpki, SubjectRef subjectRef) { if (!attributes.ids().isEmpty()) {
SimpleAttributeSet.Builder builder = SimpleAttributeSet.builder().putAll(callerAttributes); throw new PkiException("CA certificate profile rejected: code=CA_REQUEST_ATTRIBUTE_UNSUPPORTED");
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, private void requireHistoricalCaProfile(Credential credential, CertificateProfileKind expectedKind) {
new AttributeValue.BytesValue(subjectSpki.bytes().clone())); if (!(credential.profileBinding() instanceof CaProfileBinding binding)) {
builder.put(BcX509Attributes.SUBJECT_DN, new AttributeValue.StringValue(subjectRef.value())); throw new PkiException("CA issuer profile rejected: code=CREDENTIAL_PROFILE_BINDING_MISMATCH");
return builder.build(); }
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<PkiId> 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) { private static String sha256Hex(byte[] in) {

View File

@@ -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.
*
* <p>
* 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.
* </p>
*
* <p>
* All mutable values are defensively snapshotted. Accessors return defensive
* copies where necessary, allowing a backend to consume the object concurrently
* without observing caller mutation.
* </p>
*/
@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<Validity> 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<Validity> 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<Validity> 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());
}
}

View File

@@ -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.
*
* <p>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.</p>
*/
@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<SubjectRdn> 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<SubjectRdn> 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<SubjectRdn> 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;
}
}

View File

@@ -36,10 +36,8 @@ package zeroecho.pki.impl.framework.x509.bc;
import java.math.BigInteger; import java.math.BigInteger;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.time.Duration; import java.time.Duration;
import java.time.Instant;
import java.util.Date; import java.util.Date;
import java.util.HexFormat; import java.util.HexFormat;
import java.util.Optional;
import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.DEROctetString; import org.bouncycastle.asn1.DEROctetString;
@@ -64,9 +62,6 @@ import zeroecho.pki.api.PkiException;
import zeroecho.pki.api.PkiId; import zeroecho.pki.api.PkiId;
import zeroecho.pki.api.SubjectRef; import zeroecho.pki.api.SubjectRef;
import zeroecho.pki.api.Validity; 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.Credential;
import zeroecho.pki.api.credential.CaProfileBinding; import zeroecho.pki.api.credential.CaProfileBinding;
import zeroecho.pki.api.credential.EndEntityProfileBinding; 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.credential.CredentialStatus;
import zeroecho.pki.api.profile.LeafKeyUsage; import zeroecho.pki.api.profile.LeafKeyUsage;
import zeroecho.pki.api.request.SubjectAlternativeName; 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.ValidatedCertificateRequest;
import zeroecho.pki.impl.core.async.PkiSigningBus; import zeroecho.pki.impl.core.async.PkiSigningBus;
import zeroecho.pki.impl.core.attr.SimpleAttributeSet; import zeroecho.pki.impl.core.attr.SimpleAttributeSet;
@@ -94,9 +89,9 @@ import zeroecho.pki.spi.framework.CredentialIssuerBackend;
* <p> * <p>
* End-entity issuance derives all requester-influenced certificate material * End-entity issuance derives all requester-influenced certificate material
* exclusively from a profile-gated {@link ValidatedCertificateRequest}. * exclusively from a profile-gated {@link ValidatedCertificateRequest}.
* Intermediate CA issuance retains its proof-gated managed-CA input and * CA issuance derives certificate content exclusively from a proof-bound,
* framework attributes because it operates on an existing CA subject entity * active-profile-validated request and separately supplied trusted issuer
* rather than the end-entity CSR flow. * material.
* </p> * </p>
* *
* <h2>Signing model</h2> * <h2>Signing model</h2>
@@ -188,7 +183,7 @@ public final class BcX509CredentialIssuerBackend implements CredentialIssuerBack
byte[] issuerDer = issuerCertificate.bytes(); byte[] issuerDer = issuerCertificate.bytes();
X509CertificateHolder issuer; X509CertificateHolder issuer;
try { try {
issuer = IssuanceContext.parseIssuerCertificateOrThrow(issuerDer); issuer = parseIssuerCertificateOrThrow(issuerDer);
} finally { } finally {
java.util.Arrays.fill(issuerDer, (byte) 0); java.util.Arrays.fill(issuerDer, (byte) 0);
} }
@@ -288,16 +283,19 @@ public final class BcX509CredentialIssuerBackend implements CredentialIssuerBack
* Issues an intermediate CA X.509 certificate. * Issues an intermediate CA X.509 certificate.
* *
* <p> * <p>
* The method derives issuer and subject wiring from framework attributes, * The method consumes only the gate-produced validated CA request and trusted
* constructs an intermediate CA certificate with CA-oriented extensions, * issuer inputs, constructs an intermediate CA certificate with CA-oriented
* delegates signing through {@link PkiSigningBus}, and returns the resulting * extensions, delegates signing through {@link PkiSigningBus}, and returns the
* credential. * resulting credential.
* </p> * </p>
* *
* @param issuance gate-produced managed CA issuance authority; must not be * @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} * {@code null}
* @return issued intermediate CA credential * @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 * an unsupported format identifier
* @throws PkiException if issuer wiring or subject wiring is * @throws PkiException if issuer wiring or subject wiring is
* missing or invalid, certificate construction * missing or invalid, certificate construction
@@ -305,17 +303,23 @@ public final class BcX509CredentialIssuerBackend implements CredentialIssuerBack
* encoding fails * encoding fails
*/ */
@Override @Override
public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) { public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest request,
if (issuance == null) { EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
throw new IllegalArgumentException("issuance must not be null"); 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"); throw new IllegalArgumentException("Unsupported formatId");
} }
IssuanceContext ctx = IssuanceContext.from(issuance.attributes()); byte[] issuerDer = issuerCertificate.bytes();
X509CertificateHolder issuer = ctx.issuerCertHolder; X509CertificateHolder issuer;
byte[] subjectSpki = issuance.exactPublicKey().bytes(); try {
issuer = parseIssuerCertificateOrThrow(issuerDer);
} finally {
java.util.Arrays.fill(issuerDer, (byte) 0);
}
byte[] subjectSpki = request.exactPublicKey().bytes();
SubjectPublicKeyInfo spki; SubjectPublicKeyInfo spki;
PkiId publicKeyId; PkiId publicKeyId;
try { try {
@@ -324,24 +328,24 @@ public final class BcX509CredentialIssuerBackend implements CredentialIssuerBack
} finally { } finally {
java.util.Arrays.fill(subjectSpki, (byte) 0); java.util.Arrays.fill(subjectSpki, (byte) 0);
} }
SubjectRef subjectRef = issuance.subjectRef(); SubjectRef subjectRef = request.subjectRef();
Validity validity = request.validity();
Instant now = Instant.now(); BigInteger serial = request.serial();
Validity validity = issuance.requestedValidity().orElse(new Validity(now, now.plus(Duration.ofDays(3650))));
BigInteger serial = ctx.serial.orElse(BigInteger.valueOf(Math.abs(System.nanoTime())));
X500Name issuerDn = issuer.getSubject(); X500Name issuerDn = issuer.getSubject();
X500Name subjectDn = new X500Name(subjectRef.value()); X500Name subjectDn = BcX509ProfileSupport.subject(request.subjectRdns());
X509v3CertificateBuilder builder = new X509v3CertificateBuilder(issuerDn, serial, X509v3CertificateBuilder builder = new X509v3CertificateBuilder(issuerDn, serial,
Date.from(validity.notBefore()), Date.from(validity.notAfter()), subjectDn, spki); Date.from(validity.notBefore()), Date.from(validity.notAfter()), subjectDn, spki);
try { try {
builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(0)); builder.addExtension(Extension.basicConstraints, request.policy().basicConstraintsCritical(),
builder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign)); new BasicConstraints(request.policy().pathLengthConstraint()));
builder.addExtension(Extension.keyUsage, request.policy().keyUsageCritical(),
new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign));
} catch (Exception ex) { } catch (Exception ex) {
throw new PkiException("X.509 extension construction failed: code=EXTENSION_BUILD_FAILED"); 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; X509CertificateHolder certificate;
try { try {
certificate = builder.build(signer); certificate = builder.build(signer);
@@ -359,164 +363,15 @@ public final class BcX509CredentialIssuerBackend implements CredentialIssuerBack
PkiId credId = new PkiId("x509:" + sha256Hex(certDer)); PkiId credId = new PkiId("x509:" + sha256Hex(certDer));
try { try {
return new Credential(credId, issuance.formatId(), new IssuerRef(issuance.issuerCaId()), subjectRef, return new Credential(credId, request.formatId(), new IssuerRef(request.issuerCaId()), subjectRef,
validity, serial.toString(), publicKeyId, new CaProfileBinding(issuance.profileId()), validity, serial.toString(), publicKeyId, new CaProfileBinding(request.profileReference()),
CredentialStatus.ISSUED, CredentialStatus.ISSUED,
new EncodedObject(Encoding.DER, certDer), issuance.attributes()); new EncodedObject(Encoding.DER, certDer), SimpleAttributeSet.builder().build());
} finally { } finally {
java.util.Arrays.fill(certDer, (byte) 0); java.util.Arrays.fill(certDer, (byte) 0);
} }
} }
/**
* Immutable issuance context derived from framework-specific adapter
* attributes.
*
* <p>
* 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.
* </p>
*/
private static final class IssuanceContext {
private final X509CertificateHolder issuerCertHolder;
private final KeyRef issuerKeyRef;
private final Optional<BigInteger> 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<BigInteger> serial) {
this.issuerCertHolder = issuerCertHolder;
this.issuerKeyRef = issuerKeyRef;
this.serial = serial;
}
/**
* Builds an issuance context from framework-specific attributes.
*
* <p>
* Required attributes:
* </p>
* <ul>
* <li>{@link BcX509Attributes#ISSUER_CERT_DER} as
* {@link AttributeValue.BytesValue}</li>
* <li>{@link BcX509Attributes#ISSUER_KEYREF} as
* {@link AttributeValue.StringValue}</li>
* </ul>
*
* <p>
* Optional attributes:
* </p>
* <ul>
* <li>{@link BcX509Attributes#SERIAL} as
* {@link AttributeValue.IntegerValue}</li>
* </ul>
*
* @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<BigInteger> 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<Long> optionalInteger(AttributeSet attrs, AttributeId id, String typeMessage) {
Optional<AttributeValue> 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) { private static X509CertificateHolder parseIssuerCertificateOrThrow(byte[] issuerCertDer) {
try { try {
return new X509CertificateHolder(issuerCertDer); return new X509CertificateHolder(issuerCertDer);
@@ -524,7 +379,6 @@ public final class BcX509CredentialIssuerBackend implements CredentialIssuerBack
throw new PkiException("Invalid issuer certificate: code=ISSUER_CERTIFICATE_INVALID"); throw new PkiException("Invalid issuer certificate: code=ISSUER_CERTIFICATE_INVALID");
} }
} }
}
/** /**
* Computes the SHA-256 digest of the supplied bytes and returns it as a * Computes the SHA-256 digest of the supplied bytes and returns it as a

View File

@@ -466,6 +466,7 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
} }
return existing; return existing;
} }
requireConsistentProfileKind(version);
try { try {
FsOperations.writeNewAtomicStrict(target, FsCodec.encode(FsCodec.PROFILE_VERSION, version)); FsOperations.writeNewAtomicStrict(target, FsCodec.encode(FsCodec.PROFILE_VERSION, version));
return 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 @Override
public Optional<ImportedCertificateProfileVersion> getProfileVersion(final String profileId, public Optional<ImportedCertificateProfileVersion> getProfileVersion(final String profileId,
final long profileVersion) { final long profileVersion) {

View File

@@ -688,7 +688,7 @@ final class FsCodec {
} }
case CaProfileBinding ca -> { case CaProfileBinding ca -> {
writer.writeUnsignedByte(2); 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 { private static CredentialProfileBinding readProfileBinding(Reader reader) throws IOException {
return switch (reader.readUnsignedByte()) { return switch (reader.readUnsignedByte()) {
case 1 -> new EndEntityProfileBinding(reader.readValue(PROFILE_REF)); 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"); default -> throw new IOException("unknown credential profile binding");
}; };
} }

View File

@@ -39,7 +39,7 @@ import zeroecho.pki.api.EncodedObject;
import zeroecho.pki.api.KeyRef; import zeroecho.pki.api.KeyRef;
import zeroecho.pki.api.credential.Credential; import zeroecho.pki.api.credential.Credential;
import zeroecho.pki.api.credential.CredentialBundle; import zeroecho.pki.api.credential.CredentialBundle;
import zeroecho.pki.impl.core.ManagedCaIssuance; import zeroecho.pki.impl.core.ValidatedCaCertificateRequest;
import zeroecho.pki.impl.core.ValidatedCertificateRequest; import zeroecho.pki.impl.core.ValidatedCertificateRequest;
/** /**
@@ -153,13 +153,14 @@ public interface CredentialIssuerBackend {
* </p> * </p>
* *
* <p> * <p>
* The supplied {@link ManagedCaIssuance} can be constructed only after the core * The supplied {@link ValidatedCaCertificateRequest} can be constructed only
* CA proof gate has completed a managed-key possession challenge and bound the * after the core CA proof and active-profile gates have bound the exact public
* exact public key, subject, operation, and authoritative attributes. * key, subject, profile version, policy, validity, and serial.
* </p> * </p>
* *
* @param issuance gate-produced managed CA issuance authority; must not be * @param request gate-produced validated CA request; must not be {@code null}
* {@code null} * @param issuerCertificate trusted encoded issuer certificate
* @param issuerKeyRef trusted issuer signing-key reference
* @return issued CA credential, never {@code null} * @return issued CA credential, never {@code null}
* @throws IllegalArgumentException if {@code command} is {@code null} or * @throws IllegalArgumentException if {@code command} is {@code null} or
* structurally invalid for the concrete * structurally invalid for the concrete
@@ -168,5 +169,6 @@ public interface CredentialIssuerBackend {
* or other framework-specific issuance * or other framework-specific issuance
* processing fails * processing fails
*/ */
Credential issueIntermediateCertificate(ManagedCaIssuance issuance); Credential issueIntermediateCertificate(ValidatedCaCertificateRequest request,
EncodedObject issuerCertificate, KeyRef issuerKeyRef);
} }

View File

@@ -245,7 +245,24 @@ public interface PkiStore extends SignWorkflowStore {
*/ */
List<PublicationRecord> listPublicationRecords(); List<PublicationRecord> listPublicationRecords();
/** Atomically imports one immutable validated profile version. */ /**
* Atomically imports one immutable validated profile version.
*
* <p>
* 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.
* </p>
*
* @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); ImportedCertificateProfileVersion importProfileVersion(ImportedCertificateProfileVersion version);
/** Retrieves one imported version. */ /** Retrieves one imported version. */

View File

@@ -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"}]} {"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"}]}

View File

@@ -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"]}} {"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"]}}

View File

@@ -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"]}}

View File

@@ -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"]}}

View File

@@ -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"]}} {"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"]}}

View File

@@ -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"]}} {"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"]}}

View File

@@ -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"]}} {"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"]}}

View File

@@ -42,10 +42,13 @@ final class BuiltInCertificateProfileCatalogTest {
private static final String VPN_SERVER = ROOT + "vpn-server.json"; private static final String VPN_SERVER = ROOT + "vpn-server.json";
private static final String VPN_CLIENT = ROOT + "vpn-client.json"; private static final String VPN_CLIENT = ROOT + "vpn-client.json";
private static final String EMAIL = ROOT + "email-signing.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<String> PROFILE_RESOURCES = private static final List<String> 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<String> EXPECTED_ORDER = private static final List<String> 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<String> EXPECTED_ALGORITHMS = private static final Set<String> EXPECTED_ALGORITHMS =
Set.of("RSA", "ECDSA", "Ed25519"); Set.of("RSA", "ECDSA", "Ed25519");
private static final ExtendedKeyUsageId SERVER_AUTH = private static final ExtendedKeyUsageId SERVER_AUTH =
@@ -65,7 +68,7 @@ final class BuiltInCertificateProfileCatalogTest {
List<BuiltInCertificateProfileTemplate> second = List<BuiltInCertificateProfileTemplate> second =
BuiltInCertificateProfileCatalog.load(getClass().getClassLoader()); BuiltInCertificateProfileCatalog.load(getClass().getClassLoader());
assertEquals(4, first.size()); assertEquals(6, first.size());
assertEquals(EXPECTED_ORDER, assertEquals(EXPECTED_ORDER,
first.stream().map(template -> template.definition().profileId()).toList()); first.stream().map(template -> template.definition().profileId()).toList());
assertEquals(first, second); assertEquals(first, second);
@@ -76,7 +79,6 @@ final class BuiltInCertificateProfileCatalogTest {
byte[] hash = template.canonicalSha256(); byte[] hash = template.canonicalSha256();
assertEquals(1, definition.profileVersion()); assertEquals(1, definition.profileVersion());
assertEquals("x509", definition.formatId().value()); assertEquals("x509", definition.formatId().value());
assertEquals(Duration.ofDays(365), definition.leafPolicy().maximumValidity());
assertArrayEquals(canonical, assertArrayEquals(canonical,
CertificateProfileDocumentCodec.writeCanonical(definition)); CertificateProfileDocumentCodec.writeCanonical(definition));
assertEquals(definition, CertificateProfileDocumentCodec.parse(canonical)); assertEquals(definition, CertificateProfileDocumentCodec.parse(canonical));
@@ -103,8 +105,8 @@ final class BuiltInCertificateProfileCatalogTest {
LeafCertificatePolicy leaf = profiles.get(profileId).leafPolicy(); LeafCertificatePolicy leaf = profiles.get(profileId).leafPolicy();
SubjectAlternativeNamePolicy san = leaf.subjectAlternativeNamePolicy(); SubjectAlternativeNamePolicy san = leaf.subjectAlternativeNamePolicy();
assertOptionalCommonName(leaf); assertOptionalCommonName(profiles.get(profileId));
assertTrue(san.allowEmptySubject()); assertTrue(profiles.get(profileId).subjectPolicy().allowEmpty());
assertEquals(1, san.minimumTotal()); assertEquals(1, san.minimumTotal());
assertEquals(64, san.maximumTotal()); assertEquals(64, san.maximumTotal());
assertTrue(san.requireServiceIdentity()); assertTrue(san.requireServiceIdentity());
@@ -123,11 +125,12 @@ final class BuiltInCertificateProfileCatalogTest {
@Test @Test
void vpnClientTemplateHasOnlySpiffeUriAndRfc822Identity() { void vpnClientTemplateHasOnlySpiffeUriAndRfc822Identity() {
LeafCertificatePolicy leaf = productionDefinitions().get("vpn-client").leafPolicy(); CertificateProfileDefinition definition = productionDefinitions().get("vpn-client");
LeafCertificatePolicy leaf = definition.leafPolicy();
SubjectAlternativeNamePolicy san = leaf.subjectAlternativeNamePolicy(); SubjectAlternativeNamePolicy san = leaf.subjectAlternativeNamePolicy();
assertOptionalCommonName(leaf); assertOptionalCommonName(definition);
assertTrue(san.allowEmptySubject()); assertTrue(definition.subjectPolicy().allowEmpty());
assertEquals(1, san.minimumTotal()); assertEquals(1, san.minimumTotal());
assertEquals(16, san.maximumTotal()); assertEquals(16, san.maximumTotal());
assertFalse(san.requireServiceIdentity()); assertFalse(san.requireServiceIdentity());
@@ -142,13 +145,14 @@ final class BuiltInCertificateProfileCatalogTest {
@Test @Test
void emailTemplateRequiresRfc822SanAndDoesNotEnableSubjectEmail() { void emailTemplateRequiresRfc822SanAndDoesNotEnableSubjectEmail() {
LeafCertificatePolicy leaf = productionDefinitions().get("email-signing").leafPolicy(); CertificateProfileDefinition definition = productionDefinitions().get("email-signing");
LeafCertificatePolicy leaf = definition.leafPolicy();
SubjectAlternativeNamePolicy san = leaf.subjectAlternativeNamePolicy(); SubjectAlternativeNamePolicy san = leaf.subjectAlternativeNamePolicy();
assertOptionalCommonName(leaf); assertOptionalCommonName(definition);
assertEquals(List.of(SubjectRdnType.COMMON_NAME), assertEquals(List.of(SubjectRdnType.COMMON_NAME),
leaf.subjectPolicy().rules().stream().map(SubjectRdnRule::type).toList()); definition.subjectPolicy().rules().stream().map(SubjectRdnRule::type).toList());
assertTrue(san.allowEmptySubject()); assertTrue(definition.subjectPolicy().allowEmpty());
assertEquals(1, san.minimumTotal()); assertEquals(1, san.minimumTotal());
assertEquals(16, san.maximumTotal()); assertEquals(16, san.maximumTotal());
assertFalse(san.requireServiceIdentity()); assertFalse(san.requireServiceIdentity());
@@ -159,6 +163,15 @@ final class BuiltInCertificateProfileCatalogTest {
assertLeafPolicy(leaf, Set.of(EMAIL_PROTECTION)); assertLeafPolicy(leaf, Set.of(EMAIL_PROTECTION));
} }
@Test
void caTemplatesHaveExactKindsSubjectAndCertificatePolicies() {
Map<String, CertificateProfileDefinition> 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 @Test
void manifestRejectsUnknownDuplicateMissingInvalidPathsLimitsAndTrailingTokens() { void manifestRejectsUnknownDuplicateMissingInvalidPathsLimitsAndTrailingTokens() {
List<String> invalidManifests = List.of( List<String> invalidManifests = List.of(
@@ -226,12 +239,12 @@ final class BuiltInCertificateProfileCatalogTest {
@Test @Test
void profileDocumentsFailClosedForInvalidSchemaFieldsEncodingAndClassMetadata() { void profileDocumentsFailClosedForInvalidSchemaFieldsEncodingAndClassMetadata() {
List<byte[]> invalidDocuments = List.of( List<byte[]> invalidDocuments = List.of(
replace(mainResource(SERVER), "\"schemaVersion\":1", replace(mainResource(SERVER), "\"schemaVersion\":2",
"\"schemaVersion\":2"), "\"schemaVersion\":1"),
replace(mainResource(SERVER), "\"schemaVersion\":1,", replace(mainResource(SERVER), "\"schemaVersion\":2,",
"\"schemaVersion\":1,\"active\":true,"), "\"schemaVersion\":2,\"active\":true,"),
replace(mainResource(SERVER), "\"schemaVersion\":1,", replace(mainResource(SERVER), "\"schemaVersion\":2,",
"\"schemaVersion\":1,\"@class\":\"" "\"schemaVersion\":2,\"@class\":\""
+ InitializationSentinel.CLASS_NAME + "\","), + InitializationSentinel.CLASS_NAME + "\","),
malformedUtf8(mainResource(SERVER))); malformedUtf8(mainResource(SERVER)));
for (byte[] invalid : invalidDocuments) { for (byte[] invalid : invalidDocuments) {
@@ -251,8 +264,9 @@ final class BuiltInCertificateProfileCatalogTest {
CertificateProfileDefinition server = CertificateProfileDefinition server =
CertificateProfileDocumentCodec.parse(mainResource(SERVER)); CertificateProfileDocumentCodec.parse(mainResource(SERVER));
CertificateProfileDefinition changed = new CertificateProfileDefinition( CertificateProfileDefinition changed = new CertificateProfileDefinition(
server.profileId(), server.profileVersion(), server.formatId(), server.certificateType(), server.profileId(), server.profileVersion(),
"Changed display", server.leafPolicy()); server.formatId(), "Changed display", server.maximumValidity(),
server.subjectPolicy(), server.certificatePolicy());
Map<String, List<byte[]>> duplicateIdentity = baseResources(); Map<String, List<byte[]>> duplicateIdentity = baseResources();
duplicateIdentity.put(VPN_SERVER, List.of( duplicateIdentity.put(VPN_SERVER, List.of(
CertificateProfileDocumentCodec.writeCanonical(changed))); CertificateProfileDocumentCodec.writeCanonical(changed)));
@@ -265,9 +279,9 @@ final class BuiltInCertificateProfileCatalogTest {
assertCode(wrongSet, "BUILT_IN_PROFILE_SET_INVALID"); assertCode(wrongSet, "BUILT_IN_PROFILE_SET_INVALID");
} }
private static void assertOptionalCommonName(LeafCertificatePolicy leaf) { private static void assertOptionalCommonName(CertificateProfileDefinition definition) {
assertEquals(1, leaf.subjectPolicy().rules().size()); assertEquals(1, definition.subjectPolicy().rules().size());
SubjectRdnRule commonName = leaf.subjectPolicy().rules().get(0); SubjectRdnRule commonName = definition.subjectPolicy().rules().get(0);
assertEquals(SubjectRdnType.COMMON_NAME, commonName.type()); assertEquals(SubjectRdnType.COMMON_NAME, commonName.type());
assertEquals(0, commonName.minimumOccurrences()); assertEquals(0, commonName.minimumOccurrences());
assertEquals(1, commonName.maximumOccurrences()); assertEquals(1, commonName.maximumOccurrences());
@@ -284,7 +298,25 @@ final class BuiltInCertificateProfileCatalogTest {
assertFalse(leaf.extendedKeyUsageCritical()); assertFalse(leaf.extendedKeyUsageCritical());
assertTrue(leaf.basicConstraintsCritical()); assertTrue(leaf.basicConstraintsCritical());
assertEquals(EXPECTED_ALGORITHMS, leaf.allowedSubjectKeyAlgorithmIds()); 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, private static SubjectAlternativeNameRule rule(SubjectAlternativeNamePolicy policy,

View File

@@ -34,7 +34,8 @@ final class CertificateProfileDocumentCodecTest {
private static final String VALID_DOCUMENT = """ private static final String VALID_DOCUMENT = """
{ {
"schemaVersion": 1, "schemaVersion": 2,
"certificateType": "END_ENTITY",
"profileId": "tls-service", "profileId": "tls-service",
"profileVersion": 7, "profileVersion": 7,
"formatId": "x509", "formatId": "x509",
@@ -108,17 +109,18 @@ final class CertificateProfileDocumentCodecTest {
void parsesEverySupportedRuleShapeIntoAuthoritativeTypedPolicies() { void parsesEverySupportedRuleShapeIntoAuthoritativeTypedPolicies() {
CertificateProfileDefinition definition = parse(VALID_DOCUMENT); 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("tls-service", definition.profileId());
assertEquals(7, definition.profileVersion()); assertEquals(7, definition.profileVersion());
assertEquals("x509", definition.formatId().value()); assertEquals("x509", definition.formatId().value());
assertEquals("TLS service", definition.displayName()); assertEquals("TLS service", definition.displayName());
LeafCertificatePolicy leaf = definition.leafPolicy(); 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), assertEquals(List.of(SubjectRdnType.COMMON_NAME, SubjectRdnType.ORGANIZATION_NAME),
leaf.subjectPolicy().rules().stream().map(SubjectRdnRule::type).toList()); definition.subjectPolicy().rules().stream().map(SubjectRdnRule::type).toList());
assertEquals("ZeroEcho", leaf.subjectPolicy().rules().get(1).fixedValue().orElseThrow()); assertEquals("ZeroEcho", definition.subjectPolicy().rules().get(1).fixedValue().orElseThrow());
assertFalse(leaf.subjectAlternativeNamePolicy().allowEmptySubject()); assertFalse(definition.subjectPolicy().allowEmpty());
assertTrue(leaf.subjectAlternativeNamePolicy().allowDnsWildcard()); assertTrue(leaf.subjectAlternativeNamePolicy().allowDnsWildcard());
assertEquals(Set.of("https", "spiffe"), assertEquals(Set.of("https", "spiffe"),
leaf.subjectAlternativeNamePolicy().allowedUriSchemes()); leaf.subjectAlternativeNamePolicy().allowedUriSchemes());
@@ -141,7 +143,7 @@ final class CertificateProfileDocumentCodecTest {
definition("uri", requesterCn(), uriSan(), eku(), Set.of("Ed448")), definition("uri", requesterCn(), uriSan(), eku(), Set.of("Ed448")),
definition("email", requesterCn(), emailSan(), eku(), Set.of("RSA")), definition("email", requesterCn(), emailSan(), eku(), Set.of("RSA")),
definition("mixed", requesterCn(), mixedSan(), eku(), Set.of("RSA", "ECDSA")), 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")), eku(), Set.of("RSA")),
definition("fixed-rdn", fixedOrganization(), noSan(), eku(), Set.of("RSA")), definition("fixed-rdn", fixedOrganization(), noSan(), eku(), Set.of("RSA")),
definition("multiple-algorithms", requesterCn(), noSan(), Set.of(), definition("multiple-algorithms", requesterCn(), noSan(), Set.of(),
@@ -161,7 +163,7 @@ final class CertificateProfileDocumentCodecTest {
CertificateProfileDefinition immutable = CertificateProfileDocumentCodec.parse( CertificateProfileDefinition immutable = CertificateProfileDocumentCodec.parse(
CertificateProfileDocumentCodec.writeCanonical(definitions.get(7))); CertificateProfileDocumentCodec.writeCanonical(definitions.get(7)));
assertThrows(UnsupportedOperationException.class, assertThrows(UnsupportedOperationException.class,
() -> immutable.leafPolicy().subjectPolicy().rules().add( () -> immutable.subjectPolicy().rules().add(
new SubjectRdnRule(SubjectRdnType.PSEUDONYM, 0, 0, 32, new SubjectRdnRule(SubjectRdnType.PSEUDONYM, 0, 0, 32,
Optional.empty(), true))); Optional.empty(), true)));
assertThrows(UnsupportedOperationException.class, assertThrows(UnsupportedOperationException.class,
@@ -171,6 +173,55 @@ final class CertificateProfileDocumentCodecTest {
.allowedUriSchemes().add("ssh")); .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 @Test
void canonicalOutputHasFixedOrderSortedSetsAndIsIdempotent() { void canonicalOutputHasFixedOrderSortedSetsAndIsIdempotent() {
byte[] first = CertificateProfileDocumentCodec.writeCanonical(parse(VALID_DOCUMENT)); byte[] first = CertificateProfileDocumentCodec.writeCanonical(parse(VALID_DOCUMENT));
@@ -184,7 +235,8 @@ final class CertificateProfileDocumentCodecTest {
assertFalse(json.contains("\n")); assertFalse(json.contains("\n"));
assertFalse(json.contains(": ")); assertFalse(json.contains(": "));
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("\"DNS_NAME\"") < json.indexOf("\"IP_ADDRESS\""));
assertTrue(json.indexOf("\"IP_ADDRESS\"") < json.indexOf("\"RFC822_NAME\"")); assertTrue(json.indexOf("\"IP_ADDRESS\"") < json.indexOf("\"RFC822_NAME\""));
assertTrue(json.indexOf("\"RFC822_NAME\"") < json.indexOf("\"URI\"")); assertTrue(json.indexOf("\"RFC822_NAME\"") < json.indexOf("\"URI\""));
@@ -201,12 +253,15 @@ final class CertificateProfileDocumentCodecTest {
CertificateProfileDefinition valid = definition("valid", requesterCn(), noSan(), CertificateProfileDefinition valid = definition("valid", requesterCn(), noSan(),
Set.of(), Set.of("RSA")); Set.of(), Set.of("RSA"));
List<CertificateProfileDefinition> invalid = List.of( List<CertificateProfileDefinition> invalid = List.of(
new CertificateProfileDefinition(" padded", valid.profileVersion(), new CertificateProfileDefinition(valid.certificateType(), " padded", valid.profileVersion(),
valid.formatId(), valid.displayName(), valid.leafPolicy()), valid.formatId(), valid.displayName(), valid.maximumValidity(),
new CertificateProfileDefinition(valid.profileId(), valid.profileVersion(), valid.subjectPolicy(), valid.certificatePolicy()),
new FormatId("x509 "), valid.displayName(), valid.leafPolicy()), new CertificateProfileDefinition(valid.certificateType(), valid.profileId(),
new CertificateProfileDefinition(valid.profileId(), valid.profileVersion(), valid.profileVersion(), new FormatId("x509 "), valid.displayName(),
valid.formatId(), " padded ", valid.leafPolicy())); 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) { for (CertificateProfileDefinition definition : invalid) {
assertWriteCode(definition, "CANONICALIZATION_FAILED"); assertWriteCode(definition, "CANONICALIZATION_FAILED");
@@ -245,11 +300,11 @@ final class CertificateProfileDocumentCodecTest {
@Test @Test
void rejectsDuplicateUnknownMissingNullWrongAndNonintegralFields() { void rejectsDuplicateUnknownMissingNullWrongAndNonintegralFields() {
assertCode(VALID_DOCUMENT.replace("\"schemaVersion\": 1,", assertCode(VALID_DOCUMENT.replace("\"schemaVersion\": 2,",
"\"schemaVersion\": 1,\"schemaVersion\": 1,"), "DUPLICATE_FIELD"); "\"schemaVersion\": 2,\"schemaVersion\": 2,"), "DUPLICATE_FIELD");
for (String document : List.of( for (String document : List.of(
VALID_DOCUMENT.replace("\"schemaVersion\": 1,", VALID_DOCUMENT.replace("\"schemaVersion\": 2,",
"\"secret-field\": true,\"schemaVersion\": 1,"), "\"secret-field\": true,\"schemaVersion\": 2,"),
VALID_DOCUMENT.replace("\"allowEmpty\": false,", VALID_DOCUMENT.replace("\"allowEmpty\": false,",
"\"unknown\": true,\"allowEmpty\": false,"), "\"unknown\": true,\"allowEmpty\": false,"),
VALID_DOCUMENT.replace("\"oid\": \"2.5.4.3\",", VALID_DOCUMENT.replace("\"oid\": \"2.5.4.3\",",
@@ -272,7 +327,7 @@ final class CertificateProfileDocumentCodecTest {
assertCode(document, "DUPLICATE_FIELD"); assertCode(document, "DUPLICATE_FIELD");
} }
for (String document : List.of( 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("\"allowEmpty\": false,\n", ""),
VALID_DOCUMENT.replace("\"oid\": \"2.5.4.3\",\n", ""), VALID_DOCUMENT.replace("\"oid\": \"2.5.4.3\",\n", ""),
VALID_DOCUMENT.replace("\"minimumTotal\": 1,\n", ""), VALID_DOCUMENT.replace("\"minimumTotal\": 1,\n", ""),
@@ -303,8 +358,10 @@ final class CertificateProfileDocumentCodecTest {
@Test @Test
void rejectsUnsupportedVersionsTokensCaseWhitespaceAndNoncanonicalDuration() { void rejectsUnsupportedVersionsTokensCaseWhitespaceAndNoncanonicalDuration() {
assertCode(VALID_DOCUMENT.replace("\"schemaVersion\": 1", assertCode(VALID_DOCUMENT.replace("\"schemaVersion\": 2",
"\"schemaVersion\": 2"), "SCHEMA_VERSION_UNSUPPORTED"); "\"schemaVersion\": 1"), "SCHEMA_VERSION_UNSUPPORTED");
assertCode(VALID_DOCUMENT.replace("\"certificateType\": \"END_ENTITY\"",
"\"certificateType\": \"end_entity\""), "CERTIFICATE_TYPE_UNSUPPORTED");
assertCode(VALID_DOCUMENT.replace("\"profileVersion\": 7", assertCode(VALID_DOCUMENT.replace("\"profileVersion\": 7",
"\"profileVersion\": 0"), "PROFILE_VERSION_INVALID"); "\"profileVersion\": 0"), "PROFILE_VERSION_INVALID");
assertCode(VALID_DOCUMENT.replace("\"type\": \"DNS_NAME\"", assertCode(VALID_DOCUMENT.replace("\"type\": \"DNS_NAME\"",
@@ -386,7 +443,7 @@ final class CertificateProfileDocumentCodecTest {
eku(), Set.of("RSA"))); eku(), Set.of("RSA")));
assertCode(email.replace("\"serviceIdentityRequired\":false", assertCode(email.replace("\"serviceIdentityRequired\":false",
"\"serviceIdentityRequired\":true"), "SEMANTIC_INVALID"); "\"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"))); dnsSan(false, true), eku(), Set.of("RSA")));
assertCode(empty.replace("\"minimumTotal\":1", "\"minimumTotal\":0"), assertCode(empty.replace("\"minimumTotal\":1", "\"minimumTotal\":0"),
"SEMANTIC_INVALID"); "SEMANTIC_INVALID");
@@ -449,8 +506,8 @@ final class CertificateProfileDocumentCodecTest {
@Test @Test
void redactsHostileInputAndParserDetailsFromFailures() { void redactsHostileInputAndParserDetailsFromFailures() {
String secret = "do-not-disclose-credential"; String secret = "do-not-disclose-credential";
String hostile = VALID_DOCUMENT.replace("\"schemaVersion\": 1,", String hostile = VALID_DOCUMENT.replace("\"schemaVersion\": 2,",
"\"" + secret + "\": true,\"schemaVersion\": 1,"); "\"" + secret + "\": true,\"schemaVersion\": 2,");
PkiException exception = assertThrows(PkiException.class, () -> parse(hostile)); PkiException exception = assertThrows(PkiException.class, () -> parse(hostile));
@@ -485,8 +542,8 @@ final class CertificateProfileDocumentCodecTest {
logger.addHandler(handler); logger.addHandler(handler);
try { try {
for (String field : hostileFields) { for (String field : hostileFields) {
String document = VALID_DOCUMENT.replace("\"schemaVersion\": 1,", String document = VALID_DOCUMENT.replace("\"schemaVersion\": 2,",
"\"" + field + "\":\"" + probeName + "\",\"schemaVersion\": 1,"); "\"" + field + "\":\"" + probeName + "\",\"schemaVersion\": 2,");
PkiException exception = assertThrows(PkiException.class, () -> parse(document)); PkiException exception = assertThrows(PkiException.class, () -> parse(document));
assertTrue(exception.getMessage().contains("code=UNKNOWN_FIELD ")); assertTrue(exception.getMessage().contains("code=UNKNOWN_FIELD "));
assertFalse(exception.getMessage().contains(field)); assertFalse(exception.getMessage().contains(field));
@@ -512,57 +569,69 @@ final class CertificateProfileDocumentCodecTest {
private static CertificateProfileDefinition definition(String id, SubjectPolicy subject, private static CertificateProfileDefinition definition(String id, SubjectPolicy subject,
SubjectAlternativeNamePolicy san, Set<ExtendedKeyUsageId> extendedKeyUsages, SubjectAlternativeNamePolicy san, Set<ExtendedKeyUsageId> extendedKeyUsages,
Set<String> algorithms) { Set<String> algorithms) {
LeafCertificatePolicy leaf = new LeafCertificatePolicy(subject, san, LeafCertificatePolicy leaf = new LeafCertificatePolicy(san,
Set.of(LeafKeyUsage.DIGITAL_SIGNATURE), extendedKeyUsages, true, false, true, Set.of(LeafKeyUsage.DIGITAL_SIGNATURE), extendedKeyUsages, true, false, true,
algorithms, Duration.ofDays(1)); algorithms);
return new CertificateProfileDefinition(id, 1, new FormatId("x509"), id, leaf); 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() { 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))); 1, 1, 128, Optional.empty(), true)));
} }
private static SubjectPolicy fixedOrganization() { 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))); 1, 1, 64, Optional.of("ZeroEcho"), false)));
} }
private static SubjectAlternativeNamePolicy noSan() { 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); false, false, false);
} }
private static SubjectAlternativeNamePolicy dnsSan(boolean wildcard, boolean emptySubject) { 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, List.of(new SubjectAlternativeNameRule(SubjectAlternativeNameType.DNS_NAME,
1, 1, false, false)), 1, 1, false, false)),
wildcard, Set.of(), emptySubject, true, false); wildcard, Set.of(), emptySubject, true, false);
} }
private static SubjectAlternativeNamePolicy ipSan(boolean ipv4, boolean ipv6) { 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, List.of(new SubjectAlternativeNameRule(SubjectAlternativeNameType.IP_ADDRESS,
1, 1, ipv4, ipv6)), 1, 1, ipv4, ipv6)),
false, Set.of(), false, true, false); false, Set.of(), false, true, false);
} }
private static SubjectAlternativeNamePolicy uriSan() { private static SubjectAlternativeNamePolicy uriSan() {
return new SubjectAlternativeNamePolicy(false, 1, 1, return new SubjectAlternativeNamePolicy(1, 1,
List.of(new SubjectAlternativeNameRule(SubjectAlternativeNameType.URI, List.of(new SubjectAlternativeNameRule(SubjectAlternativeNameType.URI,
1, 1, false, false)), 1, 1, false, false)),
false, Set.of("https"), false, true, false); false, Set.of("https"), false, true, false);
} }
private static SubjectAlternativeNamePolicy emailSan() { private static SubjectAlternativeNamePolicy emailSan() {
return new SubjectAlternativeNamePolicy(false, 1, 1, return new SubjectAlternativeNamePolicy(1, 1,
List.of(new SubjectAlternativeNameRule(SubjectAlternativeNameType.RFC822_NAME, List.of(new SubjectAlternativeNameRule(SubjectAlternativeNameType.RFC822_NAME,
1, 1, false, false)), 1, 1, false, false)),
false, Set.of(), false, false, true); false, Set.of(), false, false, true);
} }
private static SubjectAlternativeNamePolicy mixedSan() { private static SubjectAlternativeNamePolicy mixedSan() {
return new SubjectAlternativeNamePolicy(false, 0, 4, List.of( return new SubjectAlternativeNamePolicy(0, 4, List.of(
new SubjectAlternativeNameRule(SubjectAlternativeNameType.DNS_NAME, new SubjectAlternativeNameRule(SubjectAlternativeNameType.DNS_NAME,
0, 1, false, false), 0, 1, false, false),
new SubjectAlternativeNameRule(SubjectAlternativeNameType.IP_ADDRESS, new SubjectAlternativeNameRule(SubjectAlternativeNameType.IP_ADDRESS,

View File

@@ -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<KeyRef, KeyPair> 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<KeyRef, KeyPair> 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<ImportedCertificateProfileVersion> getImportedVersion(String profileId,
long profileVersion) {
return delegate.getImportedVersion(profileId, profileVersion);
}
@Override
public List<ImportedCertificateProfileVersion> listImportedVersions(String profileId) {
return delegate.listImportedVersions(profileId);
}
@Override
public Optional<CertificateProfileRef> 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());
}
}
}

View File

@@ -84,7 +84,7 @@ import zeroecho.pki.api.credential.CredentialStatus;
import zeroecho.pki.api.issuance.IssueEndEntityCommand; import zeroecho.pki.api.issuance.IssueEndEntityCommand;
import zeroecho.pki.api.request.CertificationRequest; import zeroecho.pki.api.request.CertificationRequest;
import zeroecho.pki.api.request.ParsedCertificationRequest; 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.ValidatedCertificateRequest;
import zeroecho.pki.impl.core.attr.SimpleAttributeSet; import zeroecho.pki.impl.core.attr.SimpleAttributeSet;
import zeroecho.pki.spi.framework.CredentialIssuerBackend; import zeroecho.pki.spi.framework.CredentialIssuerBackend;
@@ -114,15 +114,18 @@ final class H7EndEntityAcceptanceE2eTest {
Map.of(rootKeyRef, rootKey))) { Map.of(rootKeyRef, rootKey))) {
List<BuiltInCertificateProfileTemplate> templates = BuiltInCertificateProfileCatalog.load( List<BuiltInCertificateProfileTemplate> templates = BuiltInCertificateProfileCatalog.load(
H7EndEntityAcceptanceE2eTest.class.getClassLoader()); H7EndEntityAcceptanceE2eTest.class.getClassLoader());
assertEquals(4, templates.size()); assertEquals(6, templates.size());
for (BuiltInCertificateProfileTemplate template : templates) { for (BuiltInCertificateProfileTemplate template : templates) {
String profileId = template.definition().profileId(); String profileId = template.definition().profileId();
assertTrue(runtime.profileService().getImportedVersion(profileId, 1).isEmpty()); assertTrue(runtime.profileService().getImportedVersion(profileId, 1).isEmpty());
} }
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(), 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())); 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(); String profileId = template.definition().profileId();
CertificateProfileRef imported = runtime.profileService().importBuiltIn(template); CertificateProfileRef imported = runtime.profileService().importBuiltIn(template);
assertTrue(runtime.profileService().getActiveReference(profileId).isEmpty()); assertTrue(runtime.profileService().getActiveReference(profileId).isEmpty());
@@ -170,7 +173,7 @@ final class H7EndEntityAcceptanceE2eTest {
List<byte[]> profiles = acceptanceProfileDocuments(); List<byte[]> profiles = acceptanceProfileDocuments();
profiles.forEach(runtime::importAndActivate); profiles.forEach(runtime::importAndActivate);
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(), 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())); new SimpleAttributeSet()));
CredentialIssuerBackend serialCapturingBackend = serialCapturingBackend(runtime.issuerBackend(), CredentialIssuerBackend serialCapturingBackend = serialCapturingBackend(runtime.issuerBackend(),
@@ -278,7 +281,7 @@ final class H7EndEntityAcceptanceE2eTest {
Map.of(rootKeyRef, rootKey))) { Map.of(rootKeyRef, rootKey))) {
acceptanceProfileDocuments().forEach(runtime::importAndActivate); acceptanceProfileDocuments().forEach(runtime::importAndActivate);
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(), 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())); new SimpleAttributeSet()));
assertProfileRejected(runtime, rootCaId, leafKey, "h7-dns", assertProfileRejected(runtime, rootCaId, leafKey, "h7-dns",
@@ -323,7 +326,7 @@ final class H7EndEntityAcceptanceE2eTest {
runtime.importAndActivate(H7ProfileDocuments.backendMutationProfile()); runtime.importAndActivate(H7ProfileDocuments.backendMutationProfile());
runtime.importAndActivate(H7ProfileDocuments.noSanOrEkuProfile()); runtime.importAndActivate(H7ProfileDocuments.noSanOrEkuProfile());
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(), 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())); new SimpleAttributeSet()));
ParsedCertificationRequest request = parse(runtime, leafKey, ParsedCertificationRequest request = parse(runtime, leafKey,
new X500Name("CN=Mutation Leaf,O=Example"), new X500Name("CN=Mutation Leaf,O=Example"),
@@ -444,8 +447,8 @@ final class H7EndEntityAcceptanceE2eTest {
} }
@Override @Override
public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) { public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
return delegate.issueIntermediateCertificate(issuance); return delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
} }
}; };
} }
@@ -542,8 +545,8 @@ final class H7EndEntityAcceptanceE2eTest {
} }
@Override @Override
public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) { public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
return delegate.issueIntermediateCertificate(issuance); return delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
} }
}; };
} }
@@ -598,8 +601,8 @@ final class H7EndEntityAcceptanceE2eTest {
} }
@Override @Override
public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) { public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
return delegate.issueIntermediateCertificate(issuance); return delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
} }
}; };
} }

View File

@@ -60,7 +60,7 @@ import zeroecho.pki.api.issuance.IssueEndEntityCommand;
import zeroecho.pki.api.request.CertificationRequest; import zeroecho.pki.api.request.CertificationRequest;
import zeroecho.pki.api.request.ParsedCertificationRequest; import zeroecho.pki.api.request.ParsedCertificationRequest;
import zeroecho.pki.impl.core.DefaultIssuanceService; 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.ValidatedCertificateRequest;
import zeroecho.pki.impl.core.attr.SimpleAttributeSet; import zeroecho.pki.impl.core.attr.SimpleAttributeSet;
import zeroecho.pki.impl.framework.x509.bc.BcX509Attributes; import zeroecho.pki.impl.framework.x509.bc.BcX509Attributes;
@@ -294,7 +294,7 @@ final class H7EndEntityCsrRejectionE2eTest {
runtime.importAndActivate(H7ProfileDocuments.uriProfile()); runtime.importAndActivate(H7ProfileDocuments.uriProfile());
runtime.importAndActivate(H7ProfileDocuments.subjectEmailAndRfc822Profile()); runtime.importAndActivate(H7ProfileDocuments.subjectEmailAndRfc822Profile());
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(), 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())); new SimpleAttributeSet()));
PKCS10CertificationRequest valid = signedSanCsr(leafKey, new X500Name("CN=Template"), PKCS10CertificationRequest valid = signedSanCsr(leafKey, new X500Name("CN=Template"),
List.of(dns("template.example.com"))); List.of(dns("template.example.com")));
@@ -398,8 +398,8 @@ final class H7EndEntityCsrRejectionE2eTest {
} }
@Override @Override
public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) { public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
return delegate.issueIntermediateCertificate(issuance); return delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
} }
}; };
} }

View File

@@ -101,7 +101,7 @@ import zeroecho.pki.api.status.StatusObject;
import zeroecho.pki.api.status.StatusObjectGenerateCommand; import zeroecho.pki.api.status.StatusObjectGenerateCommand;
import zeroecho.pki.api.status.StatusObjectType; import zeroecho.pki.api.status.StatusObjectType;
import zeroecho.pki.impl.core.attr.SimpleAttributeSet; 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.impl.core.ValidatedCertificateRequest;
import zeroecho.pki.spi.framework.CredentialIssuerBackend; import zeroecho.pki.spi.framework.CredentialIssuerBackend;
import zeroecho.pki.testkit.PkiTestRuntime; import zeroecho.pki.testkit.PkiTestRuntime;
@@ -140,7 +140,7 @@ public final class PkiCoreE2eTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"), keys)) { try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"), keys)) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(), 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 usable = runtime.caService().getCa(rootCaId).caCredentials().get(0);
Credential unusable = copyWithId(usable, new PkiId("credential:matrix-unusable")); Credential unusable = copyWithId(usable, new PkiId("credential:matrix-unusable"));
CaRecord root = runtime.caService().getCa(rootCaId); CaRecord root = runtime.caService().getCa(rootCaId);
@@ -166,13 +166,13 @@ public final class PkiCoreE2eTest {
resolved.clear(); resolved.clear();
PkiId intermediateCaId = caService.createIntermediate(new IntermediateCreateCommand( 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())); Optional.of(intermediateKeyRef), emptyAttributes()));
assertEquals(List.of(unusable.credentialId(), usable.credentialId()), List.copyOf(resolved)); assertEquals(List.of(unusable.credentialId(), usable.credentialId()), List.copyOf(resolved));
resolved.clear(); resolved.clear();
caService.issueIntermediateCertificate(new IntermediateCertIssueCommand(runtime.framework().formatId(), 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)); assertEquals(List.of(unusable.credentialId(), usable.credentialId()), List.copyOf(resolved));
resolved.clear(); resolved.clear();
@@ -200,10 +200,10 @@ public final class PkiCoreE2eTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"), keys)) { try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"), keys)) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(), 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() PkiId intermediateCaId = runtime.caService()
.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootCaId, .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())); emptyAttributes()));
PkiId rootCredentialId = runtime.caService().getCa(rootCaId).caCredentials().get(0).credentialId(); PkiId rootCredentialId = runtime.caService().getCa(rootCaId).caCredentials().get(0).credentialId();
runtime.revocationService().revokePermanently(new RevocationCommand.RevokePermanently(rootCredentialId, runtime.revocationService().revokePermanently(new RevocationCommand.RevokePermanently(rootCredentialId,
@@ -221,12 +221,12 @@ public final class PkiCoreE2eTest {
assertThrows(PkiException.class, assertThrows(PkiException.class,
() -> runtime.caService() () -> runtime.caService()
.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootCaId, .createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootCaId,
new SubjectRef("CN=H6 Rejected"), "default", new SubjectRef("CN=H6 Rejected"), "intermediate-ca",
Optional.of(nextIntermediateKeyRef), emptyAttributes()))); Optional.of(nextIntermediateKeyRef), emptyAttributes())));
assertThrows(PkiException.class, assertThrows(PkiException.class,
() -> runtime.caService().issueIntermediateCertificate( () -> runtime.caService().issueIntermediateCertificate(
new IntermediateCertIssueCommand(runtime.framework().formatId(), rootCaId, new IntermediateCertIssueCommand(runtime.framework().formatId(), rootCaId,
intermediateCaId, "default", Optional.empty(), emptyAttributes()))); intermediateCaId, "intermediate-ca", Optional.empty(), emptyAttributes())));
assertThrows(PkiException.class, assertThrows(PkiException.class,
() -> runtime.statusObjectService().generate(new StatusObjectGenerateCommand(rootCaId, () -> runtime.statusObjectService().generate(new StatusObjectGenerateCommand(rootCaId,
StatusObjectType.CRL, runtime.framework().formatId(), emptyAttributes()))); StatusObjectType.CRL, runtime.framework().formatId(), emptyAttributes())));
@@ -265,7 +265,7 @@ public final class PkiCoreE2eTest {
// Create ROOT CA (self-signed). // Create ROOT CA (self-signed).
PkiId rootCaId = caSvc.createRoot(new CaCreateCommand(runtime.framework().formatId(), 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()); System.out.println("...rootCaId=" + rootCaId.value());
// CSR for end entity. // CSR for end entity.
@@ -320,9 +320,9 @@ public final class PkiCoreE2eTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"), keys)) { try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"), keys)) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(), 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( 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())); Optional.of(intermediateKeyRef), emptyAttributes()));
Credential rootCredential = runtime.caService().getCa(rootCaId).caCredentials().get(0); Credential rootCredential = runtime.caService().getCa(rootCaId).caCredentials().get(0);
EffectiveCredentialStatusResolver resolver = scriptedResolver(credential -> status, resolutionFailure); EffectiveCredentialStatusResolver resolver = scriptedResolver(credential -> status, resolutionFailure);
@@ -343,11 +343,11 @@ public final class PkiCoreE2eTest {
Optional.empty()))); Optional.empty())));
assertThrows(PkiException.class, assertThrows(PkiException.class,
() -> caService.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), () -> 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()))); Optional.of(nextIntermediateKeyRef), emptyAttributes())));
assertThrows(PkiException.class, assertThrows(PkiException.class,
() -> caService.issueIntermediateCertificate(new IntermediateCertIssueCommand( () -> caService.issueIntermediateCertificate(new IntermediateCertIssueCommand(
runtime.framework().formatId(), rootCaId, intermediateCaId, "default", Optional.empty(), runtime.framework().formatId(), rootCaId, intermediateCaId, "intermediate-ca", Optional.empty(),
emptyAttributes()))); emptyAttributes())));
assertThrows(PkiException.class, assertThrows(PkiException.class,
() -> statusService.generate(new StatusObjectGenerateCommand(rootCaId, StatusObjectType.CRL, () -> statusService.generate(new StatusObjectGenerateCommand(rootCaId, StatusObjectType.CRL,
@@ -420,9 +420,9 @@ public final class PkiCoreE2eTest {
} }
@Override @Override
public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) { public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
intermediateCalls.incrementAndGet(); intermediateCalls.incrementAndGet();
return delegate.issueIntermediateCertificate(issuance); return delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
} }
} }

View File

@@ -114,7 +114,7 @@ import zeroecho.pki.api.request.ParsedCertificationRequest;
import zeroecho.pki.api.request.ProofOfPossessionResult; import zeroecho.pki.api.request.ProofOfPossessionResult;
import zeroecho.pki.api.request.ProofOfPossessionStatus; import zeroecho.pki.api.request.ProofOfPossessionStatus;
import zeroecho.pki.impl.core.DefaultIssuanceService; 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.ValidatedCertificateRequest;
import zeroecho.pki.impl.core.attr.SimpleAttributeSet; import zeroecho.pki.impl.core.attr.SimpleAttributeSet;
import zeroecho.pki.impl.framework.x509.bc.BcX509Attributes; import zeroecho.pki.impl.framework.x509.bc.BcX509Attributes;
@@ -134,10 +134,10 @@ final class PkiProofGateE2eTest {
System.out.println("issuerBackendApiRequiresOpaqueGateProducedInputs"); System.out.println("issuerBackendApiRequiresOpaqueGateProducedInputs");
assertTrue(Modifier.isFinal(ValidatedCertificateRequest.class.getModifiers())); 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()) assertTrue(java.util.Arrays.stream(ValidatedCertificateRequest.class.getDeclaredConstructors())
.noneMatch(constructor -> Modifier.isPublic(constructor.getModifiers()))); .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()))); .noneMatch(constructor -> Modifier.isPublic(constructor.getModifiers())));
Class<?> managedKeyProof = Class.forName("zeroecho.pki.impl.core.CaProofGate$ManagedKeyProof"); Class<?> managedKeyProof = Class.forName("zeroecho.pki.impl.core.CaProofGate$ManagedKeyProof");
assertTrue(java.util.Arrays.stream(managedKeyProof.getDeclaredConstructors()) assertTrue(java.util.Arrays.stream(managedKeyProof.getDeclaredConstructors())
@@ -153,7 +153,8 @@ final class PkiProofGateE2eTest {
.findFirst().orElseThrow(); .findFirst().orElseThrow();
assertArrayEquals(new Class<?>[] { ValidatedCertificateRequest.class, EncodedObject.class, KeyRef.class, assertArrayEquals(new Class<?>[] { ValidatedCertificateRequest.class, EncodedObject.class, KeyRef.class,
BigInteger.class }, endEntity.getParameterTypes()); 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()) assertTrue(java.util.Arrays.stream(BcX509CredentialIssuerBackend.class.getMethods())
.filter(method -> method.getName().startsWith("issue")) .filter(method -> method.getName().startsWith("issue"))
.noneMatch(method -> java.util.Arrays.asList(method.getParameterTypes()) .noneMatch(method -> java.util.Arrays.asList(method.getParameterTypes())
@@ -189,7 +190,7 @@ final class PkiProofGateE2eTest {
assertEquals(0, counting.endEntityCalls.get()); assertEquals(0, counting.endEntityCalls.get());
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(), 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", CredentialBundle issued = issuance.issueEndEntity(new IssueEndEntityCommand(rootCaId, valid, "default",
Optional.empty())); Optional.empty()));
assertEquals(1, counting.endEntityCalls.get()); assertEquals(1, counting.endEntityCalls.get());
@@ -199,14 +200,14 @@ final class PkiProofGateE2eTest {
CaService caService = runtime.caService(counting); CaService caService = runtime.caService(counting);
caService.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootCaId, 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())); new SimpleAttributeSet()));
assertEquals(1, counting.intermediateCalls.get()); assertEquals(1, counting.intermediateCalls.get());
runtime.replaceResolvedKey(subjectKeyRef, wrongKey.getPublic()); runtime.replaceResolvedKey(subjectKeyRef, wrongKey.getPublic());
assertThrows(PkiException.class, assertThrows(PkiException.class,
() -> caService.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), () -> 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()))); new SimpleAttributeSet())));
assertEquals(1, counting.intermediateCalls.get()); assertEquals(1, counting.intermediateCalls.get());
@@ -222,12 +223,12 @@ final class PkiProofGateE2eTest {
System.out.println("unsupportedIssuanceVariantsFailWithoutSideEffects"); System.out.println("unsupportedIssuanceVariantsFailWithoutSideEffects");
try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"), Map.of())) { try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"), Map.of())) {
int auditCount = runtime.auditSink().snapshot().size();
CountingIssuerBackend counting = new CountingIssuerBackend(runtime.issuerBackend()); CountingIssuerBackend counting = new CountingIssuerBackend(runtime.issuerBackend());
DefaultIssuanceService issuance = new DefaultIssuanceService(runtime.store(), runtime.framework(), DefaultIssuanceService issuance = new DefaultIssuanceService(runtime.store(), runtime.framework(),
counting, runtime.auditSink(), runtime.statusResolver(), runtime.profileService(), counting, runtime.auditSink(), runtime.statusResolver(), runtime.profileService(),
Clock.systemUTC()); Clock.systemUTC());
CaService caService = runtime.caService(counting); CaService caService = runtime.caService(counting);
int auditCount = runtime.auditSink().snapshot().size();
ParsedCertificationRequest request = new ParsedCertificationRequest(new PkiId("csr:unsupported"), ParsedCertificationRequest request = new ParsedCertificationRequest(new PkiId("csr:unsupported"),
runtime.framework().formatId(), new SubjectRef("CN=Unsupported"), runtime.framework().formatId(), new SubjectRef("CN=Unsupported"),
new EncodedObject(Encoding.DER, new byte[] { 1 }), Optional.empty(), Optional.empty(), 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"), try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"),
Map.of(rootKeyRef, rootKey, subjectKeyRef, subjectKey))) { Map.of(rootKeyRef, rootKey, subjectKeyRef, subjectKey))) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(), 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"); PKCS10CertificationRequest validCsr = makeCsr(subjectKey, subjectKey, "CN=Subject");
ParsedCertificationRequest valid = parse(runtime, validCsr); ParsedCertificationRequest valid = parse(runtime, validCsr);
ParsedCertificationRequest pss = parse(runtime, ParsedCertificationRequest pss = parse(runtime,
@@ -376,7 +377,7 @@ final class PkiProofGateE2eTest {
return new ProofOfPossessionResult(status, Optional.empty()); return new ProofOfPossessionResult(status, Optional.empty());
})) { })) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(), 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")); ParsedCertificationRequest parsed = parse(runtime, makeCsr(subjectKey, subjectKey, "CN=Subject"));
assertThrows(PkiException.class, () -> issue(runtime, rootCaId, parsed)); assertThrows(PkiException.class, () -> issue(runtime, rootCaId, parsed));
assertTrue(required.get()); assertTrue(required.get());
@@ -411,7 +412,7 @@ final class PkiProofGateE2eTest {
return result; return result;
})) { })) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(), 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")); ParsedCertificationRequest parsed = parse(runtime, makeCsr(subjectKey, subjectKey, "CN=Subject"));
byte[] callerCsr = csrDer(parsed); byte[] callerCsr = csrDer(parsed);
byte[] callerSpki = parsed.publicKeyInfo().bytes(); byte[] callerSpki = parsed.publicKeyInfo().bytes();
@@ -449,7 +450,7 @@ final class PkiProofGateE2eTest {
new BcX509ProofOfPossessionVerifier())) { new BcX509ProofOfPossessionVerifier())) {
assertThrows(PkiException.class, assertThrows(PkiException.class,
() -> runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(), () -> 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()))); new SimpleAttributeSet())));
assertTrue(runtime.store().listCas().isEmpty()); assertTrue(runtime.store().listCas().isEmpty());
assertTrue(runtime.store().listWorkflowStates().isEmpty()); assertTrue(runtime.store().listWorkflowStates().isEmpty());
@@ -462,7 +463,7 @@ final class PkiProofGateE2eTest {
Map.of(rootKeyRef, expectedRoot.getPublic()), new BcX509ProofOfPossessionVerifier())) { Map.of(rootKeyRef, expectedRoot.getPublic()), new BcX509ProofOfPossessionVerifier())) {
assertThrows(PkiException.class, assertThrows(PkiException.class,
() -> runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(), () -> 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()))); new SimpleAttributeSet())));
assertTrue(runtime.store().listWorkflowStates().isEmpty()); assertTrue(runtime.store().listWorkflowStates().isEmpty());
assertEquals(1, runtime.submittedSignCount()); assertEquals(1, runtime.submittedSignCount());
@@ -477,11 +478,12 @@ final class PkiProofGateE2eTest {
keys.put(intermediateKeyRef, intermediateKey); keys.put(intermediateKeyRef, intermediateKey);
try (PkiTestRuntime runtime = PkiTestRuntime.create(validDir, validDir.resolve("bus.log"), keys)) { try (PkiTestRuntime runtime = PkiTestRuntime.create(validDir, validDir.resolve("bus.log"), keys)) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(), 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()));
AttributeSet hostile = hostileIntermediateAttributes(expectedRoot.getPublic()); AttributeSet approved = new SimpleAttributeSet();
PkiId intermediateCaId = runtime.caService() PkiId intermediateCaId = runtime.caService()
.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootCaId, .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); Credential first = runtime.caService().getCa(intermediateCaId).caCredentials().get(0);
X509CertificateHolder firstHolder = new X509CertificateHolder(first.encoded().bytes()); X509CertificateHolder firstHolder = new X509CertificateHolder(first.encoded().bytes());
@@ -492,7 +494,7 @@ final class PkiProofGateE2eTest {
Credential additional = runtime.caService() Credential additional = runtime.caService()
.issueIntermediateCertificate(new IntermediateCertIssueCommand(runtime.framework().formatId(), .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()); X509CertificateHolder additionalHolder = new X509CertificateHolder(additional.encoded().bytes());
assertEquals("CN=Intermediate", additionalHolder.getSubject().toString()); assertEquals("CN=Intermediate", additionalHolder.getSubject().toString());
assertArrayEquals(intermediateKey.getPublic().getEncoded(), assertArrayEquals(intermediateKey.getPublic().getEncoded(),
@@ -517,13 +519,13 @@ final class PkiProofGateE2eTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(issuerDir, issuerDir.resolve("bus.log"), try (PkiTestRuntime runtime = PkiTestRuntime.create(issuerDir, issuerDir.resolve("bus.log"),
Map.of(rootKeyRef, rootKey, intermediateKeyRef, intermediateKey))) { Map.of(rootKeyRef, rootKey, intermediateKeyRef, intermediateKey))) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(), 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()); assertEquals(2, runtime.submittedSignCount());
runtime.replaceManagedKey(rootKeyRef, replacementRootKey); runtime.replaceManagedKey(rootKeyRef, replacementRootKey);
assertThrows(PkiException.class, assertThrows(PkiException.class,
() -> runtime.caService() () -> runtime.caService()
.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), .createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(),
rootCaId, new SubjectRef("CN=Intermediate"), "default", rootCaId, new SubjectRef("CN=Intermediate"), "intermediate-ca",
Optional.of(intermediateKeyRef), new SimpleAttributeSet()))); Optional.of(intermediateKeyRef), new SimpleAttributeSet())));
assertEquals(4, runtime.submittedSignCount()); assertEquals(4, runtime.submittedSignCount());
assertEquals(1, runtime.store().listCas().size()); assertEquals(1, runtime.store().listCas().size());
@@ -537,17 +539,17 @@ final class PkiProofGateE2eTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(additionalDir, additionalDir.resolve("bus.log"), try (PkiTestRuntime runtime = PkiTestRuntime.create(additionalDir, additionalDir.resolve("bus.log"),
Map.of(rootKeyRef, rootKey, intermediateKeyRef, intermediateKey))) { Map.of(rootKeyRef, rootKey, intermediateKeyRef, intermediateKey))) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(), 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() PkiId intermediateCaId = runtime.caService()
.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootCaId, .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())); new SimpleAttributeSet()));
assertEquals(5, runtime.submittedSignCount()); assertEquals(5, runtime.submittedSignCount());
runtime.replaceManagedKey(rootKeyRef, replacementRootKey); runtime.replaceManagedKey(rootKeyRef, replacementRootKey);
assertThrows(PkiException.class, assertThrows(PkiException.class,
() -> runtime.caService().issueIntermediateCertificate( () -> runtime.caService().issueIntermediateCertificate(
new IntermediateCertIssueCommand(runtime.framework().formatId(), rootCaId, new IntermediateCertIssueCommand(runtime.framework().formatId(), rootCaId,
intermediateCaId, "default", Optional.empty(), new SimpleAttributeSet()))); intermediateCaId, "intermediate-ca", Optional.empty(), new SimpleAttributeSet())));
assertEquals(7, runtime.submittedSignCount()); assertEquals(7, runtime.submittedSignCount());
assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size()); assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size());
assertTrue(runtime.store().listWorkflowStates().isEmpty()); assertTrue(runtime.store().listWorkflowStates().isEmpty());
@@ -560,11 +562,11 @@ final class PkiProofGateE2eTest {
try (PkiTestRuntime source = PkiTestRuntime.create(sourceDir, sourceDir.resolve("bus.log"), try (PkiTestRuntime source = PkiTestRuntime.create(sourceDir, sourceDir.resolve("bus.log"),
Map.of(rootKeyRef, rootKey, intermediateKeyRef, intermediateKey))) { Map.of(rootKeyRef, rootKey, intermediateKeyRef, intermediateKey))) {
PkiId rootCaId = source.caService().createRoot(new CaCreateCommand(source.framework().formatId(), 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(); rootCertificate = source.caService().getCa(rootCaId).caCredentials().get(0).encoded().bytes().clone();
PkiId intermediateCaId = source.caService() PkiId intermediateCaId = source.caService()
.createIntermediate(new IntermediateCreateCommand(source.framework().formatId(), rootCaId, .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())); new SimpleAttributeSet()));
intermediateCertificate = source.caService().getCa(intermediateCaId).caCredentials().get(0).encoded() intermediateCertificate = source.caService().getCa(intermediateCaId).caCredentials().get(0).encoded()
.bytes().clone(); .bytes().clone();
@@ -577,7 +579,7 @@ final class PkiProofGateE2eTest {
try (PkiTestRuntime target = PkiTestRuntime.create(importDir, importDir.resolve("bus.log"), try (PkiTestRuntime target = PkiTestRuntime.create(importDir, importDir.resolve("bus.log"),
Map.of(rootKeyRef, replacementRootKey))) { Map.of(rootKeyRef, replacementRootKey))) {
CaImportCommand command = new CaImportCommand(target.framework().formatId(), new SubjectRef("CN=Root"), 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()); new SimpleAttributeSet());
assertThrows(PkiException.class, () -> target.caService().importRoot(command)); assertThrows(PkiException.class, () -> target.caService().importRoot(command));
assertTrue(target.store().listCas().isEmpty()); assertTrue(target.store().listCas().isEmpty());
@@ -593,7 +595,7 @@ final class PkiProofGateE2eTest {
Map.of(rootKeyRef, rootKey))) { Map.of(rootKeyRef, rootKey))) {
target.onPublicKeyResolve(() -> callerOwnedCertificate[callerOwnedCertificate.length - 1] ^= 0x01); target.onPublicKeyResolve(() -> callerOwnedCertificate[callerOwnedCertificate.length - 1] ^= 0x01);
PkiId importedCaId = target.caService().importRoot(new CaImportCommand(target.framework().formatId(), 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())); new EncodedObject(Encoding.DER, callerOwnedCertificate), new SimpleAttributeSet()));
assertTrue(target.caService().getCa(importedCaId).caCredentials().get(0) assertTrue(target.caService().getCa(importedCaId).caCredentials().get(0)
.profileBinding() instanceof CaProfileBinding); .profileBinding() instanceof CaProfileBinding);
@@ -616,7 +618,7 @@ final class PkiProofGateE2eTest {
assertThrows(PkiException.class, assertThrows(PkiException.class,
() -> signingFailure.caService().createRoot(new CaCreateCommand( () -> signingFailure.caService().createRoot(new CaCreateCommand(
signingFailure.framework().formatId(), signingFailure.framework().formatId(),
new SubjectRef("CN=Root"), "default", Optional.of(rootKeyRef), new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef),
new SimpleAttributeSet()))); new SimpleAttributeSet())));
assertTrue(signingFailure.store().listCas().isEmpty()); assertTrue(signingFailure.store().listCas().isEmpty());
assertTrue(signingFailure.store().listWorkflowStates().isEmpty()); assertTrue(signingFailure.store().listWorkflowStates().isEmpty());
@@ -637,7 +639,7 @@ final class PkiProofGateE2eTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(rootDir, rootDir.resolve("bus.log"), try (PkiTestRuntime runtime = PkiTestRuntime.create(rootDir, rootDir.resolve("bus.log"),
Map.of(keyRef, keyPair))) { Map.of(keyRef, keyPair))) {
CaImportCommand command = new CaImportCommand(runtime.framework().formatId(), new SubjectRef(subject), 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)); assertThrows(PkiException.class, () -> runtime.caService().importRoot(command));
assertTrue(runtime.store().listCas().isEmpty()); assertTrue(runtime.store().listCas().isEmpty());
assertEquals(0, runtime.submittedSignCount()); assertEquals(0, runtime.submittedSignCount());
@@ -657,7 +659,7 @@ final class PkiProofGateE2eTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(runtimeDir, runtimeDir.resolve("bus.log"), try (PkiTestRuntime runtime = PkiTestRuntime.create(runtimeDir, runtimeDir.resolve("bus.log"),
Map.of(rootKeyRef, rootKey))) { Map.of(rootKeyRef, rootKey))) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(), 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 subject = parse(runtime, makeCsr(subjectKey, subjectKey, "CN=Subject"));
ParsedCertificationRequest substitute = parse(runtime, ParsedCertificationRequest substitute = parse(runtime,
makeCsr(subjectKey, subjectKey, "CN=Substitute")); makeCsr(subjectKey, subjectKey, "CN=Substitute"));
@@ -670,7 +672,7 @@ final class PkiProofGateE2eTest {
} }
@Override @Override
public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) { public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
throw new IllegalStateException("DO_NOT_LOG_SIGNATURE_SENTINEL"); throw new IllegalStateException("DO_NOT_LOG_SIGNATURE_SENTINEL");
} }
}; };
@@ -696,8 +698,8 @@ final class PkiProofGateE2eTest {
} }
@Override @Override
public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) { public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
return delegateBackend.issueIntermediateCertificate(issuance); return delegateBackend.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
} }
}; };
DefaultIssuanceService service = new DefaultIssuanceService(runtime.store(), runtime.framework(), DefaultIssuanceService service = new DefaultIssuanceService(runtime.store(), runtime.framework(),
@@ -719,15 +721,15 @@ final class PkiProofGateE2eTest {
CredentialBundle rawBundle = delegateBackend.issueEndEntity(candidate, issuerCertificate, CredentialBundle rawBundle = delegateBackend.issueEndEntity(candidate, issuerCertificate,
issuerKeyRef, serial); issuerKeyRef, serial);
Credential raw = rawBundle.credential(); Credential raw = rawBundle.credential();
Credential wrongBinding = copyWithBinding(raw, new CaProfileBinding(candidate.profileReference() Credential wrongBinding = copyWithBinding(raw,
.profileId())); new CaProfileBinding(candidate.profileReference()));
wrongBindingCredential.set(wrongBinding); wrongBindingCredential.set(wrongBinding);
return new CredentialBundle(wrongBinding, rawBundle.supportingObjects()); return new CredentialBundle(wrongBinding, rawBundle.supportingObjects());
} }
@Override @Override
public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) { public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
return delegateBackend.issueIntermediateCertificate(issuance); return delegateBackend.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
} }
}; };
DefaultIssuanceService wrongBindingService = new DefaultIssuanceService(runtime.store(), DefaultIssuanceService wrongBindingService = new DefaultIssuanceService(runtime.store(),
@@ -754,8 +756,8 @@ final class PkiProofGateE2eTest {
} }
@Override @Override
public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) { public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
return delegateBackend.issueIntermediateCertificate(issuance); return delegateBackend.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
} }
}; };
DefaultIssuanceService invalidSignatureService = new DefaultIssuanceService(runtime.store(), DefaultIssuanceService invalidSignatureService = new DefaultIssuanceService(runtime.store(),
@@ -775,8 +777,8 @@ final class PkiProofGateE2eTest {
} }
@Override @Override
public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) { public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
return delegateBackend.issueIntermediateCertificate(issuance); return delegateBackend.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
} }
}; };
DefaultIssuanceService snapshotService = new DefaultIssuanceService(runtime.store(), DefaultIssuanceService snapshotService = new DefaultIssuanceService(runtime.store(),
@@ -865,7 +867,7 @@ final class PkiProofGateE2eTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"), try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"),
Map.of(rootKeyRef, rootKey, intermediateKeyRef, intermediateKey))) { Map.of(rootKeyRef, rootKey, intermediateKeyRef, intermediateKey))) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(), 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) assertTrue(runtime.caService().getCa(rootCaId).caCredentials().get(0)
.profileBinding() instanceof CaProfileBinding); .profileBinding() instanceof CaProfileBinding);
CredentialIssuerBackend delegate = runtime.issuerBackend(); CredentialIssuerBackend delegate = runtime.issuerBackend();
@@ -876,7 +878,7 @@ final class PkiProofGateE2eTest {
PkiException rejected = assertThrows(PkiException.class, PkiException rejected = assertThrows(PkiException.class,
() -> wrongBindingService.createIntermediate(new IntermediateCreateCommand( () -> wrongBindingService.createIntermediate(new IntermediateCreateCommand(
runtime.framework().formatId(), rootCaId, runtime.framework().formatId(), rootCaId,
new SubjectRef("CN=BindingRejectedIntermediate"), "default", new SubjectRef("CN=BindingRejectedIntermediate"), "intermediate-ca",
Optional.of(intermediateKeyRef), new SimpleAttributeSet())), mutation.name()); Optional.of(intermediateKeyRef), new SimpleAttributeSet())), mutation.name());
assertTrue(rejected.getMessage().contains("CREDENTIAL_PROFILE_BINDING_MISMATCH"), mutation.name()); assertTrue(rejected.getMessage().contains("CREDENTIAL_PROFILE_BINDING_MISMATCH"), mutation.name());
assertEquals(1, runtime.store().listCas().size(), mutation.name()); assertEquals(1, runtime.store().listCas().size(), mutation.name());
@@ -892,8 +894,8 @@ final class PkiProofGateE2eTest {
} }
@Override @Override
public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) { public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
Credential raw = delegate.issueIntermediateCertificate(issuance); Credential raw = delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
return rebuildIntermediateIdentity(raw, rootKey, Optional.of(wrongKey.getPublic()), return rebuildIntermediateIdentity(raw, rootKey, Optional.of(wrongKey.getPublic()),
Optional.empty()); Optional.empty());
} }
@@ -901,13 +903,13 @@ final class PkiProofGateE2eTest {
CaService wrongKeyService = runtime.caService(wrongKeyBackend); CaService wrongKeyService = runtime.caService(wrongKeyBackend);
assertThrows(PkiException.class, assertThrows(PkiException.class,
() -> wrongKeyService.createIntermediate(new IntermediateCreateCommand(runtime.framework() () -> 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()))); Optional.of(intermediateKeyRef), new SimpleAttributeSet())));
assertEquals(1, runtime.store().listCas().size()); assertEquals(1, runtime.store().listCas().size());
PkiId intermediateCaId = runtime.caService() PkiId intermediateCaId = runtime.caService()
.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootCaId, .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())); new SimpleAttributeSet()));
for (BindingVariantMutation mutation : BindingVariantMutation.values()) { for (BindingVariantMutation mutation : BindingVariantMutation.values()) {
AtomicReference<Credential> produced = new AtomicReference<>(); AtomicReference<Credential> produced = new AtomicReference<>();
@@ -915,7 +917,7 @@ final class PkiProofGateE2eTest {
produced)); produced));
PkiException rejected = assertThrows(PkiException.class, PkiException rejected = assertThrows(PkiException.class,
() -> wrongBindingService.issueIntermediateCertificate(new IntermediateCertIssueCommand( () -> wrongBindingService.issueIntermediateCertificate(new IntermediateCertIssueCommand(
runtime.framework().formatId(), rootCaId, intermediateCaId, "default", runtime.framework().formatId(), rootCaId, intermediateCaId, "intermediate-ca",
Optional.empty(), new SimpleAttributeSet())), mutation.name()); Optional.empty(), new SimpleAttributeSet())), mutation.name());
assertTrue(rejected.getMessage().contains("CREDENTIAL_PROFILE_BINDING_MISMATCH"), mutation.name()); assertTrue(rejected.getMessage().contains("CREDENTIAL_PROFILE_BINDING_MISMATCH"), mutation.name());
assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size(), assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size(),
@@ -932,8 +934,8 @@ final class PkiProofGateE2eTest {
} }
@Override @Override
public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) { public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
Credential raw = delegate.issueIntermediateCertificate(issuance); Credential raw = delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
return rebuildIntermediateIdentity(raw, rootKey, Optional.empty(), return rebuildIntermediateIdentity(raw, rootKey, Optional.empty(),
Optional.of("CN=WrongIntermediate")); Optional.of("CN=WrongIntermediate"));
} }
@@ -941,7 +943,7 @@ final class PkiProofGateE2eTest {
CaService wrongSubjectService = runtime.caService(wrongSubjectBackend); CaService wrongSubjectService = runtime.caService(wrongSubjectBackend);
assertThrows(PkiException.class, assertThrows(PkiException.class,
() -> wrongSubjectService.issueIntermediateCertificate(new IntermediateCertIssueCommand( () -> wrongSubjectService.issueIntermediateCertificate(new IntermediateCertIssueCommand(
runtime.framework().formatId(), rootCaId, intermediateCaId, "default", Optional.empty(), runtime.framework().formatId(), rootCaId, intermediateCaId, "intermediate-ca", Optional.empty(),
new SimpleAttributeSet()))); new SimpleAttributeSet())));
assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size()); assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size());
@@ -952,8 +954,8 @@ final class PkiProofGateE2eTest {
} }
@Override @Override
public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) { public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
Credential raw = delegate.issueIntermediateCertificate(issuance); Credential raw = delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
byte[] invalid = raw.encoded().bytes().clone(); byte[] invalid = raw.encoded().bytes().clone();
invalid[invalid.length - 1] ^= 0x01; invalid[invalid.length - 1] ^= 0x01;
return new Credential(raw.credentialId(), raw.formatId(), raw.issuerRef(), return new Credential(raw.credentialId(), raw.formatId(), raw.issuerRef(),
@@ -965,7 +967,7 @@ final class PkiProofGateE2eTest {
CaService invalidSignatureService = runtime.caService(invalidSignatureBackend); CaService invalidSignatureService = runtime.caService(invalidSignatureBackend);
assertThrows(PkiException.class, assertThrows(PkiException.class,
() -> invalidSignatureService.issueIntermediateCertificate(new IntermediateCertIssueCommand( () -> invalidSignatureService.issueIntermediateCertificate(new IntermediateCertIssueCommand(
runtime.framework().formatId(), rootCaId, intermediateCaId, "default", Optional.empty(), runtime.framework().formatId(), rootCaId, intermediateCaId, "intermediate-ca", Optional.empty(),
new SimpleAttributeSet()))); new SimpleAttributeSet())));
assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size()); assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size());
@@ -975,7 +977,7 @@ final class PkiProofGateE2eTest {
assertThrows(PkiException.class, assertThrows(PkiException.class,
() -> maliciousExtensionService.issueIntermediateCertificate( () -> maliciousExtensionService.issueIntermediateCertificate(
new IntermediateCertIssueCommand(runtime.framework().formatId(), rootCaId, new IntermediateCertIssueCommand(runtime.framework().formatId(), rootCaId,
intermediateCaId, "default", Optional.empty(), new SimpleAttributeSet())), intermediateCaId, "intermediate-ca", Optional.empty(), new SimpleAttributeSet())),
variant.name()); variant.name());
assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size(), variant.name()); assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size(), variant.name());
} }
@@ -988,15 +990,15 @@ final class PkiProofGateE2eTest {
} }
@Override @Override
public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) { public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
Credential raw = delegate.issueIntermediateCertificate(issuance); Credential raw = delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
rawCredential.set(raw); rawCredential.set(raw);
return raw; return raw;
} }
}; };
CaService snapshotService = runtime.caService(mutableBackend); CaService snapshotService = runtime.caService(mutableBackend);
Credential returned = snapshotService.issueIntermediateCertificate(new IntermediateCertIssueCommand( 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())); new SimpleAttributeSet()));
byte[] expected = returned.encoded().bytes().clone(); byte[] expected = returned.encoded().bytes().clone();
rawCredential.get().encoded().bytes()[0] ^= 0x01; rawCredential.get().encoded().bytes()[0] ^= 0x01;
@@ -1041,9 +1043,9 @@ final class PkiProofGateE2eTest {
} }
@Override @Override
public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) { public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
intermediateCalls.incrementAndGet(); intermediateCalls.incrementAndGet();
return delegate.issueIntermediateCertificate(issuance); return delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
} }
} }
@@ -1057,8 +1059,8 @@ final class PkiProofGateE2eTest {
} }
@Override @Override
public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) { public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
Credential credential = delegate.issueIntermediateCertificate(issuance); Credential credential = delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
return rebuildIntermediateExtensions(credential, issuerKey, variant); return rebuildIntermediateExtensions(credential, issuerKey, variant);
} }
}; };
@@ -1074,24 +1076,27 @@ final class PkiProofGateE2eTest {
} }
@Override @Override
public Credential issueIntermediateCertificate(ManagedCaIssuance issuance) { public Credential issueIntermediateCertificate(ValidatedCaCertificateRequest issuance, EncodedObject issuerCertificate, KeyRef issuerKeyRef) {
Credential raw = delegate.issueIntermediateCertificate(issuance); Credential raw = delegate.issueIntermediateCertificate(issuance, issuerCertificate, issuerKeyRef);
produced.set(raw); produced.set(raw);
if (mutation == BindingVariantMutation.NULL_CREDENTIAL) { if (mutation == BindingVariantMutation.NULL_CREDENTIAL) {
return null; 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) { 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])); new byte[CertificateProfileRef.HASH_BYTES]));
case END_ENTITY_OTHER_ID -> new EndEntityProfileBinding(new CertificateProfileRef("other", 1, case END_ENTITY_OTHER_ID -> new EndEntityProfileBinding(new CertificateProfileRef("other", 1,
new byte[CertificateProfileRef.HASH_BYTES])); 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"); case NULL_CREDENTIAL -> throw new IllegalStateException("null credential has no binding");
}; };
} }

View File

@@ -372,7 +372,7 @@ final class DefaultStatusObjectServiceCrlTest {
CaProfileBinding binding = assertInstanceOf(CaProfileBinding.class, template.profileBinding()); CaProfileBinding binding = assertInstanceOf(CaProfileBinding.class, template.profileBinding());
return new Credential(new PkiId("credential:" + suffix), formatId, template.issuerRef(), return new Credential(new PkiId("credential:" + suffix), formatId, template.issuerRef(),
template.subjectRef(), template.validity(), template.serialOrUniqueId(), 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()); template.attributes());
} }
@@ -410,7 +410,7 @@ final class DefaultStatusObjectServiceCrlTest {
private static PkiId createRoot(PkiTestRuntime runtime, KeyRef keyRef, String commonName) { private static PkiId createRoot(PkiTestRuntime runtime, KeyRef keyRef, String commonName) {
return runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(), 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 { private static KeyPair generateRsa() throws Exception {

View File

@@ -139,13 +139,13 @@ final class H7ProfileEnforcementTest {
SubjectAlternativeNameRule dns = SubjectAlternativeNameRule dns =
new SubjectAlternativeNameRule(SubjectAlternativeNameType.DNS_NAME, 0, 1, false, false); new SubjectAlternativeNameRule(SubjectAlternativeNameType.DNS_NAME, 0, 1, false, false);
assertThrows(IllegalArgumentException.class, 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)); false, false));
assertThrows(IllegalArgumentException.class, 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)); true, false));
assertThrows(IllegalArgumentException.class, 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)); false, false));
assertThrows(IllegalArgumentException.class, () -> leaf(Set.of(LeafKeyUsage.ENCIPHER_ONLY), assertThrows(IllegalArgumentException.class, () -> leaf(Set.of(LeafKeyUsage.ENCIPHER_ONLY),
Set.of("RSA"), Set.of())); Set.of("RSA"), Set.of()));
@@ -262,7 +262,7 @@ final class H7ProfileEnforcementTest {
CertificateProfileRef reference = new CertificateProfileRef("shared-profile", 1, CertificateProfileRef reference = new CertificateProfileRef("shared-profile", 1,
new byte[CertificateProfileRef.HASH_BYTES]); new byte[CertificateProfileRef.HASH_BYTES]);
EndEntityProfileBinding endEntity = new EndEntityProfileBinding(reference); EndEntityProfileBinding endEntity = new EndEntityProfileBinding(reference);
CaProfileBinding ca = new CaProfileBinding(reference.profileId()); CaProfileBinding ca = new CaProfileBinding(reference);
assertTrue(CredentialProfileBinding.class.isSealed()); assertTrue(CredentialProfileBinding.class.isSealed());
assertEquals(Set.of(EndEntityProfileBinding.class, CaProfileBinding.class), assertEquals(Set.of(EndEntityProfileBinding.class, CaProfileBinding.class),
@@ -273,16 +273,17 @@ final class H7ProfileEnforcementTest {
.noneMatch(method -> method.getName().equals("profileId"))); .noneMatch(method -> method.getName().equals("profileId")));
CredentialProfileBindings.requireEndEntityBinding(endEntity, reference); CredentialProfileBindings.requireEndEntityBinding(endEntity, reference);
CredentialProfileBindings.requireCaBinding(ca, reference.profileId()); CredentialProfileBindings.requireCaBinding(ca, reference);
assertCode(CredentialProfileBindings.MISMATCH_CODE, assertCode(CredentialProfileBindings.MISMATCH_CODE,
() -> CredentialProfileBindings.requireEndEntityBinding(ca, reference)); () -> CredentialProfileBindings.requireEndEntityBinding(ca, reference));
assertCode(CredentialProfileBindings.MISMATCH_CODE, assertCode(CredentialProfileBindings.MISMATCH_CODE,
() -> CredentialProfileBindings.requireCaBinding(endEntity, reference.profileId())); () -> CredentialProfileBindings.requireCaBinding(endEntity, reference));
assertCode(CredentialProfileBindings.MISMATCH_CODE, assertCode(CredentialProfileBindings.MISMATCH_CODE,
() -> CredentialProfileBindings.requireCaBinding(new CaProfileBinding("other"), () -> CredentialProfileBindings.requireCaBinding(
reference.profileId())); new CaProfileBinding(new CertificateProfileRef("other", 1, new byte[32])),
reference));
assertCode(CredentialProfileBindings.MISMATCH_CODE, assertCode(CredentialProfileBindings.MISMATCH_CODE,
() -> CredentialProfileBindings.requireCaBinding(null, reference.profileId())); () -> CredentialProfileBindings.requireCaBinding(null, reference));
} }
@Test @Test
@@ -293,7 +294,7 @@ final class H7ProfileEnforcementTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"), try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"),
Map.of(rootRef, root))) { Map.of(rootRef, root))) {
PkiId caId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(), 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( ParsedCertificationRequest request = runtime.certificationRequestService().parse(
new CertificationRequest(runtime.framework().formatId(), new CertificationRequest(runtime.framework().formatId(),
new EncodedObject(Encoding.DER, new EncodedObject(Encoding.DER,
@@ -385,31 +386,34 @@ final class H7ProfileEnforcementTest {
return new Credential(new PkiId("credential:issuer"), BcX509CredentialFramework.FORMAT_ID, return new Credential(new PkiId("credential:issuer"), BcX509CredentialFramework.FORMAT_ID,
new IssuerRef(new PkiId("ca:issuer")), new SubjectRef("CN=Issuer"), new IssuerRef(new PkiId("ca:issuer")), new SubjectRef("CN=Issuer"),
new Validity(NOW.minus(Duration.ofDays(1)), NOW.plus(Duration.ofDays(1000))), "1", 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()); new EncodedObject(Encoding.DER, new byte[] { 1 }), new SimpleAttributeSet());
} }
private static CertificateProfile policyWithFixedOrganization(String algorithm) { 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.COMMON_NAME, 1, 1, 256, Optional.empty(), true),
new SubjectRdnRule(SubjectRdnType.ORGANIZATION_NAME, 1, 1, 256, new SubjectRdnRule(SubjectRdnType.ORGANIZATION_NAME, 1, 1, 256,
Optional.of("Profile Fixed"), false))); Optional.of("Profile Fixed"), false)));
SubjectAlternativeNamePolicy sans = SubjectAlternativeNamePolicy sans =
new SubjectAlternativeNamePolicy(false, 0, 0, List.of(), false, Set.of(), false, false, false); 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), Set.of(), true, false, true, Set.of(algorithm), Set.of(LeafKeyUsage.DIGITAL_SIGNATURE), Set.of(), true, false, true,
Duration.ofDays(365)); Set.of(algorithm));
return new CertificateProfile("h7", BcX509CredentialFramework.FORMAT_ID, "H7", leaf); return new CertificateProfile("h7", BcX509CredentialFramework.FORMAT_ID, "H7",
Duration.ofDays(365), subject, leaf);
} }
private static LeafCertificatePolicy leaf(Set<LeafKeyUsage> usages, Set<String> algorithms, private static LeafCertificatePolicy leaf(Set<LeafKeyUsage> usages, Set<String> algorithms,
Set<ExtendedKeyUsageId> ekus) { Set<ExtendedKeyUsageId> ekus) {
return new LeafCertificatePolicy(new SubjectPolicy(List.of()), return new LeafCertificatePolicy(
new SubjectAlternativeNamePolicy(true, 1, 1, new SubjectAlternativeNamePolicy(1, 1,
List.of(new SubjectAlternativeNameRule(SubjectAlternativeNameType.DNS_NAME, 1, 1, List.of(new SubjectAlternativeNameRule(SubjectAlternativeNameType.DNS_NAME, 1, 1,
false, false)), false, false)),
false, Set.of(), false, true, 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 { private static ParsedCertificationRequest parse(PKCS10CertificationRequest request) throws Exception {

View File

@@ -176,7 +176,7 @@ final class StoreBackedEffectiveCredentialStatusResolverTest {
Credential credential = new Credential(new PkiId("credential:audit"), new FormatId(sentinel), Credential credential = new Credential(new PkiId("credential:audit"), new FormatId(sentinel),
new IssuerRef(new PkiId("ca:issuer")), new SubjectRef("CN=Audit"), new IssuerRef(new PkiId("ca:issuer")), new SubjectRef("CN=Audit"),
new Validity(NOW.minusSeconds(60), NOW.plusSeconds(60)), "audit", new PkiId("key: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 EncodedObject(Encoding.DER, new byte[] { 1 }),
new SimpleAttributeSet()); new SimpleAttributeSet());
AtomicReference<AuditEvent> recorded = new AtomicReference<>(); AtomicReference<AuditEvent> recorded = new AtomicReference<>();
@@ -239,7 +239,7 @@ final class StoreBackedEffectiveCredentialStatusResolverTest {
return new Credential(new PkiId("credential:" + suffix), new FormatId("x509"), return new Credential(new PkiId("credential:" + suffix), new FormatId("x509"),
new IssuerRef(new PkiId("ca:issuer")), new SubjectRef("CN=" + suffix), new IssuerRef(new PkiId("ca:issuer")), new SubjectRef("CN=" + suffix),
new Validity(notBefore, notAfter), suffix, new PkiId("key:" + 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()); new EncodedObject(Encoding.DER, new byte[] { 1, 2, 3 }), new SimpleAttributeSet());
} }

View File

@@ -76,6 +76,7 @@ import zeroecho.pki.api.credential.CredentialStatus;
import zeroecho.pki.api.profile.CertificateProfile; import zeroecho.pki.api.profile.CertificateProfile;
import zeroecho.pki.api.profile.CertificateProfileDefinition; import zeroecho.pki.api.profile.CertificateProfileDefinition;
import zeroecho.pki.api.profile.CertificateProfileDocumentCodec; import zeroecho.pki.api.profile.CertificateProfileDocumentCodec;
import zeroecho.pki.api.profile.CertificateProfileKind;
import zeroecho.pki.api.profile.CertificateProfileRef; import zeroecho.pki.api.profile.CertificateProfileRef;
import zeroecho.pki.api.profile.ExtendedKeyUsageId; import zeroecho.pki.api.profile.ExtendedKeyUsageId;
import zeroecho.pki.api.profile.ImportedCertificateProfileVersion; import zeroecho.pki.api.profile.ImportedCertificateProfileVersion;
@@ -527,12 +528,15 @@ public final class FilesystemPkiStoreTest {
*/ */
static void importProfile(FilesystemPkiStore store, CertificateProfile profile, Instant importedAt) static void importProfile(FilesystemPkiStore store, CertificateProfile profile, Instant importedAt)
throws Exception { throws Exception {
CertificateProfileDefinition definition = new CertificateProfileDefinition(profile.profileId(), 1, CertificateProfileDefinition definition = new CertificateProfileDefinition(
profile.formatId(), profile.displayName(), profile.leafPolicy()); CertificateProfileKind.END_ENTITY, profile.profileId(), 1, profile.formatId(),
profile.displayName(), profile.maximumValidity(), profile.subjectPolicy(),
profile.leafPolicy());
byte[] canonical = CertificateProfileDocumentCodec.writeCanonical(definition); byte[] canonical = CertificateProfileDocumentCodec.writeCanonical(definition);
byte[] hash = MessageDigest.getInstance("SHA-256").digest(canonical); byte[] hash = MessageDigest.getInstance("SHA-256").digest(canonical);
CertificateProfileRef reference = new CertificateProfileRef(profile.profileId(), 1, hash); 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)); importedAt));
} }
@@ -554,15 +558,16 @@ public final class FilesystemPkiStoreTest {
static CertificateProfile minimalProfile(String profileId) { static CertificateProfile minimalProfile(String profileId) {
FormatId formatId = new FormatId("fmt-x509"); 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))); 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); 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(LeafKeyUsage.DIGITAL_SIGNATURE, LeafKeyUsage.KEY_ENCIPHERMENT),
Set.of(new ExtendedKeyUsageId("1.3.6.1.5.5.7.3.1")), true, false, true, Set.of(new ExtendedKeyUsageId("1.3.6.1.5.5.7.3.1")), true, false, true,
Set.of("RSA"), Duration.ofDays(365)); Set.of("RSA"));
return new CertificateProfile(profileId, formatId, "Test leaf profile", leaf); return new CertificateProfile(profileId, formatId, "Test leaf profile",
Duration.ofDays(365), subject, leaf);
} }
static Credential minimalCredential(String serial, String profileId) { static Credential minimalCredential(String serial, String profileId) {
@@ -584,7 +589,7 @@ public final class FilesystemPkiStoreTest {
AttributeSet attrs = emptyAttributes(); AttributeSet attrs = emptyAttributes();
return new Credential(credentialId, formatId, issuerRef, subjectRef, validity, serial, publicKeyId, 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() { static AttributeSet emptyAttributes() {

View File

@@ -30,7 +30,9 @@ import org.junit.jupiter.api.io.TempDir;
import zeroecho.pki.api.PkiException; import zeroecho.pki.api.PkiException;
import zeroecho.pki.api.profile.BuiltInCertificateProfileCatalog; import zeroecho.pki.api.profile.BuiltInCertificateProfileCatalog;
import zeroecho.pki.api.profile.BuiltInCertificateProfileTemplate; import zeroecho.pki.api.profile.BuiltInCertificateProfileTemplate;
import zeroecho.pki.api.profile.CertificateProfileDefinition;
import zeroecho.pki.api.profile.CertificateProfileRef; import zeroecho.pki.api.profile.CertificateProfileRef;
import zeroecho.pki.api.profile.CertificateProfileKind;
import zeroecho.pki.api.profile.ImportedCertificateProfileVersion; import zeroecho.pki.api.profile.ImportedCertificateProfileVersion;
import zeroecho.pki.impl.audit.InMemoryAuditSink; import zeroecho.pki.impl.audit.InMemoryAuditSink;
import zeroecho.pki.impl.core.DefaultProfileService; import zeroecho.pki.impl.core.DefaultProfileService;
@@ -100,7 +102,8 @@ final class FilesystemProfileLifecycleTest {
ImportedCertificateProfileVersion invalid = new ImportedCertificateProfileVersion( ImportedCertificateProfileVersion invalid = new ImportedCertificateProfileVersion(
new CertificateProfileRef(template.definition().profileId(), new CertificateProfileRef(template.definition().profileId(),
template.definition().profileVersion(), template.canonicalSha256()), 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); Path path = new FsPaths(noncanonicalRoot).profileVersion("server-tls", 1);
FsOperations.ensureDir(path.getParent()); FsOperations.ensureDir(path.getParent());
Files.write(path, FsCodec.encode(FsCodec.PROFILE_VERSION, invalid)); Files.write(path, FsCodec.encode(FsCodec.PROFILE_VERSION, invalid));
@@ -115,7 +118,8 @@ final class FilesystemProfileLifecycleTest {
new CertificateProfileRef(template.definition().profileId(), new CertificateProfileRef(template.definition().profileId(),
template.definition().profileVersion(), template.definition().profileVersion(),
new byte[CertificateProfileRef.HASH_BYTES]), 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); Path path = new FsPaths(hashMismatchRoot).profileVersion("server-tls", 1);
FsOperations.ensureDir(path.getParent()); FsOperations.ensureDir(path.getParent());
Files.write(path, FsCodec.encode(FsCodec.PROFILE_VERSION, invalid)); 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 @Test
void missingCorruptAndHashMismatchedPointersNeverFallback(@TempDir Path directory) throws Exception { void missingCorruptAndHashMismatchedPointersNeverFallback(@TempDir Path directory) throws Exception {
BuiltInCertificateProfileTemplate template = builtIn("vpn-client"); BuiltInCertificateProfileTemplate template = builtIn("vpn-client");

View File

@@ -499,7 +499,7 @@ final class FilesystemRevocationJournalTest {
return new Credential(new PkiId("credential:" + suffix), new FormatId("x509"), return new Credential(new PkiId("credential:" + suffix), new FormatId("x509"),
new IssuerRef(new PkiId("ca:issuer")), new SubjectRef("CN=" + suffix), new IssuerRef(new PkiId("ca:issuer")), new SubjectRef("CN=" + suffix),
new Validity(TIME.minusSeconds(60), TIME.plusSeconds(60)), 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()); new EncodedObject(Encoding.DER, new byte[] { 1, 2, 3 }), new SimpleAttributeSet());
} }

View File

@@ -71,6 +71,7 @@ import zeroecho.pki.api.credential.EndEntityProfileBinding;
import zeroecho.pki.api.profile.CertificateProfile; import zeroecho.pki.api.profile.CertificateProfile;
import zeroecho.pki.api.profile.CertificateProfileDefinition; import zeroecho.pki.api.profile.CertificateProfileDefinition;
import zeroecho.pki.api.profile.CertificateProfileDocumentCodec; import zeroecho.pki.api.profile.CertificateProfileDocumentCodec;
import zeroecho.pki.api.profile.CertificateProfileKind;
import zeroecho.pki.api.profile.CertificateProfileRef; import zeroecho.pki.api.profile.CertificateProfileRef;
import zeroecho.pki.api.profile.ExtendedKeyUsageId; import zeroecho.pki.api.profile.ExtendedKeyUsageId;
import zeroecho.pki.api.profile.ImportedCertificateProfileVersion; import zeroecho.pki.api.profile.ImportedCertificateProfileVersion;
@@ -215,7 +216,7 @@ final class FsCodecTest {
CertificateProfileRef reference = new CertificateProfileRef("profile-a", 1, CertificateProfileRef reference = new CertificateProfileRef("profile-a", 1,
new byte[CertificateProfileRef.HASH_BYTES]); new byte[CertificateProfileRef.HASH_BYTES]);
List<CredentialProfileBinding> bindings = List.of(new EndEntityProfileBinding(reference), List<CredentialProfileBinding> bindings = List.of(new EndEntityProfileBinding(reference),
new CaProfileBinding(reference.profileId())); new CaProfileBinding(reference));
for (CredentialProfileBinding binding : bindings) { for (CredentialProfileBinding binding : bindings) {
Credential original = credential(binding); 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) { private static ParsedCertificationRequest roundTripRequest(AttributeSet attributes) {
byte[] encoded = FsCodec.encode(FsCodec.PARSED_REQUEST, request(attributes)); byte[] encoded = FsCodec.encode(FsCodec.PARSED_REQUEST, request(attributes));
return FsCodec.decode(FsCodec.PARSED_REQUEST, encoded); return FsCodec.decode(FsCodec.PARSED_REQUEST, encoded);
@@ -239,15 +267,16 @@ final class FsCodecTest {
} }
private static CertificateProfile profile() { 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))); 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); 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(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(new ExtendedKeyUsageId("1.3.6.1.5.5.7.3.1")), true, false, true,
java.util.Set.of("RSA"), Duration.ofDays(365)); java.util.Set.of("RSA"));
return new CertificateProfile("profile-a", new FormatId("x509"), "Test leaf profile", leaf); return new CertificateProfile("profile-a", new FormatId("x509"), "Test leaf profile",
Duration.ofDays(365), subject, leaf);
} }
private static Credential credential(CredentialProfileBinding binding) { private static Credential credential(CredentialProfileBinding binding) {
@@ -261,12 +290,15 @@ final class FsCodecTest {
private static ImportedCertificateProfileVersion profileVersion() { private static ImportedCertificateProfileVersion profileVersion() {
try { try {
CertificateProfile profile = profile(); CertificateProfile profile = profile();
CertificateProfileDefinition definition = new CertificateProfileDefinition(profile.profileId(), 1, CertificateProfileDefinition definition = new CertificateProfileDefinition(
profile.formatId(), profile.displayName(), profile.leafPolicy()); CertificateProfileKind.END_ENTITY, profile.profileId(), 1, profile.formatId(),
profile.displayName(), profile.maximumValidity(), profile.subjectPolicy(),
profile.leafPolicy());
byte[] canonical = CertificateProfileDocumentCodec.writeCanonical(definition); byte[] canonical = CertificateProfileDocumentCodec.writeCanonical(definition);
CertificateProfileRef reference = new CertificateProfileRef(profile.profileId(), 1, CertificateProfileRef reference = new CertificateProfileRef(profile.profileId(), 1,
MessageDigest.getInstance("SHA-256").digest(canonical)); 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) { } catch (java.security.NoSuchAlgorithmException impossible) {
throw new IllegalStateException(impossible); throw new IllegalStateException(impossible);
} }

View File

@@ -212,7 +212,7 @@ public final class H7ProfileDocuments {
boolean critical, List<String> sanRules, boolean noKeyEncipherment, List<String> eku) { boolean critical, List<String> sanRules, boolean noKeyEncipherment, List<String> eku) {
String keyUsage = noKeyEncipherment String keyUsage = noKeyEncipherment
? "[\"DIGITAL_SIGNATURE\"]" : "[\"DIGITAL_SIGNATURE\",\"KEY_ENCIPHERMENT\"]"; ? "[\"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 + "\",\"profileVersion\":1,\"formatId\":\"" + formatId
+ "\",\"displayName\":\"H7 Test Profile\"," + "\",\"displayName\":\"H7 Test Profile\","
+ "\"maxValidity\":\"PT8760H\",\"subject\":{\"allowEmpty\":" + allowEmpty + "\"maxValidity\":\"PT8760H\",\"subject\":{\"allowEmpty\":" + allowEmpty

View File

@@ -54,6 +54,7 @@ import zeroecho.pki.api.RevocationService;
import zeroecho.pki.api.ProfileService; import zeroecho.pki.api.ProfileService;
import zeroecho.pki.api.StatusObjectService; import zeroecho.pki.api.StatusObjectService;
import zeroecho.pki.api.credential.EffectiveCredentialStatusResolver; import zeroecho.pki.api.credential.EffectiveCredentialStatusResolver;
import zeroecho.pki.api.profile.BuiltInCertificateProfileCatalog;
import zeroecho.pki.impl.core.DefaultCaService; import zeroecho.pki.impl.core.DefaultCaService;
import zeroecho.pki.impl.core.DefaultCertificationRequestService; import zeroecho.pki.impl.core.DefaultCertificationRequestService;
import zeroecho.pki.impl.core.DefaultIssuanceService; import zeroecho.pki.impl.core.DefaultIssuanceService;
@@ -104,6 +105,7 @@ public final class PkiTestRuntime implements AutoCloseable {
private final Map<String, PublicKey> publicKeysByKeyRef; private final Map<String, PublicKey> publicKeysByKeyRef;
private Runnable publicKeyResolveHook; private Runnable publicKeyResolveHook;
private boolean caProfilesProvisioned;
private PkiTestRuntime(FilesystemPkiStore store, PkiSigningBus signingBus, SignatureWorkflow signatureWorkflow, private PkiTestRuntime(FilesystemPkiStore store, PkiSigningBus signingBus, SignatureWorkflow signatureWorkflow,
CredentialFramework framework, CredentialIssuerBackend issuerBackend, CredentialFramework framework, CredentialIssuerBackend issuerBackend,
@@ -129,7 +131,7 @@ public final class PkiTestRuntime implements AutoCloseable {
this.statusObjectService = new DefaultStatusObjectService(store, framework, auditSink, statusResolver); this.statusObjectService = new DefaultStatusObjectService(store, framework, auditSink, statusResolver);
this.caService = new DefaultCaService(store, framework, issuerBackend, this::resolvePublicKeyInfo, signingBus, 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() { public CaService caService() {
provisionCaProfiles();
return caService; return caService;
} }
public CaService caService(CredentialFramework credentialFramework) { public CaService caService(CredentialFramework credentialFramework) {
provisionCaProfiles();
return new DefaultCaService(store, Objects.requireNonNull(credentialFramework, "credentialFramework"), return new DefaultCaService(store, Objects.requireNonNull(credentialFramework, "credentialFramework"),
issuerBackend, this::resolvePublicKeyInfo, signingBus, auditSink, statusResolver, "SHA256withRSA", issuerBackend, this::resolvePublicKeyInfo, signingBus, auditSink, statusResolver, profileService,
Duration.ofSeconds(2)); Clock.systemUTC(), "SHA256withRSA", Duration.ofSeconds(2));
} }
public CaService caService(CredentialIssuerBackend backend) { public CaService caService(CredentialIssuerBackend backend) {
provisionCaProfiles();
return new DefaultCaService(store, framework, Objects.requireNonNull(backend, "backend"), return new DefaultCaService(store, framework, Objects.requireNonNull(backend, "backend"),
this::resolvePublicKeyInfo, signingBus, auditSink, statusResolver, "SHA256withRSA", this::resolvePublicKeyInfo, signingBus, auditSink, statusResolver, profileService, Clock.systemUTC(),
Duration.ofSeconds(2)); "SHA256withRSA", Duration.ofSeconds(2));
} }
public CaService caService(CredentialIssuerBackend backend, EffectiveCredentialStatusResolver resolver) { public CaService caService(CredentialIssuerBackend backend, EffectiveCredentialStatusResolver resolver) {
provisionCaProfiles();
return new DefaultCaService(store, framework, Objects.requireNonNull(backend, "backend"), return new DefaultCaService(store, framework, Objects.requireNonNull(backend, "backend"),
this::resolvePublicKeyInfo, signingBus, auditSink, Objects.requireNonNull(resolver, "resolver"), 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)); "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() { public CertificationRequestService certificationRequestService() {
return certificationRequestService; return certificationRequestService;
} }