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:
@@ -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. */
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
15
pki/src/main/java/zeroecho/pki/api/profile/CaKeyUsage.java
Normal file
15
pki/src/main/java/zeroecho/pki/api/profile/CaKeyUsage.java
Normal 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
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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"}]}
|
||||
@@ -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"]}}
|
||||
@@ -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"]}}
|
||||
@@ -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"]}}
|
||||
@@ -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"]}}
|
||||
@@ -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"]}}
|
||||
@@ -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"]}}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user