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.
*
* <p>Import never activates a profile. End-entity issuance must resolve only an
* explicitly activated persisted version through {@link #requireActiveProfile(String)}.</p>
* <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 {
/** Imports a bounded strict JSON profile document. */

View File

@@ -4,16 +4,18 @@
******************************************************************************/
package zeroecho.pki.api.credential;
import java.util.Objects;
import zeroecho.pki.api.profile.CertificateProfileRef;
/**
* Explicit CA policy identity for a root or intermediate credential.
* Exact versioned CA profile identity for a root or intermediate credential.
*
* @param profileId existing CA policy identifier
* @param reference imported CA profile version used for issuance
*/
public record CaProfileBinding(String profileId) implements CredentialProfileBinding {
public record CaProfileBinding(CertificateProfileRef reference) implements CredentialProfileBinding {
/** Creates a CA profile binding. */
public CaProfileBinding {
if (profileId == null || profileId.isBlank()) {
throw new IllegalArgumentException("profileId must not be null/blank");
}
Objects.requireNonNull(reference, "reference");
}
}

View File

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

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;
import java.time.Duration;
import zeroecho.pki.api.FormatId;
/**
@@ -51,10 +53,12 @@ import zeroecho.pki.api.FormatId;
* @param profileId stable profile identifier
* @param formatId framework/format supported by the profile
* @param displayName human-readable name
* @param leafPolicy complete end-entity identity and extension policy
* @param maximumValidity positive maximum validity
* @param subjectPolicy complete end-entity subject policy
* @param leafPolicy complete end-entity extension policy
*/
public record CertificateProfile(String profileId, FormatId formatId, String displayName,
LeafCertificatePolicy leafPolicy) {
Duration maximumValidity, SubjectPolicy subjectPolicy, LeafCertificatePolicy leafPolicy) {
/**
* Creates a certificate profile.
@@ -72,8 +76,9 @@ public record CertificateProfile(String profileId, FormatId formatId, String dis
if (displayName == null || displayName.isBlank()) {
throw new IllegalArgumentException("displayName must not be null/blank");
}
if (leafPolicy == null) {
throw new IllegalArgumentException("leafPolicy must not be null");
if (maximumValidity == null || maximumValidity.isZero() || maximumValidity.isNegative()
|| subjectPolicy == null || leafPolicy == null) {
throw new IllegalArgumentException("profile policies and maximum validity must be valid");
}
}
@@ -87,7 +92,10 @@ public record CertificateProfile(String profileId, FormatId formatId, String dis
if (definition == null) {
throw new IllegalArgumentException("definition must not be null");
}
if (definition.certificateType() != CertificateProfileKind.END_ENTITY) {
throw new IllegalArgumentException("Only end-entity definitions have a runtime issuance projection");
}
return new CertificateProfile(definition.profileId(), definition.formatId(), definition.displayName(),
definition.leafPolicy());
definition.maximumValidity(), definition.subjectPolicy(), definition.leafPolicy());
}
}

View File

@@ -4,28 +4,35 @@
******************************************************************************/
package zeroecho.pki.api.profile;
import java.time.Duration;
import zeroecho.pki.api.FormatId;
/**
* Immutable, versioned certificate-profile configuration.
*
* <p>
* This definition deliberately excludes runtime activation state. Its
* {@link LeafCertificatePolicy} is the authoritative typed policy used by the
* issuance path.
* Schema version 2 replaces the pre-release version 1 shape. The certificate
* kind and its closed policy variant are immutable across the definition.
* Runtime activation state is deliberately excluded.
* </p>
*
* @param profileId stable profile identifier
* @param profileVersion positive configuration version
* @param formatId framework/format identifier
* @param displayName human-readable profile name
* @param leafPolicy complete leaf certificate policy
* @param certificateType kind of certificate governed by the profile
* @param profileId stable profile identifier
* @param profileVersion positive configuration version
* @param formatId framework/format identifier
* @param displayName human-readable profile name
* @param maximumValidity positive maximum validity
* @param subjectPolicy complete subject policy
* @param certificatePolicy closed certificate-specific policy variant
*/
public record CertificateProfileDefinition(String profileId, long profileVersion, FormatId formatId,
String displayName, LeafCertificatePolicy leafPolicy) {
public record CertificateProfileDefinition(CertificateProfileKind certificateType,
String profileId, long profileVersion, FormatId formatId, String displayName,
Duration maximumValidity, SubjectPolicy subjectPolicy,
CertificatePolicy certificatePolicy) {
/** Current certificate-profile document schema version. */
public static final int SCHEMA_VERSION = 1;
public static final int SCHEMA_VERSION = 2;
/**
* Creates a certificate-profile definition.
@@ -34,6 +41,9 @@ public record CertificateProfileDefinition(String profileId, long profileVersion
* profile version is not positive
*/
public CertificateProfileDefinition {
if (certificateType == null) {
throw new IllegalArgumentException("certificateType must not be null");
}
if (profileId == null || profileId.isBlank()) {
throw new IllegalArgumentException("profileId must not be null/blank");
}
@@ -46,8 +56,46 @@ public record CertificateProfileDefinition(String profileId, long profileVersion
if (displayName == null || displayName.isBlank()) {
throw new IllegalArgumentException("displayName must not be null/blank");
}
if (leafPolicy == null) {
throw new IllegalArgumentException("leafPolicy must not be null");
if (maximumValidity == null || maximumValidity.isZero() || maximumValidity.isNegative()) {
throw new IllegalArgumentException("maximumValidity must be positive");
}
if (subjectPolicy == null || certificatePolicy == null) {
throw new IllegalArgumentException("profile policies must not be null");
}
boolean leaf = certificatePolicy instanceof LeafCertificatePolicy;
if (certificateType == CertificateProfileKind.END_ENTITY != leaf) {
throw new IllegalArgumentException("certificate type and policy variant do not match");
}
if (certificateType != CertificateProfileKind.END_ENTITY
&& (subjectPolicy.allowEmpty() || subjectPolicy.rules().isEmpty()
|| subjectPolicy.rules().stream().noneMatch(rule -> rule.minimumOccurrences() > 0))) {
throw new IllegalArgumentException("CA profiles require a nonempty subject policy");
}
}
/**
* Returns the end-entity policy.
*
* @return leaf policy
* @throws IllegalStateException if this is a CA profile
*/
public LeafCertificatePolicy leafPolicy() {
if (certificatePolicy instanceof LeafCertificatePolicy leaf) {
return leaf;
}
throw new IllegalStateException("Profile is not an end-entity profile");
}
/**
* Returns the CA policy.
*
* @return CA policy
* @throws IllegalStateException if this is an end-entity profile
*/
public CaCertificatePolicy caPolicy() {
if (certificatePolicy instanceof CaCertificatePolicy ca) {
return ca;
}
throw new IllegalStateException("Profile is not a CA profile");
}
}

View File

@@ -34,7 +34,7 @@ import zeroecho.pki.api.FormatId;
import zeroecho.pki.api.PkiException;
/**
* Strict JSON parser and canonical writer for version 1 certificate-profile
* Strict JSON parser and canonical writer for version 2 certificate-profile
* documents.
*
* <p>
@@ -42,9 +42,10 @@ import zeroecho.pki.api.PkiException;
* tokens and creates the existing typed policy model directly.
* </p>
*/
// The closed streaming grammar intentionally keeps all bounded field handlers in one codec.
@SuppressWarnings({ "PMD.AvoidDuplicateLiterals", "PMD.AvoidInstantiatingObjectsInLoops",
"PMD.AvoidUncheckedExceptionsInSignatures", "PMD.CyclomaticComplexity",
"PMD.PreserveStackTrace" })
"PMD.PreserveStackTrace", "PMD.TooManyMethods" })
public final class CertificateProfileDocumentCodec {
/** Maximum accepted encoded document size. */
@@ -157,7 +158,7 @@ public final class CertificateProfileDocumentCodec {
* @return newly allocated canonical JSON bytes without a BOM or trailing
* whitespace
* @throws PkiException if the definition cannot be represented by schema
* version 1
* version 2
*/
public static byte[] writeCanonical(CertificateProfileDefinition definition) {
validateDefinition(definition);
@@ -209,92 +210,155 @@ public final class CertificateProfileDocumentCodec {
private static CertificateProfileDefinition parseDocument(JsonParser parser) throws JacksonException {
requireToken(parser.nextToken(), JsonToken.START_OBJECT, "$");
long seen = 0;
int schemaVersion = 0;
String profileId = null;
long profileVersion = 0;
String formatId = null;
String displayName = null;
Duration maximumValidity = null;
SubjectSection subject = null;
SanSection san = null;
LeafSection leaf = null;
DocumentFields document = new DocumentFields();
while (parser.nextToken() != JsonToken.END_OBJECT) {
requireToken(parser.currentToken(), JsonToken.PROPERTY_NAME, "$");
String field = parser.currentName();
requireValue(parser, "$." + field);
switch (field) {
case "schemaVersion" -> {
seen = mark(seen, 0, "$.schemaVersion");
schemaVersion = readInt(parser, "$.schemaVersion");
}
case "profileId" -> {
seen = mark(seen, 1, "$.profileId");
profileId = readBoundedString(parser, "$.profileId", MAXIMUM_PROFILE_ID_UTF8_BYTES);
}
case "profileVersion" -> {
seen = mark(seen, 2, "$.profileVersion");
profileVersion = readLong(parser, "$.profileVersion");
}
case "formatId" -> {
seen = mark(seen, 3, "$.formatId");
formatId = readBoundedString(parser, "$.formatId", MAXIMUM_FORMAT_ID_UTF8_BYTES);
}
case "displayName" -> {
seen = mark(seen, 4, "$.displayName");
displayName = readBoundedString(parser, "$.displayName", MAXIMUM_DISPLAY_NAME_UTF8_BYTES);
}
case "maxValidity" -> {
seen = mark(seen, 5, "$.maxValidity");
maximumValidity = readDuration(parser, "$.maxValidity");
}
case "subject" -> {
seen = mark(seen, 6, "$.subject");
subject = readSubject(parser, "$.subject");
}
case "subjectAlternativeNames" -> {
seen = mark(seen, 7, "$.subjectAlternativeNames");
san = readSan(parser, "$.subjectAlternativeNames");
}
case "leafCertificate" -> {
seen = mark(seen, 8, "$.leafCertificate");
leaf = readLeaf(parser, "$.leafCertificate");
}
default -> throw failure("UNKNOWN_FIELD", "$.?");
}
readDocumentField(parser, field, document);
}
requireAll(seen, 9, "$");
if (schemaVersion != CertificateProfileDefinition.SCHEMA_VERSION) {
requireAll(document.seen, 8, "$");
if (document.schemaVersion != CertificateProfileDefinition.SCHEMA_VERSION) {
throw failure("SCHEMA_VERSION_UNSUPPORTED", "$.schemaVersion");
}
if (profileVersion <= 0) {
if (document.profileVersion <= 0) {
throw failure("PROFILE_VERSION_INVALID", "$.profileVersion");
}
validateProfileString(profileId, MAXIMUM_PROFILE_ID_UTF8_BYTES, "$.profileId",
validateProfileString(document.profileId, MAXIMUM_PROFILE_ID_UTF8_BYTES, "$.profileId",
"TOKEN_INVALID");
validateProfileString(formatId, MAXIMUM_FORMAT_ID_UTF8_BYTES, "$.formatId",
validateProfileString(document.formatId, MAXIMUM_FORMAT_ID_UTF8_BYTES, "$.formatId",
"TOKEN_INVALID");
validateProfileString(displayName, MAXIMUM_DISPLAY_NAME_UTF8_BYTES, "$.displayName",
validateProfileString(document.displayName, MAXIMUM_DISPLAY_NAME_UTF8_BYTES, "$.displayName",
"TOKEN_INVALID");
return constructDefinition(profileId, profileVersion, formatId, displayName, maximumValidity,
subject, san, leaf);
requirePolicyFields(document.certificateType, document.seen);
return constructDefinition(document);
}
private static CertificateProfileDefinition constructDefinition(String profileId, long profileVersion,
String formatId, String displayName, Duration maximumValidity, SubjectSection subject,
SanSection san, LeafSection leaf) {
private static void readDocumentField(JsonParser parser, String field, DocumentFields document)
throws JacksonException {
switch (field) {
case "schemaVersion" -> {
document.seen = mark(document.seen, 0, "$.schemaVersion");
document.schemaVersion = readInt(parser, "$.schemaVersion");
}
case "certificateType" -> {
document.seen = mark(document.seen, 1, "$.certificateType");
document.certificateType = readCertificateType(parser, "$.certificateType");
}
case "profileId" -> {
document.seen = mark(document.seen, 2, "$.profileId");
document.profileId = readBoundedString(parser, "$.profileId", MAXIMUM_PROFILE_ID_UTF8_BYTES);
}
case "profileVersion" -> {
document.seen = mark(document.seen, 3, "$.profileVersion");
document.profileVersion = readLong(parser, "$.profileVersion");
}
case "formatId" -> {
document.seen = mark(document.seen, 4, "$.formatId");
document.formatId = readBoundedString(parser, "$.formatId", MAXIMUM_FORMAT_ID_UTF8_BYTES);
}
case "displayName" -> {
document.seen = mark(document.seen, 5, "$.displayName");
document.displayName =
readBoundedString(parser, "$.displayName", MAXIMUM_DISPLAY_NAME_UTF8_BYTES);
}
case "maxValidity" -> {
document.seen = mark(document.seen, 6, "$.maxValidity");
document.maximumValidity = readDuration(parser, "$.maxValidity");
}
case "subject" -> {
document.seen = mark(document.seen, 7, "$.subject");
document.subject = readSubject(parser, "$.subject");
}
case "subjectAlternativeNames" -> readDocumentSan(parser, document);
case "leafCertificate" -> readDocumentLeaf(parser, document);
case "caCertificate" -> readDocumentCa(parser, document);
default -> throw failure("UNKNOWN_FIELD", "$.?");
}
}
private static void readDocumentSan(JsonParser parser, DocumentFields document)
throws JacksonException {
if (document.certificateType != null
&& document.certificateType != CertificateProfileKind.END_ENTITY) {
throw failure("FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE", "$.subjectAlternativeNames");
}
document.seen = mark(document.seen, 8, "$.subjectAlternativeNames");
document.san = readSan(parser, "$.subjectAlternativeNames");
}
private static void readDocumentLeaf(JsonParser parser, DocumentFields document)
throws JacksonException {
if (document.certificateType != null
&& document.certificateType != CertificateProfileKind.END_ENTITY) {
throw failure("FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE", "$.leafCertificate");
}
document.seen = mark(document.seen, 9, "$.leafCertificate");
document.leaf = readLeaf(parser, "$.leafCertificate");
}
private static void readDocumentCa(JsonParser parser, DocumentFields document)
throws JacksonException {
if (document.certificateType == CertificateProfileKind.END_ENTITY) {
throw failure("FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE", "$.caCertificate");
}
document.seen = mark(document.seen, 10, "$.caCertificate");
document.ca = readCa(parser, "$.caCertificate");
}
private static void requirePolicyFields(CertificateProfileKind certificateType, long seen) {
boolean hasSan = isSeen(seen, 8);
boolean hasLeaf = isSeen(seen, 9);
boolean hasCa = isSeen(seen, 10);
if (certificateType == CertificateProfileKind.END_ENTITY) {
if (hasCa) {
throw failure("FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE", "$.caCertificate");
}
if (!hasSan || !hasLeaf) {
throw failure("MISSING_FIELD", "$");
}
} else {
if (hasSan || hasLeaf) {
throw failure("FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE", "$");
}
if (!hasCa) {
throw failure("MISSING_FIELD", "$.caCertificate");
}
}
}
/*
* Typed policy constructors are the authoritative semantic validators, so
* their validation exceptions are deliberately normalized at this boundary.
*/
@SuppressWarnings("PMD.ExceptionAsFlowControl")
private static CertificateProfileDefinition constructDefinition(DocumentFields document) {
try {
SubjectPolicy subjectPolicy = new SubjectPolicy(subject.rules());
SubjectAlternativeNamePolicy sanPolicy = new SubjectAlternativeNamePolicy(subject.allowEmpty(),
san.minimumTotal(), san.maximumTotal(), san.rules(), san.wildcardAllowed(),
san.allowedSchemes(), san.criticalWhenSubjectNonEmpty(), san.serviceIdentityRequired(),
san.emailIdentityRequired());
LeafCertificatePolicy leafPolicy = new LeafCertificatePolicy(subjectPolicy, sanPolicy,
leaf.keyUsage(), leaf.extendedKeyUsage(), leaf.keyUsageCritical(),
leaf.extendedKeyUsageCritical(), leaf.basicConstraintsCritical(),
leaf.allowedKeyAlgorithms(), maximumValidity);
return new CertificateProfileDefinition(profileId, profileVersion, new FormatId(formatId),
displayName, leafPolicy);
SubjectPolicy subjectPolicy = new SubjectPolicy(document.subject.allowEmpty(),
document.subject.rules());
CertificatePolicy policy;
if (document.certificateType == CertificateProfileKind.END_ENTITY) {
SubjectAlternativeNamePolicy sanPolicy = new SubjectAlternativeNamePolicy(
document.san.minimumTotal(), document.san.maximumTotal(), document.san.rules(),
document.san.wildcardAllowed(), document.san.allowedSchemes(),
document.san.criticalWhenSubjectNonEmpty(),
document.san.serviceIdentityRequired(), document.san.emailIdentityRequired());
if (document.subject.allowEmpty() && document.san.minimumTotal() < 1) {
throw new IllegalArgumentException("An empty subject requires at least one SAN");
}
policy = new LeafCertificatePolicy(sanPolicy, document.leaf.keyUsage(),
document.leaf.extendedKeyUsage(), document.leaf.keyUsageCritical(),
document.leaf.extendedKeyUsageCritical(),
document.leaf.basicConstraintsCritical(),
document.leaf.allowedKeyAlgorithms());
} else {
policy = new CaCertificatePolicy(document.ca.basicConstraintsCritical(),
document.ca.pathLengthConstraint(), document.ca.keyUsageCritical(),
document.ca.keyUsages(), document.ca.allowedKeyAlgorithms());
}
return new CertificateProfileDefinition(document.certificateType, document.profileId,
document.profileVersion, new FormatId(document.formatId), document.displayName,
document.maximumValidity, subjectPolicy, policy);
} catch (IllegalArgumentException | ArithmeticException ex) {
throw failure("SEMANTIC_INVALID", "$");
}
@@ -612,6 +676,66 @@ public final class CertificateProfileDocumentCodec {
return Set.copyOf(values);
}
private static CaSection readCa(JsonParser parser, String path) throws JacksonException {
requireToken(parser.currentToken(), JsonToken.START_OBJECT, path);
long seen = 0;
boolean basicCritical = false;
int pathLength = -1;
boolean keyCritical = false;
Set<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)
throws JacksonException {
requireToken(parser.currentToken(), JsonToken.START_ARRAY, path);
@@ -650,27 +774,31 @@ public final class CertificateProfileDocumentCodec {
private static void writeDocument(JsonGenerator generator, CertificateProfileDefinition definition)
throws JacksonException {
LeafCertificatePolicy policy = definition.leafPolicy();
generator.writeStartObject();
generator.writeNumberProperty("schemaVersion", CertificateProfileDefinition.SCHEMA_VERSION);
generator.writeStringProperty("certificateType", definition.certificateType().name());
generator.writeStringProperty("profileId", definition.profileId());
generator.writeNumberProperty("profileVersion", definition.profileVersion());
generator.writeStringProperty("formatId", definition.formatId().value());
generator.writeStringProperty("displayName", definition.displayName());
generator.writeStringProperty("maxValidity", policy.maximumValidity().toString());
writeSubject(generator, policy);
writeSan(generator, policy.subjectAlternativeNamePolicy());
writeLeaf(generator, policy);
generator.writeStringProperty("maxValidity", definition.maximumValidity().toString());
writeSubject(generator, definition.subjectPolicy());
if (definition.certificateType() == CertificateProfileKind.END_ENTITY) {
LeafCertificatePolicy leaf = definition.leafPolicy();
writeSan(generator, leaf.subjectAlternativeNamePolicy());
writeLeaf(generator, leaf);
} else {
writeCa(generator, definition.caPolicy());
}
generator.writeEndObject();
}
private static void writeSubject(JsonGenerator generator, LeafCertificatePolicy policy)
private static void writeSubject(JsonGenerator generator, SubjectPolicy policy)
throws JacksonException {
generator.writeObjectPropertyStart("subject");
generator.writeBooleanProperty("allowEmpty",
policy.subjectAlternativeNamePolicy().allowEmptySubject());
generator.writeBooleanProperty("allowEmpty", policy.allowEmpty());
generator.writeArrayPropertyStart("rules");
for (SubjectRdnRule rule : policy.subjectPolicy().rules()) {
for (SubjectRdnRule rule : policy.rules()) {
generator.writeStartObject();
generator.writeStringProperty("oid", rule.type().oid());
generator.writeStringProperty("source",
@@ -740,6 +868,20 @@ public final class CertificateProfileDocumentCodec {
generator.writeEndObject();
}
private static void writeCa(JsonGenerator generator, CaCertificatePolicy policy)
throws JacksonException {
generator.writeObjectPropertyStart("caCertificate");
generator.writeBooleanProperty("basicConstraintsCritical",
policy.basicConstraintsCritical());
generator.writeNumberProperty("pathLengthConstraint", policy.pathLengthConstraint());
generator.writeBooleanProperty("keyUsageCritical", policy.keyUsageCritical());
writeSortedStrings(generator, "keyUsages",
policy.keyUsages().stream().map(Enum::name).toList());
writeSortedStrings(generator, "allowedSubjectKeyAlgorithms",
policy.allowedSubjectKeyAlgorithmIds());
generator.writeEndObject();
}
private static void writeSortedStrings(JsonGenerator generator, String field,
java.util.Collection<String> values) throws JacksonException {
generator.writeArrayPropertyStart(field);
@@ -759,11 +901,25 @@ public final class CertificateProfileDocumentCodec {
"$.formatId", "CANONICALIZATION_FAILED");
validateProfileString(definition.displayName(), MAXIMUM_DISPLAY_NAME_UTF8_BYTES,
"$.displayName", "CANONICALIZATION_FAILED");
LeafCertificatePolicy leaf = definition.leafPolicy();
validateWritableString(leaf.maximumValidity().toString(), MAXIMUM_STRING_UTF8_BYTES,
validateWritableString(definition.maximumValidity().toString(), MAXIMUM_STRING_UTF8_BYTES,
"$.maxValidity");
if (leaf.subjectPolicy().rules().size() > MAXIMUM_ARRAY_ELEMENTS
|| leaf.subjectAlternativeNamePolicy().rules().size() > MAXIMUM_ARRAY_ELEMENTS
if (definition.subjectPolicy().rules().size() > MAXIMUM_ARRAY_ELEMENTS) {
throw failure("CANONICALIZATION_FAILED", "$");
}
for (SubjectRdnRule rule : definition.subjectPolicy().rules()) {
rule.fixedValue().ifPresent(value -> validateWritableString(value,
MAXIMUM_STRING_UTF8_BYTES, "$.subject.rules.fixedValue"));
}
if (definition.certificateType() != CertificateProfileKind.END_ENTITY) {
CaCertificatePolicy ca = definition.caPolicy();
if (ca.keyUsages().size() > MAXIMUM_ARRAY_ELEMENTS
|| ca.allowedSubjectKeyAlgorithmIds().size() > MAXIMUM_ARRAY_ELEMENTS) {
throw failure("CANONICALIZATION_FAILED", "$");
}
return;
}
LeafCertificatePolicy leaf = definition.leafPolicy();
if (leaf.subjectAlternativeNamePolicy().rules().size() > MAXIMUM_ARRAY_ELEMENTS
|| leaf.subjectAlternativeNamePolicy().allowedUriSchemes().size()
> MAXIMUM_ARRAY_ELEMENTS
|| leaf.keyUsages().size() > MAXIMUM_ARRAY_ELEMENTS
@@ -771,10 +927,6 @@ public final class CertificateProfileDocumentCodec {
|| leaf.allowedSubjectKeyAlgorithmIds().size() > MAXIMUM_ARRAY_ELEMENTS) {
throw failure("CANONICALIZATION_FAILED", "$");
}
for (SubjectRdnRule rule : leaf.subjectPolicy().rules()) {
rule.fixedValue().ifPresent(value -> validateWritableString(value,
MAXIMUM_STRING_UTF8_BYTES, "$.subject.rules.fixedValue"));
}
for (String scheme : leaf.subjectAlternativeNamePolicy().allowedUriSchemes()) {
validateWritableString(scheme, MAXIMUM_URI_SCHEME_ASCII_BYTES,
"$.subjectAlternativeNames.rules.allowedSchemes");
@@ -869,6 +1021,16 @@ public final class CertificateProfileDocumentCodec {
}
}
private static CertificateProfileKind readCertificateType(JsonParser parser, String path)
throws JacksonException {
String token = readString(parser, path);
try {
return CertificateProfileKind.valueOf(token);
} catch (IllegalArgumentException ex) {
throw failure("CERTIFICATE_TYPE_UNSUPPORTED", path);
}
}
private static boolean parseSubjectSource(String source, String path) {
if (REQUESTER_SOURCE.equals(source)) {
return false;
@@ -972,6 +1134,22 @@ public final class CertificateProfileDocumentCodec {
return new PkiException(PREFIX + code + " path=" + path);
}
/** Mutable state confined to one streaming top-level document parse. */
private static final class DocumentFields {
private long seen;
private int schemaVersion;
private CertificateProfileKind certificateType;
private String profileId;
private long profileVersion;
private String formatId;
private String displayName;
private Duration maximumValidity;
private SubjectSection subject;
private SanSection san;
private LeafSection leaf;
private CaSection ca;
}
private record SubjectSection(boolean allowEmpty, List<SubjectRdnRule> rules) {
}
@@ -993,4 +1171,9 @@ public final class CertificateProfileDocumentCodec {
Set<LeafKeyUsage> keyUsage, boolean extendedKeyUsageCritical,
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;
import java.time.Duration;
import java.util.Set;
/**
* Complete issuer-controlled leaf certificate extension and identity policy.
*
* @param subjectPolicy subject policy
* @param subjectAlternativeNamePolicy SAN policy
* @param keyUsages exact key-usage set
* @param extendedKeyUsages exact extended-key-usage set
@@ -18,12 +16,12 @@ import java.util.Set;
* @param extendedKeyUsageCritical extended-key-usage criticality
* @param basicConstraintsCritical BasicConstraints criticality
* @param allowedSubjectKeyAlgorithmIds exact canonical ZeroEcho key algorithm identifiers
* @param maximumValidity positive maximum validity
*/
public record LeafCertificatePolicy(SubjectPolicy subjectPolicy,
SubjectAlternativeNamePolicy subjectAlternativeNamePolicy, Set<LeafKeyUsage> keyUsages,
public record LeafCertificatePolicy(SubjectAlternativeNamePolicy subjectAlternativeNamePolicy,
Set<LeafKeyUsage> keyUsages,
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 =
Set.of("RSA", "ECDSA", "Ed25519", "Ed448");
@@ -32,8 +30,8 @@ public record LeafCertificatePolicy(SubjectPolicy subjectPolicy,
* Validates and constructs the policy.
*/
public LeafCertificatePolicy {
if (subjectPolicy == null || subjectAlternativeNamePolicy == null || keyUsages == null
|| extendedKeyUsages == null || allowedSubjectKeyAlgorithmIds == null || maximumValidity == null) {
if (subjectAlternativeNamePolicy == null || keyUsages == null
|| extendedKeyUsages == null || allowedSubjectKeyAlgorithmIds == null) {
throw new IllegalArgumentException("Leaf certificate policy values must not be null");
}
keyUsages = Set.copyOf(keyUsages);
@@ -47,8 +45,5 @@ public record LeafCertificatePolicy(SubjectPolicy subjectPolicy,
&& !keyUsages.contains(LeafKeyUsage.KEY_AGREEMENT)) {
throw new IllegalArgumentException("encipherOnly and decipherOnly require keyAgreement");
}
if (maximumValidity.isZero() || maximumValidity.isNegative()) {
throw new IllegalArgumentException("Maximum validity must be positive");
}
}
}

View File

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

View File

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

View File

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

View File

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

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.PkiException;
import zeroecho.pki.api.PkiId;
import zeroecho.pki.api.SubjectRef;
import zeroecho.pki.api.Validity;
import zeroecho.pki.api.attr.AttributeSet;
import zeroecho.pki.api.audit.AccessContext;
import zeroecho.pki.api.audit.AuditEvent;
import zeroecho.pki.api.audit.Principal;
@@ -127,16 +124,7 @@ final class CaProofGate {
/* default */ ManagedKeyProof proveManagedKey(KeyRef keyRef, FormatId formatId, String auditAction,
Optional<PkiId> subjectCaId) {
EncodedObject resolved;
try {
resolved = publicKeyResolver.resolveSpkiDer(keyRef);
} catch (RuntimeException ex) { // NOPMD - managed-key resolution boundary fails closed
throw rejection(auditAction, formatId, subjectCaId, "MANAGED_KEY_UNAVAILABLE");
}
if (resolved == null || resolved.encoding() != Encoding.DER) {
throw rejection(auditAction, formatId, subjectCaId, "MANAGED_KEY_INVALID");
}
EncodedObject resolved = resolveManagedKey(keyRef, formatId, auditAction, subjectCaId);
byte[] exactSpki = resolved.bytes().clone();
byte[] challenge = new byte[MANAGED_KEY_CHALLENGE_DOMAIN.length + CHALLENGE_NONCE_BYTES];
CHALLENGE_RANDOM.nextBytes(challenge);
@@ -160,12 +148,18 @@ final class CaProofGate {
}
}
/* default */ ManagedCaIssuance authorizeIntermediate(ManagedKeyProof proof,
ManagedCaIssuance.Operation operation, PkiId issuerCaId, PkiId subjectCaId, String profileId,
Optional<Validity> requestedValidity, AttributeSet attributes, SubjectRef subjectRef) {
Objects.requireNonNull(proof, "proof");
return new ManagedCaIssuance(proof, operation, issuerCaId, subjectCaId, profileId, requestedValidity,
attributes, subjectRef);
/* default */ EncodedObject resolveManagedKey(KeyRef keyRef, FormatId formatId, String auditAction,
Optional<PkiId> subjectCaId) {
EncodedObject resolved;
try {
resolved = publicKeyResolver.resolveSpkiDer(keyRef);
} catch (RuntimeException ex) { // NOPMD - managed-key resolution boundary fails closed
throw rejection(auditAction, formatId, subjectCaId, "MANAGED_KEY_UNAVAILABLE");
}
if (resolved == null || resolved.encoding() != Encoding.DER) {
throw rejection(auditAction, formatId, subjectCaId, "MANAGED_KEY_INVALID");
}
return new EncodedObject(Encoding.DER, resolved.bytes());
}
/* default */ PkiException rejection(String action, FormatId formatId, Optional<PkiId> objectId, String code) {

View File

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

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

View File

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

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

View File

@@ -466,6 +466,7 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
}
return existing;
}
requireConsistentProfileKind(version);
try {
FsOperations.writeNewAtomicStrict(target, FsCodec.encode(FsCodec.PROFILE_VERSION, version));
return version;
@@ -487,6 +488,16 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
}
}
private void requireConsistentProfileKind(ImportedCertificateProfileVersion candidate) {
for (ImportedCertificateProfileVersion existing :
listProfileVersions(candidate.reference().profileId())) {
if (existing.definition().certificateType()
!= candidate.definition().certificateType()) {
throw new ProfileLifecycleFailure(Code.PROFILE_KIND_CONFLICT);
}
}
}
@Override
public Optional<ImportedCertificateProfileVersion> getProfileVersion(final String profileId,
final long profileVersion) {

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -372,7 +372,7 @@ final class DefaultStatusObjectServiceCrlTest {
CaProfileBinding binding = assertInstanceOf(CaProfileBinding.class, template.profileBinding());
return new Credential(new PkiId("credential:" + suffix), formatId, template.issuerRef(),
template.subjectRef(), template.validity(), template.serialOrUniqueId(),
template.publicKeyId(), new CaProfileBinding(binding.profileId()), CredentialStatus.ISSUED, encoded,
template.publicKeyId(), new CaProfileBinding(binding.reference()), CredentialStatus.ISSUED, encoded,
template.attributes());
}
@@ -410,7 +410,7 @@ final class DefaultStatusObjectServiceCrlTest {
private static PkiId createRoot(PkiTestRuntime runtime, KeyRef keyRef, String commonName) {
return runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
new SubjectRef("CN=" + commonName), "default", Optional.of(keyRef), emptyAttributes()));
new SubjectRef("CN=" + commonName), "root-ca", Optional.of(keyRef), emptyAttributes()));
}
private static KeyPair generateRsa() throws Exception {

View File

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

View File

@@ -176,7 +176,7 @@ final class StoreBackedEffectiveCredentialStatusResolverTest {
Credential credential = new Credential(new PkiId("credential:audit"), new FormatId(sentinel),
new IssuerRef(new PkiId("ca:issuer")), new SubjectRef("CN=Audit"),
new Validity(NOW.minusSeconds(60), NOW.plusSeconds(60)), "audit", new PkiId("key:audit"),
new CaProfileBinding("default"), CredentialStatus.ISSUED,
new CaProfileBinding(new zeroecho.pki.api.profile.CertificateProfileRef("default", 1, new byte[32])), CredentialStatus.ISSUED,
new EncodedObject(Encoding.DER, new byte[] { 1 }),
new SimpleAttributeSet());
AtomicReference<AuditEvent> recorded = new AtomicReference<>();
@@ -239,7 +239,7 @@ final class StoreBackedEffectiveCredentialStatusResolverTest {
return new Credential(new PkiId("credential:" + suffix), new FormatId("x509"),
new IssuerRef(new PkiId("ca:issuer")), new SubjectRef("CN=" + suffix),
new Validity(notBefore, notAfter), suffix, new PkiId("key:" + suffix),
new CaProfileBinding("default"), status,
new CaProfileBinding(new zeroecho.pki.api.profile.CertificateProfileRef("default", 1, new byte[32])), status,
new EncodedObject(Encoding.DER, new byte[] { 1, 2, 3 }), new SimpleAttributeSet());
}

View File

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

View File

@@ -30,7 +30,9 @@ import org.junit.jupiter.api.io.TempDir;
import zeroecho.pki.api.PkiException;
import zeroecho.pki.api.profile.BuiltInCertificateProfileCatalog;
import zeroecho.pki.api.profile.BuiltInCertificateProfileTemplate;
import zeroecho.pki.api.profile.CertificateProfileDefinition;
import zeroecho.pki.api.profile.CertificateProfileRef;
import zeroecho.pki.api.profile.CertificateProfileKind;
import zeroecho.pki.api.profile.ImportedCertificateProfileVersion;
import zeroecho.pki.impl.audit.InMemoryAuditSink;
import zeroecho.pki.impl.core.DefaultProfileService;
@@ -100,7 +102,8 @@ final class FilesystemProfileLifecycleTest {
ImportedCertificateProfileVersion invalid = new ImportedCertificateProfileVersion(
new CertificateProfileRef(template.definition().profileId(),
template.definition().profileVersion(), template.canonicalSha256()),
1, template.definition(), noncanonical, CLOCK.instant());
CertificateProfileDefinition.SCHEMA_VERSION, template.definition(),
noncanonical, CLOCK.instant());
Path path = new FsPaths(noncanonicalRoot).profileVersion("server-tls", 1);
FsOperations.ensureDir(path.getParent());
Files.write(path, FsCodec.encode(FsCodec.PROFILE_VERSION, invalid));
@@ -115,7 +118,8 @@ final class FilesystemProfileLifecycleTest {
new CertificateProfileRef(template.definition().profileId(),
template.definition().profileVersion(),
new byte[CertificateProfileRef.HASH_BYTES]),
1, template.definition(), template.canonicalJson(), CLOCK.instant());
CertificateProfileDefinition.SCHEMA_VERSION, template.definition(),
template.canonicalJson(), CLOCK.instant());
Path path = new FsPaths(hashMismatchRoot).profileVersion("server-tls", 1);
FsOperations.ensureDir(path.getParent());
Files.write(path, FsCodec.encode(FsCodec.PROFILE_VERSION, invalid));
@@ -168,6 +172,51 @@ final class FilesystemProfileLifecycleTest {
}
}
@Test
void caKindsUseLifecycleAndOneLogicalIdCannotChangeKindAcrossVersions(
@TempDir Path directory) throws Exception {
Path root = directory.resolve("store");
CertificateProfileRef rootRef;
CertificateProfileRef intermediateRef;
try (FilesystemPkiStore store = store(root)) {
DefaultProfileService service = service(store);
rootRef = service.importBuiltIn(builtIn("root-ca"));
intermediateRef = service.importBuiltIn(builtIn("intermediate-ca"));
assertTrue(service.getActiveReference("root-ca").isEmpty());
assertTrue(service.getActiveReference("intermediate-ca").isEmpty());
assertEquals(rootRef, service.activateProfile("root-ca", 1));
assertEquals(intermediateRef, service.activateProfile("intermediate-ca", 1));
assertEquals(CertificateProfileKind.ROOT_CA,
service.requireActiveProfile("root-ca").definition().certificateType());
assertEquals(CertificateProfileKind.INTERMEDIATE_CA,
service.requireActiveProfile("intermediate-ca").definition().certificateType());
byte[] sameKindVersion = version(builtIn("root-ca"), 2, "Root CA v2");
CertificateProfileRef versionTwo = service.importProfile(sameKindVersion);
assertEquals(2, versionTwo.profileVersion());
byte[] conflictingKind = new String(builtIn("intermediate-ca").canonicalJson(),
StandardCharsets.UTF_8)
.replace("\"profileId\":\"intermediate-ca\"", "\"profileId\":\"root-ca\"")
.replace("\"profileVersion\":1", "\"profileVersion\":3")
.getBytes(StandardCharsets.UTF_8);
assertCode(() -> service.importProfile(conflictingKind), "PROFILE_KIND_CONFLICT");
assertTrue(service.getImportedVersion("root-ca", 3).isEmpty());
assertEquals(rootRef, service.getActiveReference("root-ca").orElseThrow());
}
try (FilesystemPkiStore reopened = store(root)) {
DefaultProfileService service = service(reopened);
assertEquals(rootRef, service.getActiveReference("root-ca").orElseThrow());
assertEquals(intermediateRef,
service.getActiveReference("intermediate-ca").orElseThrow());
assertEquals(CertificateProfileKind.ROOT_CA,
service.requireActiveProfile("root-ca").definition().certificateType());
assertEquals(CertificateProfileKind.INTERMEDIATE_CA,
service.requireActiveProfile("intermediate-ca").definition().certificateType());
}
}
@Test
void missingCorruptAndHashMismatchedPointersNeverFallback(@TempDir Path directory) throws Exception {
BuiltInCertificateProfileTemplate template = builtIn("vpn-client");

View File

@@ -499,7 +499,7 @@ final class FilesystemRevocationJournalTest {
return new Credential(new PkiId("credential:" + suffix), new FormatId("x509"),
new IssuerRef(new PkiId("ca:issuer")), new SubjectRef("CN=" + suffix),
new Validity(TIME.minusSeconds(60), TIME.plusSeconds(60)), suffix,
new PkiId("key:" + suffix), new CaProfileBinding("default"), CredentialStatus.ISSUED,
new PkiId("key:" + suffix), new CaProfileBinding(new zeroecho.pki.api.profile.CertificateProfileRef("default", 1, new byte[32])), CredentialStatus.ISSUED,
new EncodedObject(Encoding.DER, new byte[] { 1, 2, 3 }), new SimpleAttributeSet());
}

View File

@@ -71,6 +71,7 @@ import zeroecho.pki.api.credential.EndEntityProfileBinding;
import zeroecho.pki.api.profile.CertificateProfile;
import zeroecho.pki.api.profile.CertificateProfileDefinition;
import zeroecho.pki.api.profile.CertificateProfileDocumentCodec;
import zeroecho.pki.api.profile.CertificateProfileKind;
import zeroecho.pki.api.profile.CertificateProfileRef;
import zeroecho.pki.api.profile.ExtendedKeyUsageId;
import zeroecho.pki.api.profile.ImportedCertificateProfileVersion;
@@ -215,7 +216,7 @@ final class FsCodecTest {
CertificateProfileRef reference = new CertificateProfileRef("profile-a", 1,
new byte[CertificateProfileRef.HASH_BYTES]);
List<CredentialProfileBinding> bindings = List.of(new EndEntityProfileBinding(reference),
new CaProfileBinding(reference.profileId()));
new CaProfileBinding(reference));
for (CredentialProfileBinding binding : bindings) {
Credential original = credential(binding);
@@ -225,6 +226,33 @@ final class FsCodecTest {
}
}
@Test
void obsoleteBareStringCaBindingFailsStrictDecode() {
byte[] encoded = FsCodec.encode(FsCodec.CREDENTIAL,
credential(new CaProfileBinding(new CertificateProfileRef("profile-a", 1,
new byte[CertificateProfileRef.HASH_BYTES]))));
int binding = indexOf(encoded, new byte[] { 73, 2, 72 });
assertTrue(binding >= 0);
encoded[binding + 2] = 1;
assertInvalid(encoded);
}
private static int indexOf(byte[] source, byte[] target) {
for (int index = 0; index <= source.length - target.length; index++) {
boolean matches = true;
for (int offset = 0; offset < target.length; offset++) {
if (source[index + offset] != target[offset]) {
matches = false;
break;
}
}
if (matches) {
return index;
}
}
return -1;
}
private static ParsedCertificationRequest roundTripRequest(AttributeSet attributes) {
byte[] encoded = FsCodec.encode(FsCodec.PARSED_REQUEST, request(attributes));
return FsCodec.decode(FsCodec.PARSED_REQUEST, encoded);
@@ -239,15 +267,16 @@ final class FsCodecTest {
}
private static CertificateProfile profile() {
SubjectPolicy subject = new SubjectPolicy(List.of(new SubjectRdnRule(
SubjectPolicy subject = new SubjectPolicy(false, List.of(new SubjectRdnRule(
SubjectRdnType.COMMON_NAME, 1, 1, 256, Optional.empty(), true)));
SubjectAlternativeNamePolicy sans = new SubjectAlternativeNamePolicy(false, 0, 0,
SubjectAlternativeNamePolicy sans = new SubjectAlternativeNamePolicy(0, 0,
List.of(), false, java.util.Set.of(), false, false, false);
LeafCertificatePolicy leaf = new LeafCertificatePolicy(subject, sans,
LeafCertificatePolicy leaf = new LeafCertificatePolicy(sans,
java.util.Set.of(LeafKeyUsage.DIGITAL_SIGNATURE, LeafKeyUsage.KEY_ENCIPHERMENT),
java.util.Set.of(new ExtendedKeyUsageId("1.3.6.1.5.5.7.3.1")), true, false, true,
java.util.Set.of("RSA"), Duration.ofDays(365));
return new CertificateProfile("profile-a", new FormatId("x509"), "Test leaf profile", leaf);
java.util.Set.of("RSA"));
return new CertificateProfile("profile-a", new FormatId("x509"), "Test leaf profile",
Duration.ofDays(365), subject, leaf);
}
private static Credential credential(CredentialProfileBinding binding) {
@@ -261,12 +290,15 @@ final class FsCodecTest {
private static ImportedCertificateProfileVersion profileVersion() {
try {
CertificateProfile profile = profile();
CertificateProfileDefinition definition = new CertificateProfileDefinition(profile.profileId(), 1,
profile.formatId(), profile.displayName(), profile.leafPolicy());
CertificateProfileDefinition definition = new CertificateProfileDefinition(
CertificateProfileKind.END_ENTITY, profile.profileId(), 1, profile.formatId(),
profile.displayName(), profile.maximumValidity(), profile.subjectPolicy(),
profile.leafPolicy());
byte[] canonical = CertificateProfileDocumentCodec.writeCanonical(definition);
CertificateProfileRef reference = new CertificateProfileRef(profile.profileId(), 1,
MessageDigest.getInstance("SHA-256").digest(canonical));
return new ImportedCertificateProfileVersion(reference, 1, definition, canonical, Instant.EPOCH);
return new ImportedCertificateProfileVersion(reference,
CertificateProfileDefinition.SCHEMA_VERSION, definition, canonical, Instant.EPOCH);
} catch (java.security.NoSuchAlgorithmException impossible) {
throw new IllegalStateException(impossible);
}

View File

@@ -212,7 +212,7 @@ public final class H7ProfileDocuments {
boolean critical, List<String> sanRules, boolean noKeyEncipherment, List<String> eku) {
String keyUsage = noKeyEncipherment
? "[\"DIGITAL_SIGNATURE\"]" : "[\"DIGITAL_SIGNATURE\",\"KEY_ENCIPHERMENT\"]";
String json = "{\"schemaVersion\":1,\"profileId\":\"" + id
String json = "{\"schemaVersion\":2,\"certificateType\":\"END_ENTITY\",\"profileId\":\"" + id
+ "\",\"profileVersion\":1,\"formatId\":\"" + formatId
+ "\",\"displayName\":\"H7 Test Profile\","
+ "\"maxValidity\":\"PT8760H\",\"subject\":{\"allowEmpty\":" + allowEmpty

View File

@@ -54,6 +54,7 @@ import zeroecho.pki.api.RevocationService;
import zeroecho.pki.api.ProfileService;
import zeroecho.pki.api.StatusObjectService;
import zeroecho.pki.api.credential.EffectiveCredentialStatusResolver;
import zeroecho.pki.api.profile.BuiltInCertificateProfileCatalog;
import zeroecho.pki.impl.core.DefaultCaService;
import zeroecho.pki.impl.core.DefaultCertificationRequestService;
import zeroecho.pki.impl.core.DefaultIssuanceService;
@@ -104,6 +105,7 @@ public final class PkiTestRuntime implements AutoCloseable {
private final Map<String, PublicKey> publicKeysByKeyRef;
private Runnable publicKeyResolveHook;
private boolean caProfilesProvisioned;
private PkiTestRuntime(FilesystemPkiStore store, PkiSigningBus signingBus, SignatureWorkflow signatureWorkflow,
CredentialFramework framework, CredentialIssuerBackend issuerBackend,
@@ -129,7 +131,7 @@ public final class PkiTestRuntime implements AutoCloseable {
this.statusObjectService = new DefaultStatusObjectService(store, framework, auditSink, statusResolver);
this.caService = new DefaultCaService(store, framework, issuerBackend, this::resolvePublicKeyInfo, signingBus,
auditSink, statusResolver, "SHA256withRSA", signingTtl);
auditSink, statusResolver, profileService, clock, "SHA256withRSA", signingTtl);
}
/**
@@ -306,27 +308,53 @@ public final class PkiTestRuntime implements AutoCloseable {
}
public CaService caService() {
provisionCaProfiles();
return caService;
}
public CaService caService(CredentialFramework credentialFramework) {
provisionCaProfiles();
return new DefaultCaService(store, Objects.requireNonNull(credentialFramework, "credentialFramework"),
issuerBackend, this::resolvePublicKeyInfo, signingBus, auditSink, statusResolver, "SHA256withRSA",
Duration.ofSeconds(2));
issuerBackend, this::resolvePublicKeyInfo, signingBus, auditSink, statusResolver, profileService,
Clock.systemUTC(), "SHA256withRSA", Duration.ofSeconds(2));
}
public CaService caService(CredentialIssuerBackend backend) {
provisionCaProfiles();
return new DefaultCaService(store, framework, Objects.requireNonNull(backend, "backend"),
this::resolvePublicKeyInfo, signingBus, auditSink, statusResolver, "SHA256withRSA",
Duration.ofSeconds(2));
this::resolvePublicKeyInfo, signingBus, auditSink, statusResolver, profileService, Clock.systemUTC(),
"SHA256withRSA", Duration.ofSeconds(2));
}
public CaService caService(CredentialIssuerBackend backend, EffectiveCredentialStatusResolver resolver) {
provisionCaProfiles();
return new DefaultCaService(store, framework, Objects.requireNonNull(backend, "backend"),
this::resolvePublicKeyInfo, signingBus, auditSink, Objects.requireNonNull(resolver, "resolver"),
profileService, Clock.systemUTC(), "SHA256withRSA", Duration.ofSeconds(2));
}
public CaService caService(ProfileService profiles) {
provisionCaProfiles();
return new DefaultCaService(store, framework, issuerBackend, this::resolvePublicKeyInfo, signingBus,
auditSink, statusResolver, Objects.requireNonNull(profiles, "profiles"), Clock.systemUTC(),
"SHA256withRSA", Duration.ofSeconds(2));
}
private synchronized void provisionCaProfiles() {
if (caProfilesProvisioned) {
return;
}
BuiltInCertificateProfileCatalog.load(PkiTestRuntime.class.getClassLoader()).stream()
.filter(template -> template.definition().profileId().equals("root-ca")
|| template.definition().profileId().equals("intermediate-ca"))
.forEach(template -> {
zeroecho.pki.api.profile.CertificateProfileRef reference =
profileService.importBuiltIn(template);
profileService.activateProfile(reference.profileId(), reference.profileVersion());
});
caProfilesProvisioned = true;
}
public CertificationRequestService certificationRequestService() {
return certificationRequestService;
}