refactor(pki): normalize credential authority and snapshots

Make standalone Credential records the sole durable certificate-content
authority and replace embedded CaRecord credentials with stable
credential ID references.

Rework snapshot export and restore to stream certificate content,
mint target-store references, preserve dependency ordering, and reject
legacy embedded-credential and v2 persistence formats.

Validated:
- lib tests: 516/516
- FilesystemPkiStoreTest: 16/16
- PMD and JavaDoc pass
- app compilation passes
- only the 29 independently classified revocation fixture failures remain
This commit is contained in:
2026-08-01 22:18:08 +02:00
parent aa89c09238
commit 0a07dc6ee2
19 changed files with 1002 additions and 130 deletions

View File

@@ -152,6 +152,12 @@ public interface CaService {
/** /**
* Retrieves a CA record. * Retrieves a CA record.
* *
* <p>
* The returned record contains ordered credential identifiers. Credential
* content and metadata remain authoritative only through the configured
* credential store.
* </p>
*
* @param caId CA identifier * @param caId CA identifier
* @return CA record * @return CA record
* @throws IllegalArgumentException if {@code caId} is invalid * @throws IllegalArgumentException if {@code caId} is invalid
@@ -162,6 +168,11 @@ public interface CaService {
/** /**
* Lists CA records matching query constraints. * Lists CA records matching query constraints.
* *
* <p>
* Returned records contain ordered credential identifiers rather than embedded
* credential values.
* </p>
*
* @param query query constraints * @param query query constraints
* @return list of CA records * @return list of CA records
* @throws IllegalArgumentException if {@code query} is invalid * @throws IllegalArgumentException if {@code query} is invalid

View File

@@ -33,15 +33,17 @@
******************************************************************************/ ******************************************************************************/
package zeroecho.pki.api.ca; package zeroecho.pki.api.ca;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import zeroecho.pki.api.KeyRef; import zeroecho.pki.api.KeyRef;
import zeroecho.pki.api.PkiId; import zeroecho.pki.api.PkiId;
import zeroecho.pki.api.SubjectRef; import zeroecho.pki.api.SubjectRef;
import zeroecho.pki.api.credential.Credential;
/** /**
* Represents a CA entity and its issued CA credentials. * Represents a CA entity and the ordered identifiers of its issued CA
* credentials.
* *
* <p> * <p>
* A CA entity may have multiple CA credentials to support: * A CA entity may have multiple CA credentials to support:
@@ -58,16 +60,19 @@ import zeroecho.pki.api.credential.Credential;
* @param issuerKeyRef key reference used for issuing operations (private key * @param issuerKeyRef key reference used for issuing operations (private key
* reference) * reference)
* @param subjectRef normalized subject reference * @param subjectRef normalized subject reference
* @param caCredentials CA credentials currently associated with the entity * @param credentialIds ordered identifiers of the credentials currently
* (historical and active) * associated with the entity (historical and active);
* duplicates and {@code null} elements are rejected
*/ */
public record CaRecord(PkiId caId, CaKind kind, CaState state, KeyRef issuerKeyRef, SubjectRef subjectRef, public record CaRecord(PkiId caId, CaKind kind, CaState state, KeyRef issuerKeyRef, SubjectRef subjectRef,
List<Credential> caCredentials) { List<PkiId> credentialIds) {
/** /**
* Creates a CA record. * Creates a CA record.
* *
* @throws IllegalArgumentException if inputs are null * @throws IllegalArgumentException if a mandatory input or credential
* identifier is {@code null}, or if a
* credential identifier is duplicated
*/ */
public CaRecord { public CaRecord {
if (caId == null) { if (caId == null) {
@@ -85,8 +90,18 @@ public record CaRecord(PkiId caId, CaKind kind, CaState state, KeyRef issuerKeyR
if (subjectRef == null) { if (subjectRef == null) {
throw new IllegalArgumentException("subjectRef must not be null"); throw new IllegalArgumentException("subjectRef must not be null");
} }
if (caCredentials == null) { if (credentialIds == null) {
throw new IllegalArgumentException("caCredentials must not be null"); throw new IllegalArgumentException("credentialIds must not be null");
}
Set<PkiId> uniqueCredentialIds = new HashSet<>(credentialIds.size());
for (PkiId credentialId : credentialIds) {
if (credentialId == null) {
throw new IllegalArgumentException("credentialIds must not contain null");
}
if (!uniqueCredentialIds.add(credentialId)) {
throw new IllegalArgumentException("credentialIds must not contain duplicates");
} }
} }
credentialIds = List.copyOf(credentialIds);
}
} }

View File

@@ -359,10 +359,10 @@ public final class DefaultCaService implements CaService {
CredentialProfileBindings.requireCaBinding(credential.profileBinding(), request.profileReference()); CredentialProfileBindings.requireCaBinding(credential.profileBinding(), request.profileReference());
requireCaCertificateMatches(credential, credential, request, caId, CREATE_ROOT_REJECTED, BACKEND_CRED_MISMATCH); 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); store.putCredential(credential);
CaRecord ca = new CaRecord(caId, CaKind.ROOT, CaState.ACTIVE, keyRef, request.subjectRef(),
List.of(credential.credentialId()));
store.putCa(ca);
return caId; return caId;
} }
@@ -447,7 +447,7 @@ public final class DefaultCaService implements CaService {
requireValidImportedRoot(command, holder); requireValidImportedRoot(command, holder);
store.putCredential(credential); store.putCredential(credential);
CaRecord ca = new CaRecord(caId, CaKind.ROOT, CaState.ACTIVE, command.keyRef(), request.subjectRef(), CaRecord ca = new CaRecord(caId, CaKind.ROOT, CaState.ACTIVE, command.keyRef(), request.subjectRef(),
List.of(credential)); List.of(credential.credentialId()));
store.putCa(ca); store.putCa(ca);
return caId; return caId;
} }
@@ -499,7 +499,7 @@ public final class DefaultCaService implements CaService {
CaRecord issuer = getCa(command.issuerCaId()); CaRecord issuer = getCa(command.issuerCaId());
ensureActive(issuer, "issuer"); ensureActive(issuer, "issuer");
if (issuer.caCredentials().isEmpty()) { if (issuer.credentialIds().isEmpty()) {
throw new PkiException("Issuer CA has no credentials"); throw new PkiException("Issuer CA has no credentials");
} }
if (!framework.formatId().equals(command.formatId())) { if (!framework.formatId().equals(command.formatId())) {
@@ -552,7 +552,7 @@ public final class DefaultCaService implements CaService {
store.putCredential(cred); store.putCredential(cred);
CaRecord subject = new CaRecord(caId, CaKind.INTERMEDIATE, CaState.ACTIVE, command.keyRef().get(), CaRecord subject = new CaRecord(caId, CaKind.INTERMEDIATE, CaState.ACTIVE, command.keyRef().get(),
issue.subjectRef(), List.of(cred)); issue.subjectRef(), List.of(cred.credentialId()));
store.putCa(subject); store.putCa(subject);
return caId; return caId;
} }
@@ -645,8 +645,8 @@ public final class DefaultCaService implements CaService {
BACKEND_CRED_MISMATCH); BACKEND_CRED_MISMATCH);
store.putCredential(cred); store.putCredential(cred);
List<Credential> updated = new ArrayList<>(subject.caCredentials()); List<PkiId> updated = new ArrayList<>(subject.credentialIds());
updated.add(cred); updated.add(cred.credentialId());
CaRecord updatedCa = new CaRecord(subject.caId(), subject.kind(), subject.state(), subject.issuerKeyRef(), CaRecord updatedCa = new CaRecord(subject.caId(), subject.kind(), subject.state(), subject.issuerKeyRef(),
subject.subjectRef(), List.copyOf(updated)); subject.subjectRef(), List.copyOf(updated));
store.putCa(updatedCa); store.putCa(updatedCa);
@@ -731,7 +731,7 @@ public final class DefaultCaService implements CaService {
} }
CaRecord updated = new CaRecord(existing.caId(), existing.kind(), state, existing.issuerKeyRef(), CaRecord updated = new CaRecord(existing.caId(), existing.kind(), state, existing.issuerKeyRef(),
existing.subjectRef(), existing.caCredentials()); existing.subjectRef(), existing.credentialIds());
store.putCa(updated); store.putCa(updated);
if (LOG.isLoggable(Level.INFO)) { if (LOG.isLoggable(Level.INFO)) {
@@ -783,10 +783,11 @@ public final class DefaultCaService implements CaService {
return false; return false;
} }
if (query.formatId().isPresent()) { if (query.formatId().isPresent()) {
if (r.caCredentials().isEmpty()) { if (r.credentialIds().isEmpty()) {
return false; return false;
} }
Credential last = r.caCredentials().get(r.caCredentials().size() - 1); PkiId lastId = r.credentialIds().get(r.credentialIds().size() - 1);
Credential last = requireCredential(lastId);
return query.formatId().get().equals(last.formatId()); return query.formatId().get().equals(last.formatId());
} }
return true; return true;
@@ -803,7 +804,8 @@ public final class DefaultCaService implements CaService {
EffectiveCredentialStatusResolver.Evaluation evaluation) { EffectiveCredentialStatusResolver.Evaluation evaluation) {
Credential lastRejected = null; Credential lastRejected = null;
EffectiveCredentialStatus lastStatus = null; EffectiveCredentialStatus lastStatus = null;
for (Credential credential : issuer.caCredentials()) { for (PkiId credentialId : issuer.credentialIds()) {
Credential credential = requireCredential(credentialId);
if (credential == null || !formatId.equals(credential.formatId())) { if (credential == null || !formatId.equals(credential.formatId())) {
continue; continue;
} }
@@ -828,6 +830,11 @@ public final class DefaultCaService implements CaService {
throw new PkiException("Issuer credential unavailable: code=ISSUER_CREDENTIAL_UNAVAILABLE"); throw new PkiException("Issuer credential unavailable: code=ISSUER_CREDENTIAL_UNAVAILABLE");
} }
private Credential requireCredential(PkiId credentialId) {
return store.getCredential(credentialId)
.orElseThrow(() -> new PkiException("CA credential not found"));
}
private void requireValidImportedRoot(CaImportCommand command, X509CertificateHolder holder) { private void requireValidImportedRoot(CaImportCommand command, X509CertificateHolder holder) {
try { try {
CaProofGate.ManagedKeyProof proof = proofGate.proveManagedKey(command.keyRef(), command.formatId(), CaProofGate.ManagedKeyProof proof = proofGate.proveManagedKey(command.keyRef(), command.formatId(),

View File

@@ -234,7 +234,7 @@ public final class DefaultIssuanceService implements IssuanceService {
if (issuer.state() != CaState.ACTIVE) { if (issuer.state() != CaState.ACTIVE) {
throw new PkiException("Issuer CA not ACTIVE"); throw new PkiException("Issuer CA not ACTIVE");
} }
if (issuer.caCredentials().isEmpty()) { if (issuer.credentialIds().isEmpty()) {
throw new PkiException("Issuer CA has no credentials"); throw new PkiException("Issuer CA has no credentials");
} }
@@ -305,7 +305,8 @@ public final class DefaultIssuanceService implements IssuanceService {
Credential lastRejected = null; Credential lastRejected = null;
EffectiveCredentialStatus lastStatus = null; EffectiveCredentialStatus lastStatus = null;
for (Credential c : issuer.caCredentials()) { for (PkiId credentialId : issuer.credentialIds()) {
Credential c = requireIssuerCredential(credentialId);
if (c == null || !formatId.equals(c.formatId())) { if (c == null || !formatId.equals(c.formatId())) {
continue; continue;
} }
@@ -330,6 +331,11 @@ public final class DefaultIssuanceService implements IssuanceService {
throw new PkiException("Issuer credential unavailable: code=ISSUER_CREDENTIAL_UNAVAILABLE"); throw new PkiException("Issuer credential unavailable: code=ISSUER_CREDENTIAL_UNAVAILABLE");
} }
private Credential requireIssuerCredential(PkiId credentialId) {
return store.getCredential(credentialId)
.orElseThrow(() -> new PkiException("Issuer CA credential not found"));
}
private VerifiedIssuanceCandidate verifyIssuanceCandidate(IssueEndEntityCommand command) { private VerifiedIssuanceCandidate verifyIssuanceCandidate(IssueEndEntityCommand command) {
ParsedCertificationRequest supplied = command.request(); ParsedCertificationRequest supplied = command.request();
byte[] csrDer = extractCsrDer(supplied); byte[] csrDer = extractCsrDer(supplied);

View File

@@ -214,7 +214,7 @@ public final class DefaultStatusObjectService implements StatusObjectService {
if (ca.state() != CaState.ACTIVE) { if (ca.state() != CaState.ACTIVE) {
throw new PkiException("Issuer CA not ACTIVE"); throw new PkiException("Issuer CA not ACTIVE");
} }
if (ca.caCredentials().isEmpty()) { if (ca.credentialIds().isEmpty()) {
throw new PkiException("Issuer CA has no credentials"); throw new PkiException("Issuer CA has no credentials");
} }
EffectiveCredentialStatusResolver.Evaluation statusEvaluation = statusResolver.beginEvaluation(); EffectiveCredentialStatusResolver.Evaluation statusEvaluation = statusResolver.beginEvaluation();
@@ -466,9 +466,10 @@ public final class DefaultStatusObjectService implements StatusObjectService {
EffectiveCredentialStatusResolver.Evaluation evaluation) { EffectiveCredentialStatusResolver.Evaluation evaluation) {
Credential lastRejected = null; Credential lastRejected = null;
EffectiveCredentialStatus lastStatus = null; EffectiveCredentialStatus lastStatus = null;
List<Credential> credentials = ca.caCredentials(); List<PkiId> credentialIds = ca.credentialIds();
for (int index = credentials.size() - 1; index >= 0; index--) { for (int index = credentialIds.size() - 1; index >= 0; index--) {
Credential credential = credentials.get(index); Credential credential = store.getCredential(credentialIds.get(index))
.orElseThrow(DefaultStatusObjectService::crlGenerationFailure);
if (credential == null || !command.formatId().equals(credential.formatId())) { if (credential == null || !command.formatId().equals(credential.formatId())) {
continue; continue;
} }

View File

@@ -166,7 +166,7 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
private static final Logger LOG = Logger.getLogger(FilesystemPkiStore.class.getName()); private static final Logger LOG = Logger.getLogger(FilesystemPkiStore.class.getName());
/* package */ static final String CURRENT_STORE_VERSION = "v2"; /* package */ static final String CURRENT_STORE_VERSION = "v3";
private static final String SIGN_RECORD_NAMESPACE = "io.zeroecho.pki.signing-record"; private static final String SIGN_RECORD_NAMESPACE = "io.zeroecho.pki.signing-record";
private static final String SIGN_OWNER_NAMESPACE = "io.zeroecho.pki.signing-owner"; private static final String SIGN_OWNER_NAMESPACE = "io.zeroecho.pki.signing-owner";
private static final int CURRENT_SIGN_RECORD_VERSION = 2; private static final int CURRENT_SIGN_RECORD_VERSION = 2;
@@ -401,8 +401,11 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
* <p> * <p>
* This method is an implementation-only feature. It does not modify the current * This method is an implementation-only feature. It does not modify the current
* store; it clones a new store layout and reconstructs {@code current.bin} for * store; it clones a new store layout and reconstructs {@code current.bin} for
* history-tracked entities. The signing namespace, monotonic signing-time * history-tracked entities. Standalone credential authority is reconstructed by
* watermark, and authoritative signing workflow records are safety metadata: * streaming validated live-source credential content into new target-owned
* references before publishing selected ID-only CA records. The signing namespace,
* monotonic signing-time watermark, and authoritative signing workflow records
* are safety metadata:
* their current export-time values are copied regardless of {@code at}, because * their current export-time values are copied regardless of {@code at}, because
* historical reconstruction could permit identifier reuse or lose a terminal * historical reconstruction could permit identifier reuse or lose a terminal
* result. * result.
@@ -417,13 +420,30 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
requireStoreUsable(); requireStoreUsable();
Objects.requireNonNull(targetRoot, "targetRoot"); Objects.requireNonNull(targetRoot, "targetRoot");
Objects.requireNonNull(at, "at"); Objects.requireNonNull(at, "at");
new FsSnapshotExporter(this.options).exportSnapshot(this.paths.root(), targetRoot, at); new FsSnapshotExporter(this.options).exportSnapshot(this, targetRoot, at);
}
/* default */ Path snapshotRoot() {
return paths.root();
}
/* default */ Set<String> snapshotNonCredentialContentIds() {
requireStoreUsable();
Set<String> contentIds = new HashSet<>();
for (StatusObject status : listBinaryFiles(paths.statusRoot(), FsCodec.STATUS_OBJECT)) {
contentIds.add(status.content().contentId());
}
for (StoredSign stored : listStoredSigns()) {
stored.reference().map(DurableContentReference::contentId).ifPresent(contentIds::add);
}
return Set.copyOf(contentIds);
} }
@Override @Override
public void putCa(final CaRecord record) { public void putCa(final CaRecord record) {
requireStoreUsable(); requireStoreUsable();
Objects.requireNonNull(record, "record"); Objects.requireNonNull(record, "record");
validateCaCredentialReferences(record);
PkiId caId = record.caId(); PkiId caId = record.caId();
Path current = this.paths.caCurrent(caId); Path current = this.paths.caCurrent(caId);
@@ -436,14 +456,15 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
requireStoreUsable(); requireStoreUsable();
Objects.requireNonNull(caId, "caId"); Objects.requireNonNull(caId, "caId");
Path p = this.paths.caCurrent(caId); Path p = this.paths.caCurrent(caId);
return readOptional(p, FsCodec.CA_RECORD); return readOptional(p, FsCodec.CA_RECORD).map(this::validateCaCredentialReferences);
} }
@Override @Override
public List<CaRecord> listCas() { public List<CaRecord> listCas() {
requireStoreUsable(); requireStoreUsable();
Path casRoot = this.paths.root().resolve("cas").resolve("by-id"); Path casRoot = this.paths.root().resolve("cas").resolve("by-id");
return listCurrentRecords(casRoot, FsCodec.CA_RECORD); return listCurrentRecords(casRoot, FsCodec.CA_RECORD).stream().map(this::validateCaCredentialReferences)
.toList();
} }
@Override @Override
@@ -469,6 +490,15 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
} }
} }
private CaRecord validateCaCredentialReferences(CaRecord record) {
for (PkiId credentialId : record.credentialIds()) {
if (getCredential(credentialId).isEmpty()) {
throw new IllegalStateException("CA credential reference is missing");
}
}
return record;
}
@Override @Override
public void putRequest(final ParsedCertificationRequest request) { public void putRequest(final ParsedCertificationRequest request) {
requireStoreUsable(); requireStoreUsable();

View File

@@ -448,7 +448,7 @@ final class FsCodec {
private static final ValueSchema<Credential> CREDENTIAL_VALUE = valueSchema(TYPE_CREDENTIAL_RECORD, private static final ValueSchema<Credential> CREDENTIAL_VALUE = valueSchema(TYPE_CREDENTIAL_RECORD,
FsCodec::writeCredential, FsCodec::readCredential); FsCodec::writeCredential, FsCodec::readCredential);
private static final ValueSchema<List<Credential>> CREDENTIALS = listOf(CREDENTIAL_VALUE); private static final ValueSchema<List<PkiId>> PKI_IDS = listOf(PKI_ID);
/* package */ static final Schema<CaRecord> CA_RECORD = topLevel(TOP_CA_RECORD, "CA_RECORD", /* package */ static final Schema<CaRecord> CA_RECORD = topLevel(TOP_CA_RECORD, "CA_RECORD",
valueSchema(100, FsCodec::writeCaRecord, FsCodec::readCaRecord)); valueSchema(100, FsCodec::writeCaRecord, FsCodec::readCaRecord));
@@ -711,12 +711,12 @@ final class FsCodec {
writer.writeValue(CA_STATE, value.state()); writer.writeValue(CA_STATE, value.state());
writer.writeValue(KEY_REF, value.issuerKeyRef()); writer.writeValue(KEY_REF, value.issuerKeyRef());
writer.writeValue(SUBJECT_REF, value.subjectRef()); writer.writeValue(SUBJECT_REF, value.subjectRef());
writer.writeValue(CREDENTIALS, value.caCredentials()); writer.writeValue(PKI_IDS, value.credentialIds());
} }
private static CaRecord readCaRecord(Reader reader) throws IOException { private static CaRecord readCaRecord(Reader reader) throws IOException {
return new CaRecord(reader.readValue(PKI_ID), reader.readValue(CA_KIND), reader.readValue(CA_STATE), return new CaRecord(reader.readValue(PKI_ID), reader.readValue(CA_KIND), reader.readValue(CA_STATE),
reader.readValue(KEY_REF), reader.readValue(SUBJECT_REF), reader.readValue(CREDENTIALS)); reader.readValue(KEY_REF), reader.readValue(SUBJECT_REF), reader.readValue(PKI_IDS));
} }
private static void writeParsedRequest(Writer writer, ParsedCertificationRequest value) throws IOException { private static void writeParsedRequest(Writer writer, ParsedCertificationRequest value) throws IOException {

View File

@@ -158,6 +158,61 @@ final class FsOperations {
forceDirectoryBestEffort(parent); forceDirectoryBestEffort(parent);
} }
/**
* Copies one file through a bounded buffer and atomically replaces the target.
*
* @param source source file
* @param target target file
* @param bufferBytes positive transfer-buffer size
* @throws IOException if streaming or atomic publication fails
*/
/* default */ static void copyAtomic(final Path source, final Path target, final int bufferBytes)
throws IOException {
Objects.requireNonNull(source, "source");
Objects.requireNonNull(target, "target");
if (bufferBytes <= 0) {
throw new IllegalArgumentException("bufferBytes must be positive");
}
Path parent = requireParent(target);
ensureDir(parent);
Path temporary = Files.createTempFile(parent, ".snapshot-copy-", ".tmp", fileAttributesIfSupported());
byte[] buffer = new byte[bufferBytes];
boolean published = false;
try {
try (InputStream input = Files.newInputStream(source);
OutputStream output = Files.newOutputStream(temporary, StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING)) {
long transferred = 0L;
int read;
while ((read = input.read(buffer)) >= 0) {
if (read == 0) {
int value = input.read();
if (value < 0) {
break;
}
output.write(value);
transferred = Math.addExact(transferred, 1L);
} else {
output.write(buffer, 0, read);
transferred = Math.addExact(transferred, read);
}
}
output.flush();
} catch (ArithmeticException failure) {
throw new IOException("File transfer length overflow", failure);
}
forceFileBestEffort(temporary);
Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
published = true;
forceDirectoryBestEffort(parent);
} finally {
java.util.Arrays.fill(buffer, (byte) 0);
if (!published) {
Files.deleteIfExists(temporary);
}
}
}
/** /**
* Strictly persists one authoritative revocation journal image. * Strictly persists one authoritative revocation journal image.
* *

View File

@@ -186,6 +186,10 @@ final class FsPaths {
return this.root.resolve("status").resolve(BY_ID).resolve(FsUtil.safeId(statusObjectId) + ".bin"); return this.root.resolve("status").resolve(BY_ID).resolve(FsUtil.safeId(statusObjectId) + ".bin");
} }
/* default */ Path statusRoot() {
return this.root.resolve("status").resolve(BY_ID);
}
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Policy traces (immutable .bin) // Policy traces (immutable .bin)
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------

View File

@@ -34,21 +34,35 @@
package zeroecho.pki.impl.fs; package zeroecho.pki.impl.fs;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.LinkOption; import java.nio.file.LinkOption;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import zeroecho.core.io.RepeatableContent;
import zeroecho.pki.api.PkiId;
import zeroecho.pki.api.ca.CaRecord;
import zeroecho.pki.api.content.DurableContentReference;
import zeroecho.pki.api.credential.Credential;
import zeroecho.pki.impl.ProfileLifecycleFailure; import zeroecho.pki.impl.ProfileLifecycleFailure;
import zeroecho.pki.impl.ProfileLifecycleFailure.Code; import zeroecho.pki.impl.ProfileLifecycleFailure.Code;
import zeroecho.pki.spi.store.ContentSink;
/** /**
* Snapshot exporter ("time travel") for {@link FilesystemPkiStore}. * Snapshot exporter ("time travel") for {@link FilesystemPkiStore}.
@@ -66,20 +80,28 @@ import zeroecho.pki.impl.ProfileLifecycleFailure.Code;
* <li>For entities with history enabled, the exporter selects the latest * <li>For entities with history enabled, the exporter selects the latest
* history entry with timestamp {@code <= at}. If none exists, it may fall back * history entry with timestamp {@code <= at}. If none exists, it may fall back
* to {@code current.bin} only when strict mode is disabled.</li> * to {@code current.bin} only when strict mode is disabled.</li>
* <li>Write-once objects are copied as-is (they are immutable). This exporter * <li>Write-once objects other than credentials are copied as-is. Every valid
* does not attempt to prune them by time unless an upstream index exists.</li> * standalone credential is streamed from the live source into a new target-owned
* content reference before selected ID-only CA records are published.</li>
* <li>Signing namespace, signing-time watermark, and authoritative * <li>Signing namespace, signing-time watermark, and authoritative
* {@code sign-workflows} records are current safety metadata copied at export * {@code sign-workflows} records are current safety metadata copied at export
* time. They are intentionally not reconstructed at {@code at}: rolling them * time. They are intentionally not reconstructed at {@code at}: rolling them
* back could make a stable identifier reusable or discard a completed signing * back could make a stable identifier reusable or discard a completed signing
* result.</li> * result.</li>
* </ul> * </ul>
*
* <p>
* Export requires time linear in copied files plus selected credential bytes,
* with auxiliary memory linear in selected CA and standalone credential metadata,
* plus one fixed-size transfer buffer.
* </p>
*/ */
final class FsSnapshotExporter { final class FsSnapshotExporter {
private static final Logger LOG = Logger.getLogger(FsSnapshotExporter.class.getName()); private static final Logger LOG = Logger.getLogger(FsSnapshotExporter.class.getName());
private static final String ACTIVE_POINTER_FILE = "active.bin"; private static final String ACTIVE_POINTER_FILE = "active.bin";
private static final String BINARY_EXTENSION = ".bin"; private static final String BINARY_EXTENSION = ".bin";
private static final int TRANSFER_BUFFER_BYTES = 16 * 1024;
private final FsPkiStoreOptions options; private final FsPkiStoreOptions options;
@@ -92,42 +114,350 @@ final class FsSnapshotExporter {
* bearing causes or filesystem paths. * bearing causes or filesystem paths.
*/ */
@SuppressWarnings("PMD.PreserveStackTrace") @SuppressWarnings("PMD.PreserveStackTrace")
/* default */ void exportSnapshot(final Path sourceRoot, final Path targetRoot, final Instant at) { /* default */ void exportSnapshot(final FilesystemPkiStore source, final Path targetRoot, final Instant at) {
Objects.requireNonNull(sourceRoot, "sourceRoot"); Objects.requireNonNull(source, "source");
Objects.requireNonNull(targetRoot, "targetRoot"); Objects.requireNonNull(targetRoot, "targetRoot");
Objects.requireNonNull(at, "at"); Objects.requireNonNull(at, "at");
try { try {
requireNewTarget(targetRoot);
Path sourceRoot = source.snapshotRoot();
List<SnapshotProfileArtifact> profiles = preflightImportedProfiles(sourceRoot.resolve("profiles"), at); List<SnapshotProfileArtifact> profiles = preflightImportedProfiles(sourceRoot.resolve("profiles"), at);
FsOperations.ensureDir(targetRoot); SnapshotAuthority authority = new AuthorityPlanner(source, options).plan(at);
FsPaths dst = new FsPaths(targetRoot); Set<String> nonCredentialContentIds = source.snapshotNonCredentialContentIds();
SnapshotPlan plan = new SnapshotPlan(profiles, authority, nonCredentialContentIds);
new SnapshotPublisher(source, options, plan).publish(targetRoot, at);
} catch (SnapshotProfileFailure failure) {
throw new IllegalStateException(failure.getMessage());
} catch (IOException e) {
throw new IllegalStateException("Snapshot export failed: code=SNAPSHOT_EXPORT_FAILED");
}
}
Files.writeString(dst.versionFile(), FilesystemPkiStore.CURRENT_STORE_VERSION); private static DurableContentReference transferCredentialContent(FilesystemPkiStore source,
FilesystemPkiStore target, DurableContentReference sourceReference) throws IOException {
try (RepeatableContent content = source.stagedContent().openContent(sourceReference);
InputStream input = content.openStream();
ContentSink sink = target.stagedContent().beginContent(sourceReference.encoding(),
DurableContentReference.Lifecycle.PERSISTED);
OutputStream output = sink.outputStream()) {
byte[] buffer = new byte[TRANSFER_BUFFER_BYTES];
try {
int read;
while ((read = input.read(buffer)) >= 0) {
if (read > 0) {
output.write(buffer, 0, read);
}
}
return sink.complete();
} finally {
Arrays.fill(buffer, (byte) 0);
}
}
}
private static Credential withContent(Credential source, DurableContentReference content) {
return new Credential(source.credentialId(), source.formatId(), source.issuerRef(), source.subjectRef(),
source.validity(), source.serialOrUniqueId(), source.publicKeyId(), source.profileBinding(),
source.status(), content, source.attributes());
}
private static void requireNewTarget(Path targetRoot) {
if (Files.exists(targetRoot, LinkOption.NOFOLLOW_LINKS)) {
throw new IllegalStateException("Snapshot target already exists");
}
}
private static List<Path> selectMutableRecords(Path srcTree, Instant at, FsHistoryPolicy policy, boolean strict)
throws IOException {
if (!Files.exists(srcTree)) {
return List.of();
}
List<Path> selected = new ArrayList<>();
try (java.util.stream.Stream<Path> paths = Files.walk(srcTree)) {
for (Path current : paths.filter(Files::isRegularFile)
.filter(path -> FsPaths.CURRENT_FILE.equals(path.getFileName().toString()))
.sorted(Comparator.comparing(Path::toString)).toList()) {
Path selectedRecord = null;
if (policy.enabled()) {
Path historyDir = current.getParent().resolve(FsPaths.HISTORY_DIR);
if (Files.isDirectory(historyDir)) {
selectedRecord = selectHistoryEntry(historyDir, at);
}
}
if (selectedRecord == null) {
if (strict && policy.enabled()) {
throw new IllegalStateException("No CA history entry is available for snapshot time");
}
selectedRecord = current;
}
selected.add(selectedRecord);
}
}
return List.copyOf(selected);
}
/** Builds the validated logical credential and selected-CA export plan. */
private static final class AuthorityPlanner {
private final FilesystemPkiStore source;
private final FsPkiStoreOptions options;
private final FsPaths sourcePaths;
private AuthorityPlanner(FilesystemPkiStore source, FsPkiStoreOptions options) {
this.source = source;
this.options = options;
this.sourcePaths = new FsPaths(source.snapshotRoot());
}
private SnapshotAuthority plan(Instant at) throws IOException {
CredentialInventory inventory = inventoryCredentials();
List<CaRecord> cas = selectCas(at, inventory.credentials());
return new SnapshotAuthority(cas, inventory.credentials(), inventory.contentIds());
}
private CredentialInventory inventoryCredentials() throws IOException {
Map<PkiId, Credential> credentials = new LinkedHashMap<>();
Set<String> contentIds = new HashSet<>();
Path credentialsRoot = source.snapshotRoot().resolve("credentials").resolve("by-id");
if (!Files.isDirectory(credentialsRoot)) {
return new CredentialInventory(credentials, contentIds);
}
try (java.util.stream.Stream<Path> records = Files.list(credentialsRoot)) {
for (Path record : records.filter(Files::isRegularFile)
.filter(path -> path.getFileName().toString().endsWith(BINARY_EXTENSION))
.sorted(Comparator.comparing(Path::toString)).toList()) {
loadCredential(record, credentials, contentIds);
}
}
return new CredentialInventory(credentials, contentIds);
}
private void loadCredential(Path record, Map<PkiId, Credential> credentials, Set<String> contentIds)
throws IOException {
Credential decoded;
try {
decoded = FsCodec.decode(FsCodec.CREDENTIAL, FsOperations.readAll(record), source.stagedContent());
} catch (IllegalStateException failure) {
rejectCredential();
return;
}
contentIds.add(decoded.content().contentId());
if (!sourcePaths.credentialPath(decoded.credentialId()).equals(record)
|| credentials.containsKey(decoded.credentialId())) {
rejectCredential();
return;
}
try {
Credential validated = source.getCredential(decoded.credentialId()).orElse(null);
if (validated == null) {
rejectCredential();
return;
}
credentials.put(decoded.credentialId(), validated);
} catch (IllegalStateException failure) {
rejectCredential();
}
}
private List<CaRecord> selectCas(Instant at, Map<PkiId, Credential> credentials) throws IOException {
List<Path> selectedRecords = selectMutableRecords(source.snapshotRoot().resolve("cas"), at,
options.caHistoryPolicy(), options.strictSnapshotExport());
List<CaRecord> selected = new ArrayList<>();
for (Path record : selectedRecords) {
CaRecord ca = loadCa(record);
if (ca != null && credentials.keySet().containsAll(ca.credentialIds())) {
selected.add(ca);
} else if (ca != null) {
rejectCa();
}
}
return List.copyOf(selected);
}
private CaRecord loadCa(Path record) throws IOException {
try {
return FsCodec.decode(FsCodec.CA_RECORD, FsOperations.readAll(record), source.stagedContent());
} catch (IllegalStateException failure) {
rejectCa();
return null;
}
}
private void rejectCredential() {
if (options.strictSnapshotExport()) {
throw new SnapshotAuthorityFailure("Snapshot credential preflight failed");
}
}
private void rejectCa() {
if (options.strictSnapshotExport()) {
throw new SnapshotAuthorityFailure("Snapshot CA dependency preflight failed");
}
}
}
/** Owns the sibling temporary root until one atomic target publication. */
private static final class SnapshotPublisher {
private final FilesystemPkiStore source;
private final FsPkiStoreOptions options;
private final SnapshotPlan plan;
private SnapshotPublisher(FilesystemPkiStore source, FsPkiStoreOptions options, SnapshotPlan plan) {
this.source = source;
this.options = options;
this.plan = plan;
}
private void publish(Path targetRoot, Instant at) throws IOException {
Path absoluteTarget = targetRoot.toAbsolutePath();
Path parent = absoluteTarget.getParent();
if (parent == null) {
throw new IOException("Snapshot target has no parent");
}
FsOperations.ensureDir(parent);
Path temporaryRoot = Files.createTempDirectory(parent, ".zeroecho-snapshot-");
boolean published = false;
try {
build(temporaryRoot, at);
Files.move(temporaryRoot, absoluteTarget, java.nio.file.StandardCopyOption.ATOMIC_MOVE);
published = true;
} catch (IOException | IllegalStateException failure) {
if (!published) {
addCleanupFailure(failure, temporaryRoot);
}
throw failure;
}
}
private void build(Path targetRoot, Instant at) throws IOException {
Path sourceRoot = source.snapshotRoot();
FsPaths destination = new FsPaths(targetRoot);
Files.writeString(destination.versionFile(), FilesystemPkiStore.CURRENT_STORE_VERSION);
copyFile(sourceRoot.resolve("SIGNING_NAMESPACE"), targetRoot.resolve("SIGNING_NAMESPACE")); copyFile(sourceRoot.resolve("SIGNING_NAMESPACE"), targetRoot.resolve("SIGNING_NAMESPACE"));
copyFile(sourceRoot.resolve("SIGNING_TIME_WATERMARK"), targetRoot.resolve("SIGNING_TIME_WATERMARK")); copyFile(sourceRoot.resolve("SIGNING_TIME_WATERMARK"), targetRoot.resolve("SIGNING_TIME_WATERMARK"));
// copy write-once trees as-is (best-effort, deterministic order)
copyTreeIfExists(sourceRoot.resolve("credentials"), targetRoot.resolve("credentials"));
copyTreeIfExists(sourceRoot.resolve("requests"), targetRoot.resolve("requests")); copyTreeIfExists(sourceRoot.resolve("requests"), targetRoot.resolve("requests"));
copyTreeIfExists(sourceRoot.resolve("status"), targetRoot.resolve("status")); copyTreeIfExists(sourceRoot.resolve("status"), targetRoot.resolve("status"));
copyTreeIfExists(sourceRoot.resolve("policy"), targetRoot.resolve("policy")); copyTreeIfExists(sourceRoot.resolve("policy"), targetRoot.resolve("policy"));
copyTreeIfExists(sourceRoot.resolve("publications"), targetRoot.resolve("publications")); copyTreeIfExists(sourceRoot.resolve("publications"), targetRoot.resolve("publications"));
copyTreeIfExists(sourceRoot.resolve("sign-workflows"), targetRoot.resolve("sign-workflows")); copyTreeIfExists(sourceRoot.resolve("sign-workflows"), targetRoot.resolve("sign-workflows"));
copyTreeIfExists(sourceRoot.resolve("staged-content"), targetRoot.resolve("staged-content")); copyStagedContent(sourceRoot.resolve("staged-content"), targetRoot.resolve("staged-content"),
plan.authority().sourceCredentialContentIds(), plan.nonCredentialContentIds());
copyTreeIfExists(sourceRoot.resolve("revocations"), targetRoot.resolve("revocations")); copyTreeIfExists(sourceRoot.resolve("revocations"), targetRoot.resolve("revocations"));
copyImportedProfilesAsOf(profiles, targetRoot.resolve("profiles")); copyImportedProfilesAsOf(plan.profiles(), targetRoot.resolve("profiles"));
// reconstruct mutable entities from history (CAS and profiles)
reconstructMutableTree(sourceRoot.resolve("cas"), targetRoot.resolve("cas"), at,
this.options.caHistoryPolicy(), this.options.strictSnapshotExport());
// reconstruct workflow continuation state from history
reconstructMutableTree(sourceRoot.resolve("workflows"), targetRoot.resolve("workflows"), at, reconstructMutableTree(sourceRoot.resolve("workflows"), targetRoot.resolve("workflows"), at,
this.options.workflowHistoryPolicy(), this.options.strictSnapshotExport()); options.workflowHistoryPolicy(), options.strictSnapshotExport());
new AuthorityRestorer(source, options, plan.authority()).restore(targetRoot);
}
} catch (SnapshotProfileFailure failure) { private static void addCleanupFailure(Exception primary, Path temporaryRoot) {
throw new IllegalStateException(failure.getMessage()); try {
} catch (IOException e) { deleteOwnedTree(temporaryRoot);
throw new IllegalStateException("Snapshot export failed: code=SNAPSHOT_EXPORT_FAILED"); } catch (IOException cleanupFailure) {
primary.addSuppressed(new IOException("Snapshot temporary cleanup failed"));
}
}
}
/** Reconstructs target credential authority before publishing selected CAs. */
private static final class AuthorityRestorer {
private final FilesystemPkiStore source;
private final FsPkiStoreOptions options;
private final SnapshotAuthority authority;
private AuthorityRestorer(FilesystemPkiStore source, FsPkiStoreOptions options,
SnapshotAuthority authority) {
this.source = source;
this.options = options;
this.authority = authority;
}
private void restore(Path targetRoot) throws IOException {
Set<PkiId> transferred = new HashSet<>();
try (FilesystemPkiStore target = new FilesystemPkiStore(targetRoot, options)) {
for (Map.Entry<PkiId, Credential> entry : authority.credentials().entrySet()) {
if (persistCredential(target, entry.getValue())) {
transferred.add(entry.getKey());
}
}
for (CaRecord ca : authority.cas()) {
if (transferred.containsAll(ca.credentialIds())) {
persistCa(target, ca);
}
}
}
}
private boolean persistCredential(FilesystemPkiStore target, Credential sourceCredential)
throws IOException {
if (options.strictSnapshotExport()) {
return persistCredentialStrict(target, sourceCredential);
}
try (CredentialTransfer transfer = new CredentialTransfer(source, target, sourceCredential)) {
transfer.persist();
return true;
} catch (IOException | IllegalArgumentException | IllegalStateException failure) {
return false;
}
}
private boolean persistCredentialStrict(FilesystemPkiStore target, Credential sourceCredential)
throws IOException {
try (CredentialTransfer transfer = new CredentialTransfer(source, target, sourceCredential)) {
transfer.persist();
return true;
}
}
private boolean persistCa(FilesystemPkiStore target, CaRecord ca) {
if (options.strictSnapshotExport()) {
target.putCa(ca);
return true;
}
try {
target.putCa(ca);
return true;
} catch (IllegalArgumentException | IllegalStateException failure) {
return false;
}
}
}
/** Owns a completed target reference until its credential is persisted. */
private static final class CredentialTransfer implements AutoCloseable {
private final FilesystemPkiStore target;
private final Credential sourceCredential;
private final DurableContentReference targetReference;
private boolean persisted;
private CredentialTransfer(FilesystemPkiStore source, FilesystemPkiStore target,
Credential sourceCredential) throws IOException {
this.target = target;
this.sourceCredential = sourceCredential;
this.targetReference = transferCredentialContent(source, target, sourceCredential.content());
}
private void persist() {
target.putCredential(withContent(sourceCredential, targetReference));
persisted = true;
}
@Override
public void close() throws IOException {
if (!persisted) {
target.stagedContent().retireUnownedContent(targetReference);
}
}
}
private static void deleteOwnedTree(Path root) throws IOException {
if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) {
return;
}
try (java.util.stream.Stream<Path> paths = Files.walk(root)) {
List<Path> ordered = paths.sorted(Comparator.reverseOrder()).toList();
for (Path path : ordered) {
Files.delete(path);
}
} }
} }
@@ -291,6 +621,42 @@ final class FsSnapshotExporter {
}); });
} }
private static void copyStagedContent(Path src, Path dst, Set<String> credentialContentIds,
Set<String> nonCredentialContentIds) throws IOException {
if (!Files.exists(src)) {
return;
}
try (java.util.stream.Stream<Path> paths = Files.walk(src)) {
for (Path path : paths.sorted(Comparator.comparing(Path::toString)).toList()) {
Path relative = src.relativize(path);
Path output = dst.resolve(relative);
if (Files.isDirectory(path)) {
FsOperations.ensureDir(output);
} else if (Files.isRegularFile(path)
&& shouldCopyStagedArtifact(path, credentialContentIds, nonCredentialContentIds)) {
FsOperations.ensureDir(output.getParent());
FsOperations.copyAtomic(path, output, TRANSFER_BUFFER_BYTES);
}
}
}
}
private static boolean shouldCopyStagedArtifact(Path path, Set<String> credentialContentIds,
Set<String> nonCredentialContentIds) {
String fileName = path.getFileName().toString();
int separator = fileName.lastIndexOf('.');
if (separator <= 0) {
return true;
}
String extension = fileName.substring(separator);
if (!".content".equals(extension) && !".meta".equals(extension) && !".owners".equals(extension)) {
return true;
}
String contentId = fileName.substring(0, separator);
return !credentialContentIds.contains(contentId)
|| nonCredentialContentIds.contains(contentId) && !".owners".equals(extension);
}
private static void copyFile(final Path source, final Path target) throws IOException { private static void copyFile(final Path source, final Path target) throws IOException {
if (!Files.isRegularFile(source)) { if (!Files.isRegularFile(source)) {
throw new IllegalStateException("Required snapshot metadata is missing: " + source.getFileName()); throw new IllegalStateException("Required snapshot metadata is missing: " + source.getFileName());
@@ -309,6 +675,40 @@ final class FsSnapshotExporter {
} }
} }
private record CredentialInventory(Map<PkiId, Credential> credentials, Set<String> contentIds) {
private CredentialInventory {
credentials = Collections.unmodifiableMap(new LinkedHashMap<>(credentials));
contentIds = Set.copyOf(contentIds);
}
}
private record SnapshotAuthority(List<CaRecord> cas, Map<PkiId, Credential> credentials,
Set<String> sourceCredentialContentIds) {
private SnapshotAuthority {
cas = List.copyOf(cas);
credentials = Collections.unmodifiableMap(new LinkedHashMap<>(credentials));
sourceCredentialContentIds = Set.copyOf(sourceCredentialContentIds);
}
}
private record SnapshotPlan(List<SnapshotProfileArtifact> profiles, SnapshotAuthority authority,
Set<String> nonCredentialContentIds) {
private SnapshotPlan {
profiles = List.copyOf(profiles);
Objects.requireNonNull(authority, "authority");
nonCredentialContentIds = Set.copyOf(nonCredentialContentIds);
}
}
/** Cause-free marker for invalid source credential or CA authority. */
private static final class SnapshotAuthorityFailure extends IllegalStateException {
private static final long serialVersionUID = -8051398945014616477L;
private SnapshotAuthorityFailure(String message) {
super(message);
}
}
/** Cause-free internal marker for profile preflight rejection. */ /** Cause-free internal marker for profile preflight rejection. */
private static final class SnapshotProfileFailure extends RuntimeException { private static final class SnapshotProfileFailure extends RuntimeException {
private static final long serialVersionUID = -4451876406166515230L; private static final long serialVersionUID = -4451876406166515230L;

View File

@@ -88,12 +88,15 @@ public interface PkiStore extends SignWorkflowStore {
* *
* <p> * <p>
* Implementations must store CA records atomically. Replacing an existing * Implementations must store CA records atomically. Replacing an existing
* record should be either fully visible or not visible at all. * record should be either fully visible or not visible at all. Every
* identifier in {@link CaRecord#credentialIds()} must resolve through
* {@link #getCredential(PkiId)} before the CA record is published.
* </p> * </p>
* *
* @param record CA record (never {@code null}) * @param record CA record (never {@code null})
* @throws NullPointerException if {@code record} is {@code null} * @throws NullPointerException if {@code record} is {@code null}
* @throws IllegalStateException if persistence fails * @throws IllegalStateException if persistence fails or a referenced
* credential is missing or invalid
*/ */
void putCa(CaRecord record); void putCa(CaRecord record);
@@ -101,17 +104,22 @@ public interface PkiStore extends SignWorkflowStore {
* Retrieves a CA record. * Retrieves a CA record.
* *
* @param caId CA identifier (never {@code null}) * @param caId CA identifier (never {@code null})
* @return CA record if present * @return CA record if present; all credential identifiers have been validated
* against the standalone credential authority
* @throws NullPointerException if {@code caId} is {@code null} * @throws NullPointerException if {@code caId} is {@code null}
* @throws IllegalStateException if retrieval fails * @throws IllegalStateException if retrieval fails or a referenced credential
* is missing or invalid
*/ */
Optional<CaRecord> getCa(PkiId caId); Optional<CaRecord> getCa(PkiId caId);
/** /**
* Lists all stored CA records. * Lists all stored CA records.
* *
* @return list of CA records (never {@code null}) * @return list of CA records (never {@code null}); every returned credential
* @throws IllegalStateException if listing fails * identifier has been validated against the standalone credential
* authority
* @throws IllegalStateException if listing fails or a referenced credential is
* missing or invalid
*/ */
List<CaRecord> listCas(); List<CaRecord> listCas();

View File

@@ -0,0 +1,72 @@
/*******************************************************************************
* 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.api.ca;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import zeroecho.pki.api.KeyRef;
import zeroecho.pki.api.PkiId;
import zeroecho.pki.api.SubjectRef;
final class CaRecordTest {
@Test
void credentialIdentifiersAreOrderedImmutableAndUnique() {
System.out.println("credentialIdentifiersAreOrderedImmutableAndUnique");
PkiId first = new PkiId("credential-first");
PkiId second = new PkiId("credential-second");
List<PkiId> source = new ArrayList<>(List.of(first, second));
CaRecord record = record(source);
source.clear();
assertEquals(List.of(first, second), record.credentialIds());
assertThrows(UnsupportedOperationException.class,
() -> record.credentialIds().add(new PkiId("credential-third")));
assertThrows(IllegalArgumentException.class, () -> record(List.of(first, first)));
assertThrows(IllegalArgumentException.class, () -> record(java.util.Arrays.asList(first, null)));
System.out.println("credentialIdentifiersAreOrderedImmutableAndUnique...ok");
}
private static CaRecord record(List<PkiId> credentialIds) {
return new CaRecord(new PkiId("ca-test"), CaKind.ROOT, CaState.ACTIVE, new KeyRef("key-test"),
new SubjectRef("CN=Test"), credentialIds);
}
}

View File

@@ -130,7 +130,7 @@ final class CaProfileIssuanceEnforcementTest {
"fixed-intermediate-ca", Optional.empty(), new SimpleAttributeSet())); "fixed-intermediate-ca", Optional.empty(), new SimpleAttributeSet()));
CaRecord intermediate = runtime.caService().getCa(intermediateId); CaRecord intermediate = runtime.caService().getCa(intermediateId);
Credential credential = intermediate.caCredentials().get(0); Credential credential = runtime.caCredential(intermediate, 0);
X509CertificateHolder holder = new X509CertificateHolder(runtime.credentialBytes(credential)); X509CertificateHolder holder = new X509CertificateHolder(runtime.credentialBytes(credential));
X509CertificateHolder additionalHolder = new X509CertificateHolder(runtime.credentialBytes(additional)); X509CertificateHolder additionalHolder = new X509CertificateHolder(runtime.credentialBytes(additional));
assertEquals("Fixed Organization", assertEquals("Fixed Organization",
@@ -140,7 +140,7 @@ final class CaProfileIssuanceEnforcementTest {
assertEquals(holder.getSubject().toString(), intermediate.subjectRef().value()); assertEquals(holder.getSubject().toString(), intermediate.subjectRef().value());
assertEquals(intermediate.subjectRef(), credential.subjectRef()); assertEquals(intermediate.subjectRef(), credential.subjectRef());
assertEquals(intermediate.subjectRef(), additional.subjectRef()); assertEquals(intermediate.subjectRef(), additional.subjectRef());
assertEquals(2, intermediate.caCredentials().size()); assertEquals(2, intermediate.credentialIds().size());
} }
} }
@@ -167,19 +167,19 @@ final class CaProfileIssuanceEnforcementTest {
Credential additional = runtime.caService() Credential additional = runtime.caService()
.issueIntermediateCertificate(new IntermediateCertIssueCommand(runtime.framework().formatId(), .issueIntermediateCertificate(new IntermediateCertIssueCommand(runtime.framework().formatId(),
rootId, intermediateId, "intermediate-ca", Optional.empty(), new SimpleAttributeSet())); rootId, intermediateId, "intermediate-ca", Optional.empty(), new SimpleAttributeSet()));
rootProfile = ((CaProfileBinding) runtime.caService().getCa(rootId).caCredentials().get(0).profileBinding()) rootProfile = ((CaProfileBinding) runtime.caCredential(runtime.caService().getCa(rootId), 0).profileBinding())
.reference(); .reference();
intermediateProfile = ((CaProfileBinding) additional.profileBinding()).reference(); intermediateProfile = ((CaProfileBinding) additional.profileBinding()).reference();
assertCaCertificate(runtime, runtime.caService().getCa(rootId).caCredentials().get(0), 1); assertCaCertificate(runtime, runtime.caCredential(runtime.caService().getCa(rootId), 0), 1);
assertCaCertificate(runtime, additional, 0); assertCaCertificate(runtime, additional, 0);
} }
try (PkiTestRuntime reopened = PkiTestRuntime.create(store, directory.resolve("reopened-bus.log"), try (PkiTestRuntime reopened = PkiTestRuntime.create(store, directory.resolve("reopened-bus.log"),
Map.of(rootRef, rootKey, intermediateRef, intermediateKey))) { Map.of(rootRef, rootKey, intermediateRef, intermediateKey))) {
assertEquals(rootProfile, assertEquals(rootProfile,
((CaProfileBinding) reopened.caService().getCa(rootId).caCredentials().get(0).profileBinding()) ((CaProfileBinding) reopened.caCredential(reopened.caService().getCa(rootId), 0).profileBinding())
.reference()); .reference());
assertEquals(intermediateProfile, ((CaProfileBinding) reopened.caService().getCa(intermediateId) assertEquals(intermediateProfile, ((CaProfileBinding) reopened
.caCredentials().get(1).profileBinding()).reference()); .caCredential(reopened.caService().getCa(intermediateId), 1).profileBinding()).reference());
} }
} }
@@ -221,10 +221,11 @@ final class CaProfileIssuanceEnforcementTest {
.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootId, .createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootId,
new SubjectRef("CN=Historical Intermediate"), "intermediate-ca", new SubjectRef("CN=Historical Intermediate"), "intermediate-ca",
Optional.of(intermediateRef), new SimpleAttributeSet())); Optional.of(intermediateRef), new SimpleAttributeSet()));
CaProfileBinding issuerBinding = (CaProfileBinding) runtime.caService().getCa(rootId).caCredentials().get(0) CaProfileBinding issuerBinding = (CaProfileBinding) runtime
.caCredential(runtime.caService().getCa(rootId), 0)
.profileBinding(); .profileBinding();
assertEquals(1, issuerBinding.reference().profileVersion()); assertEquals(1, issuerBinding.reference().profileVersion());
assertEquals(1, runtime.caService().getCa(intermediateId).caCredentials().size()); assertEquals(1, runtime.caService().getCa(intermediateId).credentialIds().size());
} }
} }
@@ -260,7 +261,7 @@ final class CaProfileIssuanceEnforcementTest {
runtime.profileService().activateProfile(profileId, 1); runtime.profileService().activateProfile(profileId, 1);
caA = createRoot(runtime, keyRefA, "CN=Version Switch Root A", profileId); caA = createRoot(runtime, keyRefA, "CN=Version Switch Root A", profileId);
Credential issuedA = onlyCredential(runtime.caService(), caA); Credential issuedA = onlyCredential(runtime, caA);
assertCaProfileCredential(runtime, "A", issuedA, versionOne, 1); assertCaProfileCredential(runtime, "A", issuedA, versionOne, 1);
credentialA = issuedA.credentialId(); credentialA = issuedA.credentialId();
@@ -276,13 +277,13 @@ final class CaProfileIssuanceEnforcementTest {
assertEquals(versionOne, runtime.profileService().getActiveReference(profileId).orElseThrow()); assertEquals(versionOne, runtime.profileService().getActiveReference(profileId).orElseThrow());
caB = createRoot(runtime, keyRefB, "CN=Version Switch Root B", profileId); caB = createRoot(runtime, keyRefB, "CN=Version Switch Root B", profileId);
Credential issuedB = onlyCredential(runtime.caService(), caB); Credential issuedB = onlyCredential(runtime, caB);
assertCaProfileCredential(runtime, "B", issuedB, versionOne, 1); assertCaProfileCredential(runtime, "B", issuedB, versionOne, 1);
credentialB = issuedB.credentialId(); credentialB = issuedB.credentialId();
assertEquals(versionTwo, runtime.profileService().activateProfile(profileId, 2)); assertEquals(versionTwo, runtime.profileService().activateProfile(profileId, 2));
caC = createRoot(runtime, keyRefC, "CN=Version Switch Root C", profileId); caC = createRoot(runtime, keyRefC, "CN=Version Switch Root C", profileId);
Credential issuedC = onlyCredential(runtime.caService(), caC); Credential issuedC = onlyCredential(runtime, caC);
assertCaProfileCredential(runtime, "C", issuedC, versionTwo, 2); assertCaProfileCredential(runtime, "C", issuedC, versionTwo, 2);
credentialC = issuedC.credentialId(); credentialC = issuedC.credentialId();
@@ -347,7 +348,7 @@ final class CaProfileIssuanceEnforcementTest {
rootProfile = runtime.profileService().importProfile(rootProfileDocument); rootProfile = runtime.profileService().importProfile(rootProfileDocument);
runtime.profileService().activateProfile(rootProfileId, 1); runtime.profileService().activateProfile(rootProfileId, 1);
rootId = createRoot(runtime, rootKeyRef, "CN=Version Switch Issuer Root", rootProfileId); rootId = createRoot(runtime, rootKeyRef, "CN=Version Switch Issuer Root", rootProfileId);
Credential rootCredential = onlyCredential(runtime.caService(), rootId); Credential rootCredential = onlyCredential(runtime, rootId);
assertCaProfileCredential(runtime, "issuer", rootCredential, rootProfile, 2); assertCaProfileCredential(runtime, "issuer", rootCredential, rootProfile, 2);
versionOne = runtime.profileService().importProfile(versionOneDocument); versionOne = runtime.profileService().importProfile(versionOneDocument);
@@ -357,10 +358,10 @@ final class CaProfileIssuanceEnforcementTest {
caA = createIntermediate(runtime, rootId, keyRefA, "CN=Version Switch Intermediate A", caA = createIntermediate(runtime, rootId, keyRefA, "CN=Version Switch Intermediate A",
intermediateProfileId); intermediateProfileId);
Credential issuedA = onlyCredential(runtime.caService(), caA); Credential issuedA = onlyCredential(runtime, caA);
assertCaProfileCredential(runtime, "A", issuedA, versionOne, 0); assertCaProfileCredential(runtime, "A", issuedA, versionOne, 0);
credentialA = issuedA.credentialId(); credentialA = issuedA.credentialId();
assertCaProfileCredential(runtime, "issuer-after-A", onlyCredential(runtime.caService(), rootId), rootProfile, 2); assertCaProfileCredential(runtime, "issuer-after-A", onlyCredential(runtime, rootId), rootProfile, 2);
ImportedCertificateProfileVersion storedOne = runtime.profileService() ImportedCertificateProfileVersion storedOne = runtime.profileService()
.getImportedVersion(intermediateProfileId, 1).orElseThrow(); .getImportedVersion(intermediateProfileId, 1).orElseThrow();
@@ -375,14 +376,14 @@ final class CaProfileIssuanceEnforcementTest {
caB = createIntermediate(runtime, rootId, keyRefB, "CN=Version Switch Intermediate B", caB = createIntermediate(runtime, rootId, keyRefB, "CN=Version Switch Intermediate B",
intermediateProfileId); intermediateProfileId);
Credential issuedB = onlyCredential(runtime.caService(), caB); Credential issuedB = onlyCredential(runtime, caB);
assertCaProfileCredential(runtime, "B", issuedB, versionOne, 0); assertCaProfileCredential(runtime, "B", issuedB, versionOne, 0);
credentialB = issuedB.credentialId(); credentialB = issuedB.credentialId();
assertEquals(versionTwo, runtime.profileService().activateProfile(intermediateProfileId, 2)); assertEquals(versionTwo, runtime.profileService().activateProfile(intermediateProfileId, 2));
caC = createIntermediate(runtime, rootId, keyRefC, "CN=Version Switch Intermediate C", caC = createIntermediate(runtime, rootId, keyRefC, "CN=Version Switch Intermediate C",
intermediateProfileId); intermediateProfileId);
Credential issuedC = onlyCredential(runtime.caService(), caC); Credential issuedC = onlyCredential(runtime, caC);
assertCaProfileCredential(runtime, "C", issuedC, versionTwo, 1); assertCaProfileCredential(runtime, "C", issuedC, versionTwo, 1);
credentialC = issuedC.credentialId(); credentialC = issuedC.credentialId();
@@ -390,7 +391,7 @@ final class CaProfileIssuanceEnforcementTest {
0); 0);
assertCaProfileCredential(runtime, "B-reread", runtime.store().getCredential(credentialB).orElseThrow(), versionOne, assertCaProfileCredential(runtime, "B-reread", runtime.store().getCredential(credentialB).orElseThrow(), versionOne,
0); 0);
assertCaProfileCredential(runtime, "issuer-reread", onlyCredential(runtime.caService(), rootId), rootProfile, 2); assertCaProfileCredential(runtime, "issuer-reread", onlyCredential(runtime, rootId), rootProfile, 2);
ImportedCertificateProfileVersion unchanged = runtime.profileService() ImportedCertificateProfileVersion unchanged = runtime.profileService()
.getImportedVersion(intermediateProfileId, 1).orElseThrow(); .getImportedVersion(intermediateProfileId, 1).orElseThrow();
assertArrayEquals(persistedVersionOne, unchanged.canonicalJson()); assertArrayEquals(persistedVersionOne, unchanged.canonicalJson());
@@ -405,7 +406,7 @@ final class CaProfileIssuanceEnforcementTest {
versionOne, 0); versionOne, 0);
assertCaProfileCredential(reopened, "C-restart", reopened.store().getCredential(credentialC).orElseThrow(), assertCaProfileCredential(reopened, "C-restart", reopened.store().getCredential(credentialC).orElseThrow(),
versionTwo, 1); versionTwo, 1);
assertCaProfileCredential(reopened, "issuer-restart", onlyCredential(reopened.caService(), rootId), assertCaProfileCredential(reopened, "issuer-restart", onlyCredential(reopened, rootId),
rootProfile, 2); rootProfile, 2);
ImportedCertificateProfileVersion unchanged = reopened.profileService() ImportedCertificateProfileVersion unchanged = reopened.profileService()
.getImportedVersion(intermediateProfileId, 1).orElseThrow(); .getImportedVersion(intermediateProfileId, 1).orElseThrow();
@@ -429,12 +430,14 @@ final class CaProfileIssuanceEnforcementTest {
.replace("\"profileId\":\"root-ca\"", "\"profileId\":\"other-format-root\"") .replace("\"profileId\":\"root-ca\"", "\"profileId\":\"other-format-root\"")
.replace("\"formatId\":\"x509\"", "\"formatId\":\"other\"").getBytes(StandardCharsets.UTF_8)); .replace("\"formatId\":\"x509\"", "\"formatId\":\"other\"").getBytes(StandardCharsets.UTF_8));
CaRecord root = runtime.caService().getCa(rootId); CaRecord root = runtime.caService().getCa(rootId);
Credential original = root.caCredentials().get(0); Credential original = runtime.caCredential(root, 0);
Credential mutated = new Credential(original.credentialId(), original.formatId(), original.issuerRef(), Credential mutated = new Credential(new PkiId(original.credentialId().value() + ":wrong-profile"),
original.subjectRef(), original.validity(), original.serialOrUniqueId(), original.publicKeyId(), original.formatId(), original.issuerRef(), original.subjectRef(), original.validity(),
new CaProfileBinding(wrongFormat), original.status(), original.content(), original.attributes()); original.serialOrUniqueId(), original.publicKeyId(), new CaProfileBinding(wrongFormat),
original.status(), original.content(), original.attributes());
runtime.store().putCredential(mutated);
runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(), runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(),
root.subjectRef(), List.of(mutated))); root.subjectRef(), List.of(mutated.credentialId())));
int signCount = runtime.submittedSignCount(); int signCount = runtime.submittedSignCount();
assertThrows(PkiException.class, assertThrows(PkiException.class,
@@ -491,7 +494,7 @@ final class CaProfileIssuanceEnforcementTest {
service.issueIntermediateCertificate(new IntermediateCertIssueCommand(runtime.framework().formatId(), service.issueIntermediateCertificate(new IntermediateCertIssueCommand(runtime.framework().formatId(),
rootId, intermediateId, "intermediate-ca", Optional.empty(), new SimpleAttributeSet())); rootId, intermediateId, "intermediate-ca", Optional.empty(), new SimpleAttributeSet()));
profiles.assertAndReset("intermediate-ca"); profiles.assertAndReset("intermediate-ca");
rootCertificate = runtime.credentialBytes(service.getCa(rootId).caCredentials().get(0)); rootCertificate = runtime.credentialBytes(runtime.caCredential(service.getCa(rootId), 0));
} }
try (PkiTestRuntime target = PkiTestRuntime.create(directory.resolve("import"), try (PkiTestRuntime target = PkiTestRuntime.create(directory.resolve("import"),
directory.resolve("import-bus.log"), Map.of(rootRef, rootKey))) { directory.resolve("import-bus.log"), Map.of(rootRef, rootKey))) {
@@ -518,7 +521,7 @@ final class CaProfileIssuanceEnforcementTest {
new IntermediateCertIssueCommand(runtime.framework().formatId(), rootId, rootId, new IntermediateCertIssueCommand(runtime.framework().formatId(), rootId, rootId,
"intermediate-ca", Optional.empty(), new SimpleAttributeSet()))); "intermediate-ca", Optional.empty(), new SimpleAttributeSet())));
assertEquals(signCount, runtime.submittedSignCount()); assertEquals(signCount, runtime.submittedSignCount());
assertEquals(1, runtime.caService().getCa(rootId).caCredentials().size()); assertEquals(1, runtime.caService().getCa(rootId).credentialIds().size());
} }
} }
@@ -545,7 +548,7 @@ final class CaProfileIssuanceEnforcementTest {
runtime.framework().formatId(), rootId, intermediateId, "intermediate-ca", runtime.framework().formatId(), rootId, intermediateId, "intermediate-ca",
Optional.of(invalid), new SimpleAttributeSet()))); Optional.of(invalid), new SimpleAttributeSet())));
assertEquals(signCount, runtime.submittedSignCount()); assertEquals(signCount, runtime.submittedSignCount());
assertEquals(1, runtime.caService().getCa(intermediateId).caCredentials().size()); assertEquals(1, runtime.caService().getCa(intermediateId).credentialIds().size());
} }
} }
@@ -558,14 +561,14 @@ final class CaProfileIssuanceEnforcementTest {
directory.resolve("source-bus.log"), Map.of(rootRef, rootKey))) { directory.resolve("source-bus.log"), Map.of(rootRef, rootKey))) {
PkiId rootId = source.caService().createRoot(new CaCreateCommand(source.framework().formatId(), PkiId rootId = source.caService().createRoot(new CaCreateCommand(source.framework().formatId(),
new SubjectRef("CN=Imported Root"), "root-ca", Optional.of(rootRef), new SimpleAttributeSet())); new SubjectRef("CN=Imported Root"), "root-ca", Optional.of(rootRef), new SimpleAttributeSet()));
encoded = source.credentialBytes(source.caService().getCa(rootId).caCredentials().get(0)); encoded = source.credentialBytes(source.caCredential(source.caService().getCa(rootId), 0));
} }
try (PkiTestRuntime target = PkiTestRuntime.create(directory.resolve("target"), try (PkiTestRuntime target = PkiTestRuntime.create(directory.resolve("target"),
directory.resolve("target-bus.log"), Map.of(rootRef, rootKey))) { directory.resolve("target-bus.log"), Map.of(rootRef, rootKey))) {
PkiId imported = target.caService() PkiId imported = target.caService()
.importRoot(new CaImportCommand(target.framework().formatId(), new SubjectRef("CN=Imported Root"), .importRoot(new CaImportCommand(target.framework().formatId(), new SubjectRef("CN=Imported Root"),
"root-ca", rootRef, target.stageCredential(encoded), new SimpleAttributeSet())); "root-ca", rootRef, target.stageCredential(encoded), new SimpleAttributeSet()));
Credential credential = target.caService().getCa(imported).caCredentials().get(0); Credential credential = target.caCredential(target.caService().getCa(imported), 0);
assertEquals(target.profileService().getActiveReference("root-ca").orElseThrow(), assertEquals(target.profileService().getActiveReference("root-ca").orElseThrow(),
((CaProfileBinding) credential.profileBinding()).reference()); ((CaProfileBinding) credential.profileBinding()).reference());
} }
@@ -583,7 +586,7 @@ final class CaProfileIssuanceEnforcementTest {
.createRoot(new CaCreateCommand(source.framework().formatId(), .createRoot(new CaCreateCommand(source.framework().formatId(),
new SubjectRef("CN=Import Mutation Root"), "root-ca", Optional.of(rootRef), new SubjectRef("CN=Import Mutation Root"), "root-ca", Optional.of(rootRef),
new SimpleAttributeSet())); new SimpleAttributeSet()));
Credential sourceCredential = source.caService().getCa(rootId).caCredentials().get(0); Credential sourceCredential = source.caCredential(source.caService().getCa(rootId), 0);
encoded = mutation.mutate(source, sourceCredential, rootKey); encoded = mutation.mutate(source, sourceCredential, rootKey);
} }
try (PkiTestRuntime target = PkiTestRuntime.create(directory.resolve("target"), try (PkiTestRuntime target = PkiTestRuntime.create(directory.resolve("target"),
@@ -665,10 +668,10 @@ final class CaProfileIssuanceEnforcementTest {
issuerId, new SubjectRef(subject), profileId, Optional.of(keyRef), new SimpleAttributeSet())); issuerId, new SubjectRef(subject), profileId, Optional.of(keyRef), new SimpleAttributeSet()));
} }
private static Credential onlyCredential(CaService service, PkiId caId) { private static Credential onlyCredential(PkiTestRuntime runtime, PkiId caId) {
CaRecord ca = service.getCa(caId); CaRecord ca = runtime.caService().getCa(caId);
assertEquals(1, ca.caCredentials().size()); assertEquals(1, ca.credentialIds().size());
return ca.caCredentials().get(0); return runtime.caCredential(ca, 0);
} }
private static byte[] caProfileDocument(String builtInProfileId, String profileId, long profileVersion, private static byte[] caProfileDocument(String builtInProfileId, String profileId, long profileVersion,

View File

@@ -137,11 +137,12 @@ public final class PkiCoreE2eTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"), keys)) { try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"), keys)) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(), PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
new SubjectRef("CN=Matrix Root"), "root-ca", Optional.of(rootKeyRef), emptyAttributes())); new SubjectRef("CN=Matrix Root"), "root-ca", Optional.of(rootKeyRef), emptyAttributes()));
Credential usable = runtime.caService().getCa(rootCaId).caCredentials().get(0); Credential usable = runtime.caCredential(runtime.caService().getCa(rootCaId), 0);
Credential unusable = copyWithId(usable, new PkiId("credential:matrix-unusable")); Credential unusable = copyWithId(usable, new PkiId("credential:matrix-unusable"));
runtime.store().putCredential(unusable);
CaRecord root = runtime.caService().getCa(rootCaId); CaRecord root = runtime.caService().getCa(rootCaId);
runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(), runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(),
root.subjectRef(), List.of(unusable, usable))); root.subjectRef(), List.of(unusable.credentialId(), usable.credentialId())));
List<PkiId> resolved = new ArrayList<>(); List<PkiId> resolved = new ArrayList<>();
EffectiveCredentialStatusResolver resolver = scriptedResolver(credential -> { EffectiveCredentialStatusResolver resolver = scriptedResolver(credential -> {
@@ -173,7 +174,7 @@ public final class PkiCoreE2eTest {
resolved.clear(); resolved.clear();
runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(), runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(),
root.subjectRef(), List.of(usable, unusable))); root.subjectRef(), List.of(usable.credentialId(), unusable.credentialId())));
statusService.generate(new StatusObjectGenerateCommand(rootCaId, StatusObjectType.CRL, statusService.generate(new StatusObjectGenerateCommand(rootCaId, StatusObjectType.CRL,
runtime.framework().formatId(), emptyAttributes())); runtime.framework().formatId(), emptyAttributes()));
assertEquals(List.of(unusable.credentialId(), usable.credentialId()), List.copyOf(resolved)); assertEquals(List.of(unusable.credentialId(), usable.credentialId()), List.copyOf(resolved));
@@ -201,7 +202,7 @@ public final class PkiCoreE2eTest {
.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootCaId, .createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootCaId,
new SubjectRef("CN=H6 Intermediate"), "intermediate-ca", Optional.of(intermediateKeyRef), new SubjectRef("CN=H6 Intermediate"), "intermediate-ca", Optional.of(intermediateKeyRef),
emptyAttributes())); emptyAttributes()));
PkiId rootCredentialId = runtime.caService().getCa(rootCaId).caCredentials().get(0).credentialId(); PkiId rootCredentialId = runtime.caService().getCa(rootCaId).credentialIds().get(0);
runtime.revocationService().revokePermanently(new RevocationCommand.RevokePermanently(rootCredentialId, runtime.revocationService().revokePermanently(new RevocationCommand.RevokePermanently(rootCredentialId,
RevocationReason.KEY_COMPROMISE, emptyAttributes())); RevocationReason.KEY_COMPROMISE, emptyAttributes()));
int submissionsBeforeRejections = runtime.submittedSignCount(); int submissionsBeforeRejections = runtime.submittedSignCount();
@@ -230,7 +231,7 @@ public final class PkiCoreE2eTest {
assertEquals(submissionsBeforeRejections, runtime.submittedSignCount()); assertEquals(submissionsBeforeRejections, runtime.submittedSignCount());
assertTrue(runtime.store().getCredential(rootCredentialId).isPresent()); assertTrue(runtime.store().getCredential(rootCredentialId).isPresent());
assertTrue(runtime.caService().getCa(intermediateCaId).caCredentials().size() == 1); assertTrue(runtime.caService().getCa(intermediateCaId).credentialIds().size() == 1);
} }
} }
@@ -320,7 +321,7 @@ public final class PkiCoreE2eTest {
.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootCaId, .createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootCaId,
new SubjectRef("CN=Rejected Intermediate"), "intermediate-ca", new SubjectRef("CN=Rejected Intermediate"), "intermediate-ca",
Optional.of(intermediateKeyRef), emptyAttributes())); Optional.of(intermediateKeyRef), emptyAttributes()));
Credential rootCredential = runtime.caService().getCa(rootCaId).caCredentials().get(0); Credential rootCredential = runtime.caCredential(runtime.caService().getCa(rootCaId), 0);
EffectiveCredentialStatusResolver resolver = scriptedResolver(credential -> status, resolutionFailure); EffectiveCredentialStatusResolver resolver = scriptedResolver(credential -> status, resolutionFailure);
CountingIssuerBackend backend = new CountingIssuerBackend(runtime.issuerBackend()); CountingIssuerBackend backend = new CountingIssuerBackend(runtime.issuerBackend());
IssuanceService issuance = runtime.issuanceService(backend, resolver); IssuanceService issuance = runtime.issuanceService(backend, resolver);
@@ -332,7 +333,7 @@ public final class PkiCoreE2eTest {
int signCount = runtime.submittedSignCount(); int signCount = runtime.submittedSignCount();
int caCount = runtime.store().listCas().size(); int caCount = runtime.store().listCas().size();
int statusCount = runtime.store().listStatusObjects(rootCaId).size(); int statusCount = runtime.store().listStatusObjects(rootCaId).size();
int intermediateCredentialCount = runtime.caService().getCa(intermediateCaId).caCredentials().size(); int intermediateCredentialCount = runtime.caService().getCa(intermediateCaId).credentialIds().size();
assertThrows(PkiException.class, () -> issuance assertThrows(PkiException.class, () -> issuance
.issueEndEntity(new IssueEndEntityCommand(rootCaId, leafRequest, "default", Optional.empty()))); .issueEndEntity(new IssueEndEntityCommand(rootCaId, leafRequest, "default", Optional.empty())));
@@ -355,7 +356,7 @@ public final class PkiCoreE2eTest {
assertEquals(caCount, runtime.store().listCas().size()); assertEquals(caCount, runtime.store().listCas().size());
assertEquals(statusCount, runtime.store().listStatusObjects(rootCaId).size()); assertEquals(statusCount, runtime.store().listStatusObjects(rootCaId).size());
assertEquals(intermediateCredentialCount, assertEquals(intermediateCredentialCount,
runtime.caService().getCa(intermediateCaId).caCredentials().size()); runtime.caService().getCa(intermediateCaId).credentialIds().size());
assertTrue(runtime.store().getCredential(rootCredential.credentialId()).isPresent()); assertTrue(runtime.store().getCredential(rootCredential.credentialId()).isPresent());
assertFalse(runtime.auditSink().snapshot().toString().contains("DO_NOT_EXPOSE_REVOCATION_SENTINEL")); assertFalse(runtime.auditSink().snapshot().toString().contains("DO_NOT_EXPOSE_REVOCATION_SENTINEL"));
} }

View File

@@ -472,7 +472,7 @@ final class PkiProofGateE2eTest {
new SubjectRef("CN=Intermediate"), "intermediate-ca", Optional.of(intermediateKeyRef), new SubjectRef("CN=Intermediate"), "intermediate-ca", Optional.of(intermediateKeyRef),
approved)); approved));
Credential first = runtime.caService().getCa(intermediateCaId).caCredentials().get(0); Credential first = runtime.caCredential(runtime.caService().getCa(intermediateCaId), 0);
X509CertificateHolder firstHolder = new X509CertificateHolder(runtime.credentialBytes(first)); X509CertificateHolder firstHolder = new X509CertificateHolder(runtime.credentialBytes(first));
assertEquals("CN=Root", firstHolder.getIssuer().toString()); assertEquals("CN=Root", firstHolder.getIssuer().toString());
assertEquals("CN=Intermediate", firstHolder.getSubject().toString()); assertEquals("CN=Intermediate", firstHolder.getSubject().toString());
@@ -538,7 +538,7 @@ final class PkiProofGateE2eTest {
runtime.framework().formatId(), rootCaId, intermediateCaId, "intermediate-ca", runtime.framework().formatId(), rootCaId, intermediateCaId, "intermediate-ca",
Optional.empty(), new SimpleAttributeSet()))); Optional.empty(), new SimpleAttributeSet())));
assertEquals(7, runtime.submittedSignCount()); assertEquals(7, runtime.submittedSignCount());
assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size()); assertEquals(1, runtime.caService().getCa(intermediateCaId).credentialIds().size());
assertTrue(runtime.store().listWorkflowStates().isEmpty()); assertTrue(runtime.store().listWorkflowStates().isEmpty());
} }
@@ -551,13 +551,13 @@ final class PkiProofGateE2eTest {
PkiId rootCaId = source.caService().createRoot(new CaCreateCommand(source.framework().formatId(), PkiId rootCaId = source.caService().createRoot(new CaCreateCommand(source.framework().formatId(),
new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef), new SimpleAttributeSet())); new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef), new SimpleAttributeSet()));
rootCertificate = source rootCertificate = source
.credentialBytes(source.caService().getCa(rootCaId).caCredentials().get(0)).clone(); .credentialBytes(source.caCredential(source.caService().getCa(rootCaId), 0)).clone();
PkiId intermediateCaId = source.caService() PkiId intermediateCaId = source.caService()
.createIntermediate(new IntermediateCreateCommand(source.framework().formatId(), rootCaId, .createIntermediate(new IntermediateCreateCommand(source.framework().formatId(), rootCaId,
new SubjectRef("CN=Intermediate"), "intermediate-ca", Optional.of(intermediateKeyRef), new SubjectRef("CN=Intermediate"), "intermediate-ca", Optional.of(intermediateKeyRef),
new SimpleAttributeSet())); new SimpleAttributeSet()));
intermediateCertificate = source intermediateCertificate = source
.credentialBytes(source.caService().getCa(intermediateCaId).caCredentials().get(0)).clone(); .credentialBytes(source.caCredential(source.caService().getCa(intermediateCaId), 0)).clone();
ParsedCertificationRequest leaf = parse(source, makeCsr(subjectKey, subjectKey, "CN=Leaf")); ParsedCertificationRequest leaf = parse(source, makeCsr(subjectKey, subjectKey, "CN=Leaf"));
leafCertificate = source.credentialBytes(source.issuanceService() leafCertificate = source.credentialBytes(source.issuanceService()
.issueEndEntity(new IssueEndEntityCommand(rootCaId, leaf, "default", Optional.empty())) .issueEndEntity(new IssueEndEntityCommand(rootCaId, leaf, "default", Optional.empty()))
@@ -585,10 +585,10 @@ final class PkiProofGateE2eTest {
.importRoot(new CaImportCommand(target.framework().formatId(), new SubjectRef("CN=Root"), "root-ca", .importRoot(new CaImportCommand(target.framework().formatId(), new SubjectRef("CN=Root"), "root-ca",
rootKeyRef, target.stageCredential(callerOwnedCertificate), rootKeyRef, target.stageCredential(callerOwnedCertificate),
new SimpleAttributeSet())); new SimpleAttributeSet()));
assertTrue(target.caService().getCa(importedCaId).caCredentials().get(0) assertTrue(target.caCredential(target.caService().getCa(importedCaId), 0)
.profileBinding() instanceof CaProfileBinding); .profileBinding() instanceof CaProfileBinding);
assertArrayEquals(expectedImportedCertificate, assertArrayEquals(expectedImportedCertificate,
target.credentialBytes(target.caService().getCa(importedCaId).caCredentials().get(0))); target.credentialBytes(target.caCredential(target.caService().getCa(importedCaId), 0)));
} }
assertInvalidRootImport(tempDir.resolve("import-leaf"), rootKeyRef, rootKey, leafCertificate, "CN=Leaf"); assertInvalidRootImport(tempDir.resolve("import-leaf"), rootKeyRef, rootKey, leafCertificate, "CN=Leaf");
@@ -787,12 +787,14 @@ final class PkiProofGateE2eTest {
runtime.store().getCredential(returned.credential().credentialId()).orElseThrow())); runtime.store().getCredential(returned.credential().credentialId()).orElseThrow()));
CaRecord root = runtime.caService().getCa(rootCaId); CaRecord root = runtime.caService().getCa(rootCaId);
Credential original = root.caCredentials().get(0); Credential original = runtime.caCredential(root, 0);
Credential revoked = new Credential(original.credentialId(), original.formatId(), original.issuerRef(), Credential revoked = new Credential(new PkiId(original.credentialId().value() + ":revoked"),
original.formatId(), original.issuerRef(),
original.subjectRef(), original.validity(), original.serialOrUniqueId(), original.publicKeyId(), original.subjectRef(), original.validity(), original.serialOrUniqueId(), original.publicKeyId(),
original.profileBinding(), CredentialStatus.REVOKED, original.content(), original.attributes()); original.profileBinding(), CredentialStatus.REVOKED, original.content(), original.attributes());
runtime.store().putCredential(revoked);
runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(), runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(),
root.subjectRef(), List.of(revoked))); root.subjectRef(), List.of(revoked.credentialId())));
int before = runtime.submittedSignCount(); int before = runtime.submittedSignCount();
assertThrows(PkiException.class, () -> runtime.issuanceService() assertThrows(PkiException.class, () -> runtime.issuanceService()
.issueEndEntity(new IssueEndEntityCommand(rootCaId, subject, "default", Optional.empty()))); .issueEndEntity(new IssueEndEntityCommand(rootCaId, subject, "default", Optional.empty())));
@@ -800,17 +802,19 @@ final class PkiProofGateE2eTest {
Validity expiredValidity = new Validity(Instant.now().minus(Duration.ofDays(2)), Validity expiredValidity = new Validity(Instant.now().minus(Duration.ofDays(2)),
Instant.now().minus(Duration.ofDays(1))); Instant.now().minus(Duration.ofDays(1)));
Credential expired = new Credential(original.credentialId(), original.formatId(), original.issuerRef(), Credential expired = new Credential(new PkiId(original.credentialId().value() + ":expired"),
original.subjectRef(), expiredValidity, original.serialOrUniqueId(), original.publicKeyId(), original.formatId(), original.issuerRef(), original.subjectRef(), expiredValidity,
original.profileBinding(), CredentialStatus.ISSUED, original.content(), original.attributes()); original.serialOrUniqueId(), original.publicKeyId(), original.profileBinding(),
CredentialStatus.ISSUED, original.content(), original.attributes());
runtime.store().putCredential(expired);
runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(), runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(),
root.subjectRef(), List.of(expired))); root.subjectRef(), List.of(expired.credentialId())));
assertThrows(PkiException.class, () -> runtime.issuanceService() assertThrows(PkiException.class, () -> runtime.issuanceService()
.issueEndEntity(new IssueEndEntityCommand(rootCaId, subject, "default", Optional.empty()))); .issueEndEntity(new IssueEndEntityCommand(rootCaId, subject, "default", Optional.empty())));
assertEquals(before, runtime.submittedSignCount()); assertEquals(before, runtime.submittedSignCount());
runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(), runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(),
root.subjectRef(), List.of(original))); root.subjectRef(), List.of(original.credentialId())));
ParsedCertificationRequest missing = withAttributes(subject, new SimpleAttributeSet()); ParsedCertificationRequest missing = withAttributes(subject, new SimpleAttributeSet());
AttributeSet hostileAttributes = new AttributeSet() { AttributeSet hostileAttributes = new AttributeSet() {
@Override @Override
@@ -857,7 +861,7 @@ final class PkiProofGateE2eTest {
Map.of(rootKeyRef, rootKey, intermediateKeyRef, intermediateKey))) { Map.of(rootKeyRef, rootKey, intermediateKeyRef, intermediateKey))) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(), PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef), new SimpleAttributeSet())); new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef), new SimpleAttributeSet()));
assertTrue(runtime.caService().getCa(rootCaId).caCredentials().get(0) assertTrue(runtime.caCredential(runtime.caService().getCa(rootCaId), 0)
.profileBinding() instanceof CaProfileBinding); .profileBinding() instanceof CaProfileBinding);
CredentialIssuerBackend delegate = runtime.issuerBackend(); CredentialIssuerBackend delegate = runtime.issuerBackend();
for (BindingVariantMutation mutation : BindingVariantMutation.values()) { for (BindingVariantMutation mutation : BindingVariantMutation.values()) {
@@ -910,7 +914,7 @@ final class PkiProofGateE2eTest {
Optional.empty(), new SimpleAttributeSet())), Optional.empty(), new SimpleAttributeSet())),
mutation.name()); mutation.name());
assertTrue(rejected.getMessage().contains("CREDENTIAL_PROFILE_BINDING_MISMATCH"), mutation.name()); assertTrue(rejected.getMessage().contains("CREDENTIAL_PROFILE_BINDING_MISMATCH"), mutation.name());
assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size(), mutation.name()); assertEquals(1, runtime.caService().getCa(intermediateCaId).credentialIds().size(), mutation.name());
if (produced.get() != null) { if (produced.get() != null) {
assertTrue(runtime.store().getCredential(produced.get().credentialId()).isEmpty(), mutation.name()); assertTrue(runtime.store().getCredential(produced.get().credentialId()).isEmpty(), mutation.name());
} }
@@ -935,7 +939,7 @@ final class PkiProofGateE2eTest {
() -> wrongSubjectService.issueIntermediateCertificate( () -> wrongSubjectService.issueIntermediateCertificate(
new IntermediateCertIssueCommand(runtime.framework().formatId(), rootCaId, intermediateCaId, new IntermediateCertIssueCommand(runtime.framework().formatId(), rootCaId, intermediateCaId,
"intermediate-ca", Optional.empty(), new SimpleAttributeSet()))); "intermediate-ca", Optional.empty(), new SimpleAttributeSet())));
assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size()); assertEquals(1, runtime.caService().getCa(intermediateCaId).credentialIds().size());
CredentialIssuerBackend invalidSignatureBackend = new CredentialIssuerBackend() { CredentialIssuerBackend invalidSignatureBackend = new CredentialIssuerBackend() {
@Override @Override
@@ -960,7 +964,7 @@ final class PkiProofGateE2eTest {
() -> invalidSignatureService.issueIntermediateCertificate( () -> invalidSignatureService.issueIntermediateCertificate(
new IntermediateCertIssueCommand(runtime.framework().formatId(), rootCaId, intermediateCaId, new IntermediateCertIssueCommand(runtime.framework().formatId(), rootCaId, intermediateCaId,
"intermediate-ca", Optional.empty(), new SimpleAttributeSet()))); "intermediate-ca", Optional.empty(), new SimpleAttributeSet())));
assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size()); assertEquals(1, runtime.caService().getCa(intermediateCaId).credentialIds().size());
for (IntermediateExtensionVariant variant : IntermediateExtensionVariant.values()) { for (IntermediateExtensionVariant variant : IntermediateExtensionVariant.values()) {
CaService maliciousExtensionService = runtime CaService maliciousExtensionService = runtime
@@ -970,7 +974,7 @@ final class PkiProofGateE2eTest {
runtime.framework().formatId(), rootCaId, intermediateCaId, "intermediate-ca", runtime.framework().formatId(), rootCaId, intermediateCaId, "intermediate-ca",
Optional.empty(), new SimpleAttributeSet())), Optional.empty(), new SimpleAttributeSet())),
variant.name()); variant.name());
assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size(), variant.name()); assertEquals(1, runtime.caService().getCa(intermediateCaId).credentialIds().size(), variant.name());
} }
AtomicReference<Credential> rawCredential = new AtomicReference<>(); AtomicReference<Credential> rawCredential = new AtomicReference<>();

View File

@@ -122,7 +122,7 @@ final class DefaultStatusObjectServiceCrlTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(root, root.resolve("bus.log"), try (PkiTestRuntime runtime = PkiTestRuntime.create(root, root.resolve("bus.log"),
Map.of(rootKeyRef, rootKey))) { Map.of(rootKeyRef, rootKey))) {
PkiId caId = createRoot(runtime, rootKeyRef, "CRL Generator Root"); PkiId caId = createRoot(runtime, rootKeyRef, "CRL Generator Root");
Credential issuer = runtime.caService().getCa(caId).caCredentials().get(0); Credential issuer = runtime.caCredential(runtime.caService().getCa(caId), 0);
StatusObjectGenerateCommand command = crlCommand(runtime, caId, issuer, rootKeyRef); StatusObjectGenerateCommand command = crlCommand(runtime, caId, issuer, rootKeyRef);
List<RevocationReason> reasons = activeReasons(); List<RevocationReason> reasons = activeReasons();
List<BigInteger> serials = serials(reasons.size()); List<BigInteger> serials = serials(reasons.size());
@@ -188,7 +188,7 @@ final class DefaultStatusObjectServiceCrlTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(root, root.resolve("bus.log"), try (PkiTestRuntime runtime = PkiTestRuntime.create(root, root.resolve("bus.log"),
Map.of(rootKeyRef, rootKey))) { Map.of(rootKeyRef, rootKey))) {
PkiId caId = createRoot(runtime, rootKeyRef, "CRL Failure Root"); PkiId caId = createRoot(runtime, rootKeyRef, "CRL Failure Root");
Credential template = runtime.caService().getCa(caId).caCredentials().get(0); Credential template = runtime.caCredential(runtime.caService().getCa(caId), 0);
StatusObjectGenerateCommand command = new StatusObjectGenerateCommand(caId, StatusObjectType.CRL, StatusObjectGenerateCommand command = new StatusObjectGenerateCommand(caId, StatusObjectType.CRL,
runtime.framework().formatId(), emptyAttributes()); runtime.framework().formatId(), emptyAttributes());

View File

@@ -34,6 +34,7 @@
package zeroecho.pki.impl.fs; package zeroecho.pki.impl.fs;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -218,7 +219,7 @@ public final class FilesystemPkiStoreTest {
store.putCa(ca1); store.putCa(ca1);
CaRecord ca2 = new CaRecord(ca1.caId(), ca1.kind(), CaState.DISABLED, ca1.issuerKeyRef(), ca1.subjectRef(), CaRecord ca2 = new CaRecord(ca1.caId(), ca1.kind(), CaState.DISABLED, ca1.issuerKeyRef(), ca1.subjectRef(),
ca1.caCredentials()); ca1.credentialIds());
store.putCa(ca2); store.putCa(ca2);
Optional<CaRecord> loaded = store.getCa(ca1.caId()); Optional<CaRecord> loaded = store.getCa(ca1.caId());
@@ -232,6 +233,216 @@ public final class FilesystemPkiStoreTest {
System.out.println("caHistoryCreatesCurrentAndHistory...ok"); System.out.println("caHistoryCreatesCurrentAndHistory...ok");
} }
@Test
void caReferencesRequireValidStandaloneCredentialsAndPreserveOrder() throws Exception {
System.out.println("caReferencesRequireValidStandaloneCredentialsAndPreserveOrder");
Path root = tmp.resolve("store-ca-authority");
try (FilesystemPkiStore store = new FilesystemPkiStore(root, FsPkiStoreOptions.defaults())) {
Credential first = TestObjects.minimalCredential(store, "SERIAL-FIRST", "profile-ca");
Credential second = TestObjects.minimalCredential(store, "SERIAL-SECOND", "profile-ca");
store.putCredential(first);
store.putCredential(second);
CaRecord ordered = new CaRecord(new PkiId("ca-ordered"), CaKind.ROOT, CaState.ACTIVE,
new KeyRef("key-ordered"), new SubjectRef("CN=Ordered"),
List.of(second.credentialId(), first.credentialId()));
store.putCa(ordered);
assertEquals(List.of(second.credentialId(), first.credentialId()),
store.getCa(ordered.caId()).orElseThrow().credentialIds());
CaRecord missing = new CaRecord(new PkiId("ca-missing"), CaKind.ROOT, CaState.ACTIVE,
new KeyRef("key-missing"), new SubjectRef("CN=Missing"),
List.of(new PkiId("credential-missing")));
assertThrows(IllegalStateException.class, () -> store.putCa(missing));
Files.write(root.resolve("staged-content").resolve(first.content().contentId() + ".content"),
new byte[] { 9, 9, 9 });
assertThrows(IllegalStateException.class, () -> store.getCa(ordered.caId()));
assertThrows(IllegalStateException.class, store::listCas);
}
System.out.println("caReferencesRequireValidStandaloneCredentialsAndPreserveOrder...ok");
}
@Test
void oldStoreVersionIsRejected() throws Exception {
System.out.println("oldStoreVersionIsRejected");
Path root = tmp.resolve("store-old-version");
Files.createDirectories(root);
Files.writeString(root.resolve(FsPaths.VERSION_FILE), "v2");
assertThrows(IllegalStateException.class,
() -> new FilesystemPkiStore(root, FsPkiStoreOptions.defaults()));
System.out.println("oldStoreVersionIsRejected...ok");
}
@Test
void snapshotReconstructsSharedCredentialOnceWithNewTargetReference() throws Exception {
System.out.println("snapshotReconstructsSharedCredentialOnceWithNewTargetReference");
Path root = tmp.resolve("store-snapshot-authority");
Path snapshot = tmp.resolve("snapshot-authority");
FsPkiStoreOptions options = nonStrictSnapshotOptions();
PkiId credentialId;
String sourceContentId;
try (FilesystemPkiStore source = new FilesystemPkiStore(root, options)) {
Credential shared = TestObjects.minimalCredential(source, "SERIAL-SHARED", "profile-ca");
source.putCredential(shared);
credentialId = shared.credentialId();
sourceContentId = shared.content().contentId();
source.putCa(new CaRecord(new PkiId("ca-shared-one"), CaKind.ROOT, CaState.ACTIVE,
new KeyRef("key-shared-one"), new SubjectRef("CN=Shared One"), List.of(credentialId)));
source.putCa(new CaRecord(new PkiId("ca-shared-two"), CaKind.ROOT, CaState.ACTIVE,
new KeyRef("key-shared-two"), new SubjectRef("CN=Shared Two"), List.of(credentialId)));
source.exportSnapshot(snapshot, Instant.now());
}
try (FilesystemPkiStore restored = new FilesystemPkiStore(snapshot, options)) {
assertEquals(2, restored.listCas().size());
Credential credential = restored.getCredential(credentialId).orElseThrow();
assertFalse(sourceContentId.equals(credential.content().contentId()));
assertEquals(List.of(credentialId),
restored.getCa(new PkiId("ca-shared-one")).orElseThrow().credentialIds());
}
try (java.util.stream.Stream<Path> records = Files.list(snapshot.resolve("credentials").resolve("by-id"))) {
assertEquals(1, records.filter(Files::isRegularFile).count());
}
assertFalse(Files.exists(snapshot.resolve("staged-content").resolve(sourceContentId + ".content")));
assertFalse(Files.exists(snapshot.resolve("staged-content").resolve(sourceContentId + ".meta")));
assertFalse(Files.exists(snapshot.resolve("staged-content").resolve(sourceContentId + ".owners")));
System.out.println("snapshotReconstructsSharedCredentialOnceWithNewTargetReference...ok");
}
@Test
void snapshotPreservesStandaloneCredentialWithNewReferenceAndResolvableCa() throws Exception {
System.out.println("snapshotPreservesStandaloneCredentialWithNewReferenceAndResolvableCa");
Path root = tmp.resolve("store-snapshot-standalone");
Path snapshot = tmp.resolve("snapshot-standalone");
FsPkiStoreOptions options = nonStrictSnapshotOptions();
PkiId standaloneId;
String standaloneSourceContentId;
PkiId caId;
try (FilesystemPkiStore source = new FilesystemPkiStore(root, options)) {
Credential standalone = TestObjects.minimalCredential(source, "SERIAL-END-ENTITY", "profile-leaf");
source.putCredential(standalone);
standaloneId = standalone.credentialId();
standaloneSourceContentId = standalone.content().contentId();
CaRecord ca = TestObjects.minimalCaRecord(source, "ca-with-authority", CaState.ACTIVE);
source.putCa(ca);
caId = ca.caId();
source.exportSnapshot(snapshot, Instant.now());
}
try (FilesystemPkiStore restored = new FilesystemPkiStore(snapshot, options)) {
Credential standalone = restored.getCredential(standaloneId).orElseThrow();
assertFalse(standaloneSourceContentId.equals(standalone.content().contentId()));
CaRecord ca = restored.getCa(caId).orElseThrow();
assertTrue(restored.getCredential(ca.credentialIds().get(0)).isPresent());
}
System.out.println("snapshotPreservesStandaloneCredentialWithNewReferenceAndResolvableCa...ok");
}
@Test
void snapshotPreservesSharedStatusContentWithoutStaleCredentialOwner() throws Exception {
System.out.println("snapshotPreservesSharedStatusContentWithoutStaleCredentialOwner");
Path root = tmp.resolve("store-snapshot-shared-status");
Path snapshot = tmp.resolve("snapshot-shared-status");
FsPkiStoreOptions options = nonStrictSnapshotOptions();
PkiId credentialId;
PkiId statusId = new PkiId("status-shared-content");
String sourceContentId;
byte[] expected = new byte[] { 1, 2, 3 };
try (FilesystemPkiStore source = new FilesystemPkiStore(root, options)) {
Credential credential = TestObjects.minimalCredential(source, "SERIAL-SHARED-STATUS", "profile-ca");
source.putCredential(credential);
credentialId = credential.credentialId();
sourceContentId = credential.content().contentId();
StatusObject status = new StatusObject(statusId, credential.formatId(), credential.issuerRef().caId(),
StatusObjectType.CRL, Instant.EPOCH, Optional.empty(), credential.content(),
TestObjects.emptyAttributes());
source.putStatusObject(status);
source.exportSnapshot(snapshot, Instant.now());
}
try (FilesystemPkiStore restored = new FilesystemPkiStore(snapshot, options)) {
Credential credential = restored.getCredential(credentialId).orElseThrow();
StatusObject status = restored.getStatusObject(statusId).orElseThrow();
assertFalse(sourceContentId.equals(credential.content().contentId()));
assertEquals(sourceContentId, status.content().contentId());
assertArrayEquals(expected, zeroecho.pki.testkit.PkiTestRuntime.readContent(restored, credential.content()));
assertArrayEquals(expected, zeroecho.pki.testkit.PkiTestRuntime.readContent(restored, status.content()));
}
assertTrue(Files.exists(snapshot.resolve("staged-content").resolve(sourceContentId + ".content")));
assertTrue(Files.exists(snapshot.resolve("staged-content").resolve(sourceContentId + ".meta")));
assertFalse(Files.exists(snapshot.resolve("staged-content").resolve(sourceContentId + ".owners")));
System.out.println("snapshotPreservesSharedStatusContentWithoutStaleCredentialOwner...ok");
}
@Test
void snapshotBuildFailureCleansOwnedTemporaryRootAndPublishesNothing() throws Exception {
System.out.println("snapshotBuildFailureCleansOwnedTemporaryRootAndPublishesNothing");
Path root = tmp.resolve("store-snapshot-build-failure");
Path snapshot = tmp.resolve("snapshot-build-failure");
FsPkiStoreOptions options = nonStrictSnapshotOptions();
try (FilesystemPkiStore source = new FilesystemPkiStore(root, options)) {
Credential standalone = TestObjects.minimalCredential(source, "SERIAL-BUILD-FAIL", "profile-leaf");
source.putCredential(standalone);
Files.delete(root.resolve("SIGNING_TIME_WATERMARK"));
assertThrows(IllegalStateException.class, () -> source.exportSnapshot(snapshot, Instant.now()));
assertFalse(Files.exists(snapshot));
}
try (java.util.stream.Stream<Path> siblings = Files.list(tmp)) {
assertFalse(siblings.anyMatch(path -> path.getFileName().toString().startsWith(".zeroecho-snapshot-")));
}
System.out.println("snapshotBuildFailureCleansOwnedTemporaryRootAndPublishesNothing...ok");
}
@Test
void snapshotRejectsExistingTargetWithoutChangingIt() throws Exception {
System.out.println("snapshotRejectsExistingTargetWithoutChangingIt");
Path root = tmp.resolve("store-snapshot-existing-target");
Path snapshot = tmp.resolve("snapshot-existing-target");
Path sentinel = snapshot.resolve("sentinel.txt");
Files.createDirectories(snapshot);
Files.writeString(sentinel, "owner data");
try (FilesystemPkiStore source = new FilesystemPkiStore(root, nonStrictSnapshotOptions())) {
assertThrows(IllegalStateException.class, () -> source.exportSnapshot(snapshot, Instant.now()));
}
assertEquals("owner data", Files.readString(sentinel));
try (java.util.stream.Stream<Path> siblings = Files.list(tmp)) {
assertFalse(siblings.anyMatch(path -> path.getFileName().toString().startsWith(".zeroecho-snapshot-")));
}
System.out.println("snapshotRejectsExistingTargetWithoutChangingIt...ok");
}
@Test
void snapshotStrictFailsAndNonStrictOmitsOnlyInconsistentCa() throws Exception {
System.out.println("snapshotStrictFailsAndNonStrictOmitsOnlyInconsistentCa");
Path root = tmp.resolve("store-snapshot-inconsistent");
Path strictSnapshot = tmp.resolve("snapshot-inconsistent-strict");
Path nonStrictSnapshot = tmp.resolve("snapshot-inconsistent-nonstrict");
FsPkiStoreOptions nonStrict = nonStrictSnapshotOptions();
try (FilesystemPkiStore source = new FilesystemPkiStore(root, nonStrict)) {
CaRecord valid = TestObjects.minimalCaRecord(source, "ca-valid", CaState.ACTIVE);
CaRecord inconsistent = TestObjects.minimalCaRecord(source, "ca-inconsistent", CaState.ACTIVE);
source.putCa(valid);
source.putCa(inconsistent);
Files.delete(new FsPaths(root).credentialPath(inconsistent.credentialIds().get(0)));
FsPkiStoreOptions strict = strictSnapshotOptions();
assertThrows(IllegalStateException.class,
() -> new FsSnapshotExporter(strict).exportSnapshot(source, strictSnapshot, Instant.now()));
assertFalse(Files.exists(strictSnapshot));
source.exportSnapshot(nonStrictSnapshot, Instant.now());
}
try (FilesystemPkiStore restored = new FilesystemPkiStore(nonStrictSnapshot, nonStrict)) {
assertEquals(List.of("ca-valid"), restored.listCas().stream()
.map(ca -> ca.caId().value()).toList());
}
System.out.println("snapshotStrictFailsAndNonStrictOmitsOnlyInconsistentCa...ok");
}
@Test @Test
void revocationJournalPersistsLegalTransitions() throws Exception { void revocationJournalPersistsLegalTransitions() throws Exception {
System.out.println("revocationJournalPersistsLegalTransitions"); System.out.println("revocationJournalPersistsLegalTransitions");
@@ -553,9 +764,9 @@ public final class FilesystemPkiStoreTest {
SubjectRef subjectRef = new SubjectRef("CN=" + caId); SubjectRef subjectRef = new SubjectRef("CN=" + caId);
Credential cred = minimalCredential(store, "CA-" + caId, "profile-ca"); Credential cred = minimalCredential(store, "CA-" + caId, "profile-ca");
List<Credential> caCredentials = List.of(cred); store.putCredential(cred);
return new CaRecord(id, CaKind.ROOT, state, issuerKeyRef, subjectRef,
return new CaRecord(id, CaKind.ROOT, state, issuerKeyRef, subjectRef, caCredentials); List.of(cred.credentialId()));
} }
static CertificateProfile minimalProfile(String profileId) { static CertificateProfile minimalProfile(String profileId) {

View File

@@ -59,12 +59,16 @@ import zeroecho.pki.api.EncodedObject;
import zeroecho.pki.api.Encoding; import zeroecho.pki.api.Encoding;
import zeroecho.pki.api.FormatId; import zeroecho.pki.api.FormatId;
import zeroecho.pki.api.IssuerRef; import zeroecho.pki.api.IssuerRef;
import zeroecho.pki.api.KeyRef;
import zeroecho.pki.api.PkiId; import zeroecho.pki.api.PkiId;
import zeroecho.pki.api.SubjectRef; import zeroecho.pki.api.SubjectRef;
import zeroecho.pki.api.Validity; import zeroecho.pki.api.Validity;
import zeroecho.pki.api.attr.AttributeId; import zeroecho.pki.api.attr.AttributeId;
import zeroecho.pki.api.attr.AttributeSet; import zeroecho.pki.api.attr.AttributeSet;
import zeroecho.pki.api.attr.AttributeValue; import zeroecho.pki.api.attr.AttributeValue;
import zeroecho.pki.api.ca.CaKind;
import zeroecho.pki.api.ca.CaRecord;
import zeroecho.pki.api.ca.CaState;
import zeroecho.pki.api.credential.CaProfileBinding; import zeroecho.pki.api.credential.CaProfileBinding;
import zeroecho.pki.api.credential.Credential; import zeroecho.pki.api.credential.Credential;
import zeroecho.pki.api.credential.CredentialProfileBinding; import zeroecho.pki.api.credential.CredentialProfileBinding;
@@ -242,6 +246,30 @@ final class FsCodecTest {
assertInvalid(encoded); assertInvalid(encoded);
} }
@Test
void caRecordRoundTripsOnlyOrderedCredentialIdentifiersAndRejectsEmbeddedValueTag() {
System.out.println("caRecordRoundTripsOnlyOrderedCredentialIdentifiersAndRejectsEmbeddedValueTag");
PkiId first = new PkiId("credential-first");
PkiId second = new PkiId("credential-second");
CaRecord original = new CaRecord(new PkiId("ca-codec"), CaKind.ROOT, CaState.ACTIVE,
new KeyRef("key-codec"), new SubjectRef("CN=Codec"), List.of(first, second));
byte[] encoded = FsCodec.encode(FsCodec.CA_RECORD, original);
CaRecord decoded = FsCodec.decode(FsCodec.CA_RECORD, encoded);
assertEquals(original, decoded);
assertEquals(List.of(first, second), decoded.credentialIds());
byte[] embeddedValueTag = encoded.clone();
int firstIdentifier = indexOf(embeddedValueTag, first.value().getBytes(StandardCharsets.UTF_8));
assertTrue(firstIdentifier > 0);
int pkiIdTag = findPrevious(embeddedValueTag, firstIdentifier, (byte) 20);
assertTrue(pkiIdTag >= 0);
embeddedValueTag[pkiIdTag] = 34;
assertThrows(IllegalStateException.class,
() -> FsCodec.decode(FsCodec.CA_RECORD, embeddedValueTag));
System.out.println("caRecordRoundTripsOnlyOrderedCredentialIdentifiersAndRejectsEmbeddedValueTag...ok");
}
private static int indexOf(byte[] source, byte[] target) { private static int indexOf(byte[] source, byte[] target) {
for (int index = 0; index <= source.length - target.length; index++) { for (int index = 0; index <= source.length - target.length; index++) {
boolean matches = true; boolean matches = true;
@@ -258,6 +286,15 @@ final class FsCodecTest {
return -1; return -1;
} }
private static int findPrevious(byte[] source, int startExclusive, byte value) {
for (int index = startExclusive - 1; index >= 0; index--) {
if (source[index] == value) {
return index;
}
}
return -1;
}
private static ParsedCertificationRequest roundTripRequest(AttributeSet attributes) { private static ParsedCertificationRequest roundTripRequest(AttributeSet attributes) {
byte[] encoded = FsCodec.encode(FsCodec.PARSED_REQUEST, request(attributes)); byte[] encoded = FsCodec.encode(FsCodec.PARSED_REQUEST, request(attributes));
return FsCodec.decode(FsCodec.PARSED_REQUEST, encoded); return FsCodec.decode(FsCodec.PARSED_REQUEST, encoded);

View File

@@ -59,10 +59,12 @@ import zeroecho.pki.api.EncodedObject;
import zeroecho.pki.api.Encoding; import zeroecho.pki.api.Encoding;
import zeroecho.pki.api.IssuanceService; import zeroecho.pki.api.IssuanceService;
import zeroecho.pki.api.KeyRef; import zeroecho.pki.api.KeyRef;
import zeroecho.pki.api.PkiId;
import zeroecho.pki.api.ProfileService; import zeroecho.pki.api.ProfileService;
import zeroecho.pki.api.RevocationService; import zeroecho.pki.api.RevocationService;
import zeroecho.pki.api.StatusObjectService; import zeroecho.pki.api.StatusObjectService;
import zeroecho.pki.api.content.DurableContentReference; import zeroecho.pki.api.content.DurableContentReference;
import zeroecho.pki.api.credential.Credential;
import zeroecho.pki.api.credential.EffectiveCredentialStatusResolver; import zeroecho.pki.api.credential.EffectiveCredentialStatusResolver;
import zeroecho.pki.api.profile.BuiltInCertificateProfileCatalog; import zeroecho.pki.api.profile.BuiltInCertificateProfileCatalog;
import zeroecho.pki.impl.audit.InMemoryAuditSink; import zeroecho.pki.impl.audit.InMemoryAuditSink;
@@ -206,6 +208,11 @@ public final class PkiTestRuntime implements AutoCloseable {
return readContent(store, credential.content()); return readContent(store, credential.content());
} }
public Credential caCredential(zeroecho.pki.api.ca.CaRecord ca, int index) {
PkiId credentialId = ca.credentialIds().get(index);
return store.getCredential(credentialId).orElseThrow();
}
private record UntrustedReference(String storeId, String contentId, Encoding encoding, long length, String sha256, private record UntrustedReference(String storeId, String contentId, Encoding encoding, long length, String sha256,
DurableContentReference.Lifecycle lifecycle) implements DurableContentReference { DurableContentReference.Lifecycle lifecycle) implements DurableContentReference {
} }