diff --git a/pki/src/main/java/zeroecho/pki/api/CaService.java b/pki/src/main/java/zeroecho/pki/api/CaService.java
index 5b8c4f4..9a5470b 100644
--- a/pki/src/main/java/zeroecho/pki/api/CaService.java
+++ b/pki/src/main/java/zeroecho/pki/api/CaService.java
@@ -152,6 +152,12 @@ public interface CaService {
/**
* Retrieves a CA record.
*
+ *
+ * The returned record contains ordered credential identifiers. Credential
+ * content and metadata remain authoritative only through the configured
+ * credential store.
+ *
+ *
* @param caId CA identifier
* @return CA record
* @throws IllegalArgumentException if {@code caId} is invalid
@@ -162,6 +168,11 @@ public interface CaService {
/**
* Lists CA records matching query constraints.
*
+ *
+ * Returned records contain ordered credential identifiers rather than embedded
+ * credential values.
+ *
+ *
* @param query query constraints
* @return list of CA records
* @throws IllegalArgumentException if {@code query} is invalid
diff --git a/pki/src/main/java/zeroecho/pki/api/ca/CaRecord.java b/pki/src/main/java/zeroecho/pki/api/ca/CaRecord.java
index 9c2042e..ed6917a 100644
--- a/pki/src/main/java/zeroecho/pki/api/ca/CaRecord.java
+++ b/pki/src/main/java/zeroecho/pki/api/ca/CaRecord.java
@@ -33,15 +33,17 @@
******************************************************************************/
package zeroecho.pki.api.ca;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import zeroecho.pki.api.KeyRef;
import zeroecho.pki.api.PkiId;
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.
*
*
* 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
* reference)
* @param subjectRef normalized subject reference
- * @param caCredentials CA credentials currently associated with the entity
- * (historical and active)
+ * @param credentialIds ordered identifiers of the credentials currently
+ * 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,
- List caCredentials) {
+ List credentialIds) {
/**
* 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 {
if (caId == null) {
@@ -85,8 +90,18 @@ public record CaRecord(PkiId caId, CaKind kind, CaState state, KeyRef issuerKeyR
if (subjectRef == null) {
throw new IllegalArgumentException("subjectRef must not be null");
}
- if (caCredentials == null) {
- throw new IllegalArgumentException("caCredentials must not be null");
+ if (credentialIds == null) {
+ throw new IllegalArgumentException("credentialIds must not be null");
}
+ Set 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);
}
}
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/DefaultCaService.java b/pki/src/main/java/zeroecho/pki/impl/core/DefaultCaService.java
index 7bcbecf..fee1734 100644
--- a/pki/src/main/java/zeroecho/pki/impl/core/DefaultCaService.java
+++ b/pki/src/main/java/zeroecho/pki/impl/core/DefaultCaService.java
@@ -359,10 +359,10 @@ public final class DefaultCaService implements CaService {
CredentialProfileBindings.requireCaBinding(credential.profileBinding(), request.profileReference());
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);
+ CaRecord ca = new CaRecord(caId, CaKind.ROOT, CaState.ACTIVE, keyRef, request.subjectRef(),
+ List.of(credential.credentialId()));
+ store.putCa(ca);
return caId;
}
@@ -447,7 +447,7 @@ public final class DefaultCaService implements CaService {
requireValidImportedRoot(command, holder);
store.putCredential(credential);
CaRecord ca = new CaRecord(caId, CaKind.ROOT, CaState.ACTIVE, command.keyRef(), request.subjectRef(),
- List.of(credential));
+ List.of(credential.credentialId()));
store.putCa(ca);
return caId;
}
@@ -499,7 +499,7 @@ public final class DefaultCaService implements CaService {
CaRecord issuer = getCa(command.issuerCaId());
ensureActive(issuer, "issuer");
- if (issuer.caCredentials().isEmpty()) {
+ if (issuer.credentialIds().isEmpty()) {
throw new PkiException("Issuer CA has no credentials");
}
if (!framework.formatId().equals(command.formatId())) {
@@ -552,7 +552,7 @@ public final class DefaultCaService implements CaService {
store.putCredential(cred);
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);
return caId;
}
@@ -645,8 +645,8 @@ public final class DefaultCaService implements CaService {
BACKEND_CRED_MISMATCH);
store.putCredential(cred);
- List updated = new ArrayList<>(subject.caCredentials());
- updated.add(cred);
+ List updated = new ArrayList<>(subject.credentialIds());
+ updated.add(cred.credentialId());
CaRecord updatedCa = new CaRecord(subject.caId(), subject.kind(), subject.state(), subject.issuerKeyRef(),
subject.subjectRef(), List.copyOf(updated));
store.putCa(updatedCa);
@@ -731,7 +731,7 @@ public final class DefaultCaService implements CaService {
}
CaRecord updated = new CaRecord(existing.caId(), existing.kind(), state, existing.issuerKeyRef(),
- existing.subjectRef(), existing.caCredentials());
+ existing.subjectRef(), existing.credentialIds());
store.putCa(updated);
if (LOG.isLoggable(Level.INFO)) {
@@ -783,10 +783,11 @@ public final class DefaultCaService implements CaService {
return false;
}
if (query.formatId().isPresent()) {
- if (r.caCredentials().isEmpty()) {
+ if (r.credentialIds().isEmpty()) {
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 true;
@@ -803,7 +804,8 @@ public final class DefaultCaService implements CaService {
EffectiveCredentialStatusResolver.Evaluation evaluation) {
Credential lastRejected = 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())) {
continue;
}
@@ -828,6 +830,11 @@ public final class DefaultCaService implements CaService {
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) {
try {
CaProofGate.ManagedKeyProof proof = proofGate.proveManagedKey(command.keyRef(), command.formatId(),
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/DefaultIssuanceService.java b/pki/src/main/java/zeroecho/pki/impl/core/DefaultIssuanceService.java
index 3878015..14cc188 100644
--- a/pki/src/main/java/zeroecho/pki/impl/core/DefaultIssuanceService.java
+++ b/pki/src/main/java/zeroecho/pki/impl/core/DefaultIssuanceService.java
@@ -234,7 +234,7 @@ public final class DefaultIssuanceService implements IssuanceService {
if (issuer.state() != CaState.ACTIVE) {
throw new PkiException("Issuer CA not ACTIVE");
}
- if (issuer.caCredentials().isEmpty()) {
+ if (issuer.credentialIds().isEmpty()) {
throw new PkiException("Issuer CA has no credentials");
}
@@ -305,7 +305,8 @@ public final class DefaultIssuanceService implements IssuanceService {
Credential lastRejected = 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())) {
continue;
}
@@ -330,6 +331,11 @@ public final class DefaultIssuanceService implements IssuanceService {
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) {
ParsedCertificationRequest supplied = command.request();
byte[] csrDer = extractCsrDer(supplied);
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/DefaultStatusObjectService.java b/pki/src/main/java/zeroecho/pki/impl/core/DefaultStatusObjectService.java
index 78ac0fc..9d01636 100644
--- a/pki/src/main/java/zeroecho/pki/impl/core/DefaultStatusObjectService.java
+++ b/pki/src/main/java/zeroecho/pki/impl/core/DefaultStatusObjectService.java
@@ -214,7 +214,7 @@ public final class DefaultStatusObjectService implements StatusObjectService {
if (ca.state() != CaState.ACTIVE) {
throw new PkiException("Issuer CA not ACTIVE");
}
- if (ca.caCredentials().isEmpty()) {
+ if (ca.credentialIds().isEmpty()) {
throw new PkiException("Issuer CA has no credentials");
}
EffectiveCredentialStatusResolver.Evaluation statusEvaluation = statusResolver.beginEvaluation();
@@ -466,9 +466,10 @@ public final class DefaultStatusObjectService implements StatusObjectService {
EffectiveCredentialStatusResolver.Evaluation evaluation) {
Credential lastRejected = null;
EffectiveCredentialStatus lastStatus = null;
- List credentials = ca.caCredentials();
- for (int index = credentials.size() - 1; index >= 0; index--) {
- Credential credential = credentials.get(index);
+ List credentialIds = ca.credentialIds();
+ for (int index = credentialIds.size() - 1; index >= 0; index--) {
+ Credential credential = store.getCredential(credentialIds.get(index))
+ .orElseThrow(DefaultStatusObjectService::crlGenerationFailure);
if (credential == null || !command.formatId().equals(credential.formatId())) {
continue;
}
diff --git a/pki/src/main/java/zeroecho/pki/impl/fs/FilesystemPkiStore.java b/pki/src/main/java/zeroecho/pki/impl/fs/FilesystemPkiStore.java
index f4e3f3b..f3a7dae 100644
--- a/pki/src/main/java/zeroecho/pki/impl/fs/FilesystemPkiStore.java
+++ b/pki/src/main/java/zeroecho/pki/impl/fs/FilesystemPkiStore.java
@@ -166,7 +166,7 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
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_OWNER_NAMESPACE = "io.zeroecho.pki.signing-owner";
private static final int CURRENT_SIGN_RECORD_VERSION = 2;
@@ -401,8 +401,11 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
*
* 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
- * history-tracked entities. The signing namespace, monotonic signing-time
- * watermark, and authoritative signing workflow records are safety metadata:
+ * history-tracked entities. Standalone credential authority is reconstructed by
+ * 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
* historical reconstruction could permit identifier reuse or lose a terminal
* result.
@@ -417,13 +420,30 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
requireStoreUsable();
Objects.requireNonNull(targetRoot, "targetRoot");
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 snapshotNonCredentialContentIds() {
+ requireStoreUsable();
+ Set 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
public void putCa(final CaRecord record) {
requireStoreUsable();
Objects.requireNonNull(record, "record");
+ validateCaCredentialReferences(record);
PkiId caId = record.caId();
Path current = this.paths.caCurrent(caId);
@@ -436,14 +456,15 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
requireStoreUsable();
Objects.requireNonNull(caId, "caId");
Path p = this.paths.caCurrent(caId);
- return readOptional(p, FsCodec.CA_RECORD);
+ return readOptional(p, FsCodec.CA_RECORD).map(this::validateCaCredentialReferences);
}
@Override
public List listCas() {
requireStoreUsable();
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
@@ -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
public void putRequest(final ParsedCertificationRequest request) {
requireStoreUsable();
diff --git a/pki/src/main/java/zeroecho/pki/impl/fs/FsCodec.java b/pki/src/main/java/zeroecho/pki/impl/fs/FsCodec.java
index feca88e..392718f 100644
--- a/pki/src/main/java/zeroecho/pki/impl/fs/FsCodec.java
+++ b/pki/src/main/java/zeroecho/pki/impl/fs/FsCodec.java
@@ -448,7 +448,7 @@ final class FsCodec {
private static final ValueSchema CREDENTIAL_VALUE = valueSchema(TYPE_CREDENTIAL_RECORD,
FsCodec::writeCredential, FsCodec::readCredential);
- private static final ValueSchema> CREDENTIALS = listOf(CREDENTIAL_VALUE);
+ private static final ValueSchema> PKI_IDS = listOf(PKI_ID);
/* package */ static final Schema CA_RECORD = topLevel(TOP_CA_RECORD, "CA_RECORD",
valueSchema(100, FsCodec::writeCaRecord, FsCodec::readCaRecord));
@@ -711,12 +711,12 @@ final class FsCodec {
writer.writeValue(CA_STATE, value.state());
writer.writeValue(KEY_REF, value.issuerKeyRef());
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 {
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 {
diff --git a/pki/src/main/java/zeroecho/pki/impl/fs/FsOperations.java b/pki/src/main/java/zeroecho/pki/impl/fs/FsOperations.java
index 9db35ad..87cc25e 100644
--- a/pki/src/main/java/zeroecho/pki/impl/fs/FsOperations.java
+++ b/pki/src/main/java/zeroecho/pki/impl/fs/FsOperations.java
@@ -158,6 +158,61 @@ final class FsOperations {
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.
*
diff --git a/pki/src/main/java/zeroecho/pki/impl/fs/FsPaths.java b/pki/src/main/java/zeroecho/pki/impl/fs/FsPaths.java
index 88efa11..2d09c56 100644
--- a/pki/src/main/java/zeroecho/pki/impl/fs/FsPaths.java
+++ b/pki/src/main/java/zeroecho/pki/impl/fs/FsPaths.java
@@ -186,6 +186,10 @@ final class FsPaths {
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)
// -------------------------------------------------------------------------
diff --git a/pki/src/main/java/zeroecho/pki/impl/fs/FsSnapshotExporter.java b/pki/src/main/java/zeroecho/pki/impl/fs/FsSnapshotExporter.java
index 57aa73b..26744e9 100644
--- a/pki/src/main/java/zeroecho/pki/impl/fs/FsSnapshotExporter.java
+++ b/pki/src/main/java/zeroecho/pki/impl/fs/FsSnapshotExporter.java
@@ -34,21 +34,35 @@
package zeroecho.pki.impl.fs;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.time.Instant;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Base64;
+import java.util.Collections;
import java.util.Comparator;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.logging.Level;
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.Code;
+import zeroecho.pki.spi.store.ContentSink;
/**
* Snapshot exporter ("time travel") for {@link FilesystemPkiStore}.
@@ -66,20 +80,28 @@ import zeroecho.pki.impl.ProfileLifecycleFailure.Code;
* For entities with history enabled, the exporter selects the latest
* history entry with timestamp {@code <= at}. If none exists, it may fall back
* to {@code current.bin} only when strict mode is disabled.
- * Write-once objects are copied as-is (they are immutable). This exporter
- * does not attempt to prune them by time unless an upstream index exists.
+ * Write-once objects other than credentials are copied as-is. Every valid
+ * standalone credential is streamed from the live source into a new target-owned
+ * content reference before selected ID-only CA records are published.
* Signing namespace, signing-time watermark, and authoritative
* {@code sign-workflows} records are current safety metadata copied at export
* time. They are intentionally not reconstructed at {@code at}: rolling them
* back could make a stable identifier reusable or discard a completed signing
* result.
*
+ *
+ *
+ * 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.
+ *
*/
final class FsSnapshotExporter {
private static final Logger LOG = Logger.getLogger(FsSnapshotExporter.class.getName());
private static final String ACTIVE_POINTER_FILE = "active.bin";
private static final String BINARY_EXTENSION = ".bin";
+ private static final int TRANSFER_BUFFER_BYTES = 16 * 1024;
private final FsPkiStoreOptions options;
@@ -92,42 +114,350 @@ final class FsSnapshotExporter {
* bearing causes or filesystem paths.
*/
@SuppressWarnings("PMD.PreserveStackTrace")
- /* default */ void exportSnapshot(final Path sourceRoot, final Path targetRoot, final Instant at) {
- Objects.requireNonNull(sourceRoot, "sourceRoot");
+ /* default */ void exportSnapshot(final FilesystemPkiStore source, final Path targetRoot, final Instant at) {
+ Objects.requireNonNull(source, "source");
Objects.requireNonNull(targetRoot, "targetRoot");
Objects.requireNonNull(at, "at");
try {
+ requireNewTarget(targetRoot);
+ Path sourceRoot = source.snapshotRoot();
List profiles = preflightImportedProfiles(sourceRoot.resolve("profiles"), at);
- FsOperations.ensureDir(targetRoot);
- FsPaths dst = new FsPaths(targetRoot);
+ SnapshotAuthority authority = new AuthorityPlanner(source, options).plan(at);
+ Set 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 selectMutableRecords(Path srcTree, Instant at, FsHistoryPolicy policy, boolean strict)
+ throws IOException {
+ if (!Files.exists(srcTree)) {
+ return List.of();
+ }
+ List selected = new ArrayList<>();
+ try (java.util.stream.Stream 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 cas = selectCas(at, inventory.credentials());
+ return new SnapshotAuthority(cas, inventory.credentials(), inventory.contentIds());
+ }
+
+ private CredentialInventory inventoryCredentials() throws IOException {
+ Map credentials = new LinkedHashMap<>();
+ Set 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 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 credentials, Set 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 selectCas(Instant at, Map credentials) throws IOException {
+ List selectedRecords = selectMutableRecords(source.snapshotRoot().resolve("cas"), at,
+ options.caHistoryPolicy(), options.strictSnapshotExport());
+ List 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_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("status"), targetRoot.resolve("status"));
copyTreeIfExists(sourceRoot.resolve("policy"), targetRoot.resolve("policy"));
copyTreeIfExists(sourceRoot.resolve("publications"), targetRoot.resolve("publications"));
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"));
- copyImportedProfilesAsOf(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
+ copyImportedProfilesAsOf(plan.profiles(), targetRoot.resolve("profiles"));
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) {
- throw new IllegalStateException(failure.getMessage());
- } catch (IOException e) {
- throw new IllegalStateException("Snapshot export failed: code=SNAPSHOT_EXPORT_FAILED");
+ private static void addCleanupFailure(Exception primary, Path temporaryRoot) {
+ try {
+ deleteOwnedTree(temporaryRoot);
+ } 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 transferred = new HashSet<>();
+ try (FilesystemPkiStore target = new FilesystemPkiStore(targetRoot, options)) {
+ for (Map.Entry 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 paths = Files.walk(root)) {
+ List 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 credentialContentIds,
+ Set nonCredentialContentIds) throws IOException {
+ if (!Files.exists(src)) {
+ return;
+ }
+ try (java.util.stream.Stream 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 credentialContentIds,
+ Set 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 {
if (!Files.isRegularFile(source)) {
throw new IllegalStateException("Required snapshot metadata is missing: " + source.getFileName());
@@ -309,6 +675,40 @@ final class FsSnapshotExporter {
}
}
+ private record CredentialInventory(Map credentials, Set contentIds) {
+ private CredentialInventory {
+ credentials = Collections.unmodifiableMap(new LinkedHashMap<>(credentials));
+ contentIds = Set.copyOf(contentIds);
+ }
+ }
+
+ private record SnapshotAuthority(List cas, Map credentials,
+ Set sourceCredentialContentIds) {
+ private SnapshotAuthority {
+ cas = List.copyOf(cas);
+ credentials = Collections.unmodifiableMap(new LinkedHashMap<>(credentials));
+ sourceCredentialContentIds = Set.copyOf(sourceCredentialContentIds);
+ }
+ }
+
+ private record SnapshotPlan(List profiles, SnapshotAuthority authority,
+ Set 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. */
private static final class SnapshotProfileFailure extends RuntimeException {
private static final long serialVersionUID = -4451876406166515230L;
diff --git a/pki/src/main/java/zeroecho/pki/spi/store/PkiStore.java b/pki/src/main/java/zeroecho/pki/spi/store/PkiStore.java
index baf1ea2..0d37b59 100644
--- a/pki/src/main/java/zeroecho/pki/spi/store/PkiStore.java
+++ b/pki/src/main/java/zeroecho/pki/spi/store/PkiStore.java
@@ -88,12 +88,15 @@ public interface PkiStore extends SignWorkflowStore {
*
*
* 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.
*
*
* @param record CA record (never {@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);
@@ -101,17 +104,22 @@ public interface PkiStore extends SignWorkflowStore {
* Retrieves a CA record.
*
* @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 IllegalStateException if retrieval fails
+ * @throws IllegalStateException if retrieval fails or a referenced credential
+ * is missing or invalid
*/
Optional getCa(PkiId caId);
/**
* Lists all stored CA records.
*
- * @return list of CA records (never {@code null})
- * @throws IllegalStateException if listing fails
+ * @return list of CA records (never {@code null}); every returned credential
+ * identifier has been validated against the standalone credential
+ * authority
+ * @throws IllegalStateException if listing fails or a referenced credential is
+ * missing or invalid
*/
List listCas();
diff --git a/pki/src/test/java/zeroecho/pki/api/ca/CaRecordTest.java b/pki/src/test/java/zeroecho/pki/api/ca/CaRecordTest.java
new file mode 100644
index 0000000..3502026
--- /dev/null
+++ b/pki/src/test/java/zeroecho/pki/api/ca/CaRecordTest.java
@@ -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 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 credentialIds) {
+ return new CaRecord(new PkiId("ca-test"), CaKind.ROOT, CaState.ACTIVE, new KeyRef("key-test"),
+ new SubjectRef("CN=Test"), credentialIds);
+ }
+}
diff --git a/pki/src/test/java/zeroecho/pki/e2e/CaProfileIssuanceEnforcementTest.java b/pki/src/test/java/zeroecho/pki/e2e/CaProfileIssuanceEnforcementTest.java
index 326218d..851facb 100644
--- a/pki/src/test/java/zeroecho/pki/e2e/CaProfileIssuanceEnforcementTest.java
+++ b/pki/src/test/java/zeroecho/pki/e2e/CaProfileIssuanceEnforcementTest.java
@@ -130,7 +130,7 @@ final class CaProfileIssuanceEnforcementTest {
"fixed-intermediate-ca", Optional.empty(), new SimpleAttributeSet()));
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 additionalHolder = new X509CertificateHolder(runtime.credentialBytes(additional));
assertEquals("Fixed Organization",
@@ -140,7 +140,7 @@ final class CaProfileIssuanceEnforcementTest {
assertEquals(holder.getSubject().toString(), intermediate.subjectRef().value());
assertEquals(intermediate.subjectRef(), credential.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()
.issueIntermediateCertificate(new IntermediateCertIssueCommand(runtime.framework().formatId(),
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();
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);
}
try (PkiTestRuntime reopened = PkiTestRuntime.create(store, directory.resolve("reopened-bus.log"),
Map.of(rootRef, rootKey, intermediateRef, intermediateKey))) {
assertEquals(rootProfile,
- ((CaProfileBinding) reopened.caService().getCa(rootId).caCredentials().get(0).profileBinding())
+ ((CaProfileBinding) reopened.caCredential(reopened.caService().getCa(rootId), 0).profileBinding())
.reference());
- assertEquals(intermediateProfile, ((CaProfileBinding) reopened.caService().getCa(intermediateId)
- .caCredentials().get(1).profileBinding()).reference());
+ assertEquals(intermediateProfile, ((CaProfileBinding) reopened
+ .caCredential(reopened.caService().getCa(intermediateId), 1).profileBinding()).reference());
}
}
@@ -221,10 +221,11 @@ final class CaProfileIssuanceEnforcementTest {
.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootId,
new SubjectRef("CN=Historical Intermediate"), "intermediate-ca",
Optional.of(intermediateRef), new SimpleAttributeSet()));
- CaProfileBinding issuerBinding = (CaProfileBinding) runtime.caService().getCa(rootId).caCredentials().get(0)
+ CaProfileBinding issuerBinding = (CaProfileBinding) runtime
+ .caCredential(runtime.caService().getCa(rootId), 0)
.profileBinding();
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);
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);
credentialA = issuedA.credentialId();
@@ -276,13 +277,13 @@ final class CaProfileIssuanceEnforcementTest {
assertEquals(versionOne, runtime.profileService().getActiveReference(profileId).orElseThrow());
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);
credentialB = issuedB.credentialId();
assertEquals(versionTwo, runtime.profileService().activateProfile(profileId, 2));
caC = createRoot(runtime, keyRefC, "CN=Version Switch Root C", profileId);
- Credential issuedC = onlyCredential(runtime.caService(), caC);
+ Credential issuedC = onlyCredential(runtime, caC);
assertCaProfileCredential(runtime, "C", issuedC, versionTwo, 2);
credentialC = issuedC.credentialId();
@@ -347,7 +348,7 @@ final class CaProfileIssuanceEnforcementTest {
rootProfile = runtime.profileService().importProfile(rootProfileDocument);
runtime.profileService().activateProfile(rootProfileId, 1);
rootId = createRoot(runtime, rootKeyRef, "CN=Version Switch Issuer Root", rootProfileId);
- Credential rootCredential = onlyCredential(runtime.caService(), rootId);
+ Credential rootCredential = onlyCredential(runtime, rootId);
assertCaProfileCredential(runtime, "issuer", rootCredential, rootProfile, 2);
versionOne = runtime.profileService().importProfile(versionOneDocument);
@@ -357,10 +358,10 @@ final class CaProfileIssuanceEnforcementTest {
caA = createIntermediate(runtime, rootId, keyRefA, "CN=Version Switch Intermediate A",
intermediateProfileId);
- Credential issuedA = onlyCredential(runtime.caService(), caA);
+ Credential issuedA = onlyCredential(runtime, caA);
assertCaProfileCredential(runtime, "A", issuedA, versionOne, 0);
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()
.getImportedVersion(intermediateProfileId, 1).orElseThrow();
@@ -375,14 +376,14 @@ final class CaProfileIssuanceEnforcementTest {
caB = createIntermediate(runtime, rootId, keyRefB, "CN=Version Switch Intermediate B",
intermediateProfileId);
- Credential issuedB = onlyCredential(runtime.caService(), caB);
+ Credential issuedB = onlyCredential(runtime, caB);
assertCaProfileCredential(runtime, "B", issuedB, versionOne, 0);
credentialB = issuedB.credentialId();
assertEquals(versionTwo, runtime.profileService().activateProfile(intermediateProfileId, 2));
caC = createIntermediate(runtime, rootId, keyRefC, "CN=Version Switch Intermediate C",
intermediateProfileId);
- Credential issuedC = onlyCredential(runtime.caService(), caC);
+ Credential issuedC = onlyCredential(runtime, caC);
assertCaProfileCredential(runtime, "C", issuedC, versionTwo, 1);
credentialC = issuedC.credentialId();
@@ -390,7 +391,7 @@ final class CaProfileIssuanceEnforcementTest {
0);
assertCaProfileCredential(runtime, "B-reread", runtime.store().getCredential(credentialB).orElseThrow(), versionOne,
0);
- assertCaProfileCredential(runtime, "issuer-reread", onlyCredential(runtime.caService(), rootId), rootProfile, 2);
+ assertCaProfileCredential(runtime, "issuer-reread", onlyCredential(runtime, rootId), rootProfile, 2);
ImportedCertificateProfileVersion unchanged = runtime.profileService()
.getImportedVersion(intermediateProfileId, 1).orElseThrow();
assertArrayEquals(persistedVersionOne, unchanged.canonicalJson());
@@ -405,7 +406,7 @@ final class CaProfileIssuanceEnforcementTest {
versionOne, 0);
assertCaProfileCredential(reopened, "C-restart", reopened.store().getCredential(credentialC).orElseThrow(),
versionTwo, 1);
- assertCaProfileCredential(reopened, "issuer-restart", onlyCredential(reopened.caService(), rootId),
+ assertCaProfileCredential(reopened, "issuer-restart", onlyCredential(reopened, rootId),
rootProfile, 2);
ImportedCertificateProfileVersion unchanged = reopened.profileService()
.getImportedVersion(intermediateProfileId, 1).orElseThrow();
@@ -429,12 +430,14 @@ final class CaProfileIssuanceEnforcementTest {
.replace("\"profileId\":\"root-ca\"", "\"profileId\":\"other-format-root\"")
.replace("\"formatId\":\"x509\"", "\"formatId\":\"other\"").getBytes(StandardCharsets.UTF_8));
CaRecord root = runtime.caService().getCa(rootId);
- Credential original = root.caCredentials().get(0);
- Credential mutated = new Credential(original.credentialId(), original.formatId(), original.issuerRef(),
- original.subjectRef(), original.validity(), original.serialOrUniqueId(), original.publicKeyId(),
- new CaProfileBinding(wrongFormat), original.status(), original.content(), original.attributes());
+ Credential original = runtime.caCredential(root, 0);
+ Credential mutated = new Credential(new PkiId(original.credentialId().value() + ":wrong-profile"),
+ original.formatId(), original.issuerRef(), original.subjectRef(), original.validity(),
+ 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(),
- root.subjectRef(), List.of(mutated)));
+ root.subjectRef(), List.of(mutated.credentialId())));
int signCount = runtime.submittedSignCount();
assertThrows(PkiException.class,
@@ -491,7 +494,7 @@ final class CaProfileIssuanceEnforcementTest {
service.issueIntermediateCertificate(new IntermediateCertIssueCommand(runtime.framework().formatId(),
rootId, intermediateId, "intermediate-ca", Optional.empty(), new SimpleAttributeSet()));
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"),
directory.resolve("import-bus.log"), Map.of(rootRef, rootKey))) {
@@ -518,7 +521,7 @@ final class CaProfileIssuanceEnforcementTest {
new IntermediateCertIssueCommand(runtime.framework().formatId(), rootId, rootId,
"intermediate-ca", Optional.empty(), new SimpleAttributeSet())));
assertEquals(signCount, runtime.submittedSignCount());
- assertEquals(1, runtime.caService().getCa(rootId).caCredentials().size());
+ assertEquals(1, runtime.caService().getCa(rootId).credentialIds().size());
}
}
@@ -545,7 +548,7 @@ final class CaProfileIssuanceEnforcementTest {
runtime.framework().formatId(), rootId, intermediateId, "intermediate-ca",
Optional.of(invalid), new SimpleAttributeSet())));
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))) {
PkiId rootId = source.caService().createRoot(new CaCreateCommand(source.framework().formatId(),
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"),
directory.resolve("target-bus.log"), Map.of(rootRef, rootKey))) {
PkiId imported = target.caService()
.importRoot(new CaImportCommand(target.framework().formatId(), new SubjectRef("CN=Imported Root"),
"root-ca", rootRef, 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(),
((CaProfileBinding) credential.profileBinding()).reference());
}
@@ -583,7 +586,7 @@ final class CaProfileIssuanceEnforcementTest {
.createRoot(new CaCreateCommand(source.framework().formatId(),
new SubjectRef("CN=Import Mutation Root"), "root-ca", Optional.of(rootRef),
new SimpleAttributeSet()));
- Credential sourceCredential = source.caService().getCa(rootId).caCredentials().get(0);
+ Credential sourceCredential = source.caCredential(source.caService().getCa(rootId), 0);
encoded = mutation.mutate(source, sourceCredential, rootKey);
}
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()));
}
- private static Credential onlyCredential(CaService service, PkiId caId) {
- CaRecord ca = service.getCa(caId);
- assertEquals(1, ca.caCredentials().size());
- return ca.caCredentials().get(0);
+ private static Credential onlyCredential(PkiTestRuntime runtime, PkiId caId) {
+ CaRecord ca = runtime.caService().getCa(caId);
+ assertEquals(1, ca.credentialIds().size());
+ return runtime.caCredential(ca, 0);
}
private static byte[] caProfileDocument(String builtInProfileId, String profileId, long profileVersion,
diff --git a/pki/src/test/java/zeroecho/pki/e2e/PkiCoreE2eTest.java b/pki/src/test/java/zeroecho/pki/e2e/PkiCoreE2eTest.java
index c552761..52b20b0 100644
--- a/pki/src/test/java/zeroecho/pki/e2e/PkiCoreE2eTest.java
+++ b/pki/src/test/java/zeroecho/pki/e2e/PkiCoreE2eTest.java
@@ -137,11 +137,12 @@ public final class PkiCoreE2eTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"), keys)) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
new SubjectRef("CN=Matrix Root"), "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"));
+ runtime.store().putCredential(unusable);
CaRecord root = runtime.caService().getCa(rootCaId);
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 resolved = new ArrayList<>();
EffectiveCredentialStatusResolver resolver = scriptedResolver(credential -> {
@@ -173,7 +174,7 @@ public final class PkiCoreE2eTest {
resolved.clear();
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,
runtime.framework().formatId(), emptyAttributes()));
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,
new SubjectRef("CN=H6 Intermediate"), "intermediate-ca", Optional.of(intermediateKeyRef),
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,
RevocationReason.KEY_COMPROMISE, emptyAttributes()));
int submissionsBeforeRejections = runtime.submittedSignCount();
@@ -230,7 +231,7 @@ public final class PkiCoreE2eTest {
assertEquals(submissionsBeforeRejections, runtime.submittedSignCount());
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,
new SubjectRef("CN=Rejected Intermediate"), "intermediate-ca",
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);
CountingIssuerBackend backend = new CountingIssuerBackend(runtime.issuerBackend());
IssuanceService issuance = runtime.issuanceService(backend, resolver);
@@ -332,7 +333,7 @@ public final class PkiCoreE2eTest {
int signCount = runtime.submittedSignCount();
int caCount = runtime.store().listCas().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
.issueEndEntity(new IssueEndEntityCommand(rootCaId, leafRequest, "default", Optional.empty())));
@@ -355,7 +356,7 @@ public final class PkiCoreE2eTest {
assertEquals(caCount, runtime.store().listCas().size());
assertEquals(statusCount, runtime.store().listStatusObjects(rootCaId).size());
assertEquals(intermediateCredentialCount,
- runtime.caService().getCa(intermediateCaId).caCredentials().size());
+ runtime.caService().getCa(intermediateCaId).credentialIds().size());
assertTrue(runtime.store().getCredential(rootCredential.credentialId()).isPresent());
assertFalse(runtime.auditSink().snapshot().toString().contains("DO_NOT_EXPOSE_REVOCATION_SENTINEL"));
}
diff --git a/pki/src/test/java/zeroecho/pki/e2e/PkiProofGateE2eTest.java b/pki/src/test/java/zeroecho/pki/e2e/PkiProofGateE2eTest.java
index 73e443d..f30348c 100644
--- a/pki/src/test/java/zeroecho/pki/e2e/PkiProofGateE2eTest.java
+++ b/pki/src/test/java/zeroecho/pki/e2e/PkiProofGateE2eTest.java
@@ -472,7 +472,7 @@ final class PkiProofGateE2eTest {
new SubjectRef("CN=Intermediate"), "intermediate-ca", Optional.of(intermediateKeyRef),
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));
assertEquals("CN=Root", firstHolder.getIssuer().toString());
assertEquals("CN=Intermediate", firstHolder.getSubject().toString());
@@ -538,7 +538,7 @@ final class PkiProofGateE2eTest {
runtime.framework().formatId(), rootCaId, intermediateCaId, "intermediate-ca",
Optional.empty(), new SimpleAttributeSet())));
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());
}
@@ -551,13 +551,13 @@ final class PkiProofGateE2eTest {
PkiId rootCaId = source.caService().createRoot(new CaCreateCommand(source.framework().formatId(),
new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef), new SimpleAttributeSet()));
rootCertificate = source
- .credentialBytes(source.caService().getCa(rootCaId).caCredentials().get(0)).clone();
+ .credentialBytes(source.caCredential(source.caService().getCa(rootCaId), 0)).clone();
PkiId intermediateCaId = source.caService()
.createIntermediate(new IntermediateCreateCommand(source.framework().formatId(), rootCaId,
new SubjectRef("CN=Intermediate"), "intermediate-ca", Optional.of(intermediateKeyRef),
new SimpleAttributeSet()));
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"));
leafCertificate = source.credentialBytes(source.issuanceService()
.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",
rootKeyRef, target.stageCredential(callerOwnedCertificate),
new SimpleAttributeSet()));
- assertTrue(target.caService().getCa(importedCaId).caCredentials().get(0)
+ assertTrue(target.caCredential(target.caService().getCa(importedCaId), 0)
.profileBinding() instanceof CaProfileBinding);
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");
@@ -787,12 +787,14 @@ final class PkiProofGateE2eTest {
runtime.store().getCredential(returned.credential().credentialId()).orElseThrow()));
CaRecord root = runtime.caService().getCa(rootCaId);
- Credential original = root.caCredentials().get(0);
- Credential revoked = new Credential(original.credentialId(), original.formatId(), original.issuerRef(),
+ Credential original = runtime.caCredential(root, 0);
+ Credential revoked = new Credential(new PkiId(original.credentialId().value() + ":revoked"),
+ original.formatId(), original.issuerRef(),
original.subjectRef(), original.validity(), original.serialOrUniqueId(), original.publicKeyId(),
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(),
- root.subjectRef(), List.of(revoked)));
+ root.subjectRef(), List.of(revoked.credentialId())));
int before = runtime.submittedSignCount();
assertThrows(PkiException.class, () -> runtime.issuanceService()
.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)),
Instant.now().minus(Duration.ofDays(1)));
- Credential expired = new Credential(original.credentialId(), original.formatId(), original.issuerRef(),
- original.subjectRef(), expiredValidity, original.serialOrUniqueId(), original.publicKeyId(),
- original.profileBinding(), CredentialStatus.ISSUED, original.content(), original.attributes());
+ Credential expired = new Credential(new PkiId(original.credentialId().value() + ":expired"),
+ original.formatId(), original.issuerRef(), original.subjectRef(), expiredValidity,
+ 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(),
- root.subjectRef(), List.of(expired)));
+ root.subjectRef(), List.of(expired.credentialId())));
assertThrows(PkiException.class, () -> runtime.issuanceService()
.issueEndEntity(new IssueEndEntityCommand(rootCaId, subject, "default", Optional.empty())));
assertEquals(before, runtime.submittedSignCount());
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());
AttributeSet hostileAttributes = new AttributeSet() {
@Override
@@ -857,7 +861,7 @@ final class PkiProofGateE2eTest {
Map.of(rootKeyRef, rootKey, intermediateKeyRef, intermediateKey))) {
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
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);
CredentialIssuerBackend delegate = runtime.issuerBackend();
for (BindingVariantMutation mutation : BindingVariantMutation.values()) {
@@ -910,7 +914,7 @@ final class PkiProofGateE2eTest {
Optional.empty(), new SimpleAttributeSet())),
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) {
assertTrue(runtime.store().getCredential(produced.get().credentialId()).isEmpty(), mutation.name());
}
@@ -935,7 +939,7 @@ final class PkiProofGateE2eTest {
() -> wrongSubjectService.issueIntermediateCertificate(
new IntermediateCertIssueCommand(runtime.framework().formatId(), rootCaId, intermediateCaId,
"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() {
@Override
@@ -960,7 +964,7 @@ final class PkiProofGateE2eTest {
() -> invalidSignatureService.issueIntermediateCertificate(
new IntermediateCertIssueCommand(runtime.framework().formatId(), rootCaId, intermediateCaId,
"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()) {
CaService maliciousExtensionService = runtime
@@ -970,7 +974,7 @@ final class PkiProofGateE2eTest {
runtime.framework().formatId(), rootCaId, intermediateCaId, "intermediate-ca",
Optional.empty(), new SimpleAttributeSet())),
variant.name());
- assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size(), variant.name());
+ assertEquals(1, runtime.caService().getCa(intermediateCaId).credentialIds().size(), variant.name());
}
AtomicReference rawCredential = new AtomicReference<>();
diff --git a/pki/src/test/java/zeroecho/pki/impl/core/DefaultStatusObjectServiceCrlTest.java b/pki/src/test/java/zeroecho/pki/impl/core/DefaultStatusObjectServiceCrlTest.java
index 3ebbc27..4ff2503 100644
--- a/pki/src/test/java/zeroecho/pki/impl/core/DefaultStatusObjectServiceCrlTest.java
+++ b/pki/src/test/java/zeroecho/pki/impl/core/DefaultStatusObjectServiceCrlTest.java
@@ -122,7 +122,7 @@ final class DefaultStatusObjectServiceCrlTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(root, root.resolve("bus.log"),
Map.of(rootKeyRef, rootKey))) {
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);
List reasons = activeReasons();
List serials = serials(reasons.size());
@@ -188,7 +188,7 @@ final class DefaultStatusObjectServiceCrlTest {
try (PkiTestRuntime runtime = PkiTestRuntime.create(root, root.resolve("bus.log"),
Map.of(rootKeyRef, rootKey))) {
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,
runtime.framework().formatId(), emptyAttributes());
diff --git a/pki/src/test/java/zeroecho/pki/impl/fs/FilesystemPkiStoreTest.java b/pki/src/test/java/zeroecho/pki/impl/fs/FilesystemPkiStoreTest.java
index b5ee4e0..5eac025 100644
--- a/pki/src/test/java/zeroecho/pki/impl/fs/FilesystemPkiStoreTest.java
+++ b/pki/src/test/java/zeroecho/pki/impl/fs/FilesystemPkiStoreTest.java
@@ -34,6 +34,7 @@
package zeroecho.pki.impl.fs;
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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -218,7 +219,7 @@ public final class FilesystemPkiStoreTest {
store.putCa(ca1);
CaRecord ca2 = new CaRecord(ca1.caId(), ca1.kind(), CaState.DISABLED, ca1.issuerKeyRef(), ca1.subjectRef(),
- ca1.caCredentials());
+ ca1.credentialIds());
store.putCa(ca2);
Optional loaded = store.getCa(ca1.caId());
@@ -232,6 +233,216 @@ public final class FilesystemPkiStoreTest {
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 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 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 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
void revocationJournalPersistsLegalTransitions() throws Exception {
System.out.println("revocationJournalPersistsLegalTransitions");
@@ -553,9 +764,9 @@ public final class FilesystemPkiStoreTest {
SubjectRef subjectRef = new SubjectRef("CN=" + caId);
Credential cred = minimalCredential(store, "CA-" + caId, "profile-ca");
- List caCredentials = List.of(cred);
-
- return new CaRecord(id, CaKind.ROOT, state, issuerKeyRef, subjectRef, caCredentials);
+ store.putCredential(cred);
+ return new CaRecord(id, CaKind.ROOT, state, issuerKeyRef, subjectRef,
+ List.of(cred.credentialId()));
}
static CertificateProfile minimalProfile(String profileId) {
diff --git a/pki/src/test/java/zeroecho/pki/impl/fs/FsCodecTest.java b/pki/src/test/java/zeroecho/pki/impl/fs/FsCodecTest.java
index a0f5583..e85db0f 100644
--- a/pki/src/test/java/zeroecho/pki/impl/fs/FsCodecTest.java
+++ b/pki/src/test/java/zeroecho/pki/impl/fs/FsCodecTest.java
@@ -59,12 +59,16 @@ import zeroecho.pki.api.EncodedObject;
import zeroecho.pki.api.Encoding;
import zeroecho.pki.api.FormatId;
import zeroecho.pki.api.IssuerRef;
+import zeroecho.pki.api.KeyRef;
import zeroecho.pki.api.PkiId;
import zeroecho.pki.api.SubjectRef;
import zeroecho.pki.api.Validity;
import zeroecho.pki.api.attr.AttributeId;
import zeroecho.pki.api.attr.AttributeSet;
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.Credential;
import zeroecho.pki.api.credential.CredentialProfileBinding;
@@ -242,6 +246,30 @@ final class FsCodecTest {
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) {
for (int index = 0; index <= source.length - target.length; index++) {
boolean matches = true;
@@ -258,6 +286,15 @@ final class FsCodecTest {
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) {
byte[] encoded = FsCodec.encode(FsCodec.PARSED_REQUEST, request(attributes));
return FsCodec.decode(FsCodec.PARSED_REQUEST, encoded);
diff --git a/pki/src/test/java/zeroecho/pki/testkit/PkiTestRuntime.java b/pki/src/test/java/zeroecho/pki/testkit/PkiTestRuntime.java
index fcebb40..fbff6d1 100644
--- a/pki/src/test/java/zeroecho/pki/testkit/PkiTestRuntime.java
+++ b/pki/src/test/java/zeroecho/pki/testkit/PkiTestRuntime.java
@@ -59,10 +59,12 @@ import zeroecho.pki.api.EncodedObject;
import zeroecho.pki.api.Encoding;
import zeroecho.pki.api.IssuanceService;
import zeroecho.pki.api.KeyRef;
+import zeroecho.pki.api.PkiId;
import zeroecho.pki.api.ProfileService;
import zeroecho.pki.api.RevocationService;
import zeroecho.pki.api.StatusObjectService;
import zeroecho.pki.api.content.DurableContentReference;
+import zeroecho.pki.api.credential.Credential;
import zeroecho.pki.api.credential.EffectiveCredentialStatusResolver;
import zeroecho.pki.api.profile.BuiltInCertificateProfileCatalog;
import zeroecho.pki.impl.audit.InMemoryAuditSink;
@@ -206,6 +208,11 @@ public final class PkiTestRuntime implements AutoCloseable {
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,
DurableContentReference.Lifecycle lifecycle) implements DurableContentReference {
}