refactor(pki): remove legacy revocation journals
Remove the obsolete per-credential complete-journal persistence model after the global revocation authority cutover. Preserve transition, durability, concurrency, history, CRL and snapshot coverage while closing the backend on one scalable revocation authority.
This commit is contained in:
@@ -68,7 +68,7 @@ public sealed interface RevocationCommand
|
|||||||
/** Validates and snapshots the command. */
|
/** Validates and snapshots the command. */
|
||||||
public Hold {
|
public Hold {
|
||||||
Objects.requireNonNull(credentialId, "credentialId");
|
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. */
|
/** Validates and snapshots the command. */
|
||||||
public Unhold {
|
public Unhold {
|
||||||
Objects.requireNonNull(credentialId, "credentialId");
|
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) {
|
if (reason == RevocationReason.CERTIFICATE_HOLD || reason == RevocationReason.REMOVE_FROM_CRL) {
|
||||||
throw new IllegalArgumentException("reason must be permanent");
|
throw new IllegalArgumentException("reason must be permanent");
|
||||||
}
|
}
|
||||||
attributes = RevocationJournal.snapshot(attributes);
|
attributes = RevocationTransition.snapshotAttributes(attributes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<RevocationTransition> 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<RevocationTransition> 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<SnapshotEntry> entries = new ArrayList<>();
|
|
||||||
for (AttributeId id : source.ids()) {
|
|
||||||
List<AttributeValue> 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<AttributeValue> values) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Immutable defensive implementation used for journal metadata ownership.
|
|
||||||
*/
|
|
||||||
private static final class SnapshotAttributeSet implements AttributeSet {
|
|
||||||
private final List<SnapshotEntry> entries;
|
|
||||||
|
|
||||||
private SnapshotAttributeSet(List<SnapshotEntry> entries) {
|
|
||||||
this.entries = entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Set<AttributeId> ids() {
|
|
||||||
Set<AttributeId> ids = new LinkedHashSet<>();
|
|
||||||
for (SnapshotEntry entry : entries) {
|
|
||||||
ids.add(entry.id());
|
|
||||||
}
|
|
||||||
return Set.copyOf(ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<AttributeValue> get(AttributeId id) {
|
|
||||||
List<AttributeValue> 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<AttributeValue> getAll(AttributeId id) {
|
|
||||||
Objects.requireNonNull(id, "id");
|
|
||||||
for (SnapshotEntry entry : entries) {
|
|
||||||
if (entry.id().equals(id)) {
|
|
||||||
List<AttributeValue> copies = new ArrayList<>(entry.values().size());
|
|
||||||
for (AttributeValue value : entry.values()) {
|
|
||||||
copies.add(snapshotValue(value));
|
|
||||||
}
|
|
||||||
return List.copyOf(copies);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
package zeroecho.pki.api.revocation;
|
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 {
|
public enum RevocationState {
|
||||||
/** Credential has no effective revocation restriction. */
|
/** Credential has no effective revocation restriction. */
|
||||||
|
|||||||
@@ -34,12 +34,19 @@
|
|||||||
package zeroecho.pki.api.revocation;
|
package zeroecho.pki.api.revocation;
|
||||||
|
|
||||||
import java.time.Instant;
|
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.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import zeroecho.pki.api.attr.AttributeId;
|
||||||
import zeroecho.pki.api.attr.AttributeSet;
|
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 revision positive contiguous revision
|
||||||
* @param state resulting state
|
* @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) {
|
if (revision <= 0L || state == null || time == null || permanentReason == null || attributes == null) {
|
||||||
throw new IllegalArgumentException("Invalid revocation transition");
|
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<SnapshotEntry> entries = new ArrayList<>();
|
||||||
|
for (AttributeId id : source.ids()) {
|
||||||
|
List<AttributeValue> 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<AttributeValue> values) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Immutable defensive implementation for revocation metadata ownership. */
|
||||||
|
private static final class SnapshotAttributeSet implements AttributeSet {
|
||||||
|
private final List<SnapshotEntry> entries;
|
||||||
|
|
||||||
|
private SnapshotAttributeSet(List<SnapshotEntry> entries) {
|
||||||
|
this.entries = entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<AttributeId> ids() {
|
||||||
|
Set<AttributeId> ids = new LinkedHashSet<>();
|
||||||
|
for (SnapshotEntry entry : entries) {
|
||||||
|
ids.add(entry.id());
|
||||||
|
}
|
||||||
|
return Set.copyOf(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<AttributeValue> get(AttributeId id) {
|
||||||
|
List<AttributeValue> 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<AttributeValue> getAll(AttributeId id) {
|
||||||
|
Objects.requireNonNull(id, "id");
|
||||||
|
for (SnapshotEntry entry : entries) {
|
||||||
|
if (entry.id().equals(id)) {
|
||||||
|
List<AttributeValue> copies = new ArrayList<>(entry.values().size());
|
||||||
|
for (AttributeValue value : entry.values()) {
|
||||||
|
copies.add(snapshotValue(value));
|
||||||
|
}
|
||||||
|
return List.copyOf(copies);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,15 +89,15 @@ import zeroecho.pki.spi.store.RevocationSnapshot;
|
|||||||
* the configured {@link PkiStore} and delegates format-specific object creation
|
* the configured {@link PkiStore} and delegates format-specific object creation
|
||||||
* to the active {@link CredentialFramework}. The store acts as the
|
* to the active {@link CredentialFramework}. The store acts as the
|
||||||
* authoritative source of issuer CA state, issuer credentials, revocation
|
* authoritative source of issuer CA state, issuer credentials, revocation
|
||||||
* journals, and previously generated status objects.
|
* transitions, and previously generated status objects.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* The current runtime implementation primarily supports generation workflows
|
* The current runtime implementation primarily supports generation workflows
|
||||||
* that require issuer certificate material and issuer signing key indirection
|
* that require issuer certificate material and issuer signing key indirection
|
||||||
* to be provided through status-object attributes. For X.509 CRL generation,
|
* to be provided through status-object attributes. For X.509 CRL generation,
|
||||||
* this class derives structured CRL entries from authoritative journals and the
|
* this class derives structured CRL entries from a stable ordered current-state
|
||||||
* referenced X.509 credentials.
|
* snapshot and the referenced X.509 credentials.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* <h2>Persistence model</h2>
|
* <h2>Persistence model</h2>
|
||||||
@@ -114,7 +114,7 @@ import zeroecho.pki.spi.store.RevocationSnapshot;
|
|||||||
* <li>This service does not access private key material directly.</li>
|
* <li>This service does not access private key material directly.</li>
|
||||||
* <li>Issuer signing capability is conveyed only through
|
* <li>Issuer signing capability is conveyed only through
|
||||||
* {@link BcX509Attributes#ISSUER_KEYREF}.</li>
|
* {@link BcX509Attributes#ISSUER_KEYREF}.</li>
|
||||||
* <li>For CRL generation, every active journal and referenced target is
|
* <li>For CRL generation, every selected current state and referenced target is
|
||||||
* validated before signing. Any unresolved or malformed state aborts generation
|
* validated before signing. Any unresolved or malformed state aborts generation
|
||||||
* with a stable redacted failure.</li>
|
* with a stable redacted failure.</li>
|
||||||
* <li>The correctness of the generated status object depends on the configured
|
* <li>The correctness of the generated status object depends on the configured
|
||||||
@@ -184,7 +184,7 @@ public final class DefaultStatusObjectService implements StatusObjectService {
|
|||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* When {@link StatusObjectType#CRL} is requested, the service validates every
|
* 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
|
* states are transported with the exact positive X.509 serial, authoritative
|
||||||
* transition time, and explicit reason. Current {@code CLEAR} states are
|
* transition time, and explicit reason. Current {@code CLEAR} states are
|
||||||
* omitted.
|
* omitted.
|
||||||
@@ -192,7 +192,7 @@ public final class DefaultStatusObjectService implements StatusObjectService {
|
|||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* Missing targets, malformed credentials, duplicate serials, future
|
* 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.
|
* before generator invocation or persistence with a stable redacted error.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
@@ -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 class CheckpointCrlCursor implements CrlEntrySource.Cursor {
|
||||||
private final RevocationSnapshot.Cursor cursor;
|
private final RevocationSnapshot.Cursor cursor;
|
||||||
private final PkiId issuerCaId;
|
private final PkiId issuerCaId;
|
||||||
|
|||||||
@@ -231,9 +231,15 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
this(root, options, clock, FilesystemRevocationAuthority.IndexUpdateFaultInjector.NONE);
|
this(root, options, clock, FilesystemRevocationAuthority.IndexUpdateFaultInjector.NONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("PMD.CloseResource")
|
|
||||||
/* package */ FilesystemPkiStore(final Path root, final FsPkiStoreOptions options, final Clock clock,
|
/* package */ FilesystemPkiStore(final Path root, final FsPkiStoreOptions options, final Clock clock,
|
||||||
final FilesystemRevocationAuthority.IndexUpdateFaultInjector indexUpdateFaults) {
|
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");
|
this.options = Objects.requireNonNull(options, "options");
|
||||||
Objects.requireNonNull(root, "root");
|
Objects.requireNonNull(root, "root");
|
||||||
this.clock = Objects.requireNonNull(clock, "clock");
|
this.clock = Objects.requireNonNull(clock, "clock");
|
||||||
@@ -258,6 +264,9 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
FilesystemRevocationAuthority openedRevocations = null;
|
FilesystemRevocationAuthority openedRevocations = null;
|
||||||
try {
|
try {
|
||||||
boolean newStore = !Files.exists(this.paths.versionFile());
|
boolean newStore = !Files.exists(this.paths.versionFile());
|
||||||
|
if (newStore && !snapshotAssembly) {
|
||||||
|
rejectNonEmptyUnversionedStore();
|
||||||
|
}
|
||||||
ensureVersionFile();
|
ensureVersionFile();
|
||||||
this.signingNamespace = ensureSigningNamespace();
|
this.signingNamespace = ensureSigningNamespace();
|
||||||
this.stagedContent = new FilesystemStagedContentStore(this.paths.stagedContentRoot(),
|
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 {
|
private boolean requireSnapshotBoundary() throws IOException {
|
||||||
Path marker = paths.revocationSnapshotBoundary();
|
Path marker = paths.revocationSnapshotBoundary();
|
||||||
if (!Files.exists(marker, java.nio.file.LinkOption.NOFOLLOW_LINKS)) {
|
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<Path> entries = Files.list(this.paths.root())) {
|
||||||
|
if (entries.anyMatch(entry -> !entry.equals(lockDirectory))) {
|
||||||
|
throw new IllegalStateException("unversioned store is not empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try (Stream<Path> entries = Files.list(lockDirectory)) {
|
||||||
|
if (entries.anyMatch(entry -> !entry.equals(this.paths.lockFile()))) {
|
||||||
|
throw new IllegalStateException("unversioned store is not empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private <T> Optional<T> readOptional(final Path path, final FsCodec.Schema<T> schema) {
|
private <T> Optional<T> readOptional(final Path path, final FsCodec.Schema<T> schema) {
|
||||||
try {
|
try {
|
||||||
if (!Files.exists(path)) {
|
if (!Files.exists(path)) {
|
||||||
|
|||||||
@@ -87,10 +87,6 @@ import zeroecho.pki.api.publication.PublicationTargetType;
|
|||||||
import zeroecho.pki.api.request.ParsedCertificationRequest;
|
import zeroecho.pki.api.request.ParsedCertificationRequest;
|
||||||
import zeroecho.pki.api.request.SubjectAlternativeName;
|
import zeroecho.pki.api.request.SubjectAlternativeName;
|
||||||
import zeroecho.pki.api.request.SubjectRdn;
|
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.StatusObject;
|
||||||
import zeroecho.pki.api.status.StatusObjectType;
|
import zeroecho.pki.api.status.StatusObjectType;
|
||||||
import zeroecho.pki.spi.store.StagedContentStore;
|
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_CA_RECORD = 1;
|
||||||
private static final int TOP_CREDENTIAL = 2;
|
private static final int TOP_CREDENTIAL = 2;
|
||||||
private static final int TOP_PARSED_REQUEST = 3;
|
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_STATUS_OBJECT = 5;
|
||||||
private static final int TOP_PUBLICATION = 6;
|
private static final int TOP_PUBLICATION = 6;
|
||||||
private static final int TOP_POLICY_TRACE = 8;
|
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_KIND_ENUM = 51;
|
||||||
private static final int TYPE_CA_STATE_ENUM = 52;
|
private static final int TYPE_CA_STATE_ENUM = 52;
|
||||||
private static final int TYPE_CREDENTIAL_STATUS_ENUM = 53;
|
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_STATUS_OBJECT_TYPE_ENUM = 55;
|
||||||
private static final int TYPE_PUBLICATION_TARGET_TYPE_ENUM = 56;
|
private static final int TYPE_PUBLICATION_TARGET_TYPE_ENUM = 56;
|
||||||
private static final int TYPE_PUBLICATION_STATUS_ENUM = 57;
|
private static final int TYPE_PUBLICATION_STATUS_ENUM = 57;
|
||||||
private static final int TYPE_DURABILITY_POLICY_ENUM = 58;
|
private static final int TYPE_DURABILITY_POLICY_ENUM = 58;
|
||||||
private static final int TYPE_SIGN_STATE_ENUM = 59;
|
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_SUBJECT_RDN_TYPE_ENUM = 62;
|
||||||
private static final int TYPE_SAN_TYPE_ENUM = 63;
|
private static final int TYPE_SAN_TYPE_ENUM = 63;
|
||||||
private static final int TYPE_SUBJECT_RDN = 65;
|
private static final int TYPE_SUBJECT_RDN = 65;
|
||||||
@@ -245,42 +237,6 @@ final class FsCodec {
|
|||||||
case 3 -> CredentialStatus.EXPIRED;
|
case 3 -> CredentialStatus.EXPIRED;
|
||||||
default -> throw unknownEnum("CredentialStatus", code);
|
default -> throw unknownEnum("CredentialStatus", code);
|
||||||
});
|
});
|
||||||
private static final ValueSchema<RevocationReason> 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<RevocationState> 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<StatusObjectType> STATUS_OBJECT_TYPE = enumSchema(TYPE_STATUS_OBJECT_TYPE_ENUM,
|
private static final ValueSchema<StatusObjectType> STATUS_OBJECT_TYPE = enumSchema(TYPE_STATUS_OBJECT_TYPE_ENUM,
|
||||||
value -> switch (value) {
|
value -> switch (value) {
|
||||||
case CRL -> 1;
|
case CRL -> 1;
|
||||||
@@ -440,11 +396,6 @@ final class FsCodec {
|
|||||||
private static final ValueSchema<Optional<String>> OPTIONAL_STRING = optionalOf(STRING);
|
private static final ValueSchema<Optional<String>> OPTIONAL_STRING = optionalOf(STRING);
|
||||||
private static final ValueSchema<Optional<Instant>> OPTIONAL_INSTANT = optionalOf(INSTANT);
|
private static final ValueSchema<Optional<Instant>> OPTIONAL_INSTANT = optionalOf(INSTANT);
|
||||||
private static final ValueSchema<Optional<EncodedObject>> OPTIONAL_ENCODED_OBJECT = optionalOf(ENCODED_OBJECT);
|
private static final ValueSchema<Optional<EncodedObject>> OPTIONAL_ENCODED_OBJECT = optionalOf(ENCODED_OBJECT);
|
||||||
private static final ValueSchema<Optional<RevocationReason>> OPTIONAL_REVOCATION_REASON = optionalOf(
|
|
||||||
REVOCATION_REASON);
|
|
||||||
private static final ValueSchema<RevocationTransition> REVOCATION_TRANSITION = valueSchema(
|
|
||||||
TYPE_REVOCATION_TRANSITION, FsCodec::writeRevocationTransition, FsCodec::readRevocationTransition);
|
|
||||||
private static final ValueSchema<List<RevocationTransition>> REVOCATION_TRANSITIONS = listOf(REVOCATION_TRANSITION);
|
|
||||||
|
|
||||||
private static final ValueSchema<Credential> CREDENTIAL_VALUE = valueSchema(TYPE_CREDENTIAL_RECORD,
|
private static final ValueSchema<Credential> CREDENTIAL_VALUE = valueSchema(TYPE_CREDENTIAL_RECORD,
|
||||||
FsCodec::writeCredential, FsCodec::readCredential);
|
FsCodec::writeCredential, FsCodec::readCredential);
|
||||||
@@ -455,8 +406,6 @@ final class FsCodec {
|
|||||||
/* package */ static final Schema<Credential> CREDENTIAL = topLevel(TOP_CREDENTIAL, "CREDENTIAL", CREDENTIAL_VALUE);
|
/* package */ static final Schema<Credential> CREDENTIAL = topLevel(TOP_CREDENTIAL, "CREDENTIAL", CREDENTIAL_VALUE);
|
||||||
/* package */ static final Schema<ParsedCertificationRequest> PARSED_REQUEST = topLevel(TOP_PARSED_REQUEST,
|
/* package */ static final Schema<ParsedCertificationRequest> PARSED_REQUEST = topLevel(TOP_PARSED_REQUEST,
|
||||||
"PARSED_REQUEST", valueSchema(101, FsCodec::writeParsedRequest, FsCodec::readParsedRequest));
|
"PARSED_REQUEST", valueSchema(101, FsCodec::writeParsedRequest, FsCodec::readParsedRequest));
|
||||||
/* package */ static final Schema<RevocationJournal> REVOCATION_JOURNAL = topLevel(TOP_REVOCATION,
|
|
||||||
"REVOCATION_JOURNAL", valueSchema(102, FsCodec::writeRevocationJournal, FsCodec::readRevocationJournal));
|
|
||||||
/* package */ static final Schema<StatusObject> STATUS_OBJECT = topLevel(TOP_STATUS_OBJECT, "STATUS_OBJECT",
|
/* package */ static final Schema<StatusObject> STATUS_OBJECT = topLevel(TOP_STATUS_OBJECT, "STATUS_OBJECT",
|
||||||
valueSchema(103, FsCodec::writeStatusObject, FsCodec::readStatusObject));
|
valueSchema(103, FsCodec::writeStatusObject, FsCodec::readStatusObject));
|
||||||
/* package */ static final Schema<PublicationRecord> PUBLICATION = topLevel(TOP_PUBLICATION, "PUBLICATION",
|
/* package */ static final Schema<PublicationRecord> PUBLICATION = topLevel(TOP_PUBLICATION, "PUBLICATION",
|
||||||
@@ -475,7 +424,7 @@ final class FsCodec {
|
|||||||
|
|
||||||
private static final Map<Integer, Schema<?>> TOP_LEVEL_SCHEMAS = Map.ofEntries(Map.entry(TOP_CA_RECORD, CA_RECORD),
|
private static final Map<Integer, Schema<?>> 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_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_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_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));
|
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));
|
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 {
|
private static void writeStatusObject(Writer writer, StatusObject value) throws IOException {
|
||||||
writer.writeValue(PKI_ID, value.statusObjectId());
|
writer.writeValue(PKI_ID, value.statusObjectId());
|
||||||
writer.writeValue(FORMAT_ID, value.formatId());
|
writer.writeValue(FORMAT_ID, value.formatId());
|
||||||
|
|||||||
@@ -214,17 +214,17 @@ final class FsOperations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strictly persists one authoritative revocation journal image.
|
* Strictly persists one complete atomic file image.
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* The namespace commit point is an {@link StandardCopyOption#ATOMIC_MOVE} in
|
* The namespace commit point is an {@link StandardCopyOption#ATOMIC_MOVE} in
|
||||||
* the target directory. No non-atomic fallback is permitted. A directory force
|
* the target directory. No non-atomic fallback is permitted. A directory force
|
||||||
* failure after that move is reported distinctly because the durable
|
* failure after that move is reported distinctly because the durable
|
||||||
* authoritative image is then uncertain.
|
* image is then uncertain.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @param target journal target
|
* @param target file target
|
||||||
* @param data complete encoded journal
|
* @param data complete encoded image
|
||||||
* @throws IOException on a pre-commit persistence failure
|
* @throws IOException on a pre-commit persistence failure
|
||||||
* @throws DurabilityUncertainException after a committed move whose directory
|
* @throws DurabilityUncertainException after a committed move whose directory
|
||||||
* force failed
|
* force failed
|
||||||
|
|||||||
@@ -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() {
|
/* default */ Path revocationTransitionLog() {
|
||||||
return this.root.resolve(REVOCATIONS_DIRECTORY).resolve("transitions.log");
|
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");
|
return this.root.resolve(REVOCATIONS_DIRECTORY).resolve("current-state.idx.lock");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* default */ Path revocationSnapshotRoot() {
|
|
||||||
return this.root.resolve("revocation-snapshots");
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Policy traces (immutable .bin)
|
// Policy traces (immutable .bin)
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -416,7 +416,7 @@ final class FsSnapshotExporter {
|
|||||||
|
|
||||||
private void restore(Path targetRoot) throws IOException {
|
private void restore(Path targetRoot) throws IOException {
|
||||||
Set<PkiId> transferred = new HashSet<>();
|
Set<PkiId> transferred = new HashSet<>();
|
||||||
try (FilesystemPkiStore target = new FilesystemPkiStore(targetRoot, options)) {
|
try (FilesystemPkiStore target = FilesystemPkiStore.openSnapshotAssembly(targetRoot, options)) {
|
||||||
for (Map.Entry<PkiId, Credential> entry : authority.credentials().entrySet()) {
|
for (Map.Entry<PkiId, Credential> entry : authority.credentials().entrySet()) {
|
||||||
if (persistCredential(target, entry.getValue())) {
|
if (persistCredential(target, entry.getValue())) {
|
||||||
transferred.add(entry.getKey());
|
transferred.add(entry.getKey());
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ import zeroecho.pki.api.PkiException;
|
|||||||
import zeroecho.pki.api.PkiId;
|
import zeroecho.pki.api.PkiId;
|
||||||
import zeroecho.pki.api.audit.AuditEvent;
|
import zeroecho.pki.api.audit.AuditEvent;
|
||||||
import zeroecho.pki.api.revocation.RevocationCommand;
|
import zeroecho.pki.api.revocation.RevocationCommand;
|
||||||
import zeroecho.pki.api.revocation.RevocationJournal;
|
|
||||||
import zeroecho.pki.api.revocation.RevocationRecord;
|
import zeroecho.pki.api.revocation.RevocationRecord;
|
||||||
import zeroecho.pki.api.revocation.RevocationQuery;
|
import zeroecho.pki.api.revocation.RevocationQuery;
|
||||||
import zeroecho.pki.api.revocation.RevocationState;
|
import zeroecho.pki.api.revocation.RevocationState;
|
||||||
|
|||||||
@@ -97,7 +97,6 @@ import zeroecho.pki.api.issuance.VerificationPolicy;
|
|||||||
import zeroecho.pki.api.request.CertificationRequest;
|
import zeroecho.pki.api.request.CertificationRequest;
|
||||||
import zeroecho.pki.api.request.ParsedCertificationRequest;
|
import zeroecho.pki.api.request.ParsedCertificationRequest;
|
||||||
import zeroecho.pki.api.revocation.RevocationCommand;
|
import zeroecho.pki.api.revocation.RevocationCommand;
|
||||||
import zeroecho.pki.api.revocation.RevocationJournal;
|
|
||||||
import zeroecho.pki.api.revocation.RevocationRecord;
|
import zeroecho.pki.api.revocation.RevocationRecord;
|
||||||
import zeroecho.pki.api.revocation.RevocationReason;
|
import zeroecho.pki.api.revocation.RevocationReason;
|
||||||
import zeroecho.pki.api.revocation.RevocationState;
|
import zeroecho.pki.api.revocation.RevocationState;
|
||||||
@@ -225,12 +224,12 @@ final class DefaultStatusObjectServiceCrlTest {
|
|||||||
StatusObjectGenerateCommand command = new StatusObjectGenerateCommand(caId, StatusObjectType.CRL,
|
StatusObjectGenerateCommand command = new StatusObjectGenerateCommand(caId, StatusObjectType.CRL,
|
||||||
runtime.framework().formatId(), emptyAttributes());
|
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);
|
EVALUATION_TIME.minusSeconds(1), Optional.empty())), Map.of(), false);
|
||||||
|
|
||||||
Credential wrongFormat = copy(template, "wrong-format", new FormatId("not-x509"), template.content());
|
Credential wrongFormat = copy(template, "wrong-format", new FormatId("not-x509"), template.content());
|
||||||
assertCrlFailure(
|
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())),
|
EVALUATION_TIME.minusSeconds(1), Optional.empty())),
|
||||||
Map.of(wrongFormat.credentialId(), wrongFormat), false);
|
Map.of(wrongFormat.credentialId(), wrongFormat), false);
|
||||||
|
|
||||||
@@ -240,14 +239,14 @@ final class DefaultStatusObjectServiceCrlTest {
|
|||||||
Credential wrongEncoding = copy(template, "wrong-encoding", BcX509CredentialFramework.FORMAT_ID,
|
Credential wrongEncoding = copy(template, "wrong-encoding", BcX509CredentialFramework.FORMAT_ID,
|
||||||
wrongEncodingContent);
|
wrongEncodingContent);
|
||||||
assertCrlFailure(
|
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())),
|
EVALUATION_TIME.minusSeconds(1), Optional.empty())),
|
||||||
Map.of(wrongEncoding.credentialId(), wrongEncoding), false);
|
Map.of(wrongEncoding.credentialId(), wrongEncoding), false);
|
||||||
|
|
||||||
Credential malformed = copy(template, "malformed", BcX509CredentialFramework.FORMAT_ID,
|
Credential malformed = copy(template, "malformed", BcX509CredentialFramework.FORMAT_ID,
|
||||||
runtime.stageCredential(new byte[] { 1, 2, 3 }));
|
runtime.stageCredential(new byte[] { 1, 2, 3 }));
|
||||||
assertCrlFailure(
|
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())),
|
EVALUATION_TIME.minusSeconds(1), Optional.empty())),
|
||||||
Map.of(malformed.credentialId(), malformed), false);
|
Map.of(malformed.credentialId(), malformed), false);
|
||||||
|
|
||||||
@@ -256,16 +255,16 @@ final class DefaultStatusObjectServiceCrlTest {
|
|||||||
Credential duplicateTwo = copy(template, "duplicate-two", BcX509CredentialFramework.FORMAT_ID,
|
Credential duplicateTwo = copy(template, "duplicate-two", BcX509CredentialFramework.FORMAT_ID,
|
||||||
template.content());
|
template.content());
|
||||||
assertCrlFailure(runtime, command,
|
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()),
|
Optional.empty()),
|
||||||
journal(duplicateTwo.credentialId(), RevocationState.PERMANENTLY_REVOKED,
|
record(duplicateTwo.credentialId(), RevocationState.PERMANENTLY_REVOKED,
|
||||||
EVALUATION_TIME.minusSeconds(1), Optional.of(RevocationReason.KEY_COMPROMISE))),
|
EVALUATION_TIME.minusSeconds(1), Optional.of(RevocationReason.KEY_COMPROMISE))),
|
||||||
Map.of(duplicateOne.credentialId(), duplicateOne, duplicateTwo.credentialId(), duplicateTwo),
|
Map.of(duplicateOne.credentialId(), duplicateOne, duplicateTwo.credentialId(), duplicateTwo),
|
||||||
false);
|
false);
|
||||||
|
|
||||||
Credential future = copy(template, "future", BcX509CredentialFramework.FORMAT_ID, template.content());
|
Credential future = copy(template, "future", BcX509CredentialFramework.FORMAT_ID, template.content());
|
||||||
assertCrlFailure(
|
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())),
|
EVALUATION_TIME.plusSeconds(1), Optional.empty())),
|
||||||
Map.of(future.credentialId(), future), false);
|
Map.of(future.credentialId(), future), false);
|
||||||
|
|
||||||
@@ -320,10 +319,10 @@ final class DefaultStatusObjectServiceCrlTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void assertCrlFailure(PkiTestRuntime runtime, StatusObjectGenerateCommand command,
|
private static void assertCrlFailure(PkiTestRuntime runtime, StatusObjectGenerateCommand command,
|
||||||
List<RevocationJournal> journals, Map<PkiId, Credential> credentials, boolean failListing) {
|
List<RevocationRecord> records, Map<PkiId, Credential> credentials, boolean failListing) {
|
||||||
int signCount = runtime.submittedSignCount();
|
int signCount = runtime.submittedSignCount();
|
||||||
int statusCount = runtime.store().listStatusObjects(command.issuerCaId()).size();
|
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(),
|
DefaultStatusObjectService service = new DefaultStatusObjectService(view, runtime.framework(),
|
||||||
runtime.auditSink(), usableResolver(), runtime.signingBus().authority());
|
runtime.auditSink(), usableResolver(), runtime.signingBus().authority());
|
||||||
|
|
||||||
@@ -335,7 +334,7 @@ final class DefaultStatusObjectServiceCrlTest {
|
|||||||
assertEquals(statusCount, runtime.store().listStatusObjects(command.issuerCaId()).size());
|
assertEquals(statusCount, runtime.store().listStatusObjects(command.issuerCaId()).size());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PkiStore storeView(PkiStore delegate, List<RevocationJournal> journals,
|
private static PkiStore storeView(PkiStore delegate, List<RevocationRecord> records,
|
||||||
Map<PkiId, Credential> credentials, boolean failListing) {
|
Map<PkiId, Credential> credentials, boolean failListing) {
|
||||||
return (PkiStore) Proxy.newProxyInstance(PkiStore.class.getClassLoader(), new Class<?>[] { PkiStore.class },
|
return (PkiStore) Proxy.newProxyInstance(PkiStore.class.getClassLoader(), new Class<?>[] { PkiStore.class },
|
||||||
(proxy, method, arguments) -> {
|
(proxy, method, arguments) -> {
|
||||||
@@ -343,9 +342,7 @@ final class DefaultStatusObjectServiceCrlTest {
|
|||||||
if (failListing) {
|
if (failListing) {
|
||||||
throw new IllegalStateException(SENTINEL);
|
throw new IllegalStateException(SENTINEL);
|
||||||
}
|
}
|
||||||
List<RevocationRecord> snapshot = journals.stream()
|
List<RevocationRecord> snapshot = List.copyOf(records);
|
||||||
.map(journal -> new RevocationRecord(journal.credentialId(), journal.latest()))
|
|
||||||
.toList();
|
|
||||||
return new zeroecho.pki.spi.store.RevocationSnapshot() {
|
return new zeroecho.pki.spi.store.RevocationSnapshot() {
|
||||||
@Override
|
@Override
|
||||||
public String snapshotId() {
|
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<RevocationReason> reason) {
|
Optional<RevocationReason> reason) {
|
||||||
return new RevocationJournal(credentialId,
|
return new RevocationRecord(credentialId,
|
||||||
List.of(new RevocationTransition(1L, state, time, reason, emptyAttributes())));
|
new RevocationTransition(1L, state, time, reason, emptyAttributes()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Credential copy(Credential template, String suffix, FormatId formatId,
|
private static Credential copy(Credential template, String suffix, FormatId formatId,
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ import zeroecho.pki.api.credential.CredentialStatus;
|
|||||||
import zeroecho.pki.api.credential.CredentialUse;
|
import zeroecho.pki.api.credential.CredentialUse;
|
||||||
import zeroecho.pki.api.credential.EffectiveCredentialStatus;
|
import zeroecho.pki.api.credential.EffectiveCredentialStatus;
|
||||||
import zeroecho.pki.api.credential.EffectiveCredentialStatusResolver;
|
import zeroecho.pki.api.credential.EffectiveCredentialStatusResolver;
|
||||||
import zeroecho.pki.api.revocation.RevocationJournal;
|
|
||||||
import zeroecho.pki.api.revocation.RevocationRecord;
|
import zeroecho.pki.api.revocation.RevocationRecord;
|
||||||
import zeroecho.pki.api.revocation.RevocationReason;
|
import zeroecho.pki.api.revocation.RevocationReason;
|
||||||
import zeroecho.pki.api.revocation.RevocationState;
|
import zeroecho.pki.api.revocation.RevocationState;
|
||||||
@@ -122,13 +121,13 @@ final class StoreBackedEffectiveCredentialStatusResolverTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void futureAndMismatchedJournalsFailClosed() {
|
void futureAndMismatchedRecordsFailClosed() {
|
||||||
Credential credential = credential("invalid", CredentialStatus.ISSUED, NOW.minusSeconds(60),
|
Credential credential = credential("invalid", CredentialStatus.ISSUED, NOW.minusSeconds(60),
|
||||||
NOW.plusSeconds(60));
|
NOW.plusSeconds(60));
|
||||||
assertResolutionFailure(credential, revocation(credential, RevocationReason.KEY_COMPROMISE, NOW.plusNanos(1)));
|
assertResolutionFailure(credential, revocation(credential, RevocationReason.KEY_COMPROMISE, NOW.plusNanos(1)));
|
||||||
RevocationJournal mismatch = new RevocationJournal(new PkiId("credential:other"),
|
RevocationRecord mismatch = new RevocationRecord(new PkiId("credential:other"),
|
||||||
List.of(new RevocationTransition(1L, RevocationState.PERMANENTLY_REVOKED, NOW,
|
new RevocationTransition(1L, RevocationState.PERMANENTLY_REVOKED, NOW,
|
||||||
Optional.of(RevocationReason.KEY_COMPROMISE), new SimpleAttributeSet())));
|
Optional.of(RevocationReason.KEY_COMPROMISE), new SimpleAttributeSet()));
|
||||||
assertResolutionFailure(credential, Optional.of(mismatch));
|
assertResolutionFailure(credential, Optional.of(mismatch));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,50 +192,47 @@ final class StoreBackedEffectiveCredentialStatusResolverTest {
|
|||||||
assertFalse(event.details().toString().contains(sentinel));
|
assertFalse(event.details().toString().contains(sentinel));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void assertStatus(Credential credential, Optional<RevocationJournal> revocation,
|
private static void assertStatus(Credential credential, Optional<RevocationRecord> revocation,
|
||||||
EffectiveCredentialStatus expected) {
|
EffectiveCredentialStatus expected) {
|
||||||
assertEquals(expected, resolver(revocation).beginEvaluation().resolve(credential));
|
assertEquals(expected, resolver(revocation).beginEvaluation().resolve(credential));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void assertResolutionFailure(Credential credential, Optional<RevocationJournal> revocation) {
|
private static void assertResolutionFailure(Credential credential, Optional<RevocationRecord> revocation) {
|
||||||
PkiException failure = assertThrows(PkiException.class,
|
PkiException failure = assertThrows(PkiException.class,
|
||||||
() -> resolver(revocation).beginEvaluation().resolve(credential));
|
() -> resolver(revocation).beginEvaluation().resolve(credential));
|
||||||
assertEquals("Credential status resolution failed: code=CREDENTIAL_STATUS_RESOLUTION_FAILED",
|
assertEquals("Credential status resolution failed: code=CREDENTIAL_STATUS_RESOLUTION_FAILED",
|
||||||
failure.getMessage());
|
failure.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static StoreBackedEffectiveCredentialStatusResolver resolver(Optional<RevocationJournal> revocation) {
|
private static StoreBackedEffectiveCredentialStatusResolver resolver(Optional<RevocationRecord> revocation) {
|
||||||
return new StoreBackedEffectiveCredentialStatusResolver(store(id -> revocation, new AtomicInteger()),
|
return new StoreBackedEffectiveCredentialStatusResolver(store(id -> revocation, new AtomicInteger()),
|
||||||
Clock.fixed(NOW, ZoneOffset.UTC));
|
Clock.fixed(NOW, ZoneOffset.UTC));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PkiStore store(Function<PkiId, Optional<RevocationJournal>> lookup, AtomicInteger calls) {
|
private static PkiStore store(Function<PkiId, Optional<RevocationRecord>> lookup, AtomicInteger calls) {
|
||||||
return (PkiStore) Proxy.newProxyInstance(PkiStore.class.getClassLoader(), new Class<?>[] { PkiStore.class },
|
return (PkiStore) Proxy.newProxyInstance(PkiStore.class.getClassLoader(), new Class<?>[] { PkiStore.class },
|
||||||
(proxy, method, arguments) -> {
|
(proxy, method, arguments) -> {
|
||||||
if ("getRevocation".equals(method.getName())) {
|
if ("getRevocation".equals(method.getName())) {
|
||||||
calls.incrementAndGet();
|
calls.incrementAndGet();
|
||||||
return lookup.apply((PkiId) arguments[0])
|
return lookup.apply((PkiId) arguments[0]);
|
||||||
.map(journal -> new RevocationRecord(journal.credentialId(), journal.latest()));
|
|
||||||
}
|
}
|
||||||
throw new UnsupportedOperationException(method.getName());
|
throw new UnsupportedOperationException(method.getName());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Optional<RevocationJournal> revocation(Credential credential, RevocationReason reason,
|
private static Optional<RevocationRecord> revocation(Credential credential, RevocationReason reason,
|
||||||
Instant time) {
|
Instant time) {
|
||||||
if (reason == RevocationReason.REMOVE_FROM_CRL) {
|
if (reason == RevocationReason.REMOVE_FROM_CRL) {
|
||||||
return Optional.of(new RevocationJournal(credential.credentialId(),
|
return Optional.of(new RevocationRecord(credential.credentialId(),
|
||||||
List.of(new RevocationTransition(1L, RevocationState.HELD, time.minusNanos(1), Optional.empty(),
|
new RevocationTransition(2L, RevocationState.CLEAR, time, Optional.empty(),
|
||||||
new SimpleAttributeSet()),
|
new SimpleAttributeSet())));
|
||||||
new RevocationTransition(2L, RevocationState.CLEAR, time, Optional.empty(),
|
|
||||||
new SimpleAttributeSet()))));
|
|
||||||
}
|
}
|
||||||
RevocationState state = reason == RevocationReason.CERTIFICATE_HOLD ? RevocationState.HELD
|
RevocationState state = reason == RevocationReason.CERTIFICATE_HOLD ? RevocationState.HELD
|
||||||
: RevocationState.PERMANENTLY_REVOKED;
|
: RevocationState.PERMANENTLY_REVOKED;
|
||||||
Optional<RevocationReason> permanentReason = state == RevocationState.PERMANENTLY_REVOKED ? Optional.of(reason)
|
Optional<RevocationReason> permanentReason = state == RevocationState.PERMANENTLY_REVOKED ? Optional.of(reason)
|
||||||
: Optional.empty();
|
: Optional.empty();
|
||||||
return Optional.of(new RevocationJournal(credential.credentialId(),
|
return Optional.of(new RevocationRecord(credential.credentialId(),
|
||||||
List.of(new RevocationTransition(1L, state, time, permanentReason, new SimpleAttributeSet()))));
|
new RevocationTransition(1L, state, time, permanentReason, new SimpleAttributeSet())));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Credential credential(String suffix, CredentialStatus status, Instant notBefore, Instant notAfter) {
|
private Credential credential(String suffix, CredentialStatus status, Instant notBefore, Instant notAfter) {
|
||||||
|
|||||||
@@ -116,7 +116,6 @@ import zeroecho.pki.api.publication.PublicationTarget;
|
|||||||
import zeroecho.pki.api.publication.PublicationTargetType;
|
import zeroecho.pki.api.publication.PublicationTargetType;
|
||||||
import zeroecho.pki.api.request.ParsedCertificationRequest;
|
import zeroecho.pki.api.request.ParsedCertificationRequest;
|
||||||
import zeroecho.pki.api.revocation.RevocationCommand;
|
import zeroecho.pki.api.revocation.RevocationCommand;
|
||||||
import zeroecho.pki.api.revocation.RevocationJournal;
|
|
||||||
import zeroecho.pki.api.revocation.RevocationRecord;
|
import zeroecho.pki.api.revocation.RevocationRecord;
|
||||||
import zeroecho.pki.api.revocation.RevocationReason;
|
import zeroecho.pki.api.revocation.RevocationReason;
|
||||||
import zeroecho.pki.api.status.StatusObject;
|
import zeroecho.pki.api.status.StatusObject;
|
||||||
@@ -779,8 +778,8 @@ public final class FilesystemPkiStoreTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void revocationJournalPersistsLegalTransitions() throws Exception {
|
void globalRevocationLogPersistsStreamingHistory() throws Exception {
|
||||||
System.out.println("revocationJournalPersistsLegalTransitions");
|
System.out.println("globalRevocationLogPersistsStreamingHistory");
|
||||||
|
|
||||||
Path root = tmp.resolve("store-revocation-history");
|
Path root = tmp.resolve("store-revocation-history");
|
||||||
Path restoredRoot = tmp.resolve("store-revocation-history-snapshot");
|
Path restoredRoot = tmp.resolve("store-revocation-history-snapshot");
|
||||||
@@ -830,7 +829,7 @@ public final class FilesystemPkiStoreTest {
|
|||||||
System.out.println("...store tree:");
|
System.out.println("...store tree:");
|
||||||
dumpTree(root);
|
dumpTree(root);
|
||||||
|
|
||||||
System.out.println("revocationJournalPersistsLegalTransitions...ok");
|
System.out.println("globalRevocationLogPersistsStreamingHistory...ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -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.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@@ -57,14 +57,10 @@ import java.util.concurrent.ExecutorService;
|
|||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
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.Test;
|
||||||
import org.junit.jupiter.api.TestFactory;
|
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
import zeroecho.core.io.Util;
|
|
||||||
import zeroecho.pki.api.FormatId;
|
import zeroecho.pki.api.FormatId;
|
||||||
import zeroecho.pki.api.IssuerRef;
|
import zeroecho.pki.api.IssuerRef;
|
||||||
import zeroecho.pki.api.PkiException;
|
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.CredentialStatus;
|
||||||
import zeroecho.pki.api.credential.EffectiveCredentialStatus;
|
import zeroecho.pki.api.credential.EffectiveCredentialStatus;
|
||||||
import zeroecho.pki.api.revocation.RevocationCommand;
|
import zeroecho.pki.api.revocation.RevocationCommand;
|
||||||
import zeroecho.pki.api.revocation.RevocationJournal;
|
|
||||||
import zeroecho.pki.api.revocation.RevocationReason;
|
import zeroecho.pki.api.revocation.RevocationReason;
|
||||||
import zeroecho.pki.api.revocation.RevocationState;
|
import zeroecho.pki.api.revocation.RevocationState;
|
||||||
import zeroecho.pki.api.revocation.RevocationTransition;
|
import zeroecho.pki.api.revocation.RevocationTransition;
|
||||||
import zeroecho.pki.impl.core.StoreBackedEffectiveCredentialStatusResolver;
|
import zeroecho.pki.impl.core.StoreBackedEffectiveCredentialStatusResolver;
|
||||||
import zeroecho.pki.impl.core.attr.SimpleAttributeSet;
|
import zeroecho.pki.impl.core.attr.SimpleAttributeSet;
|
||||||
|
|
||||||
final class FilesystemRevocationJournalTest {
|
final class FilesystemRevocationAuthorityTest {
|
||||||
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;
|
|
||||||
private static final Instant TIME = Instant.parse("2026-07-01T12:00:00Z");
|
private static final Instant TIME = Instant.parse("2026-07-01T12:00:00Z");
|
||||||
private static final AttributeId NOTE = new AttributeId("test.note");
|
private static final AttributeId NOTE = new AttributeId("test.note");
|
||||||
|
|
||||||
@@ -124,10 +106,10 @@ final class FilesystemRevocationJournalTest {
|
|||||||
() -> transition(store, revoke(credential, RevocationReason.CA_COMPROMISE)));
|
() -> transition(store, revoke(credential, RevocationReason.CA_COMPROMISE)));
|
||||||
}
|
}
|
||||||
try (FilesystemPkiStore reopened = store(root)) {
|
try (FilesystemPkiStore reopened = store(root)) {
|
||||||
RevocationJournal journal = readJournal(reopened, credential.credentialId()).orElseThrow();
|
CredentialHistory history = readHistory(reopened, credential.credentialId()).orElseThrow();
|
||||||
assertEquals(4, journal.transitions().size());
|
assertEquals(4, history.transitions().size());
|
||||||
assertEquals(RevocationState.PERMANENTLY_REVOKED, journal.latest().state());
|
assertEquals(RevocationState.PERMANENTLY_REVOKED, history.latest().state());
|
||||||
assertEquals(RevocationReason.KEY_COMPROMISE, journal.latest().permanentReason().orElseThrow());
|
assertEquals(RevocationReason.KEY_COMPROMISE, history.latest().permanentReason().orElseThrow());
|
||||||
}
|
}
|
||||||
System.out.println("legalTransitionsAreContiguousDurableAndPermanentIsTerminal...ok");
|
System.out.println("legalTransitionsAreContiguousDurableAndPermanentIsTerminal...ok");
|
||||||
}
|
}
|
||||||
@@ -143,28 +125,28 @@ final class FilesystemRevocationJournalTest {
|
|||||||
store.putCredential(fromClear);
|
store.putCredential(fromClear);
|
||||||
store.putCredential(fromHeld);
|
store.putCredential(fromHeld);
|
||||||
|
|
||||||
RevocationJournal directJournal = transition(store, revoke(direct, RevocationReason.KEY_COMPROMISE));
|
CredentialHistory directHistory = transition(store, revoke(direct, RevocationReason.KEY_COMPROMISE));
|
||||||
assertEquals(List.of(RevocationState.PERMANENTLY_REVOKED), states(directJournal));
|
assertEquals(List.of(RevocationState.PERMANENTLY_REVOKED), states(directHistory));
|
||||||
assertEquals(1L, directJournal.latest().revision());
|
assertEquals(1L, directHistory.latest().revision());
|
||||||
|
|
||||||
transition(store, hold(fromClear));
|
transition(store, hold(fromClear));
|
||||||
transition(store, unhold(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),
|
assertEquals(List.of(RevocationState.HELD, RevocationState.CLEAR, RevocationState.PERMANENTLY_REVOKED),
|
||||||
states(clearJournal));
|
states(clearHistory));
|
||||||
assertEquals(3L, clearJournal.latest().revision());
|
assertEquals(3L, clearHistory.latest().revision());
|
||||||
|
|
||||||
transition(store, hold(fromHeld));
|
transition(store, hold(fromHeld));
|
||||||
RevocationJournal heldJournal = transition(store, revoke(fromHeld, RevocationReason.SUPERSEDED));
|
CredentialHistory heldHistory = transition(store, revoke(fromHeld, RevocationReason.SUPERSEDED));
|
||||||
assertEquals(List.of(RevocationState.HELD, RevocationState.PERMANENTLY_REVOKED), states(heldJournal));
|
assertEquals(List.of(RevocationState.HELD, RevocationState.PERMANENTLY_REVOKED), states(heldHistory));
|
||||||
assertEquals(2L, heldJournal.latest().revision());
|
assertEquals(2L, heldHistory.latest().revision());
|
||||||
}
|
}
|
||||||
System.out.println("everyLegalPermanentTransitionIsAccepted...ok");
|
System.out.println("everyLegalPermanentTransitionIsAccepted...ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void illegalAndInvalidCommandsFailBeforeJournalMutation() throws Exception {
|
void illegalAndInvalidCommandsFailBeforeHistoryMutation() throws Exception {
|
||||||
System.out.println("illegalAndInvalidCommandsFailBeforeJournalMutation");
|
System.out.println("illegalAndInvalidCommandsFailBeforeHistoryMutation");
|
||||||
try (FilesystemPkiStore store = store(temporaryDirectory.resolve("illegal"))) {
|
try (FilesystemPkiStore store = store(temporaryDirectory.resolve("illegal"))) {
|
||||||
Credential unknown = credential(store, "unknown");
|
Credential unknown = credential(store, "unknown");
|
||||||
assertCode("REVOCATION_CREDENTIAL_NOT_FOUND", () -> transition(store, hold(unknown)));
|
assertCode("REVOCATION_CREDENTIAL_NOT_FOUND", () -> transition(store, hold(unknown)));
|
||||||
@@ -173,11 +155,11 @@ final class FilesystemRevocationJournalTest {
|
|||||||
store.putCredential(credential);
|
store.putCredential(credential);
|
||||||
assertCode("REVOCATION_TRANSITION_ILLEGAL", () -> transition(store, unhold(credential)));
|
assertCode("REVOCATION_TRANSITION_ILLEGAL", () -> transition(store, unhold(credential)));
|
||||||
assertThrows(IllegalArgumentException.class, () -> revoke(credential, RevocationReason.CERTIFICATE_HOLD));
|
assertThrows(IllegalArgumentException.class, () -> revoke(credential, RevocationReason.CERTIFICATE_HOLD));
|
||||||
assertTrue(readJournal(store, credential.credentialId()).isEmpty());
|
assertTrue(readHistory(store, credential.credentialId()).isEmpty());
|
||||||
|
|
||||||
transition(store, hold(credential));
|
transition(store, hold(credential));
|
||||||
assertCode("REVOCATION_TRANSITION_ILLEGAL", () -> 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));
|
transition(store, unhold(credential));
|
||||||
Path logPath = new FsPaths(temporaryDirectory.resolve("illegal")).revocationTransitionLog();
|
Path logPath = new FsPaths(temporaryDirectory.resolve("illegal")).revocationTransitionLog();
|
||||||
@@ -197,9 +179,9 @@ final class FilesystemRevocationJournalTest {
|
|||||||
Credential invalidRemove = credential(store, "invalid-remove");
|
Credential invalidRemove = credential(store, "invalid-remove");
|
||||||
store.putCredential(invalidRemove);
|
store.putCredential(invalidRemove);
|
||||||
assertThrows(IllegalArgumentException.class, () -> revoke(invalidRemove, RevocationReason.REMOVE_FROM_CRL));
|
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
|
@Test
|
||||||
@@ -217,9 +199,9 @@ final class FilesystemRevocationJournalTest {
|
|||||||
} finally {
|
} finally {
|
||||||
executor.shutdownNow();
|
executor.shutdownNow();
|
||||||
}
|
}
|
||||||
RevocationJournal journal = readJournal(store, credential.credentialId()).orElseThrow();
|
CredentialHistory history = readHistory(store, credential.credentialId()).orElseThrow();
|
||||||
assertEquals(1, journal.transitions().size());
|
assertEquals(1, history.transitions().size());
|
||||||
assertEquals(1L, journal.latest().revision());
|
assertEquals(1L, history.latest().revision());
|
||||||
}
|
}
|
||||||
System.out.println("sameCredentialConcurrentHoldHasExactlyOneCommittedWinner...ok");
|
System.out.println("sameCredentialConcurrentHoldHasExactlyOneCommittedWinner...ok");
|
||||||
}
|
}
|
||||||
@@ -233,26 +215,26 @@ final class FilesystemRevocationJournalTest {
|
|||||||
Credential fromNone = credential(store, "hold-vs-permanent");
|
Credential fromNone = credential(store, "hold-vs-permanent");
|
||||||
store.putCredential(fromNone);
|
store.putCredential(fromNone);
|
||||||
runConcurrent(store, hold(fromNone), revoke(fromNone, RevocationReason.KEY_COMPROMISE), executor);
|
runConcurrent(store, hold(fromNone), revoke(fromNone, RevocationReason.KEY_COMPROMISE), executor);
|
||||||
RevocationJournal fromNoneJournal = readJournal(store, fromNone.credentialId()).orElseThrow();
|
CredentialHistory fromNoneHistory = readHistory(store, fromNone.credentialId()).orElseThrow();
|
||||||
assertEquals(RevocationState.PERMANENTLY_REVOKED, fromNoneJournal.latest().state());
|
assertEquals(RevocationState.PERMANENTLY_REVOKED, fromNoneHistory.latest().state());
|
||||||
assertTrue(fromNoneJournal.transitions().size() == 1 || fromNoneJournal.transitions().size() == 2);
|
assertTrue(fromNoneHistory.transitions().size() == 1 || fromNoneHistory.transitions().size() == 2);
|
||||||
|
|
||||||
Credential fromHeld = credential(store, "unhold-vs-permanent");
|
Credential fromHeld = credential(store, "unhold-vs-permanent");
|
||||||
store.putCredential(fromHeld);
|
store.putCredential(fromHeld);
|
||||||
transition(store, hold(fromHeld));
|
transition(store, hold(fromHeld));
|
||||||
runConcurrent(store, unhold(fromHeld), revoke(fromHeld, RevocationReason.CA_COMPROMISE), executor);
|
runConcurrent(store, unhold(fromHeld), revoke(fromHeld, RevocationReason.CA_COMPROMISE), executor);
|
||||||
RevocationJournal fromHeldJournal = readJournal(store, fromHeld.credentialId()).orElseThrow();
|
CredentialHistory fromHeldHistory = readHistory(store, fromHeld.credentialId()).orElseThrow();
|
||||||
assertEquals(RevocationState.PERMANENTLY_REVOKED, fromHeldJournal.latest().state());
|
assertEquals(RevocationState.PERMANENTLY_REVOKED, fromHeldHistory.latest().state());
|
||||||
assertTrue(fromHeldJournal.transitions().size() == 2 || fromHeldJournal.transitions().size() == 3);
|
assertTrue(fromHeldHistory.transitions().size() == 2 || fromHeldHistory.transitions().size() == 3);
|
||||||
|
|
||||||
Credential reasons = credential(store, "competing-permanent-reasons");
|
Credential reasons = credential(store, "competing-permanent-reasons");
|
||||||
store.putCredential(reasons);
|
store.putCredential(reasons);
|
||||||
List<Boolean> results = runConcurrent(store, revoke(reasons, RevocationReason.KEY_COMPROMISE),
|
List<Boolean> results = runConcurrent(store, revoke(reasons, RevocationReason.KEY_COMPROMISE),
|
||||||
revoke(reasons, RevocationReason.CA_COMPROMISE), executor);
|
revoke(reasons, RevocationReason.CA_COMPROMISE), executor);
|
||||||
assertEquals(1L, results.stream().filter(Boolean::booleanValue).count());
|
assertEquals(1L, results.stream().filter(Boolean::booleanValue).count());
|
||||||
RevocationJournal reasonsJournal = readJournal(store, reasons.credentialId()).orElseThrow();
|
CredentialHistory reasonsHistory = readHistory(store, reasons.credentialId()).orElseThrow();
|
||||||
assertEquals(1, reasonsJournal.transitions().size());
|
assertEquals(1, reasonsHistory.transitions().size());
|
||||||
assertTrue(reasonsJournal.latest().permanentReason().filter(
|
assertTrue(reasonsHistory.latest().permanentReason().filter(
|
||||||
reason -> reason == RevocationReason.KEY_COMPROMISE || reason == RevocationReason.CA_COMPROMISE)
|
reason -> reason == RevocationReason.KEY_COMPROMISE || reason == RevocationReason.CA_COMPROMISE)
|
||||||
.isPresent());
|
.isPresent());
|
||||||
} finally {
|
} finally {
|
||||||
@@ -280,12 +262,12 @@ final class FilesystemRevocationJournalTest {
|
|||||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||||
CountDownLatch blockedStarted = new CountDownLatch(1);
|
CountDownLatch blockedStarted = new CountDownLatch(1);
|
||||||
try {
|
try {
|
||||||
CompletableFuture<RevocationJournal> blockedTransition = CompletableFuture.supplyAsync(() -> {
|
CompletableFuture<CredentialHistory> blockedTransition = CompletableFuture.supplyAsync(() -> {
|
||||||
blockedStarted.countDown();
|
blockedStarted.countDown();
|
||||||
return transition(store, hold(blocked));
|
return transition(store, hold(blocked));
|
||||||
}, executor);
|
}, executor);
|
||||||
assertTrue(blockedStarted.await(5, TimeUnit.SECONDS));
|
assertTrue(blockedStarted.await(5, TimeUnit.SECONDS));
|
||||||
CompletableFuture<RevocationJournal> independentTransition = CompletableFuture
|
CompletableFuture<CredentialHistory> independentTransition = CompletableFuture
|
||||||
.supplyAsync(() -> transition(store, hold(independent)), executor);
|
.supplyAsync(() -> transition(store, hold(independent)), executor);
|
||||||
assertEquals(RevocationState.HELD, independentTransition.get(5, TimeUnit.SECONDS).latest().state());
|
assertEquals(RevocationState.HELD, independentTransition.get(5, TimeUnit.SECONDS).latest().state());
|
||||||
assertFalse(blockedTransition.isDone());
|
assertFalse(blockedTransition.isDone());
|
||||||
@@ -303,8 +285,8 @@ final class FilesystemRevocationJournalTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void copiedNamespaceJournalAndRegressingTransitionTimeFailClosed() throws Exception {
|
void regressingTransitionTimeFailsClosed() throws Exception {
|
||||||
System.out.println("copiedNamespaceJournalAndRegressingTransitionTimeFailClosed");
|
System.out.println("regressingTransitionTimeFailsClosed");
|
||||||
Path root = temporaryDirectory.resolve("corrupt");
|
Path root = temporaryDirectory.resolve("corrupt");
|
||||||
try (FilesystemPkiStore store = store(root)) {
|
try (FilesystemPkiStore store = store(root)) {
|
||||||
Credential credential = credential(store, "corrupt");
|
Credential credential = credential(store, "corrupt");
|
||||||
@@ -313,19 +295,15 @@ final class FilesystemRevocationJournalTest {
|
|||||||
assertCode("REVOCATION_TRANSITION_CONFLICT",
|
assertCode("REVOCATION_TRANSITION_CONFLICT",
|
||||||
() -> store.transitionRevocation(unhold(credential), TIME.minusSeconds(1)));
|
() -> 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,
|
assertEquals(RevocationState.HELD,
|
||||||
store.getRevocation(credential.credentialId()).orElseThrow().transition().state());
|
store.getRevocation(credential.credentialId()).orElseThrow().transition().state());
|
||||||
}
|
}
|
||||||
System.out.println("copiedNamespaceJournalAndRegressingTransitionTimeFailClosed...ok");
|
System.out.println("regressingTransitionTimeFailsClosed...ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void transitionMetadataDefensivelySnapshotsByteValues() {
|
void transitionMetadataDefensivelySnapshotsByteValues() {
|
||||||
|
System.out.println("transitionMetadataDefensivelySnapshotsByteValues");
|
||||||
byte[] source = { 1, 2, 3 };
|
byte[] source = { 1, 2, 3 };
|
||||||
AttributeSet attributes = SimpleAttributeSet.builder().put(NOTE, new AttributeValue.BytesValue(source)).build();
|
AttributeSet attributes = SimpleAttributeSet.builder().put(NOTE, new AttributeValue.BytesValue(source)).build();
|
||||||
RevocationTransition transition = new RevocationTransition(1L, RevocationState.HELD, TIME, Optional.empty(),
|
RevocationTransition transition = new RevocationTransition(1L, RevocationState.HELD, TIME, Optional.empty(),
|
||||||
@@ -336,65 +314,62 @@ final class FilesystemRevocationJournalTest {
|
|||||||
first[1] = 9;
|
first[1] = 9;
|
||||||
byte[] second = ((AttributeValue.BytesValue) transition.attributes().get(NOTE).orElseThrow()).value();
|
byte[] second = ((AttributeValue.BytesValue) transition.attributes().get(NOTE).orElseThrow()).value();
|
||||||
assertArrayEquals(new byte[] { 1, 2, 3 }, second);
|
assertArrayEquals(new byte[] { 1, 2, 3 }, second);
|
||||||
|
System.out.println("transitionMetadataDefensivelySnapshotsByteValues...ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void journalConstructorRejectsEveryHostileSequenceShape() {
|
void obsoleteCurrentAndHistoryLayoutCannotInfluenceTrustedHistoryOrResolver() throws Exception {
|
||||||
PkiId id = new PkiId("credential:hostile");
|
System.out.println("obsoleteCurrentAndHistoryLayoutCannotInfluenceTrustedHistoryOrResolver");
|
||||||
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<DynamicTest> strictCodecCorruptionFailsReadAndTransitionWithoutOverwrite() {
|
|
||||||
return Arrays.stream(CorruptionCase.values()).map(corruption -> DynamicTest.dynamicTest(corruption.description,
|
|
||||||
() -> assertPersistedCorruptionRejected(corruption)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void obsoleteCurrentAndHistoryLayoutCannotInfluenceTrustedJournalOrResolver() throws Exception {
|
|
||||||
System.out.println("obsoleteCurrentAndHistoryLayoutCannotInfluenceTrustedJournalOrResolver");
|
|
||||||
Path root = temporaryDirectory.resolve("obsolete-layout");
|
Path root = temporaryDirectory.resolve("obsolete-layout");
|
||||||
|
Path snapshot = temporaryDirectory.resolve("obsolete-layout-snapshot");
|
||||||
Credential credential;
|
Credential credential;
|
||||||
try (FilesystemPkiStore store = store(root)) {
|
try (FilesystemPkiStore store = store(root)) {
|
||||||
credential = credential(store, "obsolete-layout");
|
credential = credential(store, "obsolete-layout");
|
||||||
store.putCredential(credential);
|
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.ensureDir(legacyDirectory.resolve("history"));
|
||||||
FsOperations.writeAtomic(legacyDirectory.resolve("current.bin"),
|
FsOperations.writeAtomic(legacyDirectory.resolve("current.bin"), new byte[] { 0x01, 0x02 });
|
||||||
oldRevokedRecordPayload(credential.credentialId()));
|
|
||||||
FsOperations.writeAtomic(legacyDirectory.resolve("history").resolve("legacy.bin"),
|
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)) {
|
try (FilesystemPkiStore reopened = store(root)) {
|
||||||
assertTrue(readJournal(reopened, credential.credentialId()).isEmpty());
|
assertTrue(readHistory(reopened, credential.credentialId()).isEmpty());
|
||||||
StoreBackedEffectiveCredentialStatusResolver resolver = new StoreBackedEffectiveCredentialStatusResolver(
|
StoreBackedEffectiveCredentialStatusResolver resolver = new StoreBackedEffectiveCredentialStatusResolver(
|
||||||
reopened, Clock.fixed(TIME, java.time.ZoneOffset.UTC));
|
reopened, Clock.fixed(TIME, java.time.ZoneOffset.UTC));
|
||||||
assertEquals(EffectiveCredentialStatus.USABLE, resolver.beginEvaluation()
|
assertEquals(EffectiveCredentialStatus.USABLE, resolver.beginEvaluation()
|
||||||
.resolve(reopened.getCredential(credential.credentialId()).orElseThrow()));
|
.resolve(reopened.getCredential(credential.credentialId()).orElseThrow()));
|
||||||
transition(reopened, hold(credential));
|
transition(reopened, hold(credential));
|
||||||
assertEquals(RevocationState.HELD,
|
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
|
@Test
|
||||||
@@ -455,48 +430,23 @@ final class FilesystemRevocationJournalTest {
|
|||||||
}, executor);
|
}, executor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertPersistedCorruptionRejected(CorruptionCase corruption) throws Exception {
|
private static List<RevocationState> states(CredentialHistory history) {
|
||||||
System.out.println("strictCodecCorruptionFailsReadAndTransitionWithoutOverwrite[" + corruption.description
|
return history.transitions().stream().map(RevocationTransition::state).toList();
|
||||||
+ "]");
|
|
||||||
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<RevocationState> states(RevocationJournal journal) {
|
|
||||||
return journal.transitions().stream().map(RevocationTransition::state).toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void assertTransition(FilesystemPkiStore store, RevocationCommand command, long revision,
|
private static void assertTransition(FilesystemPkiStore store, RevocationCommand command, long revision,
|
||||||
RevocationState state) {
|
RevocationState state) {
|
||||||
RevocationJournal journal = transition(store, command);
|
CredentialHistory history = transition(store, command);
|
||||||
assertEquals(revision, journal.latest().revision());
|
assertEquals(revision, history.latest().revision());
|
||||||
assertEquals(state, journal.latest().state());
|
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);
|
store.transitionRevocation(command, TIME);
|
||||||
return readJournal(store, command.credentialId()).orElseThrow();
|
return readHistory(store, command.credentialId()).orElseThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Optional<RevocationJournal> readJournal(FilesystemPkiStore store, PkiId credentialId) {
|
private static Optional<CredentialHistory> readHistory(FilesystemPkiStore store, PkiId credentialId) {
|
||||||
if (store.getRevocation(credentialId).isEmpty()) {
|
if (store.getRevocation(credentialId).isEmpty()) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
@@ -508,7 +458,7 @@ final class FilesystemRevocationJournalTest {
|
|||||||
} catch (IOException failure) {
|
} catch (IOException failure) {
|
||||||
throw new IllegalStateException(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) {
|
private static RevocationCommand.Hold hold(Credential credential) {
|
||||||
@@ -542,171 +492,9 @@ final class FilesystemRevocationJournalTest {
|
|||||||
new SimpleAttributeSet());
|
new SimpleAttributeSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] journalPayload(PkiId credentialId, long journalVersion, RawTransition... transitions)
|
private record CredentialHistory(PkiId credentialId, List<RevocationTransition> transitions) {
|
||||||
throws IOException {
|
private RevocationTransition latest() {
|
||||||
ByteArrayOutputStream output = envelope();
|
return transitions.get(transitions.size() - 1);
|
||||||
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<RevocationReason> 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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user