diff --git a/pki/src/main/java/zeroecho/pki/api/revocation/RevocationCommand.java b/pki/src/main/java/zeroecho/pki/api/revocation/RevocationCommand.java
index a85ec8d..c2bbe3e 100644
--- a/pki/src/main/java/zeroecho/pki/api/revocation/RevocationCommand.java
+++ b/pki/src/main/java/zeroecho/pki/api/revocation/RevocationCommand.java
@@ -68,7 +68,7 @@ public sealed interface RevocationCommand
/** Validates and snapshots the command. */
public Hold {
Objects.requireNonNull(credentialId, "credentialId");
- attributes = RevocationJournal.snapshot(attributes);
+ attributes = RevocationTransition.snapshotAttributes(attributes);
}
}
@@ -82,7 +82,7 @@ public sealed interface RevocationCommand
/** Validates and snapshots the command. */
public Unhold {
Objects.requireNonNull(credentialId, "credentialId");
- attributes = RevocationJournal.snapshot(attributes);
+ attributes = RevocationTransition.snapshotAttributes(attributes);
}
}
@@ -102,7 +102,7 @@ public sealed interface RevocationCommand
if (reason == RevocationReason.CERTIFICATE_HOLD || reason == RevocationReason.REMOVE_FROM_CRL) {
throw new IllegalArgumentException("reason must be permanent");
}
- attributes = RevocationJournal.snapshot(attributes);
+ attributes = RevocationTransition.snapshotAttributes(attributes);
}
}
}
diff --git a/pki/src/main/java/zeroecho/pki/api/revocation/RevocationJournal.java b/pki/src/main/java/zeroecho/pki/api/revocation/RevocationJournal.java
deleted file mode 100644
index 54f58c3..0000000
--- a/pki/src/main/java/zeroecho/pki/api/revocation/RevocationJournal.java
+++ /dev/null
@@ -1,199 +0,0 @@
-/*******************************************************************************
- * Copyright (C) 2026, Leo Galambos
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * 3. All advertising materials mentioning features or use of this software must
- * display the following acknowledgement:
- * This product includes software developed by the Egothor project.
- *
- * 4. Neither the name of the copyright holder nor the names of its contributors
- * may be used to endorse or promote products derived from this software without
- * specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- ******************************************************************************/
-package zeroecho.pki.api.revocation;
-
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-
-import zeroecho.pki.api.PkiId;
-import zeroecho.pki.api.attr.AttributeId;
-import zeroecho.pki.api.attr.AttributeSet;
-import zeroecho.pki.api.attr.AttributeValue;
-
-/**
- * Immutable single-file authority for one credential's revocation lifecycle.
- *
- * @param credentialId credential namespace identity
- * @param transitions ordered committed transitions
- */
-public record RevocationJournal(PkiId credentialId, List transitions) {
-
- /** Current journal format version. */
- public static final int CURRENT_VERSION = 1;
-
- /**
- * Creates an immutable journal snapshot.
- */
- public RevocationJournal {
- Objects.requireNonNull(credentialId, "credentialId");
- Objects.requireNonNull(transitions, "transitions");
- transitions = List.copyOf(transitions);
- validate(transitions);
- }
-
- private static void validate(List transitions) {
- if (transitions.isEmpty()) {
- throw new IllegalArgumentException("transitions must not be empty");
- }
- RevocationState previous = null;
- Instant previousTime = null;
- long expectedRevision = 1L;
- for (RevocationTransition transition : transitions) {
- if (transition.revision() != expectedRevision
- || previousTime != null && transition.time().isBefore(previousTime)
- || !validTransition(previous, transition)) {
- throw new IllegalArgumentException("invalid revocation journal");
- }
- expectedRevision++;
- previous = transition.state();
- previousTime = transition.time();
- }
- }
-
- private static boolean validTransition(RevocationState previous, RevocationTransition transition) {
- boolean reasonValid = transition.state() == RevocationState.PERMANENTLY_REVOKED
- ? transition.permanentReason().filter(RevocationJournal::isPermanent).isPresent()
- : transition.permanentReason().isEmpty();
- if (!reasonValid || previous == RevocationState.PERMANENTLY_REVOKED) {
- return false;
- }
- if (previous == null) {
- return transition.state() == RevocationState.HELD
- || transition.state() == RevocationState.PERMANENTLY_REVOKED;
- }
- return switch (previous) {
- case CLEAR ->
- transition.state() == RevocationState.HELD || transition.state() == RevocationState.PERMANENTLY_REVOKED;
- case HELD -> transition.state() == RevocationState.CLEAR
- || transition.state() == RevocationState.PERMANENTLY_REVOKED;
- case PERMANENTLY_REVOKED -> false;
- };
- }
-
- private static boolean isPermanent(RevocationReason reason) {
- return reason != RevocationReason.CERTIFICATE_HOLD && reason != RevocationReason.REMOVE_FROM_CRL;
- }
-
- /**
- * Returns the last committed transition.
- *
- * @return last transition
- * @throws IllegalStateException if the journal is empty
- */
- public RevocationTransition latest() {
- if (transitions.isEmpty()) {
- throw new IllegalStateException("Revocation journal is empty");
- }
- return transitions.get(transitions.size() - 1);
- }
-
- // Defensive snapshotting necessarily allocates immutable entries while walking
- // the caller-owned attribute collection.
- @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
- /* default */ static AttributeSet snapshot(AttributeSet source) {
- Objects.requireNonNull(source, "source");
- List entries = new ArrayList<>();
- for (AttributeId id : source.ids()) {
- List values = new ArrayList<>();
- for (AttributeValue value : source.getAll(id)) {
- values.add(snapshotValue(value));
- }
- entries.add(new SnapshotEntry(id, List.copyOf(values)));
- }
- return new SnapshotAttributeSet(List.copyOf(entries));
- }
-
- private static AttributeValue snapshotValue(AttributeValue value) {
- Objects.requireNonNull(value, "value");
- if (value instanceof AttributeValue.BytesValue bytesValue) {
- return new AttributeValue.BytesValue(bytesValue.value().clone());
- }
- return value;
- }
-
- /**
- * One immutable attribute entry owned by the journal.
- */
- private record SnapshotEntry(AttributeId id, List values) {
- }
-
- /**
- * Immutable defensive implementation used for journal metadata ownership.
- */
- private static final class SnapshotAttributeSet implements AttributeSet {
- private final List entries;
-
- private SnapshotAttributeSet(List entries) {
- this.entries = entries;
- }
-
- @Override
- public Set ids() {
- Set ids = new LinkedHashSet<>();
- for (SnapshotEntry entry : entries) {
- ids.add(entry.id());
- }
- return Set.copyOf(ids);
- }
-
- @Override
- public Optional get(AttributeId id) {
- List values = getAll(id);
- return values.isEmpty() ? Optional.empty() : Optional.of(values.get(0));
- }
-
- // Byte values are copied per accessor invocation so callers cannot mutate
- // journal-owned storage.
- @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
- @Override
- public List getAll(AttributeId id) {
- Objects.requireNonNull(id, "id");
- for (SnapshotEntry entry : entries) {
- if (entry.id().equals(id)) {
- List copies = new ArrayList<>(entry.values().size());
- for (AttributeValue value : entry.values()) {
- copies.add(snapshotValue(value));
- }
- return List.copyOf(copies);
- }
- }
- return List.of();
- }
- }
-}
diff --git a/pki/src/main/java/zeroecho/pki/api/revocation/RevocationState.java b/pki/src/main/java/zeroecho/pki/api/revocation/RevocationState.java
index a8a28b7..ef65aae 100644
--- a/pki/src/main/java/zeroecho/pki/api/revocation/RevocationState.java
+++ b/pki/src/main/java/zeroecho/pki/api/revocation/RevocationState.java
@@ -34,7 +34,7 @@
package zeroecho.pki.api.revocation;
/**
- * Effective state recorded by an authoritative revocation journal.
+ * Effective state recorded by the authoritative global revocation log.
*/
public enum RevocationState {
/** Credential has no effective revocation restriction. */
diff --git a/pki/src/main/java/zeroecho/pki/api/revocation/RevocationTransition.java b/pki/src/main/java/zeroecho/pki/api/revocation/RevocationTransition.java
index c57f72a..c6c4550 100644
--- a/pki/src/main/java/zeroecho/pki/api/revocation/RevocationTransition.java
+++ b/pki/src/main/java/zeroecho/pki/api/revocation/RevocationTransition.java
@@ -34,12 +34,19 @@
package zeroecho.pki.api.revocation;
import java.time.Instant;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
import java.util.Optional;
+import java.util.Set;
+import zeroecho.pki.api.attr.AttributeId;
import zeroecho.pki.api.attr.AttributeSet;
+import zeroecho.pki.api.attr.AttributeValue;
/**
- * One committed transition in an authoritative revocation journal.
+ * One committed transition in the authoritative global revocation log.
*
* @param revision positive contiguous revision
* @param state resulting state
@@ -57,6 +64,76 @@ public record RevocationTransition(long revision, RevocationState state, Instant
if (revision <= 0L || state == null || time == null || permanentReason == null || attributes == null) {
throw new IllegalArgumentException("Invalid revocation transition");
}
- attributes = RevocationJournal.snapshot(attributes);
+ attributes = snapshotAttributes(attributes);
+ }
+
+ // Defensive snapshotting necessarily allocates immutable entries while walking
+ // the caller-owned attribute collection.
+ @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
+ /* default */ static AttributeSet snapshotAttributes(AttributeSet source) {
+ Objects.requireNonNull(source, "source");
+ List entries = new ArrayList<>();
+ for (AttributeId id : source.ids()) {
+ List values = new ArrayList<>();
+ for (AttributeValue value : source.getAll(id)) {
+ values.add(snapshotValue(value));
+ }
+ entries.add(new SnapshotEntry(id, List.copyOf(values)));
+ }
+ return new SnapshotAttributeSet(List.copyOf(entries));
+ }
+
+ private static AttributeValue snapshotValue(AttributeValue value) {
+ Objects.requireNonNull(value, "value");
+ if (value instanceof AttributeValue.BytesValue bytesValue) {
+ return new AttributeValue.BytesValue(bytesValue.value().clone());
+ }
+ return value;
+ }
+
+ /** One immutable attribute entry owned by a transition or command. */
+ private record SnapshotEntry(AttributeId id, List values) {
+ }
+
+ /** Immutable defensive implementation for revocation metadata ownership. */
+ private static final class SnapshotAttributeSet implements AttributeSet {
+ private final List entries;
+
+ private SnapshotAttributeSet(List entries) {
+ this.entries = entries;
+ }
+
+ @Override
+ public Set ids() {
+ Set ids = new LinkedHashSet<>();
+ for (SnapshotEntry entry : entries) {
+ ids.add(entry.id());
+ }
+ return Set.copyOf(ids);
+ }
+
+ @Override
+ public Optional get(AttributeId id) {
+ List values = getAll(id);
+ return values.isEmpty() ? Optional.empty() : Optional.of(values.get(0));
+ }
+
+ // Byte values are copied per accessor invocation so callers cannot mutate
+ // transition-owned storage.
+ @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
+ @Override
+ public List getAll(AttributeId id) {
+ Objects.requireNonNull(id, "id");
+ for (SnapshotEntry entry : entries) {
+ if (entry.id().equals(id)) {
+ List copies = new ArrayList<>(entry.values().size());
+ for (AttributeValue value : entry.values()) {
+ copies.add(snapshotValue(value));
+ }
+ return List.copyOf(copies);
+ }
+ }
+ return List.of();
+ }
}
}
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 d2b335d..ff6bd08 100644
--- a/pki/src/main/java/zeroecho/pki/impl/core/DefaultStatusObjectService.java
+++ b/pki/src/main/java/zeroecho/pki/impl/core/DefaultStatusObjectService.java
@@ -89,15 +89,15 @@ import zeroecho.pki.spi.store.RevocationSnapshot;
* the configured {@link PkiStore} and delegates format-specific object creation
* to the active {@link CredentialFramework}. The store acts as the
* authoritative source of issuer CA state, issuer credentials, revocation
- * journals, and previously generated status objects.
+ * transitions, and previously generated status objects.
*
*
*
* The current runtime implementation primarily supports generation workflows
* that require issuer certificate material and issuer signing key indirection
* to be provided through status-object attributes. For X.509 CRL generation,
- * this class derives structured CRL entries from authoritative journals and the
- * referenced X.509 credentials.
+ * this class derives structured CRL entries from a stable ordered current-state
+ * snapshot and the referenced X.509 credentials.
*
*
* Persistence model
@@ -114,7 +114,7 @@ import zeroecho.pki.spi.store.RevocationSnapshot;
* This service does not access private key material directly.
* Issuer signing capability is conveyed only through
* {@link BcX509Attributes#ISSUER_KEYREF}.
- * For CRL generation, every active journal and referenced target is
+ * For CRL generation, every selected current state and referenced target is
* validated before signing. Any unresolved or malformed state aborts generation
* with a stable redacted failure.
* The correctness of the generated status object depends on the configured
@@ -184,7 +184,7 @@ public final class DefaultStatusObjectService implements StatusObjectService {
*
*
* When {@link StatusObjectType#CRL} is requested, the service validates every
- * authoritative active journal before signing. Current hold and permanent
+ * current state from one stable authoritative revision before signing. Hold and permanent
* states are transported with the exact positive X.509 serial, authoritative
* transition time, and explicit reason. Current {@code CLEAR} states are
* omitted.
@@ -192,7 +192,7 @@ public final class DefaultStatusObjectService implements StatusObjectService {
*
*
* Missing targets, malformed credentials, duplicate serials, future
- * transitions, corrupt journals, and store failures abort the complete CRL
+ * transitions, corrupt derived state, and store failures abort the complete CRL
* before generator invocation or persistence with a stable redacted error.
*
*
@@ -347,7 +347,7 @@ public final class DefaultStatusObjectService implements StatusObjectService {
}
}
- /** Bounded cursor translating authoritative journals into CRL entries. */
+ /** Bounded cursor translating stable validated current states into CRL entries. */
private final class CheckpointCrlCursor implements CrlEntrySource.Cursor {
private final RevocationSnapshot.Cursor cursor;
private final PkiId issuerCaId;
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 fd8c705..1e15080 100644
--- a/pki/src/main/java/zeroecho/pki/impl/fs/FilesystemPkiStore.java
+++ b/pki/src/main/java/zeroecho/pki/impl/fs/FilesystemPkiStore.java
@@ -231,9 +231,15 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
this(root, options, clock, FilesystemRevocationAuthority.IndexUpdateFaultInjector.NONE);
}
- @SuppressWarnings("PMD.CloseResource")
/* package */ FilesystemPkiStore(final Path root, final FsPkiStoreOptions options, final Clock clock,
final FilesystemRevocationAuthority.IndexUpdateFaultInjector indexUpdateFaults) {
+ this(root, options, clock, indexUpdateFaults, false);
+ }
+
+ @SuppressWarnings("PMD.CloseResource")
+ private FilesystemPkiStore(final Path root, final FsPkiStoreOptions options, final Clock clock,
+ final FilesystemRevocationAuthority.IndexUpdateFaultInjector indexUpdateFaults,
+ final boolean snapshotAssembly) {
this.options = Objects.requireNonNull(options, "options");
Objects.requireNonNull(root, "root");
this.clock = Objects.requireNonNull(clock, "clock");
@@ -258,6 +264,9 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
FilesystemRevocationAuthority openedRevocations = null;
try {
boolean newStore = !Files.exists(this.paths.versionFile());
+ if (newStore && !snapshotAssembly) {
+ rejectNonEmptyUnversionedStore();
+ }
ensureVersionFile();
this.signingNamespace = ensureSigningNamespace();
this.stagedContent = new FilesystemStagedContentStore(this.paths.stagedContentRoot(),
@@ -312,6 +321,12 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
}
}
+ /* package */ static FilesystemPkiStore openSnapshotAssembly(final Path root,
+ final FsPkiStoreOptions options) {
+ return new FilesystemPkiStore(root, options, Clock.systemUTC(),
+ FilesystemRevocationAuthority.IndexUpdateFaultInjector.NONE, true);
+ }
+
private boolean requireSnapshotBoundary() throws IOException {
Path marker = paths.revocationSnapshotBoundary();
if (!Files.exists(marker, java.nio.file.LinkOption.NOFOLLOW_LINKS)) {
@@ -2320,6 +2335,20 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
}
}
+ private void rejectNonEmptyUnversionedStore() throws IOException {
+ Path lockDirectory = this.paths.lockFile().getParent();
+ try (Stream entries = Files.list(this.paths.root())) {
+ if (entries.anyMatch(entry -> !entry.equals(lockDirectory))) {
+ throw new IllegalStateException("unversioned store is not empty");
+ }
+ }
+ try (Stream entries = Files.list(lockDirectory)) {
+ if (entries.anyMatch(entry -> !entry.equals(this.paths.lockFile()))) {
+ throw new IllegalStateException("unversioned store is not empty");
+ }
+ }
+ }
+
private Optional readOptional(final Path path, final FsCodec.Schema schema) {
try {
if (!Files.exists(path)) {
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 392718f..5a6187c 100644
--- a/pki/src/main/java/zeroecho/pki/impl/fs/FsCodec.java
+++ b/pki/src/main/java/zeroecho/pki/impl/fs/FsCodec.java
@@ -87,10 +87,6 @@ import zeroecho.pki.api.publication.PublicationTargetType;
import zeroecho.pki.api.request.ParsedCertificationRequest;
import zeroecho.pki.api.request.SubjectAlternativeName;
import zeroecho.pki.api.request.SubjectRdn;
-import zeroecho.pki.api.revocation.RevocationJournal;
-import zeroecho.pki.api.revocation.RevocationReason;
-import zeroecho.pki.api.revocation.RevocationState;
-import zeroecho.pki.api.revocation.RevocationTransition;
import zeroecho.pki.api.status.StatusObject;
import zeroecho.pki.api.status.StatusObjectType;
import zeroecho.pki.spi.store.StagedContentStore;
@@ -128,7 +124,6 @@ final class FsCodec {
private static final int TOP_CA_RECORD = 1;
private static final int TOP_CREDENTIAL = 2;
private static final int TOP_PARSED_REQUEST = 3;
- private static final int TOP_REVOCATION = 4;
private static final int TOP_STATUS_OBJECT = 5;
private static final int TOP_PUBLICATION = 6;
private static final int TOP_POLICY_TRACE = 8;
@@ -163,14 +158,11 @@ final class FsCodec {
private static final int TYPE_CA_KIND_ENUM = 51;
private static final int TYPE_CA_STATE_ENUM = 52;
private static final int TYPE_CREDENTIAL_STATUS_ENUM = 53;
- private static final int TYPE_REVOCATION_REASON_ENUM = 54;
private static final int TYPE_STATUS_OBJECT_TYPE_ENUM = 55;
private static final int TYPE_PUBLICATION_TARGET_TYPE_ENUM = 56;
private static final int TYPE_PUBLICATION_STATUS_ENUM = 57;
private static final int TYPE_DURABILITY_POLICY_ENUM = 58;
private static final int TYPE_SIGN_STATE_ENUM = 59;
- private static final int TYPE_REVOCATION_STATE_ENUM = 60;
- private static final int TYPE_REVOCATION_TRANSITION = 61;
private static final int TYPE_SUBJECT_RDN_TYPE_ENUM = 62;
private static final int TYPE_SAN_TYPE_ENUM = 63;
private static final int TYPE_SUBJECT_RDN = 65;
@@ -245,42 +237,6 @@ final class FsCodec {
case 3 -> CredentialStatus.EXPIRED;
default -> throw unknownEnum("CredentialStatus", code);
});
- private static final ValueSchema REVOCATION_REASON = enumSchema(TYPE_REVOCATION_REASON_ENUM,
- value -> switch (value) {
- case UNSPECIFIED -> 1;
- case KEY_COMPROMISE -> 2;
- case CA_COMPROMISE -> 3;
- case AFFILIATION_CHANGED -> 4;
- case SUPERSEDED -> 5;
- case CESSATION_OF_OPERATION -> 6;
- case CERTIFICATE_HOLD -> 7;
- case REMOVE_FROM_CRL -> 8;
- case PRIVILEGE_WITHDRAWN -> 9;
- case AA_COMPROMISE -> 10;
- }, code -> switch (code) {
- case 1 -> RevocationReason.UNSPECIFIED;
- case 2 -> RevocationReason.KEY_COMPROMISE;
- case 3 -> RevocationReason.CA_COMPROMISE;
- case 4 -> RevocationReason.AFFILIATION_CHANGED;
- case 5 -> RevocationReason.SUPERSEDED;
- case 6 -> RevocationReason.CESSATION_OF_OPERATION;
- case 7 -> RevocationReason.CERTIFICATE_HOLD;
- case 8 -> RevocationReason.REMOVE_FROM_CRL;
- case 9 -> RevocationReason.PRIVILEGE_WITHDRAWN;
- case 10 -> RevocationReason.AA_COMPROMISE;
- default -> throw unknownEnum("RevocationReason", code);
- });
- private static final ValueSchema REVOCATION_STATE = enumSchema(TYPE_REVOCATION_STATE_ENUM,
- value -> switch (value) {
- case CLEAR -> 1;
- case HELD -> 2;
- case PERMANENTLY_REVOKED -> 3;
- }, code -> switch (code) {
- case 1 -> RevocationState.CLEAR;
- case 2 -> RevocationState.HELD;
- case 3 -> RevocationState.PERMANENTLY_REVOKED;
- default -> throw unknownEnum("RevocationState", code);
- });
private static final ValueSchema STATUS_OBJECT_TYPE = enumSchema(TYPE_STATUS_OBJECT_TYPE_ENUM,
value -> switch (value) {
case CRL -> 1;
@@ -440,11 +396,6 @@ final class FsCodec {
private static final ValueSchema> OPTIONAL_STRING = optionalOf(STRING);
private static final ValueSchema> OPTIONAL_INSTANT = optionalOf(INSTANT);
private static final ValueSchema> OPTIONAL_ENCODED_OBJECT = optionalOf(ENCODED_OBJECT);
- private static final ValueSchema> OPTIONAL_REVOCATION_REASON = optionalOf(
- REVOCATION_REASON);
- private static final ValueSchema REVOCATION_TRANSITION = valueSchema(
- TYPE_REVOCATION_TRANSITION, FsCodec::writeRevocationTransition, FsCodec::readRevocationTransition);
- private static final ValueSchema> REVOCATION_TRANSITIONS = listOf(REVOCATION_TRANSITION);
private static final ValueSchema CREDENTIAL_VALUE = valueSchema(TYPE_CREDENTIAL_RECORD,
FsCodec::writeCredential, FsCodec::readCredential);
@@ -455,8 +406,6 @@ final class FsCodec {
/* package */ static final Schema CREDENTIAL = topLevel(TOP_CREDENTIAL, "CREDENTIAL", CREDENTIAL_VALUE);
/* package */ static final Schema PARSED_REQUEST = topLevel(TOP_PARSED_REQUEST,
"PARSED_REQUEST", valueSchema(101, FsCodec::writeParsedRequest, FsCodec::readParsedRequest));
- /* package */ static final Schema REVOCATION_JOURNAL = topLevel(TOP_REVOCATION,
- "REVOCATION_JOURNAL", valueSchema(102, FsCodec::writeRevocationJournal, FsCodec::readRevocationJournal));
/* package */ static final Schema STATUS_OBJECT = topLevel(TOP_STATUS_OBJECT, "STATUS_OBJECT",
valueSchema(103, FsCodec::writeStatusObject, FsCodec::readStatusObject));
/* package */ static final Schema PUBLICATION = topLevel(TOP_PUBLICATION, "PUBLICATION",
@@ -475,7 +424,7 @@ final class FsCodec {
private static final Map> TOP_LEVEL_SCHEMAS = Map.ofEntries(Map.entry(TOP_CA_RECORD, CA_RECORD),
Map.entry(TOP_CREDENTIAL, CREDENTIAL), Map.entry(TOP_PARSED_REQUEST, PARSED_REQUEST),
- Map.entry(TOP_REVOCATION, REVOCATION_JOURNAL), Map.entry(TOP_STATUS_OBJECT, STATUS_OBJECT),
+ Map.entry(TOP_STATUS_OBJECT, STATUS_OBJECT),
Map.entry(TOP_PUBLICATION, PUBLICATION), Map.entry(TOP_POLICY_TRACE, POLICY_TRACE),
Map.entry(TOP_WORKFLOW_STATE, WORKFLOW_STATE), Map.entry(TOP_SIGN_WORKFLOW_RECORD, SIGN_WORKFLOW_RECORD),
Map.entry(TOP_PROFILE_VERSION, PROFILE_VERSION), Map.entry(TOP_ACTIVE_PROFILE_REF, ACTIVE_PROFILE_REF));
@@ -739,35 +688,6 @@ final class FsCodec {
reader.readValue(BOOLEAN), reader.readValue(ATTRIBUTE_SET));
}
- private static void writeRevocationJournal(Writer writer, RevocationJournal value) throws IOException {
- writer.writeValue(PKI_ID, value.credentialId());
- writer.writeValue(LONG, (long) RevocationJournal.CURRENT_VERSION);
- writer.writeValue(REVOCATION_TRANSITIONS, value.transitions());
- }
-
- private static RevocationJournal readRevocationJournal(Reader reader) throws IOException {
- PkiId credentialId = reader.readValue(PKI_ID);
- long version = reader.readValue(LONG);
- if (version != RevocationJournal.CURRENT_VERSION) {
- throw new IOException("Unsupported revocation journal version");
- }
- return new RevocationJournal(credentialId, reader.readValue(REVOCATION_TRANSITIONS));
- }
-
- private static void writeRevocationTransition(Writer writer, RevocationTransition value) throws IOException {
- writer.writeValue(LONG, value.revision());
- writer.writeValue(REVOCATION_STATE, value.state());
- writer.writeValue(INSTANT, value.time());
- writer.writeValue(OPTIONAL_REVOCATION_REASON, value.permanentReason());
- writer.writeValue(ATTRIBUTE_SET, value.attributes());
- }
-
- private static RevocationTransition readRevocationTransition(Reader reader) throws IOException {
- return new RevocationTransition(reader.readValue(LONG), reader.readValue(REVOCATION_STATE),
- reader.readValue(INSTANT), reader.readValue(OPTIONAL_REVOCATION_REASON),
- reader.readValue(ATTRIBUTE_SET));
- }
-
private static void writeStatusObject(Writer writer, StatusObject value) throws IOException {
writer.writeValue(PKI_ID, value.statusObjectId());
writer.writeValue(FORMAT_ID, value.formatId());
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 87cc25e..f709f61 100644
--- a/pki/src/main/java/zeroecho/pki/impl/fs/FsOperations.java
+++ b/pki/src/main/java/zeroecho/pki/impl/fs/FsOperations.java
@@ -214,17 +214,17 @@ final class FsOperations {
}
/**
- * Strictly persists one authoritative revocation journal image.
+ * Strictly persists one complete atomic file image.
*
*
* The namespace commit point is an {@link StandardCopyOption#ATOMIC_MOVE} in
* the target directory. No non-atomic fallback is permitted. A directory force
* failure after that move is reported distinctly because the durable
- * authoritative image is then uncertain.
+ * image is then uncertain.
*
*
- * @param target journal target
- * @param data complete encoded journal
+ * @param target file target
+ * @param data complete encoded image
* @throws IOException on a pre-commit persistence failure
* @throws DurabilityUncertainException after a committed move whose directory
* force failed
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 734d758..8e8e6f2 100644
--- a/pki/src/main/java/zeroecho/pki/impl/fs/FsPaths.java
+++ b/pki/src/main/java/zeroecho/pki/impl/fs/FsPaths.java
@@ -167,19 +167,9 @@ final class FsPaths {
}
// -------------------------------------------------------------------------
- // Revocations (single authoritative journal)
+ // Revocations (global log authority and derived structures)
// -------------------------------------------------------------------------
- /* default */ Path revocationDir(final PkiId credentialId) {
- Objects.requireNonNull(credentialId, "credentialId");
- return this.root.resolve(REVOCATIONS_DIRECTORY).resolve("by-credential")
- .resolve(FsUtil.safeId(credentialId));
- }
-
- /* default */ Path revocationJournal(final PkiId credentialId) {
- return revocationDir(credentialId).resolve("journal.bin");
- }
-
/* default */ Path revocationTransitionLog() {
return this.root.resolve(REVOCATIONS_DIRECTORY).resolve("transitions.log");
}
@@ -200,10 +190,6 @@ final class FsPaths {
return this.root.resolve(REVOCATIONS_DIRECTORY).resolve("current-state.idx.lock");
}
- /* default */ Path revocationSnapshotRoot() {
- return this.root.resolve("revocation-snapshots");
- }
-
// -------------------------------------------------------------------------
// 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 b2b42a4..2bb3e53 100644
--- a/pki/src/main/java/zeroecho/pki/impl/fs/FsSnapshotExporter.java
+++ b/pki/src/main/java/zeroecho/pki/impl/fs/FsSnapshotExporter.java
@@ -416,7 +416,7 @@ final class FsSnapshotExporter {
private void restore(Path targetRoot) throws IOException {
Set transferred = new HashSet<>();
- try (FilesystemPkiStore target = new FilesystemPkiStore(targetRoot, options)) {
+ try (FilesystemPkiStore target = FilesystemPkiStore.openSnapshotAssembly(targetRoot, options)) {
for (Map.Entry entry : authority.credentials().entrySet()) {
if (persistCredential(target, entry.getValue())) {
transferred.add(entry.getKey());
diff --git a/pki/src/test/java/zeroecho/pki/impl/core/DefaultRevocationServiceTest.java b/pki/src/test/java/zeroecho/pki/impl/core/DefaultRevocationServiceTest.java
index ed0ef31..f0d35f6 100644
--- a/pki/src/test/java/zeroecho/pki/impl/core/DefaultRevocationServiceTest.java
+++ b/pki/src/test/java/zeroecho/pki/impl/core/DefaultRevocationServiceTest.java
@@ -57,7 +57,6 @@ import zeroecho.pki.api.PkiException;
import zeroecho.pki.api.PkiId;
import zeroecho.pki.api.audit.AuditEvent;
import zeroecho.pki.api.revocation.RevocationCommand;
-import zeroecho.pki.api.revocation.RevocationJournal;
import zeroecho.pki.api.revocation.RevocationRecord;
import zeroecho.pki.api.revocation.RevocationQuery;
import zeroecho.pki.api.revocation.RevocationState;
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 e0e42b6..da6a7fa 100644
--- a/pki/src/test/java/zeroecho/pki/impl/core/DefaultStatusObjectServiceCrlTest.java
+++ b/pki/src/test/java/zeroecho/pki/impl/core/DefaultStatusObjectServiceCrlTest.java
@@ -97,7 +97,6 @@ import zeroecho.pki.api.issuance.VerificationPolicy;
import zeroecho.pki.api.request.CertificationRequest;
import zeroecho.pki.api.request.ParsedCertificationRequest;
import zeroecho.pki.api.revocation.RevocationCommand;
-import zeroecho.pki.api.revocation.RevocationJournal;
import zeroecho.pki.api.revocation.RevocationRecord;
import zeroecho.pki.api.revocation.RevocationReason;
import zeroecho.pki.api.revocation.RevocationState;
@@ -225,12 +224,12 @@ final class DefaultStatusObjectServiceCrlTest {
StatusObjectGenerateCommand command = new StatusObjectGenerateCommand(caId, StatusObjectType.CRL,
runtime.framework().formatId(), emptyAttributes());
- assertCrlFailure(runtime, command, List.of(journal(new PkiId("credential:missing"), RevocationState.HELD,
+ assertCrlFailure(runtime, command, List.of(record(new PkiId("credential:missing"), RevocationState.HELD,
EVALUATION_TIME.minusSeconds(1), Optional.empty())), Map.of(), false);
Credential wrongFormat = copy(template, "wrong-format", new FormatId("not-x509"), template.content());
assertCrlFailure(
- runtime, command, List.of(journal(wrongFormat.credentialId(), RevocationState.HELD,
+ runtime, command, List.of(record(wrongFormat.credentialId(), RevocationState.HELD,
EVALUATION_TIME.minusSeconds(1), Optional.empty())),
Map.of(wrongFormat.credentialId(), wrongFormat), false);
@@ -240,14 +239,14 @@ final class DefaultStatusObjectServiceCrlTest {
Credential wrongEncoding = copy(template, "wrong-encoding", BcX509CredentialFramework.FORMAT_ID,
wrongEncodingContent);
assertCrlFailure(
- runtime, command, List.of(journal(wrongEncoding.credentialId(), RevocationState.HELD,
+ runtime, command, List.of(record(wrongEncoding.credentialId(), RevocationState.HELD,
EVALUATION_TIME.minusSeconds(1), Optional.empty())),
Map.of(wrongEncoding.credentialId(), wrongEncoding), false);
Credential malformed = copy(template, "malformed", BcX509CredentialFramework.FORMAT_ID,
runtime.stageCredential(new byte[] { 1, 2, 3 }));
assertCrlFailure(
- runtime, command, List.of(journal(malformed.credentialId(), RevocationState.HELD,
+ runtime, command, List.of(record(malformed.credentialId(), RevocationState.HELD,
EVALUATION_TIME.minusSeconds(1), Optional.empty())),
Map.of(malformed.credentialId(), malformed), false);
@@ -256,16 +255,16 @@ final class DefaultStatusObjectServiceCrlTest {
Credential duplicateTwo = copy(template, "duplicate-two", BcX509CredentialFramework.FORMAT_ID,
template.content());
assertCrlFailure(runtime, command,
- List.of(journal(duplicateOne.credentialId(), RevocationState.HELD, EVALUATION_TIME.minusSeconds(1),
+ List.of(record(duplicateOne.credentialId(), RevocationState.HELD, EVALUATION_TIME.minusSeconds(1),
Optional.empty()),
- journal(duplicateTwo.credentialId(), RevocationState.PERMANENTLY_REVOKED,
+ record(duplicateTwo.credentialId(), RevocationState.PERMANENTLY_REVOKED,
EVALUATION_TIME.minusSeconds(1), Optional.of(RevocationReason.KEY_COMPROMISE))),
Map.of(duplicateOne.credentialId(), duplicateOne, duplicateTwo.credentialId(), duplicateTwo),
false);
Credential future = copy(template, "future", BcX509CredentialFramework.FORMAT_ID, template.content());
assertCrlFailure(
- runtime, command, List.of(journal(future.credentialId(), RevocationState.HELD,
+ runtime, command, List.of(record(future.credentialId(), RevocationState.HELD,
EVALUATION_TIME.plusSeconds(1), Optional.empty())),
Map.of(future.credentialId(), future), false);
@@ -320,10 +319,10 @@ final class DefaultStatusObjectServiceCrlTest {
}
private static void assertCrlFailure(PkiTestRuntime runtime, StatusObjectGenerateCommand command,
- List journals, Map credentials, boolean failListing) {
+ List records, Map credentials, boolean failListing) {
int signCount = runtime.submittedSignCount();
int statusCount = runtime.store().listStatusObjects(command.issuerCaId()).size();
- PkiStore view = storeView(runtime.store(), journals, credentials, failListing);
+ PkiStore view = storeView(runtime.store(), records, credentials, failListing);
DefaultStatusObjectService service = new DefaultStatusObjectService(view, runtime.framework(),
runtime.auditSink(), usableResolver(), runtime.signingBus().authority());
@@ -335,7 +334,7 @@ final class DefaultStatusObjectServiceCrlTest {
assertEquals(statusCount, runtime.store().listStatusObjects(command.issuerCaId()).size());
}
- private static PkiStore storeView(PkiStore delegate, List journals,
+ private static PkiStore storeView(PkiStore delegate, List records,
Map credentials, boolean failListing) {
return (PkiStore) Proxy.newProxyInstance(PkiStore.class.getClassLoader(), new Class>[] { PkiStore.class },
(proxy, method, arguments) -> {
@@ -343,9 +342,7 @@ final class DefaultStatusObjectServiceCrlTest {
if (failListing) {
throw new IllegalStateException(SENTINEL);
}
- List snapshot = journals.stream()
- .map(journal -> new RevocationRecord(journal.credentialId(), journal.latest()))
- .toList();
+ List snapshot = List.copyOf(records);
return new zeroecho.pki.spi.store.RevocationSnapshot() {
@Override
public String snapshotId() {
@@ -475,10 +472,10 @@ final class DefaultStatusObjectServiceCrlTest {
};
}
- private static RevocationJournal journal(PkiId credentialId, RevocationState state, Instant time,
+ private static RevocationRecord record(PkiId credentialId, RevocationState state, Instant time,
Optional reason) {
- return new RevocationJournal(credentialId,
- List.of(new RevocationTransition(1L, state, time, reason, emptyAttributes())));
+ return new RevocationRecord(credentialId,
+ new RevocationTransition(1L, state, time, reason, emptyAttributes()));
}
private static Credential copy(Credential template, String suffix, FormatId formatId,
diff --git a/pki/src/test/java/zeroecho/pki/impl/core/StoreBackedEffectiveCredentialStatusResolverTest.java b/pki/src/test/java/zeroecho/pki/impl/core/StoreBackedEffectiveCredentialStatusResolverTest.java
index f64ba02..651bf32 100644
--- a/pki/src/test/java/zeroecho/pki/impl/core/StoreBackedEffectiveCredentialStatusResolverTest.java
+++ b/pki/src/test/java/zeroecho/pki/impl/core/StoreBackedEffectiveCredentialStatusResolverTest.java
@@ -69,7 +69,6 @@ import zeroecho.pki.api.credential.CredentialStatus;
import zeroecho.pki.api.credential.CredentialUse;
import zeroecho.pki.api.credential.EffectiveCredentialStatus;
import zeroecho.pki.api.credential.EffectiveCredentialStatusResolver;
-import zeroecho.pki.api.revocation.RevocationJournal;
import zeroecho.pki.api.revocation.RevocationRecord;
import zeroecho.pki.api.revocation.RevocationReason;
import zeroecho.pki.api.revocation.RevocationState;
@@ -122,13 +121,13 @@ final class StoreBackedEffectiveCredentialStatusResolverTest {
}
@Test
- void futureAndMismatchedJournalsFailClosed() {
+ void futureAndMismatchedRecordsFailClosed() {
Credential credential = credential("invalid", CredentialStatus.ISSUED, NOW.minusSeconds(60),
NOW.plusSeconds(60));
assertResolutionFailure(credential, revocation(credential, RevocationReason.KEY_COMPROMISE, NOW.plusNanos(1)));
- RevocationJournal mismatch = new RevocationJournal(new PkiId("credential:other"),
- List.of(new RevocationTransition(1L, RevocationState.PERMANENTLY_REVOKED, NOW,
- Optional.of(RevocationReason.KEY_COMPROMISE), new SimpleAttributeSet())));
+ RevocationRecord mismatch = new RevocationRecord(new PkiId("credential:other"),
+ new RevocationTransition(1L, RevocationState.PERMANENTLY_REVOKED, NOW,
+ Optional.of(RevocationReason.KEY_COMPROMISE), new SimpleAttributeSet()));
assertResolutionFailure(credential, Optional.of(mismatch));
}
@@ -193,50 +192,47 @@ final class StoreBackedEffectiveCredentialStatusResolverTest {
assertFalse(event.details().toString().contains(sentinel));
}
- private static void assertStatus(Credential credential, Optional revocation,
+ private static void assertStatus(Credential credential, Optional revocation,
EffectiveCredentialStatus expected) {
assertEquals(expected, resolver(revocation).beginEvaluation().resolve(credential));
}
- private static void assertResolutionFailure(Credential credential, Optional revocation) {
+ private static void assertResolutionFailure(Credential credential, Optional revocation) {
PkiException failure = assertThrows(PkiException.class,
() -> resolver(revocation).beginEvaluation().resolve(credential));
assertEquals("Credential status resolution failed: code=CREDENTIAL_STATUS_RESOLUTION_FAILED",
failure.getMessage());
}
- private static StoreBackedEffectiveCredentialStatusResolver resolver(Optional revocation) {
+ private static StoreBackedEffectiveCredentialStatusResolver resolver(Optional revocation) {
return new StoreBackedEffectiveCredentialStatusResolver(store(id -> revocation, new AtomicInteger()),
Clock.fixed(NOW, ZoneOffset.UTC));
}
- private static PkiStore store(Function> lookup, AtomicInteger calls) {
+ private static PkiStore store(Function> lookup, AtomicInteger calls) {
return (PkiStore) Proxy.newProxyInstance(PkiStore.class.getClassLoader(), new Class>[] { PkiStore.class },
(proxy, method, arguments) -> {
if ("getRevocation".equals(method.getName())) {
calls.incrementAndGet();
- return lookup.apply((PkiId) arguments[0])
- .map(journal -> new RevocationRecord(journal.credentialId(), journal.latest()));
+ return lookup.apply((PkiId) arguments[0]);
}
throw new UnsupportedOperationException(method.getName());
});
}
- private static Optional revocation(Credential credential, RevocationReason reason,
+ private static Optional revocation(Credential credential, RevocationReason reason,
Instant time) {
if (reason == RevocationReason.REMOVE_FROM_CRL) {
- return Optional.of(new RevocationJournal(credential.credentialId(),
- List.of(new RevocationTransition(1L, RevocationState.HELD, time.minusNanos(1), Optional.empty(),
- new SimpleAttributeSet()),
- new RevocationTransition(2L, RevocationState.CLEAR, time, Optional.empty(),
- new SimpleAttributeSet()))));
+ return Optional.of(new RevocationRecord(credential.credentialId(),
+ new RevocationTransition(2L, RevocationState.CLEAR, time, Optional.empty(),
+ new SimpleAttributeSet())));
}
RevocationState state = reason == RevocationReason.CERTIFICATE_HOLD ? RevocationState.HELD
: RevocationState.PERMANENTLY_REVOKED;
Optional permanentReason = state == RevocationState.PERMANENTLY_REVOKED ? Optional.of(reason)
: Optional.empty();
- return Optional.of(new RevocationJournal(credential.credentialId(),
- List.of(new RevocationTransition(1L, state, time, permanentReason, new SimpleAttributeSet()))));
+ return Optional.of(new RevocationRecord(credential.credentialId(),
+ new RevocationTransition(1L, state, time, permanentReason, new SimpleAttributeSet())));
}
private Credential credential(String suffix, CredentialStatus status, Instant notBefore, Instant notAfter) {
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 7a2a244..96f1f5a 100644
--- a/pki/src/test/java/zeroecho/pki/impl/fs/FilesystemPkiStoreTest.java
+++ b/pki/src/test/java/zeroecho/pki/impl/fs/FilesystemPkiStoreTest.java
@@ -116,7 +116,6 @@ import zeroecho.pki.api.publication.PublicationTarget;
import zeroecho.pki.api.publication.PublicationTargetType;
import zeroecho.pki.api.request.ParsedCertificationRequest;
import zeroecho.pki.api.revocation.RevocationCommand;
-import zeroecho.pki.api.revocation.RevocationJournal;
import zeroecho.pki.api.revocation.RevocationRecord;
import zeroecho.pki.api.revocation.RevocationReason;
import zeroecho.pki.api.status.StatusObject;
@@ -779,8 +778,8 @@ public final class FilesystemPkiStoreTest {
}
@Test
- void revocationJournalPersistsLegalTransitions() throws Exception {
- System.out.println("revocationJournalPersistsLegalTransitions");
+ void globalRevocationLogPersistsStreamingHistory() throws Exception {
+ System.out.println("globalRevocationLogPersistsStreamingHistory");
Path root = tmp.resolve("store-revocation-history");
Path restoredRoot = tmp.resolve("store-revocation-history-snapshot");
@@ -830,7 +829,7 @@ public final class FilesystemPkiStoreTest {
System.out.println("...store tree:");
dumpTree(root);
- System.out.println("revocationJournalPersistsLegalTransitions...ok");
+ System.out.println("globalRevocationLogPersistsStreamingHistory...ok");
}
@Test
diff --git a/pki/src/test/java/zeroecho/pki/impl/fs/FilesystemRevocationJournalTest.java b/pki/src/test/java/zeroecho/pki/impl/fs/FilesystemRevocationAuthorityTest.java
similarity index 56%
rename from pki/src/test/java/zeroecho/pki/impl/fs/FilesystemRevocationJournalTest.java
rename to pki/src/test/java/zeroecho/pki/impl/fs/FilesystemRevocationAuthorityTest.java
index fc36026..acfc20c 100644
--- a/pki/src/test/java/zeroecho/pki/impl/fs/FilesystemRevocationJournalTest.java
+++ b/pki/src/test/java/zeroecho/pki/impl/fs/FilesystemRevocationAuthorityTest.java
@@ -39,10 +39,10 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Clock;
import java.time.Instant;
@@ -57,14 +57,10 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.stream.Stream;
-import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.io.TempDir;
-import zeroecho.core.io.Util;
import zeroecho.pki.api.FormatId;
import zeroecho.pki.api.IssuerRef;
import zeroecho.pki.api.PkiException;
@@ -79,27 +75,13 @@ import zeroecho.pki.api.credential.Credential;
import zeroecho.pki.api.credential.CredentialStatus;
import zeroecho.pki.api.credential.EffectiveCredentialStatus;
import zeroecho.pki.api.revocation.RevocationCommand;
-import zeroecho.pki.api.revocation.RevocationJournal;
import zeroecho.pki.api.revocation.RevocationReason;
import zeroecho.pki.api.revocation.RevocationState;
import zeroecho.pki.api.revocation.RevocationTransition;
import zeroecho.pki.impl.core.StoreBackedEffectiveCredentialStatusResolver;
import zeroecho.pki.impl.core.attr.SimpleAttributeSet;
-final class FilesystemRevocationJournalTest {
- private static final int CODEC_MAGIC = 0x5A454346;
- private static final int CODEC_VERSION = 2;
- private static final int TOP_REVOCATION = 4;
- private static final int TYPE_STRING = 1;
- private static final int TYPE_LONG = 3;
- private static final int TYPE_INSTANT = 5;
- private static final int TYPE_LIST = 7;
- private static final int TYPE_OPTIONAL = 8;
- private static final int TYPE_PKI_ID = 20;
- private static final int TYPE_ATTRIBUTE_SET = 31;
- private static final int TYPE_REVOCATION_REASON = 54;
- private static final int TYPE_REVOCATION_STATE = 60;
- private static final int TYPE_REVOCATION_TRANSITION = 61;
+final class FilesystemRevocationAuthorityTest {
private static final Instant TIME = Instant.parse("2026-07-01T12:00:00Z");
private static final AttributeId NOTE = new AttributeId("test.note");
@@ -124,10 +106,10 @@ final class FilesystemRevocationJournalTest {
() -> transition(store, revoke(credential, RevocationReason.CA_COMPROMISE)));
}
try (FilesystemPkiStore reopened = store(root)) {
- RevocationJournal journal = readJournal(reopened, credential.credentialId()).orElseThrow();
- assertEquals(4, journal.transitions().size());
- assertEquals(RevocationState.PERMANENTLY_REVOKED, journal.latest().state());
- assertEquals(RevocationReason.KEY_COMPROMISE, journal.latest().permanentReason().orElseThrow());
+ CredentialHistory history = readHistory(reopened, credential.credentialId()).orElseThrow();
+ assertEquals(4, history.transitions().size());
+ assertEquals(RevocationState.PERMANENTLY_REVOKED, history.latest().state());
+ assertEquals(RevocationReason.KEY_COMPROMISE, history.latest().permanentReason().orElseThrow());
}
System.out.println("legalTransitionsAreContiguousDurableAndPermanentIsTerminal...ok");
}
@@ -143,28 +125,28 @@ final class FilesystemRevocationJournalTest {
store.putCredential(fromClear);
store.putCredential(fromHeld);
- RevocationJournal directJournal = transition(store, revoke(direct, RevocationReason.KEY_COMPROMISE));
- assertEquals(List.of(RevocationState.PERMANENTLY_REVOKED), states(directJournal));
- assertEquals(1L, directJournal.latest().revision());
+ CredentialHistory directHistory = transition(store, revoke(direct, RevocationReason.KEY_COMPROMISE));
+ assertEquals(List.of(RevocationState.PERMANENTLY_REVOKED), states(directHistory));
+ assertEquals(1L, directHistory.latest().revision());
transition(store, hold(fromClear));
transition(store, unhold(fromClear));
- RevocationJournal clearJournal = transition(store, revoke(fromClear, RevocationReason.CA_COMPROMISE));
+ CredentialHistory clearHistory = transition(store, revoke(fromClear, RevocationReason.CA_COMPROMISE));
assertEquals(List.of(RevocationState.HELD, RevocationState.CLEAR, RevocationState.PERMANENTLY_REVOKED),
- states(clearJournal));
- assertEquals(3L, clearJournal.latest().revision());
+ states(clearHistory));
+ assertEquals(3L, clearHistory.latest().revision());
transition(store, hold(fromHeld));
- RevocationJournal heldJournal = transition(store, revoke(fromHeld, RevocationReason.SUPERSEDED));
- assertEquals(List.of(RevocationState.HELD, RevocationState.PERMANENTLY_REVOKED), states(heldJournal));
- assertEquals(2L, heldJournal.latest().revision());
+ CredentialHistory heldHistory = transition(store, revoke(fromHeld, RevocationReason.SUPERSEDED));
+ assertEquals(List.of(RevocationState.HELD, RevocationState.PERMANENTLY_REVOKED), states(heldHistory));
+ assertEquals(2L, heldHistory.latest().revision());
}
System.out.println("everyLegalPermanentTransitionIsAccepted...ok");
}
@Test
- void illegalAndInvalidCommandsFailBeforeJournalMutation() throws Exception {
- System.out.println("illegalAndInvalidCommandsFailBeforeJournalMutation");
+ void illegalAndInvalidCommandsFailBeforeHistoryMutation() throws Exception {
+ System.out.println("illegalAndInvalidCommandsFailBeforeHistoryMutation");
try (FilesystemPkiStore store = store(temporaryDirectory.resolve("illegal"))) {
Credential unknown = credential(store, "unknown");
assertCode("REVOCATION_CREDENTIAL_NOT_FOUND", () -> transition(store, hold(unknown)));
@@ -173,11 +155,11 @@ final class FilesystemRevocationJournalTest {
store.putCredential(credential);
assertCode("REVOCATION_TRANSITION_ILLEGAL", () -> transition(store, unhold(credential)));
assertThrows(IllegalArgumentException.class, () -> revoke(credential, RevocationReason.CERTIFICATE_HOLD));
- assertTrue(readJournal(store, credential.credentialId()).isEmpty());
+ assertTrue(readHistory(store, credential.credentialId()).isEmpty());
transition(store, hold(credential));
assertCode("REVOCATION_TRANSITION_ILLEGAL", () -> transition(store, hold(credential)));
- assertEquals(1, readJournal(store, credential.credentialId()).orElseThrow().transitions().size());
+ assertEquals(1, readHistory(store, credential.credentialId()).orElseThrow().transitions().size());
transition(store, unhold(credential));
Path logPath = new FsPaths(temporaryDirectory.resolve("illegal")).revocationTransitionLog();
@@ -197,9 +179,9 @@ final class FilesystemRevocationJournalTest {
Credential invalidRemove = credential(store, "invalid-remove");
store.putCredential(invalidRemove);
assertThrows(IllegalArgumentException.class, () -> revoke(invalidRemove, RevocationReason.REMOVE_FROM_CRL));
- assertTrue(readJournal(store, invalidRemove.credentialId()).isEmpty());
+ assertTrue(readHistory(store, invalidRemove.credentialId()).isEmpty());
}
- System.out.println("illegalAndInvalidCommandsFailBeforeJournalMutation...ok");
+ System.out.println("illegalAndInvalidCommandsFailBeforeHistoryMutation...ok");
}
@Test
@@ -217,9 +199,9 @@ final class FilesystemRevocationJournalTest {
} finally {
executor.shutdownNow();
}
- RevocationJournal journal = readJournal(store, credential.credentialId()).orElseThrow();
- assertEquals(1, journal.transitions().size());
- assertEquals(1L, journal.latest().revision());
+ CredentialHistory history = readHistory(store, credential.credentialId()).orElseThrow();
+ assertEquals(1, history.transitions().size());
+ assertEquals(1L, history.latest().revision());
}
System.out.println("sameCredentialConcurrentHoldHasExactlyOneCommittedWinner...ok");
}
@@ -233,26 +215,26 @@ final class FilesystemRevocationJournalTest {
Credential fromNone = credential(store, "hold-vs-permanent");
store.putCredential(fromNone);
runConcurrent(store, hold(fromNone), revoke(fromNone, RevocationReason.KEY_COMPROMISE), executor);
- RevocationJournal fromNoneJournal = readJournal(store, fromNone.credentialId()).orElseThrow();
- assertEquals(RevocationState.PERMANENTLY_REVOKED, fromNoneJournal.latest().state());
- assertTrue(fromNoneJournal.transitions().size() == 1 || fromNoneJournal.transitions().size() == 2);
+ CredentialHistory fromNoneHistory = readHistory(store, fromNone.credentialId()).orElseThrow();
+ assertEquals(RevocationState.PERMANENTLY_REVOKED, fromNoneHistory.latest().state());
+ assertTrue(fromNoneHistory.transitions().size() == 1 || fromNoneHistory.transitions().size() == 2);
Credential fromHeld = credential(store, "unhold-vs-permanent");
store.putCredential(fromHeld);
transition(store, hold(fromHeld));
runConcurrent(store, unhold(fromHeld), revoke(fromHeld, RevocationReason.CA_COMPROMISE), executor);
- RevocationJournal fromHeldJournal = readJournal(store, fromHeld.credentialId()).orElseThrow();
- assertEquals(RevocationState.PERMANENTLY_REVOKED, fromHeldJournal.latest().state());
- assertTrue(fromHeldJournal.transitions().size() == 2 || fromHeldJournal.transitions().size() == 3);
+ CredentialHistory fromHeldHistory = readHistory(store, fromHeld.credentialId()).orElseThrow();
+ assertEquals(RevocationState.PERMANENTLY_REVOKED, fromHeldHistory.latest().state());
+ assertTrue(fromHeldHistory.transitions().size() == 2 || fromHeldHistory.transitions().size() == 3);
Credential reasons = credential(store, "competing-permanent-reasons");
store.putCredential(reasons);
List results = runConcurrent(store, revoke(reasons, RevocationReason.KEY_COMPROMISE),
revoke(reasons, RevocationReason.CA_COMPROMISE), executor);
assertEquals(1L, results.stream().filter(Boolean::booleanValue).count());
- RevocationJournal reasonsJournal = readJournal(store, reasons.credentialId()).orElseThrow();
- assertEquals(1, reasonsJournal.transitions().size());
- assertTrue(reasonsJournal.latest().permanentReason().filter(
+ CredentialHistory reasonsHistory = readHistory(store, reasons.credentialId()).orElseThrow();
+ assertEquals(1, reasonsHistory.transitions().size());
+ assertTrue(reasonsHistory.latest().permanentReason().filter(
reason -> reason == RevocationReason.KEY_COMPROMISE || reason == RevocationReason.CA_COMPROMISE)
.isPresent());
} finally {
@@ -280,12 +262,12 @@ final class FilesystemRevocationJournalTest {
ExecutorService executor = Executors.newFixedThreadPool(2);
CountDownLatch blockedStarted = new CountDownLatch(1);
try {
- CompletableFuture blockedTransition = CompletableFuture.supplyAsync(() -> {
+ CompletableFuture blockedTransition = CompletableFuture.supplyAsync(() -> {
blockedStarted.countDown();
return transition(store, hold(blocked));
}, executor);
assertTrue(blockedStarted.await(5, TimeUnit.SECONDS));
- CompletableFuture independentTransition = CompletableFuture
+ CompletableFuture independentTransition = CompletableFuture
.supplyAsync(() -> transition(store, hold(independent)), executor);
assertEquals(RevocationState.HELD, independentTransition.get(5, TimeUnit.SECONDS).latest().state());
assertFalse(blockedTransition.isDone());
@@ -303,8 +285,8 @@ final class FilesystemRevocationJournalTest {
}
@Test
- void copiedNamespaceJournalAndRegressingTransitionTimeFailClosed() throws Exception {
- System.out.println("copiedNamespaceJournalAndRegressingTransitionTimeFailClosed");
+ void regressingTransitionTimeFailsClosed() throws Exception {
+ System.out.println("regressingTransitionTimeFailsClosed");
Path root = temporaryDirectory.resolve("corrupt");
try (FilesystemPkiStore store = store(root)) {
Credential credential = credential(store, "corrupt");
@@ -313,19 +295,15 @@ final class FilesystemRevocationJournalTest {
assertCode("REVOCATION_TRANSITION_CONFLICT",
() -> store.transitionRevocation(unhold(credential), TIME.minusSeconds(1)));
- RevocationJournal invalid = new RevocationJournal(new PkiId("credential:copied"),
- List.of(new RevocationTransition(1L, RevocationState.HELD, TIME, Optional.empty(),
- new SimpleAttributeSet())));
- FsOperations.writeAtomic(new FsPaths(root).revocationJournal(credential.credentialId()),
- FsCodec.encode(FsCodec.REVOCATION_JOURNAL, invalid));
assertEquals(RevocationState.HELD,
store.getRevocation(credential.credentialId()).orElseThrow().transition().state());
}
- System.out.println("copiedNamespaceJournalAndRegressingTransitionTimeFailClosed...ok");
+ System.out.println("regressingTransitionTimeFailsClosed...ok");
}
@Test
void transitionMetadataDefensivelySnapshotsByteValues() {
+ System.out.println("transitionMetadataDefensivelySnapshotsByteValues");
byte[] source = { 1, 2, 3 };
AttributeSet attributes = SimpleAttributeSet.builder().put(NOTE, new AttributeValue.BytesValue(source)).build();
RevocationTransition transition = new RevocationTransition(1L, RevocationState.HELD, TIME, Optional.empty(),
@@ -336,65 +314,62 @@ final class FilesystemRevocationJournalTest {
first[1] = 9;
byte[] second = ((AttributeValue.BytesValue) transition.attributes().get(NOTE).orElseThrow()).value();
assertArrayEquals(new byte[] { 1, 2, 3 }, second);
+ System.out.println("transitionMetadataDefensivelySnapshotsByteValues...ok");
}
@Test
- void journalConstructorRejectsEveryHostileSequenceShape() {
- PkiId id = new PkiId("credential:hostile");
- SimpleAttributeSet attributes = new SimpleAttributeSet();
- assertThrows(IllegalArgumentException.class, () -> new RevocationJournal(id,
- List.of(new RevocationTransition(1L, RevocationState.CLEAR, TIME, Optional.empty(), attributes))));
- assertThrows(IllegalArgumentException.class, () -> new RevocationJournal(id,
- List.of(new RevocationTransition(2L, RevocationState.HELD, TIME, Optional.empty(), attributes))));
- assertThrows(IllegalArgumentException.class,
- () -> new RevocationJournal(id,
- List.of(new RevocationTransition(1L, RevocationState.HELD, TIME, Optional.empty(), attributes),
- new RevocationTransition(2L, RevocationState.CLEAR, TIME.minusNanos(1),
- Optional.empty(), attributes))));
- assertThrows(IllegalArgumentException.class,
- () -> new RevocationJournal(id,
- List.of(new RevocationTransition(1L, RevocationState.PERMANENTLY_REVOKED, TIME,
- Optional.of(RevocationReason.CERTIFICATE_HOLD), attributes))));
- assertThrows(IllegalArgumentException.class,
- () -> new RevocationJournal(id, List.of(
- new RevocationTransition(1L, RevocationState.PERMANENTLY_REVOKED, TIME,
- Optional.of(RevocationReason.KEY_COMPROMISE), attributes),
- new RevocationTransition(2L, RevocationState.HELD, TIME, Optional.empty(), attributes))));
- }
-
- @TestFactory
- Stream strictCodecCorruptionFailsReadAndTransitionWithoutOverwrite() {
- return Arrays.stream(CorruptionCase.values()).map(corruption -> DynamicTest.dynamicTest(corruption.description,
- () -> assertPersistedCorruptionRejected(corruption)));
- }
-
- @Test
- void obsoleteCurrentAndHistoryLayoutCannotInfluenceTrustedJournalOrResolver() throws Exception {
- System.out.println("obsoleteCurrentAndHistoryLayoutCannotInfluenceTrustedJournalOrResolver");
+ void obsoleteCurrentAndHistoryLayoutCannotInfluenceTrustedHistoryOrResolver() throws Exception {
+ System.out.println("obsoleteCurrentAndHistoryLayoutCannotInfluenceTrustedHistoryOrResolver");
Path root = temporaryDirectory.resolve("obsolete-layout");
+ Path snapshot = temporaryDirectory.resolve("obsolete-layout-snapshot");
Credential credential;
try (FilesystemPkiStore store = store(root)) {
credential = credential(store, "obsolete-layout");
store.putCredential(credential);
}
- Path legacyDirectory = new FsPaths(root).revocationDir(credential.credentialId());
+ Path legacyDirectory = root.resolve("revocations").resolve("by-credential")
+ .resolve(FsUtil.safeId(credential.credentialId()));
FsOperations.ensureDir(legacyDirectory.resolve("history"));
- FsOperations.writeAtomic(legacyDirectory.resolve("current.bin"),
- oldRevokedRecordPayload(credential.credentialId()));
+ FsOperations.writeAtomic(legacyDirectory.resolve("current.bin"), new byte[] { 0x01, 0x02 });
FsOperations.writeAtomic(legacyDirectory.resolve("history").resolve("legacy.bin"),
- oldRevokedRecordPayload(credential.credentialId()));
+ new byte[] { 0x03, 0x04 });
+ Path legacyJournal = legacyDirectory.resolve("journal.bin");
+ byte[] legacyJournalBytes = { 0x05, 0x06 };
+ FsOperations.writeAtomic(legacyJournal, legacyJournalBytes);
try (FilesystemPkiStore reopened = store(root)) {
- assertTrue(readJournal(reopened, credential.credentialId()).isEmpty());
+ assertTrue(readHistory(reopened, credential.credentialId()).isEmpty());
StoreBackedEffectiveCredentialStatusResolver resolver = new StoreBackedEffectiveCredentialStatusResolver(
reopened, Clock.fixed(TIME, java.time.ZoneOffset.UTC));
assertEquals(EffectiveCredentialStatus.USABLE, resolver.beginEvaluation()
.resolve(reopened.getCredential(credential.credentialId()).orElseThrow()));
transition(reopened, hold(credential));
assertEquals(RevocationState.HELD,
- readJournal(reopened, credential.credentialId()).orElseThrow().latest().state());
+ readHistory(reopened, credential.credentialId()).orElseThrow().latest().state());
+ reopened.exportSnapshot(snapshot, TIME);
}
- System.out.println("obsoleteCurrentAndHistoryLayoutCannotInfluenceTrustedJournalOrResolver...ok");
+ assertArrayEquals(legacyJournalBytes, Files.readAllBytes(legacyJournal));
+ assertFalse(Files.exists(snapshot.resolve("revocations").resolve("by-credential")));
+ System.out.println("obsoleteCurrentAndHistoryLayoutCannotInfluenceTrustedHistoryOrResolver...ok");
+ }
+
+ @Test
+ void unversionedLegacyJournalStoreIsRejected() throws Exception {
+ System.out.println("unversionedLegacyJournalStoreIsRejected");
+ Path root = temporaryDirectory.resolve("unversioned-legacy");
+ Path legacyJournal = root.resolve("revocations").resolve("by-credential").resolve("credential-legacy")
+ .resolve("journal.bin");
+ Files.createDirectories(legacyJournal.getParent());
+ Files.write(legacyJournal, new byte[] { 0x01, 0x02 });
+ Path legacyCredential = root.resolve("credentials").resolve("by-id").resolve("credential-legacy.bin");
+ Files.createDirectories(legacyCredential.getParent());
+ Files.write(legacyCredential, new byte[] { 0x03, 0x04 });
+
+ IllegalStateException failure = assertThrows(IllegalStateException.class, () -> store(root));
+ assertTrue(failure.getMessage().contains("unversioned store is not empty"));
+ assertFalse(Files.exists(root.resolve(FsPaths.VERSION_FILE)));
+ assertArrayEquals(new byte[] { 0x01, 0x02 }, Files.readAllBytes(legacyJournal));
+ System.out.println("unversionedLegacyJournalStoreIsRejected...ok");
}
@Test
@@ -455,48 +430,23 @@ final class FilesystemRevocationJournalTest {
}, executor);
}
- private void assertPersistedCorruptionRejected(CorruptionCase corruption) throws Exception {
- System.out.println("strictCodecCorruptionFailsReadAndTransitionWithoutOverwrite[" + corruption.description
- + "]");
- String suffix = "corrupt-" + corruption.name().toLowerCase(java.util.Locale.ROOT);
- Path root = temporaryDirectory.resolve(suffix);
- Credential credential;
- Path journalPath;
- byte[] corrupt;
- try (FilesystemPkiStore store = store(root)) {
- credential = credential(store, suffix);
- store.putCredential(credential);
- journalPath = new FsPaths(root).revocationJournal(credential.credentialId());
- corrupt = corruption.bytes(credential.credentialId());
- FsOperations.writeAtomic(journalPath, corrupt);
- }
-
- try (FilesystemPkiStore reopened = store(root)) {
- byte[] before = FsOperations.readAll(journalPath);
- assertTrue(reopened.getRevocation(credential.credentialId()).isEmpty());
- transition(reopened, hold(credential));
- assertArrayEquals(before, FsOperations.readAll(journalPath));
- }
- System.out.println("strictCodecCorruptionFailsReadAndTransitionWithoutOverwrite...ok");
- }
-
- private static List states(RevocationJournal journal) {
- return journal.transitions().stream().map(RevocationTransition::state).toList();
+ private static List states(CredentialHistory history) {
+ return history.transitions().stream().map(RevocationTransition::state).toList();
}
private static void assertTransition(FilesystemPkiStore store, RevocationCommand command, long revision,
RevocationState state) {
- RevocationJournal journal = transition(store, command);
- assertEquals(revision, journal.latest().revision());
- assertEquals(state, journal.latest().state());
+ CredentialHistory history = transition(store, command);
+ assertEquals(revision, history.latest().revision());
+ assertEquals(state, history.latest().state());
}
- private static RevocationJournal transition(FilesystemPkiStore store, RevocationCommand command) {
+ private static CredentialHistory transition(FilesystemPkiStore store, RevocationCommand command) {
store.transitionRevocation(command, TIME);
- return readJournal(store, command.credentialId()).orElseThrow();
+ return readHistory(store, command.credentialId()).orElseThrow();
}
- private static Optional readJournal(FilesystemPkiStore store, PkiId credentialId) {
+ private static Optional readHistory(FilesystemPkiStore store, PkiId credentialId) {
if (store.getRevocation(credentialId).isEmpty()) {
return Optional.empty();
}
@@ -508,7 +458,7 @@ final class FilesystemRevocationJournalTest {
} catch (IOException failure) {
throw new IllegalStateException(failure);
}
- return Optional.of(new RevocationJournal(credentialId, transitions));
+ return Optional.of(new CredentialHistory(credentialId, transitions));
}
private static RevocationCommand.Hold hold(Credential credential) {
@@ -542,171 +492,9 @@ final class FilesystemRevocationJournalTest {
new SimpleAttributeSet());
}
- private static byte[] journalPayload(PkiId credentialId, long journalVersion, RawTransition... transitions)
- throws IOException {
- ByteArrayOutputStream output = envelope();
- writePkiId(output, credentialId);
- writeTypedLong(output, journalVersion);
- output.write(TYPE_LIST);
- output.write(TYPE_REVOCATION_TRANSITION);
- Util.writePack7I(output, transitions.length);
- for (RawTransition transition : transitions) {
- output.write(TYPE_REVOCATION_TRANSITION);
- writeTypedLong(output, transition.revision);
- output.write(TYPE_REVOCATION_STATE);
- output.write(stateCode(transition.state));
- output.write(TYPE_INSTANT);
- Util.writeLong(output, transition.time.getEpochSecond());
- Util.writePack7I(output, transition.time.getNano());
- output.write(TYPE_OPTIONAL);
- output.write(TYPE_REVOCATION_REASON);
- if (transition.reason.isPresent()) {
- output.write(1);
- output.write(TYPE_REVOCATION_REASON);
- output.write(reasonCode(transition.reason.orElseThrow()));
- } else {
- output.write(0);
- }
- output.write(TYPE_ATTRIBUTE_SET);
- Util.writePack7I(output, 0);
- }
- return output.toByteArray();
- }
-
- private static byte[] oldRevokedRecordPayload(PkiId credentialId) throws IOException {
- ByteArrayOutputStream output = envelope();
- writePkiId(output, credentialId);
- output.write(TYPE_INSTANT);
- Util.writeLong(output, TIME.getEpochSecond());
- Util.writePack7I(output, TIME.getNano());
- output.write(TYPE_REVOCATION_REASON);
- output.write(reasonCode(RevocationReason.KEY_COMPROMISE));
- output.write(TYPE_ATTRIBUTE_SET);
- Util.writePack7I(output, 0);
- return output.toByteArray();
- }
-
- private static ByteArrayOutputStream envelope() {
- ByteArrayOutputStream output = new ByteArrayOutputStream();
- output.write(CODEC_MAGIC >>> 24);
- output.write(CODEC_MAGIC >>> 16);
- output.write(CODEC_MAGIC >>> 8);
- output.write(CODEC_MAGIC);
- output.write(CODEC_VERSION);
- output.write(TOP_REVOCATION);
- return output;
- }
-
- private static void writePkiId(ByteArrayOutputStream output, PkiId id) throws IOException {
- output.write(TYPE_PKI_ID);
- output.write(TYPE_STRING);
- Util.writeUTF8(output, id.value());
- }
-
- private static void writeTypedLong(ByteArrayOutputStream output, long value) throws IOException {
- output.write(TYPE_LONG);
- Util.writeLong(output, value);
- }
-
- private static int stateCode(RevocationState state) {
- return switch (state) {
- case CLEAR -> 1;
- case HELD -> 2;
- case PERMANENTLY_REVOKED -> 3;
- };
- }
-
- private static int reasonCode(RevocationReason reason) {
- return switch (reason) {
- case UNSPECIFIED -> 1;
- case KEY_COMPROMISE -> 2;
- case CA_COMPROMISE -> 3;
- case AFFILIATION_CHANGED -> 4;
- case SUPERSEDED -> 5;
- case CESSATION_OF_OPERATION -> 6;
- case CERTIFICATE_HOLD -> 7;
- case REMOVE_FROM_CRL -> 8;
- case PRIVILEGE_WITHDRAWN -> 9;
- case AA_COMPROMISE -> 10;
- };
- }
-
- private record RawTransition(long revision, RevocationState state, Instant time,
- Optional reason) {
- }
-
- private enum CorruptionCase {
- ZERO_TRANSITIONS("zero transitions"), ZERO_REVISION("zero transition revision"),
- FIRST_REVISION_NOT_ONE("first revision is not one"), REVISION_GAP("revision gap"),
- DUPLICATE_REVISION("duplicate revision"), BACKWARD_TIME("backward transition time"),
- CLEAR_FIRST("CLEAR as first state"), CLEAR_NOT_AFTER_HELD("CLEAR not immediately after HELD"),
- REPEATED_HELD("repeated HELD"), PERMANENT_REASON_MISSING("permanent revocation without reason"),
- PERMANENT_REASON_CERTIFICATE_HOLD("permanent revocation with hold reason"),
- PERMANENT_REASON_REMOVE_FROM_CRL("permanent revocation with remove-from-CRL reason"),
- HELD_WITH_REASON("HELD with permanent reason"), CLEAR_WITH_REASON("CLEAR with permanent reason"),
- TRANSITION_AFTER_PERMANENT("transition after permanent revocation"),
- MISMATCHED_CREDENTIAL_ID("mismatched credential namespace"),
- UNSUPPORTED_JOURNAL_VERSION("unsupported embedded journal version"),
- OLD_REVOKED_RECORD_PAYLOAD("old RevokedRecord payload"), TRUNCATED_PAYLOAD("truncated current journal"),
- TRAILING_PAYLOAD("trailing journal data");
-
- private final String description;
-
- CorruptionCase(String description) {
- this.description = description;
- }
-
- private byte[] bytes(PkiId credentialId) throws IOException {
- RawTransition held = raw(1L, RevocationState.HELD, TIME);
- return switch (this) {
- case ZERO_TRANSITIONS -> journalPayload(credentialId, 1L);
- case ZERO_REVISION -> journalPayload(credentialId, 1L, raw(0L, RevocationState.HELD, TIME));
- case FIRST_REVISION_NOT_ONE -> journalPayload(credentialId, 1L, raw(2L, RevocationState.HELD, TIME));
- case REVISION_GAP ->
- journalPayload(credentialId, 1L, held, raw(3L, RevocationState.CLEAR, TIME.plusSeconds(1)));
- case DUPLICATE_REVISION ->
- journalPayload(credentialId, 1L, held, raw(1L, RevocationState.CLEAR, TIME.plusSeconds(1)));
- case BACKWARD_TIME ->
- journalPayload(credentialId, 1L, held, raw(2L, RevocationState.CLEAR, TIME.minusSeconds(1)));
- case CLEAR_FIRST -> journalPayload(credentialId, 1L, raw(1L, RevocationState.CLEAR, TIME));
- case CLEAR_NOT_AFTER_HELD ->
- journalPayload(credentialId, 1L, held, raw(2L, RevocationState.CLEAR, TIME.plusSeconds(1)),
- raw(3L, RevocationState.CLEAR, TIME.plusSeconds(2)));
- case REPEATED_HELD ->
- journalPayload(credentialId, 1L, held, raw(2L, RevocationState.HELD, TIME.plusSeconds(1)));
- case PERMANENT_REASON_MISSING ->
- journalPayload(credentialId, 1L, raw(1L, RevocationState.PERMANENTLY_REVOKED, TIME));
- case PERMANENT_REASON_CERTIFICATE_HOLD -> journalPayload(credentialId, 1L,
- raw(1L, RevocationState.PERMANENTLY_REVOKED, TIME, RevocationReason.CERTIFICATE_HOLD));
- case PERMANENT_REASON_REMOVE_FROM_CRL -> journalPayload(credentialId, 1L,
- raw(1L, RevocationState.PERMANENTLY_REVOKED, TIME, RevocationReason.REMOVE_FROM_CRL));
- case HELD_WITH_REASON -> journalPayload(credentialId, 1L,
- raw(1L, RevocationState.HELD, TIME, RevocationReason.KEY_COMPROMISE));
- case CLEAR_WITH_REASON -> journalPayload(credentialId, 1L, held,
- raw(2L, RevocationState.CLEAR, TIME.plusSeconds(1), RevocationReason.KEY_COMPROMISE));
- case TRANSITION_AFTER_PERMANENT -> journalPayload(credentialId, 1L,
- raw(1L, RevocationState.PERMANENTLY_REVOKED, TIME, RevocationReason.KEY_COMPROMISE),
- raw(2L, RevocationState.HELD, TIME.plusSeconds(1)));
- case MISMATCHED_CREDENTIAL_ID -> journalPayload(new PkiId("credential:other-namespace"), 1L, held);
- case UNSUPPORTED_JOURNAL_VERSION -> journalPayload(credentialId, 2L, held);
- case OLD_REVOKED_RECORD_PAYLOAD -> oldRevokedRecordPayload(credentialId);
- case TRUNCATED_PAYLOAD -> {
- byte[] valid = journalPayload(credentialId, 1L, held);
- yield Arrays.copyOf(valid, valid.length - 1);
- }
- case TRAILING_PAYLOAD -> {
- byte[] valid = journalPayload(credentialId, 1L, held);
- yield Arrays.copyOf(valid, valid.length + 1);
- }
- };
- }
-
- private static RawTransition raw(long revision, RevocationState state, Instant time) {
- return new RawTransition(revision, state, time, Optional.empty());
- }
-
- private static RawTransition raw(long revision, RevocationState state, Instant time, RevocationReason reason) {
- return new RawTransition(revision, state, time, Optional.of(reason));
+ private record CredentialHistory(PkiId credentialId, List transitions) {
+ private RevocationTransition latest() {
+ return transitions.get(transitions.size() - 1);
}
}
}