refactor(pki): normalize credential authority and snapshots
Make standalone Credential records the sole durable certificate-content authority and replace embedded CaRecord credentials with stable credential ID references. Rework snapshot export and restore to stream certificate content, mint target-store references, preserve dependency ordering, and reject legacy embedded-credential and v2 persistence formats. Validated: - lib tests: 516/516 - FilesystemPkiStoreTest: 16/16 - PMD and JavaDoc pass - app compilation passes - only the 29 independently classified revocation fixture failures remain
This commit is contained in:
@@ -152,6 +152,12 @@ public interface CaService {
|
|||||||
/**
|
/**
|
||||||
* Retrieves a CA record.
|
* Retrieves a CA record.
|
||||||
*
|
*
|
||||||
|
* <p>
|
||||||
|
* The returned record contains ordered credential identifiers. Credential
|
||||||
|
* content and metadata remain authoritative only through the configured
|
||||||
|
* credential store.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
* @param caId CA identifier
|
* @param caId CA identifier
|
||||||
* @return CA record
|
* @return CA record
|
||||||
* @throws IllegalArgumentException if {@code caId} is invalid
|
* @throws IllegalArgumentException if {@code caId} is invalid
|
||||||
@@ -162,6 +168,11 @@ public interface CaService {
|
|||||||
/**
|
/**
|
||||||
* Lists CA records matching query constraints.
|
* Lists CA records matching query constraints.
|
||||||
*
|
*
|
||||||
|
* <p>
|
||||||
|
* Returned records contain ordered credential identifiers rather than embedded
|
||||||
|
* credential values.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
* @param query query constraints
|
* @param query query constraints
|
||||||
* @return list of CA records
|
* @return list of CA records
|
||||||
* @throws IllegalArgumentException if {@code query} is invalid
|
* @throws IllegalArgumentException if {@code query} is invalid
|
||||||
|
|||||||
@@ -33,15 +33,17 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package zeroecho.pki.api.ca;
|
package zeroecho.pki.api.ca;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import zeroecho.pki.api.KeyRef;
|
import zeroecho.pki.api.KeyRef;
|
||||||
import zeroecho.pki.api.PkiId;
|
import zeroecho.pki.api.PkiId;
|
||||||
import zeroecho.pki.api.SubjectRef;
|
import zeroecho.pki.api.SubjectRef;
|
||||||
import zeroecho.pki.api.credential.Credential;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a CA entity and its issued CA credentials.
|
* Represents a CA entity and the ordered identifiers of its issued CA
|
||||||
|
* credentials.
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* A CA entity may have multiple CA credentials to support:
|
* A CA entity may have multiple CA credentials to support:
|
||||||
@@ -58,16 +60,19 @@ import zeroecho.pki.api.credential.Credential;
|
|||||||
* @param issuerKeyRef key reference used for issuing operations (private key
|
* @param issuerKeyRef key reference used for issuing operations (private key
|
||||||
* reference)
|
* reference)
|
||||||
* @param subjectRef normalized subject reference
|
* @param subjectRef normalized subject reference
|
||||||
* @param caCredentials CA credentials currently associated with the entity
|
* @param credentialIds ordered identifiers of the credentials currently
|
||||||
* (historical and active)
|
* associated with the entity (historical and active);
|
||||||
|
* duplicates and {@code null} elements are rejected
|
||||||
*/
|
*/
|
||||||
public record CaRecord(PkiId caId, CaKind kind, CaState state, KeyRef issuerKeyRef, SubjectRef subjectRef,
|
public record CaRecord(PkiId caId, CaKind kind, CaState state, KeyRef issuerKeyRef, SubjectRef subjectRef,
|
||||||
List<Credential> caCredentials) {
|
List<PkiId> credentialIds) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a CA record.
|
* Creates a CA record.
|
||||||
*
|
*
|
||||||
* @throws IllegalArgumentException if inputs are null
|
* @throws IllegalArgumentException if a mandatory input or credential
|
||||||
|
* identifier is {@code null}, or if a
|
||||||
|
* credential identifier is duplicated
|
||||||
*/
|
*/
|
||||||
public CaRecord {
|
public CaRecord {
|
||||||
if (caId == null) {
|
if (caId == null) {
|
||||||
@@ -85,8 +90,18 @@ public record CaRecord(PkiId caId, CaKind kind, CaState state, KeyRef issuerKeyR
|
|||||||
if (subjectRef == null) {
|
if (subjectRef == null) {
|
||||||
throw new IllegalArgumentException("subjectRef must not be null");
|
throw new IllegalArgumentException("subjectRef must not be null");
|
||||||
}
|
}
|
||||||
if (caCredentials == null) {
|
if (credentialIds == null) {
|
||||||
throw new IllegalArgumentException("caCredentials must not be null");
|
throw new IllegalArgumentException("credentialIds must not be null");
|
||||||
|
}
|
||||||
|
Set<PkiId> uniqueCredentialIds = new HashSet<>(credentialIds.size());
|
||||||
|
for (PkiId credentialId : credentialIds) {
|
||||||
|
if (credentialId == null) {
|
||||||
|
throw new IllegalArgumentException("credentialIds must not contain null");
|
||||||
|
}
|
||||||
|
if (!uniqueCredentialIds.add(credentialId)) {
|
||||||
|
throw new IllegalArgumentException("credentialIds must not contain duplicates");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
credentialIds = List.copyOf(credentialIds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -359,10 +359,10 @@ public final class DefaultCaService implements CaService {
|
|||||||
CredentialProfileBindings.requireCaBinding(credential.profileBinding(), request.profileReference());
|
CredentialProfileBindings.requireCaBinding(credential.profileBinding(), request.profileReference());
|
||||||
|
|
||||||
requireCaCertificateMatches(credential, credential, request, caId, CREATE_ROOT_REJECTED, BACKEND_CRED_MISMATCH);
|
requireCaCertificateMatches(credential, credential, request, caId, CREATE_ROOT_REJECTED, BACKEND_CRED_MISMATCH);
|
||||||
CaRecord ca = new CaRecord(caId, CaKind.ROOT, CaState.ACTIVE, keyRef, request.subjectRef(),
|
|
||||||
List.of(credential));
|
|
||||||
store.putCa(ca);
|
|
||||||
store.putCredential(credential);
|
store.putCredential(credential);
|
||||||
|
CaRecord ca = new CaRecord(caId, CaKind.ROOT, CaState.ACTIVE, keyRef, request.subjectRef(),
|
||||||
|
List.of(credential.credentialId()));
|
||||||
|
store.putCa(ca);
|
||||||
return caId;
|
return caId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,7 +447,7 @@ public final class DefaultCaService implements CaService {
|
|||||||
requireValidImportedRoot(command, holder);
|
requireValidImportedRoot(command, holder);
|
||||||
store.putCredential(credential);
|
store.putCredential(credential);
|
||||||
CaRecord ca = new CaRecord(caId, CaKind.ROOT, CaState.ACTIVE, command.keyRef(), request.subjectRef(),
|
CaRecord ca = new CaRecord(caId, CaKind.ROOT, CaState.ACTIVE, command.keyRef(), request.subjectRef(),
|
||||||
List.of(credential));
|
List.of(credential.credentialId()));
|
||||||
store.putCa(ca);
|
store.putCa(ca);
|
||||||
return caId;
|
return caId;
|
||||||
}
|
}
|
||||||
@@ -499,7 +499,7 @@ public final class DefaultCaService implements CaService {
|
|||||||
|
|
||||||
CaRecord issuer = getCa(command.issuerCaId());
|
CaRecord issuer = getCa(command.issuerCaId());
|
||||||
ensureActive(issuer, "issuer");
|
ensureActive(issuer, "issuer");
|
||||||
if (issuer.caCredentials().isEmpty()) {
|
if (issuer.credentialIds().isEmpty()) {
|
||||||
throw new PkiException("Issuer CA has no credentials");
|
throw new PkiException("Issuer CA has no credentials");
|
||||||
}
|
}
|
||||||
if (!framework.formatId().equals(command.formatId())) {
|
if (!framework.formatId().equals(command.formatId())) {
|
||||||
@@ -552,7 +552,7 @@ public final class DefaultCaService implements CaService {
|
|||||||
store.putCredential(cred);
|
store.putCredential(cred);
|
||||||
|
|
||||||
CaRecord subject = new CaRecord(caId, CaKind.INTERMEDIATE, CaState.ACTIVE, command.keyRef().get(),
|
CaRecord subject = new CaRecord(caId, CaKind.INTERMEDIATE, CaState.ACTIVE, command.keyRef().get(),
|
||||||
issue.subjectRef(), List.of(cred));
|
issue.subjectRef(), List.of(cred.credentialId()));
|
||||||
store.putCa(subject);
|
store.putCa(subject);
|
||||||
return caId;
|
return caId;
|
||||||
}
|
}
|
||||||
@@ -645,8 +645,8 @@ public final class DefaultCaService implements CaService {
|
|||||||
BACKEND_CRED_MISMATCH);
|
BACKEND_CRED_MISMATCH);
|
||||||
store.putCredential(cred);
|
store.putCredential(cred);
|
||||||
|
|
||||||
List<Credential> updated = new ArrayList<>(subject.caCredentials());
|
List<PkiId> updated = new ArrayList<>(subject.credentialIds());
|
||||||
updated.add(cred);
|
updated.add(cred.credentialId());
|
||||||
CaRecord updatedCa = new CaRecord(subject.caId(), subject.kind(), subject.state(), subject.issuerKeyRef(),
|
CaRecord updatedCa = new CaRecord(subject.caId(), subject.kind(), subject.state(), subject.issuerKeyRef(),
|
||||||
subject.subjectRef(), List.copyOf(updated));
|
subject.subjectRef(), List.copyOf(updated));
|
||||||
store.putCa(updatedCa);
|
store.putCa(updatedCa);
|
||||||
@@ -731,7 +731,7 @@ public final class DefaultCaService implements CaService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CaRecord updated = new CaRecord(existing.caId(), existing.kind(), state, existing.issuerKeyRef(),
|
CaRecord updated = new CaRecord(existing.caId(), existing.kind(), state, existing.issuerKeyRef(),
|
||||||
existing.subjectRef(), existing.caCredentials());
|
existing.subjectRef(), existing.credentialIds());
|
||||||
store.putCa(updated);
|
store.putCa(updated);
|
||||||
|
|
||||||
if (LOG.isLoggable(Level.INFO)) {
|
if (LOG.isLoggable(Level.INFO)) {
|
||||||
@@ -783,10 +783,11 @@ public final class DefaultCaService implements CaService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (query.formatId().isPresent()) {
|
if (query.formatId().isPresent()) {
|
||||||
if (r.caCredentials().isEmpty()) {
|
if (r.credentialIds().isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Credential last = r.caCredentials().get(r.caCredentials().size() - 1);
|
PkiId lastId = r.credentialIds().get(r.credentialIds().size() - 1);
|
||||||
|
Credential last = requireCredential(lastId);
|
||||||
return query.formatId().get().equals(last.formatId());
|
return query.formatId().get().equals(last.formatId());
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -803,7 +804,8 @@ public final class DefaultCaService implements CaService {
|
|||||||
EffectiveCredentialStatusResolver.Evaluation evaluation) {
|
EffectiveCredentialStatusResolver.Evaluation evaluation) {
|
||||||
Credential lastRejected = null;
|
Credential lastRejected = null;
|
||||||
EffectiveCredentialStatus lastStatus = null;
|
EffectiveCredentialStatus lastStatus = null;
|
||||||
for (Credential credential : issuer.caCredentials()) {
|
for (PkiId credentialId : issuer.credentialIds()) {
|
||||||
|
Credential credential = requireCredential(credentialId);
|
||||||
if (credential == null || !formatId.equals(credential.formatId())) {
|
if (credential == null || !formatId.equals(credential.formatId())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -828,6 +830,11 @@ public final class DefaultCaService implements CaService {
|
|||||||
throw new PkiException("Issuer credential unavailable: code=ISSUER_CREDENTIAL_UNAVAILABLE");
|
throw new PkiException("Issuer credential unavailable: code=ISSUER_CREDENTIAL_UNAVAILABLE");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Credential requireCredential(PkiId credentialId) {
|
||||||
|
return store.getCredential(credentialId)
|
||||||
|
.orElseThrow(() -> new PkiException("CA credential not found"));
|
||||||
|
}
|
||||||
|
|
||||||
private void requireValidImportedRoot(CaImportCommand command, X509CertificateHolder holder) {
|
private void requireValidImportedRoot(CaImportCommand command, X509CertificateHolder holder) {
|
||||||
try {
|
try {
|
||||||
CaProofGate.ManagedKeyProof proof = proofGate.proveManagedKey(command.keyRef(), command.formatId(),
|
CaProofGate.ManagedKeyProof proof = proofGate.proveManagedKey(command.keyRef(), command.formatId(),
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ public final class DefaultIssuanceService implements IssuanceService {
|
|||||||
if (issuer.state() != CaState.ACTIVE) {
|
if (issuer.state() != CaState.ACTIVE) {
|
||||||
throw new PkiException("Issuer CA not ACTIVE");
|
throw new PkiException("Issuer CA not ACTIVE");
|
||||||
}
|
}
|
||||||
if (issuer.caCredentials().isEmpty()) {
|
if (issuer.credentialIds().isEmpty()) {
|
||||||
throw new PkiException("Issuer CA has no credentials");
|
throw new PkiException("Issuer CA has no credentials");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +305,8 @@ public final class DefaultIssuanceService implements IssuanceService {
|
|||||||
|
|
||||||
Credential lastRejected = null;
|
Credential lastRejected = null;
|
||||||
EffectiveCredentialStatus lastStatus = null;
|
EffectiveCredentialStatus lastStatus = null;
|
||||||
for (Credential c : issuer.caCredentials()) {
|
for (PkiId credentialId : issuer.credentialIds()) {
|
||||||
|
Credential c = requireIssuerCredential(credentialId);
|
||||||
if (c == null || !formatId.equals(c.formatId())) {
|
if (c == null || !formatId.equals(c.formatId())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -330,6 +331,11 @@ public final class DefaultIssuanceService implements IssuanceService {
|
|||||||
throw new PkiException("Issuer credential unavailable: code=ISSUER_CREDENTIAL_UNAVAILABLE");
|
throw new PkiException("Issuer credential unavailable: code=ISSUER_CREDENTIAL_UNAVAILABLE");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Credential requireIssuerCredential(PkiId credentialId) {
|
||||||
|
return store.getCredential(credentialId)
|
||||||
|
.orElseThrow(() -> new PkiException("Issuer CA credential not found"));
|
||||||
|
}
|
||||||
|
|
||||||
private VerifiedIssuanceCandidate verifyIssuanceCandidate(IssueEndEntityCommand command) {
|
private VerifiedIssuanceCandidate verifyIssuanceCandidate(IssueEndEntityCommand command) {
|
||||||
ParsedCertificationRequest supplied = command.request();
|
ParsedCertificationRequest supplied = command.request();
|
||||||
byte[] csrDer = extractCsrDer(supplied);
|
byte[] csrDer = extractCsrDer(supplied);
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ public final class DefaultStatusObjectService implements StatusObjectService {
|
|||||||
if (ca.state() != CaState.ACTIVE) {
|
if (ca.state() != CaState.ACTIVE) {
|
||||||
throw new PkiException("Issuer CA not ACTIVE");
|
throw new PkiException("Issuer CA not ACTIVE");
|
||||||
}
|
}
|
||||||
if (ca.caCredentials().isEmpty()) {
|
if (ca.credentialIds().isEmpty()) {
|
||||||
throw new PkiException("Issuer CA has no credentials");
|
throw new PkiException("Issuer CA has no credentials");
|
||||||
}
|
}
|
||||||
EffectiveCredentialStatusResolver.Evaluation statusEvaluation = statusResolver.beginEvaluation();
|
EffectiveCredentialStatusResolver.Evaluation statusEvaluation = statusResolver.beginEvaluation();
|
||||||
@@ -466,9 +466,10 @@ public final class DefaultStatusObjectService implements StatusObjectService {
|
|||||||
EffectiveCredentialStatusResolver.Evaluation evaluation) {
|
EffectiveCredentialStatusResolver.Evaluation evaluation) {
|
||||||
Credential lastRejected = null;
|
Credential lastRejected = null;
|
||||||
EffectiveCredentialStatus lastStatus = null;
|
EffectiveCredentialStatus lastStatus = null;
|
||||||
List<Credential> credentials = ca.caCredentials();
|
List<PkiId> credentialIds = ca.credentialIds();
|
||||||
for (int index = credentials.size() - 1; index >= 0; index--) {
|
for (int index = credentialIds.size() - 1; index >= 0; index--) {
|
||||||
Credential credential = credentials.get(index);
|
Credential credential = store.getCredential(credentialIds.get(index))
|
||||||
|
.orElseThrow(DefaultStatusObjectService::crlGenerationFailure);
|
||||||
if (credential == null || !command.formatId().equals(credential.formatId())) {
|
if (credential == null || !command.formatId().equals(credential.formatId())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
|
|
||||||
private static final Logger LOG = Logger.getLogger(FilesystemPkiStore.class.getName());
|
private static final Logger LOG = Logger.getLogger(FilesystemPkiStore.class.getName());
|
||||||
|
|
||||||
/* package */ static final String CURRENT_STORE_VERSION = "v2";
|
/* package */ static final String CURRENT_STORE_VERSION = "v3";
|
||||||
private static final String SIGN_RECORD_NAMESPACE = "io.zeroecho.pki.signing-record";
|
private static final String SIGN_RECORD_NAMESPACE = "io.zeroecho.pki.signing-record";
|
||||||
private static final String SIGN_OWNER_NAMESPACE = "io.zeroecho.pki.signing-owner";
|
private static final String SIGN_OWNER_NAMESPACE = "io.zeroecho.pki.signing-owner";
|
||||||
private static final int CURRENT_SIGN_RECORD_VERSION = 2;
|
private static final int CURRENT_SIGN_RECORD_VERSION = 2;
|
||||||
@@ -401,8 +401,11 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
* <p>
|
* <p>
|
||||||
* This method is an implementation-only feature. It does not modify the current
|
* This method is an implementation-only feature. It does not modify the current
|
||||||
* store; it clones a new store layout and reconstructs {@code current.bin} for
|
* store; it clones a new store layout and reconstructs {@code current.bin} for
|
||||||
* history-tracked entities. The signing namespace, monotonic signing-time
|
* history-tracked entities. Standalone credential authority is reconstructed by
|
||||||
* watermark, and authoritative signing workflow records are safety metadata:
|
* streaming validated live-source credential content into new target-owned
|
||||||
|
* references before publishing selected ID-only CA records. The signing namespace,
|
||||||
|
* monotonic signing-time watermark, and authoritative signing workflow records
|
||||||
|
* are safety metadata:
|
||||||
* their current export-time values are copied regardless of {@code at}, because
|
* their current export-time values are copied regardless of {@code at}, because
|
||||||
* historical reconstruction could permit identifier reuse or lose a terminal
|
* historical reconstruction could permit identifier reuse or lose a terminal
|
||||||
* result.
|
* result.
|
||||||
@@ -417,13 +420,30 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
requireStoreUsable();
|
requireStoreUsable();
|
||||||
Objects.requireNonNull(targetRoot, "targetRoot");
|
Objects.requireNonNull(targetRoot, "targetRoot");
|
||||||
Objects.requireNonNull(at, "at");
|
Objects.requireNonNull(at, "at");
|
||||||
new FsSnapshotExporter(this.options).exportSnapshot(this.paths.root(), targetRoot, at);
|
new FsSnapshotExporter(this.options).exportSnapshot(this, targetRoot, at);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* default */ Path snapshotRoot() {
|
||||||
|
return paths.root();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* default */ Set<String> snapshotNonCredentialContentIds() {
|
||||||
|
requireStoreUsable();
|
||||||
|
Set<String> contentIds = new HashSet<>();
|
||||||
|
for (StatusObject status : listBinaryFiles(paths.statusRoot(), FsCodec.STATUS_OBJECT)) {
|
||||||
|
contentIds.add(status.content().contentId());
|
||||||
|
}
|
||||||
|
for (StoredSign stored : listStoredSigns()) {
|
||||||
|
stored.reference().map(DurableContentReference::contentId).ifPresent(contentIds::add);
|
||||||
|
}
|
||||||
|
return Set.copyOf(contentIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void putCa(final CaRecord record) {
|
public void putCa(final CaRecord record) {
|
||||||
requireStoreUsable();
|
requireStoreUsable();
|
||||||
Objects.requireNonNull(record, "record");
|
Objects.requireNonNull(record, "record");
|
||||||
|
validateCaCredentialReferences(record);
|
||||||
PkiId caId = record.caId();
|
PkiId caId = record.caId();
|
||||||
Path current = this.paths.caCurrent(caId);
|
Path current = this.paths.caCurrent(caId);
|
||||||
|
|
||||||
@@ -436,14 +456,15 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
requireStoreUsable();
|
requireStoreUsable();
|
||||||
Objects.requireNonNull(caId, "caId");
|
Objects.requireNonNull(caId, "caId");
|
||||||
Path p = this.paths.caCurrent(caId);
|
Path p = this.paths.caCurrent(caId);
|
||||||
return readOptional(p, FsCodec.CA_RECORD);
|
return readOptional(p, FsCodec.CA_RECORD).map(this::validateCaCredentialReferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<CaRecord> listCas() {
|
public List<CaRecord> listCas() {
|
||||||
requireStoreUsable();
|
requireStoreUsable();
|
||||||
Path casRoot = this.paths.root().resolve("cas").resolve("by-id");
|
Path casRoot = this.paths.root().resolve("cas").resolve("by-id");
|
||||||
return listCurrentRecords(casRoot, FsCodec.CA_RECORD);
|
return listCurrentRecords(casRoot, FsCodec.CA_RECORD).stream().map(this::validateCaCredentialReferences)
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -469,6 +490,15 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CaRecord validateCaCredentialReferences(CaRecord record) {
|
||||||
|
for (PkiId credentialId : record.credentialIds()) {
|
||||||
|
if (getCredential(credentialId).isEmpty()) {
|
||||||
|
throw new IllegalStateException("CA credential reference is missing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void putRequest(final ParsedCertificationRequest request) {
|
public void putRequest(final ParsedCertificationRequest request) {
|
||||||
requireStoreUsable();
|
requireStoreUsable();
|
||||||
|
|||||||
@@ -448,7 +448,7 @@ final class FsCodec {
|
|||||||
|
|
||||||
private static final ValueSchema<Credential> CREDENTIAL_VALUE = valueSchema(TYPE_CREDENTIAL_RECORD,
|
private static final ValueSchema<Credential> CREDENTIAL_VALUE = valueSchema(TYPE_CREDENTIAL_RECORD,
|
||||||
FsCodec::writeCredential, FsCodec::readCredential);
|
FsCodec::writeCredential, FsCodec::readCredential);
|
||||||
private static final ValueSchema<List<Credential>> CREDENTIALS = listOf(CREDENTIAL_VALUE);
|
private static final ValueSchema<List<PkiId>> PKI_IDS = listOf(PKI_ID);
|
||||||
|
|
||||||
/* package */ static final Schema<CaRecord> CA_RECORD = topLevel(TOP_CA_RECORD, "CA_RECORD",
|
/* package */ static final Schema<CaRecord> CA_RECORD = topLevel(TOP_CA_RECORD, "CA_RECORD",
|
||||||
valueSchema(100, FsCodec::writeCaRecord, FsCodec::readCaRecord));
|
valueSchema(100, FsCodec::writeCaRecord, FsCodec::readCaRecord));
|
||||||
@@ -711,12 +711,12 @@ final class FsCodec {
|
|||||||
writer.writeValue(CA_STATE, value.state());
|
writer.writeValue(CA_STATE, value.state());
|
||||||
writer.writeValue(KEY_REF, value.issuerKeyRef());
|
writer.writeValue(KEY_REF, value.issuerKeyRef());
|
||||||
writer.writeValue(SUBJECT_REF, value.subjectRef());
|
writer.writeValue(SUBJECT_REF, value.subjectRef());
|
||||||
writer.writeValue(CREDENTIALS, value.caCredentials());
|
writer.writeValue(PKI_IDS, value.credentialIds());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CaRecord readCaRecord(Reader reader) throws IOException {
|
private static CaRecord readCaRecord(Reader reader) throws IOException {
|
||||||
return new CaRecord(reader.readValue(PKI_ID), reader.readValue(CA_KIND), reader.readValue(CA_STATE),
|
return new CaRecord(reader.readValue(PKI_ID), reader.readValue(CA_KIND), reader.readValue(CA_STATE),
|
||||||
reader.readValue(KEY_REF), reader.readValue(SUBJECT_REF), reader.readValue(CREDENTIALS));
|
reader.readValue(KEY_REF), reader.readValue(SUBJECT_REF), reader.readValue(PKI_IDS));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void writeParsedRequest(Writer writer, ParsedCertificationRequest value) throws IOException {
|
private static void writeParsedRequest(Writer writer, ParsedCertificationRequest value) throws IOException {
|
||||||
|
|||||||
@@ -158,6 +158,61 @@ final class FsOperations {
|
|||||||
forceDirectoryBestEffort(parent);
|
forceDirectoryBestEffort(parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies one file through a bounded buffer and atomically replaces the target.
|
||||||
|
*
|
||||||
|
* @param source source file
|
||||||
|
* @param target target file
|
||||||
|
* @param bufferBytes positive transfer-buffer size
|
||||||
|
* @throws IOException if streaming or atomic publication fails
|
||||||
|
*/
|
||||||
|
/* default */ static void copyAtomic(final Path source, final Path target, final int bufferBytes)
|
||||||
|
throws IOException {
|
||||||
|
Objects.requireNonNull(source, "source");
|
||||||
|
Objects.requireNonNull(target, "target");
|
||||||
|
if (bufferBytes <= 0) {
|
||||||
|
throw new IllegalArgumentException("bufferBytes must be positive");
|
||||||
|
}
|
||||||
|
Path parent = requireParent(target);
|
||||||
|
ensureDir(parent);
|
||||||
|
Path temporary = Files.createTempFile(parent, ".snapshot-copy-", ".tmp", fileAttributesIfSupported());
|
||||||
|
byte[] buffer = new byte[bufferBytes];
|
||||||
|
boolean published = false;
|
||||||
|
try {
|
||||||
|
try (InputStream input = Files.newInputStream(source);
|
||||||
|
OutputStream output = Files.newOutputStream(temporary, StandardOpenOption.WRITE,
|
||||||
|
StandardOpenOption.TRUNCATE_EXISTING)) {
|
||||||
|
long transferred = 0L;
|
||||||
|
int read;
|
||||||
|
while ((read = input.read(buffer)) >= 0) {
|
||||||
|
if (read == 0) {
|
||||||
|
int value = input.read();
|
||||||
|
if (value < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
output.write(value);
|
||||||
|
transferred = Math.addExact(transferred, 1L);
|
||||||
|
} else {
|
||||||
|
output.write(buffer, 0, read);
|
||||||
|
transferred = Math.addExact(transferred, read);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.flush();
|
||||||
|
} catch (ArithmeticException failure) {
|
||||||
|
throw new IOException("File transfer length overflow", failure);
|
||||||
|
}
|
||||||
|
forceFileBestEffort(temporary);
|
||||||
|
Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
published = true;
|
||||||
|
forceDirectoryBestEffort(parent);
|
||||||
|
} finally {
|
||||||
|
java.util.Arrays.fill(buffer, (byte) 0);
|
||||||
|
if (!published) {
|
||||||
|
Files.deleteIfExists(temporary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strictly persists one authoritative revocation journal image.
|
* Strictly persists one authoritative revocation journal image.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -186,6 +186,10 @@ final class FsPaths {
|
|||||||
return this.root.resolve("status").resolve(BY_ID).resolve(FsUtil.safeId(statusObjectId) + ".bin");
|
return this.root.resolve("status").resolve(BY_ID).resolve(FsUtil.safeId(statusObjectId) + ".bin");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* default */ Path statusRoot() {
|
||||||
|
return this.root.resolve("status").resolve(BY_ID);
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Policy traces (immutable .bin)
|
// Policy traces (immutable .bin)
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -34,21 +34,35 @@
|
|||||||
package zeroecho.pki.impl.fs;
|
package zeroecho.pki.impl.fs;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.LinkOption;
|
import java.nio.file.LinkOption;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import zeroecho.core.io.RepeatableContent;
|
||||||
|
import zeroecho.pki.api.PkiId;
|
||||||
|
import zeroecho.pki.api.ca.CaRecord;
|
||||||
|
import zeroecho.pki.api.content.DurableContentReference;
|
||||||
|
import zeroecho.pki.api.credential.Credential;
|
||||||
import zeroecho.pki.impl.ProfileLifecycleFailure;
|
import zeroecho.pki.impl.ProfileLifecycleFailure;
|
||||||
import zeroecho.pki.impl.ProfileLifecycleFailure.Code;
|
import zeroecho.pki.impl.ProfileLifecycleFailure.Code;
|
||||||
|
import zeroecho.pki.spi.store.ContentSink;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Snapshot exporter ("time travel") for {@link FilesystemPkiStore}.
|
* Snapshot exporter ("time travel") for {@link FilesystemPkiStore}.
|
||||||
@@ -66,20 +80,28 @@ import zeroecho.pki.impl.ProfileLifecycleFailure.Code;
|
|||||||
* <li>For entities with history enabled, the exporter selects the latest
|
* <li>For entities with history enabled, the exporter selects the latest
|
||||||
* history entry with timestamp {@code <= at}. If none exists, it may fall back
|
* history entry with timestamp {@code <= at}. If none exists, it may fall back
|
||||||
* to {@code current.bin} only when strict mode is disabled.</li>
|
* to {@code current.bin} only when strict mode is disabled.</li>
|
||||||
* <li>Write-once objects are copied as-is (they are immutable). This exporter
|
* <li>Write-once objects other than credentials are copied as-is. Every valid
|
||||||
* does not attempt to prune them by time unless an upstream index exists.</li>
|
* standalone credential is streamed from the live source into a new target-owned
|
||||||
|
* content reference before selected ID-only CA records are published.</li>
|
||||||
* <li>Signing namespace, signing-time watermark, and authoritative
|
* <li>Signing namespace, signing-time watermark, and authoritative
|
||||||
* {@code sign-workflows} records are current safety metadata copied at export
|
* {@code sign-workflows} records are current safety metadata copied at export
|
||||||
* time. They are intentionally not reconstructed at {@code at}: rolling them
|
* time. They are intentionally not reconstructed at {@code at}: rolling them
|
||||||
* back could make a stable identifier reusable or discard a completed signing
|
* back could make a stable identifier reusable or discard a completed signing
|
||||||
* result.</li>
|
* result.</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Export requires time linear in copied files plus selected credential bytes,
|
||||||
|
* with auxiliary memory linear in selected CA and standalone credential metadata,
|
||||||
|
* plus one fixed-size transfer buffer.
|
||||||
|
* </p>
|
||||||
*/
|
*/
|
||||||
final class FsSnapshotExporter {
|
final class FsSnapshotExporter {
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getLogger(FsSnapshotExporter.class.getName());
|
private static final Logger LOG = Logger.getLogger(FsSnapshotExporter.class.getName());
|
||||||
private static final String ACTIVE_POINTER_FILE = "active.bin";
|
private static final String ACTIVE_POINTER_FILE = "active.bin";
|
||||||
private static final String BINARY_EXTENSION = ".bin";
|
private static final String BINARY_EXTENSION = ".bin";
|
||||||
|
private static final int TRANSFER_BUFFER_BYTES = 16 * 1024;
|
||||||
|
|
||||||
private final FsPkiStoreOptions options;
|
private final FsPkiStoreOptions options;
|
||||||
|
|
||||||
@@ -92,42 +114,350 @@ final class FsSnapshotExporter {
|
|||||||
* bearing causes or filesystem paths.
|
* bearing causes or filesystem paths.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("PMD.PreserveStackTrace")
|
@SuppressWarnings("PMD.PreserveStackTrace")
|
||||||
/* default */ void exportSnapshot(final Path sourceRoot, final Path targetRoot, final Instant at) {
|
/* default */ void exportSnapshot(final FilesystemPkiStore source, final Path targetRoot, final Instant at) {
|
||||||
Objects.requireNonNull(sourceRoot, "sourceRoot");
|
Objects.requireNonNull(source, "source");
|
||||||
Objects.requireNonNull(targetRoot, "targetRoot");
|
Objects.requireNonNull(targetRoot, "targetRoot");
|
||||||
Objects.requireNonNull(at, "at");
|
Objects.requireNonNull(at, "at");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
requireNewTarget(targetRoot);
|
||||||
|
Path sourceRoot = source.snapshotRoot();
|
||||||
List<SnapshotProfileArtifact> profiles = preflightImportedProfiles(sourceRoot.resolve("profiles"), at);
|
List<SnapshotProfileArtifact> profiles = preflightImportedProfiles(sourceRoot.resolve("profiles"), at);
|
||||||
FsOperations.ensureDir(targetRoot);
|
SnapshotAuthority authority = new AuthorityPlanner(source, options).plan(at);
|
||||||
FsPaths dst = new FsPaths(targetRoot);
|
Set<String> nonCredentialContentIds = source.snapshotNonCredentialContentIds();
|
||||||
|
SnapshotPlan plan = new SnapshotPlan(profiles, authority, nonCredentialContentIds);
|
||||||
|
new SnapshotPublisher(source, options, plan).publish(targetRoot, at);
|
||||||
|
} catch (SnapshotProfileFailure failure) {
|
||||||
|
throw new IllegalStateException(failure.getMessage());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException("Snapshot export failed: code=SNAPSHOT_EXPORT_FAILED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Files.writeString(dst.versionFile(), FilesystemPkiStore.CURRENT_STORE_VERSION);
|
private static DurableContentReference transferCredentialContent(FilesystemPkiStore source,
|
||||||
|
FilesystemPkiStore target, DurableContentReference sourceReference) throws IOException {
|
||||||
|
try (RepeatableContent content = source.stagedContent().openContent(sourceReference);
|
||||||
|
InputStream input = content.openStream();
|
||||||
|
ContentSink sink = target.stagedContent().beginContent(sourceReference.encoding(),
|
||||||
|
DurableContentReference.Lifecycle.PERSISTED);
|
||||||
|
OutputStream output = sink.outputStream()) {
|
||||||
|
byte[] buffer = new byte[TRANSFER_BUFFER_BYTES];
|
||||||
|
try {
|
||||||
|
int read;
|
||||||
|
while ((read = input.read(buffer)) >= 0) {
|
||||||
|
if (read > 0) {
|
||||||
|
output.write(buffer, 0, read);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sink.complete();
|
||||||
|
} finally {
|
||||||
|
Arrays.fill(buffer, (byte) 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Credential withContent(Credential source, DurableContentReference content) {
|
||||||
|
return new Credential(source.credentialId(), source.formatId(), source.issuerRef(), source.subjectRef(),
|
||||||
|
source.validity(), source.serialOrUniqueId(), source.publicKeyId(), source.profileBinding(),
|
||||||
|
source.status(), content, source.attributes());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void requireNewTarget(Path targetRoot) {
|
||||||
|
if (Files.exists(targetRoot, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
throw new IllegalStateException("Snapshot target already exists");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Path> selectMutableRecords(Path srcTree, Instant at, FsHistoryPolicy policy, boolean strict)
|
||||||
|
throws IOException {
|
||||||
|
if (!Files.exists(srcTree)) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<Path> selected = new ArrayList<>();
|
||||||
|
try (java.util.stream.Stream<Path> paths = Files.walk(srcTree)) {
|
||||||
|
for (Path current : paths.filter(Files::isRegularFile)
|
||||||
|
.filter(path -> FsPaths.CURRENT_FILE.equals(path.getFileName().toString()))
|
||||||
|
.sorted(Comparator.comparing(Path::toString)).toList()) {
|
||||||
|
Path selectedRecord = null;
|
||||||
|
if (policy.enabled()) {
|
||||||
|
Path historyDir = current.getParent().resolve(FsPaths.HISTORY_DIR);
|
||||||
|
if (Files.isDirectory(historyDir)) {
|
||||||
|
selectedRecord = selectHistoryEntry(historyDir, at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selectedRecord == null) {
|
||||||
|
if (strict && policy.enabled()) {
|
||||||
|
throw new IllegalStateException("No CA history entry is available for snapshot time");
|
||||||
|
}
|
||||||
|
selectedRecord = current;
|
||||||
|
}
|
||||||
|
selected.add(selectedRecord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Builds the validated logical credential and selected-CA export plan. */
|
||||||
|
private static final class AuthorityPlanner {
|
||||||
|
private final FilesystemPkiStore source;
|
||||||
|
private final FsPkiStoreOptions options;
|
||||||
|
private final FsPaths sourcePaths;
|
||||||
|
|
||||||
|
private AuthorityPlanner(FilesystemPkiStore source, FsPkiStoreOptions options) {
|
||||||
|
this.source = source;
|
||||||
|
this.options = options;
|
||||||
|
this.sourcePaths = new FsPaths(source.snapshotRoot());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SnapshotAuthority plan(Instant at) throws IOException {
|
||||||
|
CredentialInventory inventory = inventoryCredentials();
|
||||||
|
List<CaRecord> cas = selectCas(at, inventory.credentials());
|
||||||
|
return new SnapshotAuthority(cas, inventory.credentials(), inventory.contentIds());
|
||||||
|
}
|
||||||
|
|
||||||
|
private CredentialInventory inventoryCredentials() throws IOException {
|
||||||
|
Map<PkiId, Credential> credentials = new LinkedHashMap<>();
|
||||||
|
Set<String> contentIds = new HashSet<>();
|
||||||
|
Path credentialsRoot = source.snapshotRoot().resolve("credentials").resolve("by-id");
|
||||||
|
if (!Files.isDirectory(credentialsRoot)) {
|
||||||
|
return new CredentialInventory(credentials, contentIds);
|
||||||
|
}
|
||||||
|
try (java.util.stream.Stream<Path> records = Files.list(credentialsRoot)) {
|
||||||
|
for (Path record : records.filter(Files::isRegularFile)
|
||||||
|
.filter(path -> path.getFileName().toString().endsWith(BINARY_EXTENSION))
|
||||||
|
.sorted(Comparator.comparing(Path::toString)).toList()) {
|
||||||
|
loadCredential(record, credentials, contentIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new CredentialInventory(credentials, contentIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadCredential(Path record, Map<PkiId, Credential> credentials, Set<String> contentIds)
|
||||||
|
throws IOException {
|
||||||
|
Credential decoded;
|
||||||
|
try {
|
||||||
|
decoded = FsCodec.decode(FsCodec.CREDENTIAL, FsOperations.readAll(record), source.stagedContent());
|
||||||
|
} catch (IllegalStateException failure) {
|
||||||
|
rejectCredential();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
contentIds.add(decoded.content().contentId());
|
||||||
|
if (!sourcePaths.credentialPath(decoded.credentialId()).equals(record)
|
||||||
|
|| credentials.containsKey(decoded.credentialId())) {
|
||||||
|
rejectCredential();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Credential validated = source.getCredential(decoded.credentialId()).orElse(null);
|
||||||
|
if (validated == null) {
|
||||||
|
rejectCredential();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
credentials.put(decoded.credentialId(), validated);
|
||||||
|
} catch (IllegalStateException failure) {
|
||||||
|
rejectCredential();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<CaRecord> selectCas(Instant at, Map<PkiId, Credential> credentials) throws IOException {
|
||||||
|
List<Path> selectedRecords = selectMutableRecords(source.snapshotRoot().resolve("cas"), at,
|
||||||
|
options.caHistoryPolicy(), options.strictSnapshotExport());
|
||||||
|
List<CaRecord> selected = new ArrayList<>();
|
||||||
|
for (Path record : selectedRecords) {
|
||||||
|
CaRecord ca = loadCa(record);
|
||||||
|
if (ca != null && credentials.keySet().containsAll(ca.credentialIds())) {
|
||||||
|
selected.add(ca);
|
||||||
|
} else if (ca != null) {
|
||||||
|
rejectCa();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CaRecord loadCa(Path record) throws IOException {
|
||||||
|
try {
|
||||||
|
return FsCodec.decode(FsCodec.CA_RECORD, FsOperations.readAll(record), source.stagedContent());
|
||||||
|
} catch (IllegalStateException failure) {
|
||||||
|
rejectCa();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rejectCredential() {
|
||||||
|
if (options.strictSnapshotExport()) {
|
||||||
|
throw new SnapshotAuthorityFailure("Snapshot credential preflight failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rejectCa() {
|
||||||
|
if (options.strictSnapshotExport()) {
|
||||||
|
throw new SnapshotAuthorityFailure("Snapshot CA dependency preflight failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Owns the sibling temporary root until one atomic target publication. */
|
||||||
|
private static final class SnapshotPublisher {
|
||||||
|
private final FilesystemPkiStore source;
|
||||||
|
private final FsPkiStoreOptions options;
|
||||||
|
private final SnapshotPlan plan;
|
||||||
|
|
||||||
|
private SnapshotPublisher(FilesystemPkiStore source, FsPkiStoreOptions options, SnapshotPlan plan) {
|
||||||
|
this.source = source;
|
||||||
|
this.options = options;
|
||||||
|
this.plan = plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void publish(Path targetRoot, Instant at) throws IOException {
|
||||||
|
Path absoluteTarget = targetRoot.toAbsolutePath();
|
||||||
|
Path parent = absoluteTarget.getParent();
|
||||||
|
if (parent == null) {
|
||||||
|
throw new IOException("Snapshot target has no parent");
|
||||||
|
}
|
||||||
|
FsOperations.ensureDir(parent);
|
||||||
|
Path temporaryRoot = Files.createTempDirectory(parent, ".zeroecho-snapshot-");
|
||||||
|
boolean published = false;
|
||||||
|
try {
|
||||||
|
build(temporaryRoot, at);
|
||||||
|
Files.move(temporaryRoot, absoluteTarget, java.nio.file.StandardCopyOption.ATOMIC_MOVE);
|
||||||
|
published = true;
|
||||||
|
} catch (IOException | IllegalStateException failure) {
|
||||||
|
if (!published) {
|
||||||
|
addCleanupFailure(failure, temporaryRoot);
|
||||||
|
}
|
||||||
|
throw failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void build(Path targetRoot, Instant at) throws IOException {
|
||||||
|
Path sourceRoot = source.snapshotRoot();
|
||||||
|
FsPaths destination = new FsPaths(targetRoot);
|
||||||
|
Files.writeString(destination.versionFile(), FilesystemPkiStore.CURRENT_STORE_VERSION);
|
||||||
copyFile(sourceRoot.resolve("SIGNING_NAMESPACE"), targetRoot.resolve("SIGNING_NAMESPACE"));
|
copyFile(sourceRoot.resolve("SIGNING_NAMESPACE"), targetRoot.resolve("SIGNING_NAMESPACE"));
|
||||||
copyFile(sourceRoot.resolve("SIGNING_TIME_WATERMARK"), targetRoot.resolve("SIGNING_TIME_WATERMARK"));
|
copyFile(sourceRoot.resolve("SIGNING_TIME_WATERMARK"), targetRoot.resolve("SIGNING_TIME_WATERMARK"));
|
||||||
|
|
||||||
// copy write-once trees as-is (best-effort, deterministic order)
|
|
||||||
copyTreeIfExists(sourceRoot.resolve("credentials"), targetRoot.resolve("credentials"));
|
|
||||||
copyTreeIfExists(sourceRoot.resolve("requests"), targetRoot.resolve("requests"));
|
copyTreeIfExists(sourceRoot.resolve("requests"), targetRoot.resolve("requests"));
|
||||||
copyTreeIfExists(sourceRoot.resolve("status"), targetRoot.resolve("status"));
|
copyTreeIfExists(sourceRoot.resolve("status"), targetRoot.resolve("status"));
|
||||||
copyTreeIfExists(sourceRoot.resolve("policy"), targetRoot.resolve("policy"));
|
copyTreeIfExists(sourceRoot.resolve("policy"), targetRoot.resolve("policy"));
|
||||||
copyTreeIfExists(sourceRoot.resolve("publications"), targetRoot.resolve("publications"));
|
copyTreeIfExists(sourceRoot.resolve("publications"), targetRoot.resolve("publications"));
|
||||||
copyTreeIfExists(sourceRoot.resolve("sign-workflows"), targetRoot.resolve("sign-workflows"));
|
copyTreeIfExists(sourceRoot.resolve("sign-workflows"), targetRoot.resolve("sign-workflows"));
|
||||||
copyTreeIfExists(sourceRoot.resolve("staged-content"), targetRoot.resolve("staged-content"));
|
copyStagedContent(sourceRoot.resolve("staged-content"), targetRoot.resolve("staged-content"),
|
||||||
|
plan.authority().sourceCredentialContentIds(), plan.nonCredentialContentIds());
|
||||||
copyTreeIfExists(sourceRoot.resolve("revocations"), targetRoot.resolve("revocations"));
|
copyTreeIfExists(sourceRoot.resolve("revocations"), targetRoot.resolve("revocations"));
|
||||||
copyImportedProfilesAsOf(profiles, targetRoot.resolve("profiles"));
|
copyImportedProfilesAsOf(plan.profiles(), targetRoot.resolve("profiles"));
|
||||||
|
|
||||||
// reconstruct mutable entities from history (CAS and profiles)
|
|
||||||
reconstructMutableTree(sourceRoot.resolve("cas"), targetRoot.resolve("cas"), at,
|
|
||||||
this.options.caHistoryPolicy(), this.options.strictSnapshotExport());
|
|
||||||
// reconstruct workflow continuation state from history
|
|
||||||
reconstructMutableTree(sourceRoot.resolve("workflows"), targetRoot.resolve("workflows"), at,
|
reconstructMutableTree(sourceRoot.resolve("workflows"), targetRoot.resolve("workflows"), at,
|
||||||
this.options.workflowHistoryPolicy(), this.options.strictSnapshotExport());
|
options.workflowHistoryPolicy(), options.strictSnapshotExport());
|
||||||
|
new AuthorityRestorer(source, options, plan.authority()).restore(targetRoot);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (SnapshotProfileFailure failure) {
|
private static void addCleanupFailure(Exception primary, Path temporaryRoot) {
|
||||||
throw new IllegalStateException(failure.getMessage());
|
try {
|
||||||
} catch (IOException e) {
|
deleteOwnedTree(temporaryRoot);
|
||||||
throw new IllegalStateException("Snapshot export failed: code=SNAPSHOT_EXPORT_FAILED");
|
} catch (IOException cleanupFailure) {
|
||||||
|
primary.addSuppressed(new IOException("Snapshot temporary cleanup failed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reconstructs target credential authority before publishing selected CAs. */
|
||||||
|
private static final class AuthorityRestorer {
|
||||||
|
private final FilesystemPkiStore source;
|
||||||
|
private final FsPkiStoreOptions options;
|
||||||
|
private final SnapshotAuthority authority;
|
||||||
|
|
||||||
|
private AuthorityRestorer(FilesystemPkiStore source, FsPkiStoreOptions options,
|
||||||
|
SnapshotAuthority authority) {
|
||||||
|
this.source = source;
|
||||||
|
this.options = options;
|
||||||
|
this.authority = authority;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restore(Path targetRoot) throws IOException {
|
||||||
|
Set<PkiId> transferred = new HashSet<>();
|
||||||
|
try (FilesystemPkiStore target = new FilesystemPkiStore(targetRoot, options)) {
|
||||||
|
for (Map.Entry<PkiId, Credential> entry : authority.credentials().entrySet()) {
|
||||||
|
if (persistCredential(target, entry.getValue())) {
|
||||||
|
transferred.add(entry.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (CaRecord ca : authority.cas()) {
|
||||||
|
if (transferred.containsAll(ca.credentialIds())) {
|
||||||
|
persistCa(target, ca);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean persistCredential(FilesystemPkiStore target, Credential sourceCredential)
|
||||||
|
throws IOException {
|
||||||
|
if (options.strictSnapshotExport()) {
|
||||||
|
return persistCredentialStrict(target, sourceCredential);
|
||||||
|
}
|
||||||
|
try (CredentialTransfer transfer = new CredentialTransfer(source, target, sourceCredential)) {
|
||||||
|
transfer.persist();
|
||||||
|
return true;
|
||||||
|
} catch (IOException | IllegalArgumentException | IllegalStateException failure) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean persistCredentialStrict(FilesystemPkiStore target, Credential sourceCredential)
|
||||||
|
throws IOException {
|
||||||
|
try (CredentialTransfer transfer = new CredentialTransfer(source, target, sourceCredential)) {
|
||||||
|
transfer.persist();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean persistCa(FilesystemPkiStore target, CaRecord ca) {
|
||||||
|
if (options.strictSnapshotExport()) {
|
||||||
|
target.putCa(ca);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
target.putCa(ca);
|
||||||
|
return true;
|
||||||
|
} catch (IllegalArgumentException | IllegalStateException failure) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Owns a completed target reference until its credential is persisted. */
|
||||||
|
private static final class CredentialTransfer implements AutoCloseable {
|
||||||
|
private final FilesystemPkiStore target;
|
||||||
|
private final Credential sourceCredential;
|
||||||
|
private final DurableContentReference targetReference;
|
||||||
|
private boolean persisted;
|
||||||
|
|
||||||
|
private CredentialTransfer(FilesystemPkiStore source, FilesystemPkiStore target,
|
||||||
|
Credential sourceCredential) throws IOException {
|
||||||
|
this.target = target;
|
||||||
|
this.sourceCredential = sourceCredential;
|
||||||
|
this.targetReference = transferCredentialContent(source, target, sourceCredential.content());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void persist() {
|
||||||
|
target.putCredential(withContent(sourceCredential, targetReference));
|
||||||
|
persisted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
if (!persisted) {
|
||||||
|
target.stagedContent().retireUnownedContent(targetReference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void deleteOwnedTree(Path root) throws IOException {
|
||||||
|
if (!Files.exists(root, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (java.util.stream.Stream<Path> paths = Files.walk(root)) {
|
||||||
|
List<Path> ordered = paths.sorted(Comparator.reverseOrder()).toList();
|
||||||
|
for (Path path : ordered) {
|
||||||
|
Files.delete(path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,6 +621,42 @@ final class FsSnapshotExporter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void copyStagedContent(Path src, Path dst, Set<String> credentialContentIds,
|
||||||
|
Set<String> nonCredentialContentIds) throws IOException {
|
||||||
|
if (!Files.exists(src)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (java.util.stream.Stream<Path> paths = Files.walk(src)) {
|
||||||
|
for (Path path : paths.sorted(Comparator.comparing(Path::toString)).toList()) {
|
||||||
|
Path relative = src.relativize(path);
|
||||||
|
Path output = dst.resolve(relative);
|
||||||
|
if (Files.isDirectory(path)) {
|
||||||
|
FsOperations.ensureDir(output);
|
||||||
|
} else if (Files.isRegularFile(path)
|
||||||
|
&& shouldCopyStagedArtifact(path, credentialContentIds, nonCredentialContentIds)) {
|
||||||
|
FsOperations.ensureDir(output.getParent());
|
||||||
|
FsOperations.copyAtomic(path, output, TRANSFER_BUFFER_BYTES);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean shouldCopyStagedArtifact(Path path, Set<String> credentialContentIds,
|
||||||
|
Set<String> nonCredentialContentIds) {
|
||||||
|
String fileName = path.getFileName().toString();
|
||||||
|
int separator = fileName.lastIndexOf('.');
|
||||||
|
if (separator <= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String extension = fileName.substring(separator);
|
||||||
|
if (!".content".equals(extension) && !".meta".equals(extension) && !".owners".equals(extension)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String contentId = fileName.substring(0, separator);
|
||||||
|
return !credentialContentIds.contains(contentId)
|
||||||
|
|| nonCredentialContentIds.contains(contentId) && !".owners".equals(extension);
|
||||||
|
}
|
||||||
|
|
||||||
private static void copyFile(final Path source, final Path target) throws IOException {
|
private static void copyFile(final Path source, final Path target) throws IOException {
|
||||||
if (!Files.isRegularFile(source)) {
|
if (!Files.isRegularFile(source)) {
|
||||||
throw new IllegalStateException("Required snapshot metadata is missing: " + source.getFileName());
|
throw new IllegalStateException("Required snapshot metadata is missing: " + source.getFileName());
|
||||||
@@ -309,6 +675,40 @@ final class FsSnapshotExporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record CredentialInventory(Map<PkiId, Credential> credentials, Set<String> contentIds) {
|
||||||
|
private CredentialInventory {
|
||||||
|
credentials = Collections.unmodifiableMap(new LinkedHashMap<>(credentials));
|
||||||
|
contentIds = Set.copyOf(contentIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record SnapshotAuthority(List<CaRecord> cas, Map<PkiId, Credential> credentials,
|
||||||
|
Set<String> sourceCredentialContentIds) {
|
||||||
|
private SnapshotAuthority {
|
||||||
|
cas = List.copyOf(cas);
|
||||||
|
credentials = Collections.unmodifiableMap(new LinkedHashMap<>(credentials));
|
||||||
|
sourceCredentialContentIds = Set.copyOf(sourceCredentialContentIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record SnapshotPlan(List<SnapshotProfileArtifact> profiles, SnapshotAuthority authority,
|
||||||
|
Set<String> nonCredentialContentIds) {
|
||||||
|
private SnapshotPlan {
|
||||||
|
profiles = List.copyOf(profiles);
|
||||||
|
Objects.requireNonNull(authority, "authority");
|
||||||
|
nonCredentialContentIds = Set.copyOf(nonCredentialContentIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cause-free marker for invalid source credential or CA authority. */
|
||||||
|
private static final class SnapshotAuthorityFailure extends IllegalStateException {
|
||||||
|
private static final long serialVersionUID = -8051398945014616477L;
|
||||||
|
|
||||||
|
private SnapshotAuthorityFailure(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Cause-free internal marker for profile preflight rejection. */
|
/** Cause-free internal marker for profile preflight rejection. */
|
||||||
private static final class SnapshotProfileFailure extends RuntimeException {
|
private static final class SnapshotProfileFailure extends RuntimeException {
|
||||||
private static final long serialVersionUID = -4451876406166515230L;
|
private static final long serialVersionUID = -4451876406166515230L;
|
||||||
|
|||||||
@@ -88,12 +88,15 @@ public interface PkiStore extends SignWorkflowStore {
|
|||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* Implementations must store CA records atomically. Replacing an existing
|
* Implementations must store CA records atomically. Replacing an existing
|
||||||
* record should be either fully visible or not visible at all.
|
* record should be either fully visible or not visible at all. Every
|
||||||
|
* identifier in {@link CaRecord#credentialIds()} must resolve through
|
||||||
|
* {@link #getCredential(PkiId)} before the CA record is published.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @param record CA record (never {@code null})
|
* @param record CA record (never {@code null})
|
||||||
* @throws NullPointerException if {@code record} is {@code null}
|
* @throws NullPointerException if {@code record} is {@code null}
|
||||||
* @throws IllegalStateException if persistence fails
|
* @throws IllegalStateException if persistence fails or a referenced
|
||||||
|
* credential is missing or invalid
|
||||||
*/
|
*/
|
||||||
void putCa(CaRecord record);
|
void putCa(CaRecord record);
|
||||||
|
|
||||||
@@ -101,17 +104,22 @@ public interface PkiStore extends SignWorkflowStore {
|
|||||||
* Retrieves a CA record.
|
* Retrieves a CA record.
|
||||||
*
|
*
|
||||||
* @param caId CA identifier (never {@code null})
|
* @param caId CA identifier (never {@code null})
|
||||||
* @return CA record if present
|
* @return CA record if present; all credential identifiers have been validated
|
||||||
|
* against the standalone credential authority
|
||||||
* @throws NullPointerException if {@code caId} is {@code null}
|
* @throws NullPointerException if {@code caId} is {@code null}
|
||||||
* @throws IllegalStateException if retrieval fails
|
* @throws IllegalStateException if retrieval fails or a referenced credential
|
||||||
|
* is missing or invalid
|
||||||
*/
|
*/
|
||||||
Optional<CaRecord> getCa(PkiId caId);
|
Optional<CaRecord> getCa(PkiId caId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists all stored CA records.
|
* Lists all stored CA records.
|
||||||
*
|
*
|
||||||
* @return list of CA records (never {@code null})
|
* @return list of CA records (never {@code null}); every returned credential
|
||||||
* @throws IllegalStateException if listing fails
|
* identifier has been validated against the standalone credential
|
||||||
|
* authority
|
||||||
|
* @throws IllegalStateException if listing fails or a referenced credential is
|
||||||
|
* missing or invalid
|
||||||
*/
|
*/
|
||||||
List<CaRecord> listCas();
|
List<CaRecord> listCas();
|
||||||
|
|
||||||
|
|||||||
72
pki/src/test/java/zeroecho/pki/api/ca/CaRecordTest.java
Normal file
72
pki/src/test/java/zeroecho/pki/api/ca/CaRecordTest.java
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2026, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
* are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
* list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software without
|
||||||
|
* specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho.pki.api.ca;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import zeroecho.pki.api.KeyRef;
|
||||||
|
import zeroecho.pki.api.PkiId;
|
||||||
|
import zeroecho.pki.api.SubjectRef;
|
||||||
|
|
||||||
|
final class CaRecordTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void credentialIdentifiersAreOrderedImmutableAndUnique() {
|
||||||
|
System.out.println("credentialIdentifiersAreOrderedImmutableAndUnique");
|
||||||
|
PkiId first = new PkiId("credential-first");
|
||||||
|
PkiId second = new PkiId("credential-second");
|
||||||
|
List<PkiId> source = new ArrayList<>(List.of(first, second));
|
||||||
|
|
||||||
|
CaRecord record = record(source);
|
||||||
|
source.clear();
|
||||||
|
|
||||||
|
assertEquals(List.of(first, second), record.credentialIds());
|
||||||
|
assertThrows(UnsupportedOperationException.class,
|
||||||
|
() -> record.credentialIds().add(new PkiId("credential-third")));
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> record(List.of(first, first)));
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> record(java.util.Arrays.asList(first, null)));
|
||||||
|
System.out.println("credentialIdentifiersAreOrderedImmutableAndUnique...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CaRecord record(List<PkiId> credentialIds) {
|
||||||
|
return new CaRecord(new PkiId("ca-test"), CaKind.ROOT, CaState.ACTIVE, new KeyRef("key-test"),
|
||||||
|
new SubjectRef("CN=Test"), credentialIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -130,7 +130,7 @@ final class CaProfileIssuanceEnforcementTest {
|
|||||||
"fixed-intermediate-ca", Optional.empty(), new SimpleAttributeSet()));
|
"fixed-intermediate-ca", Optional.empty(), new SimpleAttributeSet()));
|
||||||
|
|
||||||
CaRecord intermediate = runtime.caService().getCa(intermediateId);
|
CaRecord intermediate = runtime.caService().getCa(intermediateId);
|
||||||
Credential credential = intermediate.caCredentials().get(0);
|
Credential credential = runtime.caCredential(intermediate, 0);
|
||||||
X509CertificateHolder holder = new X509CertificateHolder(runtime.credentialBytes(credential));
|
X509CertificateHolder holder = new X509CertificateHolder(runtime.credentialBytes(credential));
|
||||||
X509CertificateHolder additionalHolder = new X509CertificateHolder(runtime.credentialBytes(additional));
|
X509CertificateHolder additionalHolder = new X509CertificateHolder(runtime.credentialBytes(additional));
|
||||||
assertEquals("Fixed Organization",
|
assertEquals("Fixed Organization",
|
||||||
@@ -140,7 +140,7 @@ final class CaProfileIssuanceEnforcementTest {
|
|||||||
assertEquals(holder.getSubject().toString(), intermediate.subjectRef().value());
|
assertEquals(holder.getSubject().toString(), intermediate.subjectRef().value());
|
||||||
assertEquals(intermediate.subjectRef(), credential.subjectRef());
|
assertEquals(intermediate.subjectRef(), credential.subjectRef());
|
||||||
assertEquals(intermediate.subjectRef(), additional.subjectRef());
|
assertEquals(intermediate.subjectRef(), additional.subjectRef());
|
||||||
assertEquals(2, intermediate.caCredentials().size());
|
assertEquals(2, intermediate.credentialIds().size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,19 +167,19 @@ final class CaProfileIssuanceEnforcementTest {
|
|||||||
Credential additional = runtime.caService()
|
Credential additional = runtime.caService()
|
||||||
.issueIntermediateCertificate(new IntermediateCertIssueCommand(runtime.framework().formatId(),
|
.issueIntermediateCertificate(new IntermediateCertIssueCommand(runtime.framework().formatId(),
|
||||||
rootId, intermediateId, "intermediate-ca", Optional.empty(), new SimpleAttributeSet()));
|
rootId, intermediateId, "intermediate-ca", Optional.empty(), new SimpleAttributeSet()));
|
||||||
rootProfile = ((CaProfileBinding) runtime.caService().getCa(rootId).caCredentials().get(0).profileBinding())
|
rootProfile = ((CaProfileBinding) runtime.caCredential(runtime.caService().getCa(rootId), 0).profileBinding())
|
||||||
.reference();
|
.reference();
|
||||||
intermediateProfile = ((CaProfileBinding) additional.profileBinding()).reference();
|
intermediateProfile = ((CaProfileBinding) additional.profileBinding()).reference();
|
||||||
assertCaCertificate(runtime, runtime.caService().getCa(rootId).caCredentials().get(0), 1);
|
assertCaCertificate(runtime, runtime.caCredential(runtime.caService().getCa(rootId), 0), 1);
|
||||||
assertCaCertificate(runtime, additional, 0);
|
assertCaCertificate(runtime, additional, 0);
|
||||||
}
|
}
|
||||||
try (PkiTestRuntime reopened = PkiTestRuntime.create(store, directory.resolve("reopened-bus.log"),
|
try (PkiTestRuntime reopened = PkiTestRuntime.create(store, directory.resolve("reopened-bus.log"),
|
||||||
Map.of(rootRef, rootKey, intermediateRef, intermediateKey))) {
|
Map.of(rootRef, rootKey, intermediateRef, intermediateKey))) {
|
||||||
assertEquals(rootProfile,
|
assertEquals(rootProfile,
|
||||||
((CaProfileBinding) reopened.caService().getCa(rootId).caCredentials().get(0).profileBinding())
|
((CaProfileBinding) reopened.caCredential(reopened.caService().getCa(rootId), 0).profileBinding())
|
||||||
.reference());
|
.reference());
|
||||||
assertEquals(intermediateProfile, ((CaProfileBinding) reopened.caService().getCa(intermediateId)
|
assertEquals(intermediateProfile, ((CaProfileBinding) reopened
|
||||||
.caCredentials().get(1).profileBinding()).reference());
|
.caCredential(reopened.caService().getCa(intermediateId), 1).profileBinding()).reference());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,10 +221,11 @@ final class CaProfileIssuanceEnforcementTest {
|
|||||||
.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootId,
|
.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootId,
|
||||||
new SubjectRef("CN=Historical Intermediate"), "intermediate-ca",
|
new SubjectRef("CN=Historical Intermediate"), "intermediate-ca",
|
||||||
Optional.of(intermediateRef), new SimpleAttributeSet()));
|
Optional.of(intermediateRef), new SimpleAttributeSet()));
|
||||||
CaProfileBinding issuerBinding = (CaProfileBinding) runtime.caService().getCa(rootId).caCredentials().get(0)
|
CaProfileBinding issuerBinding = (CaProfileBinding) runtime
|
||||||
|
.caCredential(runtime.caService().getCa(rootId), 0)
|
||||||
.profileBinding();
|
.profileBinding();
|
||||||
assertEquals(1, issuerBinding.reference().profileVersion());
|
assertEquals(1, issuerBinding.reference().profileVersion());
|
||||||
assertEquals(1, runtime.caService().getCa(intermediateId).caCredentials().size());
|
assertEquals(1, runtime.caService().getCa(intermediateId).credentialIds().size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +261,7 @@ final class CaProfileIssuanceEnforcementTest {
|
|||||||
runtime.profileService().activateProfile(profileId, 1);
|
runtime.profileService().activateProfile(profileId, 1);
|
||||||
|
|
||||||
caA = createRoot(runtime, keyRefA, "CN=Version Switch Root A", profileId);
|
caA = createRoot(runtime, keyRefA, "CN=Version Switch Root A", profileId);
|
||||||
Credential issuedA = onlyCredential(runtime.caService(), caA);
|
Credential issuedA = onlyCredential(runtime, caA);
|
||||||
assertCaProfileCredential(runtime, "A", issuedA, versionOne, 1);
|
assertCaProfileCredential(runtime, "A", issuedA, versionOne, 1);
|
||||||
credentialA = issuedA.credentialId();
|
credentialA = issuedA.credentialId();
|
||||||
|
|
||||||
@@ -276,13 +277,13 @@ final class CaProfileIssuanceEnforcementTest {
|
|||||||
assertEquals(versionOne, runtime.profileService().getActiveReference(profileId).orElseThrow());
|
assertEquals(versionOne, runtime.profileService().getActiveReference(profileId).orElseThrow());
|
||||||
|
|
||||||
caB = createRoot(runtime, keyRefB, "CN=Version Switch Root B", profileId);
|
caB = createRoot(runtime, keyRefB, "CN=Version Switch Root B", profileId);
|
||||||
Credential issuedB = onlyCredential(runtime.caService(), caB);
|
Credential issuedB = onlyCredential(runtime, caB);
|
||||||
assertCaProfileCredential(runtime, "B", issuedB, versionOne, 1);
|
assertCaProfileCredential(runtime, "B", issuedB, versionOne, 1);
|
||||||
credentialB = issuedB.credentialId();
|
credentialB = issuedB.credentialId();
|
||||||
|
|
||||||
assertEquals(versionTwo, runtime.profileService().activateProfile(profileId, 2));
|
assertEquals(versionTwo, runtime.profileService().activateProfile(profileId, 2));
|
||||||
caC = createRoot(runtime, keyRefC, "CN=Version Switch Root C", profileId);
|
caC = createRoot(runtime, keyRefC, "CN=Version Switch Root C", profileId);
|
||||||
Credential issuedC = onlyCredential(runtime.caService(), caC);
|
Credential issuedC = onlyCredential(runtime, caC);
|
||||||
assertCaProfileCredential(runtime, "C", issuedC, versionTwo, 2);
|
assertCaProfileCredential(runtime, "C", issuedC, versionTwo, 2);
|
||||||
credentialC = issuedC.credentialId();
|
credentialC = issuedC.credentialId();
|
||||||
|
|
||||||
@@ -347,7 +348,7 @@ final class CaProfileIssuanceEnforcementTest {
|
|||||||
rootProfile = runtime.profileService().importProfile(rootProfileDocument);
|
rootProfile = runtime.profileService().importProfile(rootProfileDocument);
|
||||||
runtime.profileService().activateProfile(rootProfileId, 1);
|
runtime.profileService().activateProfile(rootProfileId, 1);
|
||||||
rootId = createRoot(runtime, rootKeyRef, "CN=Version Switch Issuer Root", rootProfileId);
|
rootId = createRoot(runtime, rootKeyRef, "CN=Version Switch Issuer Root", rootProfileId);
|
||||||
Credential rootCredential = onlyCredential(runtime.caService(), rootId);
|
Credential rootCredential = onlyCredential(runtime, rootId);
|
||||||
assertCaProfileCredential(runtime, "issuer", rootCredential, rootProfile, 2);
|
assertCaProfileCredential(runtime, "issuer", rootCredential, rootProfile, 2);
|
||||||
|
|
||||||
versionOne = runtime.profileService().importProfile(versionOneDocument);
|
versionOne = runtime.profileService().importProfile(versionOneDocument);
|
||||||
@@ -357,10 +358,10 @@ final class CaProfileIssuanceEnforcementTest {
|
|||||||
|
|
||||||
caA = createIntermediate(runtime, rootId, keyRefA, "CN=Version Switch Intermediate A",
|
caA = createIntermediate(runtime, rootId, keyRefA, "CN=Version Switch Intermediate A",
|
||||||
intermediateProfileId);
|
intermediateProfileId);
|
||||||
Credential issuedA = onlyCredential(runtime.caService(), caA);
|
Credential issuedA = onlyCredential(runtime, caA);
|
||||||
assertCaProfileCredential(runtime, "A", issuedA, versionOne, 0);
|
assertCaProfileCredential(runtime, "A", issuedA, versionOne, 0);
|
||||||
credentialA = issuedA.credentialId();
|
credentialA = issuedA.credentialId();
|
||||||
assertCaProfileCredential(runtime, "issuer-after-A", onlyCredential(runtime.caService(), rootId), rootProfile, 2);
|
assertCaProfileCredential(runtime, "issuer-after-A", onlyCredential(runtime, rootId), rootProfile, 2);
|
||||||
|
|
||||||
ImportedCertificateProfileVersion storedOne = runtime.profileService()
|
ImportedCertificateProfileVersion storedOne = runtime.profileService()
|
||||||
.getImportedVersion(intermediateProfileId, 1).orElseThrow();
|
.getImportedVersion(intermediateProfileId, 1).orElseThrow();
|
||||||
@@ -375,14 +376,14 @@ final class CaProfileIssuanceEnforcementTest {
|
|||||||
|
|
||||||
caB = createIntermediate(runtime, rootId, keyRefB, "CN=Version Switch Intermediate B",
|
caB = createIntermediate(runtime, rootId, keyRefB, "CN=Version Switch Intermediate B",
|
||||||
intermediateProfileId);
|
intermediateProfileId);
|
||||||
Credential issuedB = onlyCredential(runtime.caService(), caB);
|
Credential issuedB = onlyCredential(runtime, caB);
|
||||||
assertCaProfileCredential(runtime, "B", issuedB, versionOne, 0);
|
assertCaProfileCredential(runtime, "B", issuedB, versionOne, 0);
|
||||||
credentialB = issuedB.credentialId();
|
credentialB = issuedB.credentialId();
|
||||||
|
|
||||||
assertEquals(versionTwo, runtime.profileService().activateProfile(intermediateProfileId, 2));
|
assertEquals(versionTwo, runtime.profileService().activateProfile(intermediateProfileId, 2));
|
||||||
caC = createIntermediate(runtime, rootId, keyRefC, "CN=Version Switch Intermediate C",
|
caC = createIntermediate(runtime, rootId, keyRefC, "CN=Version Switch Intermediate C",
|
||||||
intermediateProfileId);
|
intermediateProfileId);
|
||||||
Credential issuedC = onlyCredential(runtime.caService(), caC);
|
Credential issuedC = onlyCredential(runtime, caC);
|
||||||
assertCaProfileCredential(runtime, "C", issuedC, versionTwo, 1);
|
assertCaProfileCredential(runtime, "C", issuedC, versionTwo, 1);
|
||||||
credentialC = issuedC.credentialId();
|
credentialC = issuedC.credentialId();
|
||||||
|
|
||||||
@@ -390,7 +391,7 @@ final class CaProfileIssuanceEnforcementTest {
|
|||||||
0);
|
0);
|
||||||
assertCaProfileCredential(runtime, "B-reread", runtime.store().getCredential(credentialB).orElseThrow(), versionOne,
|
assertCaProfileCredential(runtime, "B-reread", runtime.store().getCredential(credentialB).orElseThrow(), versionOne,
|
||||||
0);
|
0);
|
||||||
assertCaProfileCredential(runtime, "issuer-reread", onlyCredential(runtime.caService(), rootId), rootProfile, 2);
|
assertCaProfileCredential(runtime, "issuer-reread", onlyCredential(runtime, rootId), rootProfile, 2);
|
||||||
ImportedCertificateProfileVersion unchanged = runtime.profileService()
|
ImportedCertificateProfileVersion unchanged = runtime.profileService()
|
||||||
.getImportedVersion(intermediateProfileId, 1).orElseThrow();
|
.getImportedVersion(intermediateProfileId, 1).orElseThrow();
|
||||||
assertArrayEquals(persistedVersionOne, unchanged.canonicalJson());
|
assertArrayEquals(persistedVersionOne, unchanged.canonicalJson());
|
||||||
@@ -405,7 +406,7 @@ final class CaProfileIssuanceEnforcementTest {
|
|||||||
versionOne, 0);
|
versionOne, 0);
|
||||||
assertCaProfileCredential(reopened, "C-restart", reopened.store().getCredential(credentialC).orElseThrow(),
|
assertCaProfileCredential(reopened, "C-restart", reopened.store().getCredential(credentialC).orElseThrow(),
|
||||||
versionTwo, 1);
|
versionTwo, 1);
|
||||||
assertCaProfileCredential(reopened, "issuer-restart", onlyCredential(reopened.caService(), rootId),
|
assertCaProfileCredential(reopened, "issuer-restart", onlyCredential(reopened, rootId),
|
||||||
rootProfile, 2);
|
rootProfile, 2);
|
||||||
ImportedCertificateProfileVersion unchanged = reopened.profileService()
|
ImportedCertificateProfileVersion unchanged = reopened.profileService()
|
||||||
.getImportedVersion(intermediateProfileId, 1).orElseThrow();
|
.getImportedVersion(intermediateProfileId, 1).orElseThrow();
|
||||||
@@ -429,12 +430,14 @@ final class CaProfileIssuanceEnforcementTest {
|
|||||||
.replace("\"profileId\":\"root-ca\"", "\"profileId\":\"other-format-root\"")
|
.replace("\"profileId\":\"root-ca\"", "\"profileId\":\"other-format-root\"")
|
||||||
.replace("\"formatId\":\"x509\"", "\"formatId\":\"other\"").getBytes(StandardCharsets.UTF_8));
|
.replace("\"formatId\":\"x509\"", "\"formatId\":\"other\"").getBytes(StandardCharsets.UTF_8));
|
||||||
CaRecord root = runtime.caService().getCa(rootId);
|
CaRecord root = runtime.caService().getCa(rootId);
|
||||||
Credential original = root.caCredentials().get(0);
|
Credential original = runtime.caCredential(root, 0);
|
||||||
Credential mutated = new Credential(original.credentialId(), original.formatId(), original.issuerRef(),
|
Credential mutated = new Credential(new PkiId(original.credentialId().value() + ":wrong-profile"),
|
||||||
original.subjectRef(), original.validity(), original.serialOrUniqueId(), original.publicKeyId(),
|
original.formatId(), original.issuerRef(), original.subjectRef(), original.validity(),
|
||||||
new CaProfileBinding(wrongFormat), original.status(), original.content(), original.attributes());
|
original.serialOrUniqueId(), original.publicKeyId(), new CaProfileBinding(wrongFormat),
|
||||||
|
original.status(), original.content(), original.attributes());
|
||||||
|
runtime.store().putCredential(mutated);
|
||||||
runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(),
|
runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(),
|
||||||
root.subjectRef(), List.of(mutated)));
|
root.subjectRef(), List.of(mutated.credentialId())));
|
||||||
|
|
||||||
int signCount = runtime.submittedSignCount();
|
int signCount = runtime.submittedSignCount();
|
||||||
assertThrows(PkiException.class,
|
assertThrows(PkiException.class,
|
||||||
@@ -491,7 +494,7 @@ final class CaProfileIssuanceEnforcementTest {
|
|||||||
service.issueIntermediateCertificate(new IntermediateCertIssueCommand(runtime.framework().formatId(),
|
service.issueIntermediateCertificate(new IntermediateCertIssueCommand(runtime.framework().formatId(),
|
||||||
rootId, intermediateId, "intermediate-ca", Optional.empty(), new SimpleAttributeSet()));
|
rootId, intermediateId, "intermediate-ca", Optional.empty(), new SimpleAttributeSet()));
|
||||||
profiles.assertAndReset("intermediate-ca");
|
profiles.assertAndReset("intermediate-ca");
|
||||||
rootCertificate = runtime.credentialBytes(service.getCa(rootId).caCredentials().get(0));
|
rootCertificate = runtime.credentialBytes(runtime.caCredential(service.getCa(rootId), 0));
|
||||||
}
|
}
|
||||||
try (PkiTestRuntime target = PkiTestRuntime.create(directory.resolve("import"),
|
try (PkiTestRuntime target = PkiTestRuntime.create(directory.resolve("import"),
|
||||||
directory.resolve("import-bus.log"), Map.of(rootRef, rootKey))) {
|
directory.resolve("import-bus.log"), Map.of(rootRef, rootKey))) {
|
||||||
@@ -518,7 +521,7 @@ final class CaProfileIssuanceEnforcementTest {
|
|||||||
new IntermediateCertIssueCommand(runtime.framework().formatId(), rootId, rootId,
|
new IntermediateCertIssueCommand(runtime.framework().formatId(), rootId, rootId,
|
||||||
"intermediate-ca", Optional.empty(), new SimpleAttributeSet())));
|
"intermediate-ca", Optional.empty(), new SimpleAttributeSet())));
|
||||||
assertEquals(signCount, runtime.submittedSignCount());
|
assertEquals(signCount, runtime.submittedSignCount());
|
||||||
assertEquals(1, runtime.caService().getCa(rootId).caCredentials().size());
|
assertEquals(1, runtime.caService().getCa(rootId).credentialIds().size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,7 +548,7 @@ final class CaProfileIssuanceEnforcementTest {
|
|||||||
runtime.framework().formatId(), rootId, intermediateId, "intermediate-ca",
|
runtime.framework().formatId(), rootId, intermediateId, "intermediate-ca",
|
||||||
Optional.of(invalid), new SimpleAttributeSet())));
|
Optional.of(invalid), new SimpleAttributeSet())));
|
||||||
assertEquals(signCount, runtime.submittedSignCount());
|
assertEquals(signCount, runtime.submittedSignCount());
|
||||||
assertEquals(1, runtime.caService().getCa(intermediateId).caCredentials().size());
|
assertEquals(1, runtime.caService().getCa(intermediateId).credentialIds().size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,14 +561,14 @@ final class CaProfileIssuanceEnforcementTest {
|
|||||||
directory.resolve("source-bus.log"), Map.of(rootRef, rootKey))) {
|
directory.resolve("source-bus.log"), Map.of(rootRef, rootKey))) {
|
||||||
PkiId rootId = source.caService().createRoot(new CaCreateCommand(source.framework().formatId(),
|
PkiId rootId = source.caService().createRoot(new CaCreateCommand(source.framework().formatId(),
|
||||||
new SubjectRef("CN=Imported Root"), "root-ca", Optional.of(rootRef), new SimpleAttributeSet()));
|
new SubjectRef("CN=Imported Root"), "root-ca", Optional.of(rootRef), new SimpleAttributeSet()));
|
||||||
encoded = source.credentialBytes(source.caService().getCa(rootId).caCredentials().get(0));
|
encoded = source.credentialBytes(source.caCredential(source.caService().getCa(rootId), 0));
|
||||||
}
|
}
|
||||||
try (PkiTestRuntime target = PkiTestRuntime.create(directory.resolve("target"),
|
try (PkiTestRuntime target = PkiTestRuntime.create(directory.resolve("target"),
|
||||||
directory.resolve("target-bus.log"), Map.of(rootRef, rootKey))) {
|
directory.resolve("target-bus.log"), Map.of(rootRef, rootKey))) {
|
||||||
PkiId imported = target.caService()
|
PkiId imported = target.caService()
|
||||||
.importRoot(new CaImportCommand(target.framework().formatId(), new SubjectRef("CN=Imported Root"),
|
.importRoot(new CaImportCommand(target.framework().formatId(), new SubjectRef("CN=Imported Root"),
|
||||||
"root-ca", rootRef, target.stageCredential(encoded), new SimpleAttributeSet()));
|
"root-ca", rootRef, target.stageCredential(encoded), new SimpleAttributeSet()));
|
||||||
Credential credential = target.caService().getCa(imported).caCredentials().get(0);
|
Credential credential = target.caCredential(target.caService().getCa(imported), 0);
|
||||||
assertEquals(target.profileService().getActiveReference("root-ca").orElseThrow(),
|
assertEquals(target.profileService().getActiveReference("root-ca").orElseThrow(),
|
||||||
((CaProfileBinding) credential.profileBinding()).reference());
|
((CaProfileBinding) credential.profileBinding()).reference());
|
||||||
}
|
}
|
||||||
@@ -583,7 +586,7 @@ final class CaProfileIssuanceEnforcementTest {
|
|||||||
.createRoot(new CaCreateCommand(source.framework().formatId(),
|
.createRoot(new CaCreateCommand(source.framework().formatId(),
|
||||||
new SubjectRef("CN=Import Mutation Root"), "root-ca", Optional.of(rootRef),
|
new SubjectRef("CN=Import Mutation Root"), "root-ca", Optional.of(rootRef),
|
||||||
new SimpleAttributeSet()));
|
new SimpleAttributeSet()));
|
||||||
Credential sourceCredential = source.caService().getCa(rootId).caCredentials().get(0);
|
Credential sourceCredential = source.caCredential(source.caService().getCa(rootId), 0);
|
||||||
encoded = mutation.mutate(source, sourceCredential, rootKey);
|
encoded = mutation.mutate(source, sourceCredential, rootKey);
|
||||||
}
|
}
|
||||||
try (PkiTestRuntime target = PkiTestRuntime.create(directory.resolve("target"),
|
try (PkiTestRuntime target = PkiTestRuntime.create(directory.resolve("target"),
|
||||||
@@ -665,10 +668,10 @@ final class CaProfileIssuanceEnforcementTest {
|
|||||||
issuerId, new SubjectRef(subject), profileId, Optional.of(keyRef), new SimpleAttributeSet()));
|
issuerId, new SubjectRef(subject), profileId, Optional.of(keyRef), new SimpleAttributeSet()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Credential onlyCredential(CaService service, PkiId caId) {
|
private static Credential onlyCredential(PkiTestRuntime runtime, PkiId caId) {
|
||||||
CaRecord ca = service.getCa(caId);
|
CaRecord ca = runtime.caService().getCa(caId);
|
||||||
assertEquals(1, ca.caCredentials().size());
|
assertEquals(1, ca.credentialIds().size());
|
||||||
return ca.caCredentials().get(0);
|
return runtime.caCredential(ca, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] caProfileDocument(String builtInProfileId, String profileId, long profileVersion,
|
private static byte[] caProfileDocument(String builtInProfileId, String profileId, long profileVersion,
|
||||||
|
|||||||
@@ -137,11 +137,12 @@ public final class PkiCoreE2eTest {
|
|||||||
try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"), keys)) {
|
try (PkiTestRuntime runtime = PkiTestRuntime.create(tempDir, tempDir.resolve("bus.log"), keys)) {
|
||||||
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
|
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
|
||||||
new SubjectRef("CN=Matrix Root"), "root-ca", Optional.of(rootKeyRef), emptyAttributes()));
|
new SubjectRef("CN=Matrix Root"), "root-ca", Optional.of(rootKeyRef), emptyAttributes()));
|
||||||
Credential usable = runtime.caService().getCa(rootCaId).caCredentials().get(0);
|
Credential usable = runtime.caCredential(runtime.caService().getCa(rootCaId), 0);
|
||||||
Credential unusable = copyWithId(usable, new PkiId("credential:matrix-unusable"));
|
Credential unusable = copyWithId(usable, new PkiId("credential:matrix-unusable"));
|
||||||
|
runtime.store().putCredential(unusable);
|
||||||
CaRecord root = runtime.caService().getCa(rootCaId);
|
CaRecord root = runtime.caService().getCa(rootCaId);
|
||||||
runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(),
|
runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(),
|
||||||
root.subjectRef(), List.of(unusable, usable)));
|
root.subjectRef(), List.of(unusable.credentialId(), usable.credentialId())));
|
||||||
|
|
||||||
List<PkiId> resolved = new ArrayList<>();
|
List<PkiId> resolved = new ArrayList<>();
|
||||||
EffectiveCredentialStatusResolver resolver = scriptedResolver(credential -> {
|
EffectiveCredentialStatusResolver resolver = scriptedResolver(credential -> {
|
||||||
@@ -173,7 +174,7 @@ public final class PkiCoreE2eTest {
|
|||||||
resolved.clear();
|
resolved.clear();
|
||||||
|
|
||||||
runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(),
|
runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(),
|
||||||
root.subjectRef(), List.of(usable, unusable)));
|
root.subjectRef(), List.of(usable.credentialId(), unusable.credentialId())));
|
||||||
statusService.generate(new StatusObjectGenerateCommand(rootCaId, StatusObjectType.CRL,
|
statusService.generate(new StatusObjectGenerateCommand(rootCaId, StatusObjectType.CRL,
|
||||||
runtime.framework().formatId(), emptyAttributes()));
|
runtime.framework().formatId(), emptyAttributes()));
|
||||||
assertEquals(List.of(unusable.credentialId(), usable.credentialId()), List.copyOf(resolved));
|
assertEquals(List.of(unusable.credentialId(), usable.credentialId()), List.copyOf(resolved));
|
||||||
@@ -201,7 +202,7 @@ public final class PkiCoreE2eTest {
|
|||||||
.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootCaId,
|
.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootCaId,
|
||||||
new SubjectRef("CN=H6 Intermediate"), "intermediate-ca", Optional.of(intermediateKeyRef),
|
new SubjectRef("CN=H6 Intermediate"), "intermediate-ca", Optional.of(intermediateKeyRef),
|
||||||
emptyAttributes()));
|
emptyAttributes()));
|
||||||
PkiId rootCredentialId = runtime.caService().getCa(rootCaId).caCredentials().get(0).credentialId();
|
PkiId rootCredentialId = runtime.caService().getCa(rootCaId).credentialIds().get(0);
|
||||||
runtime.revocationService().revokePermanently(new RevocationCommand.RevokePermanently(rootCredentialId,
|
runtime.revocationService().revokePermanently(new RevocationCommand.RevokePermanently(rootCredentialId,
|
||||||
RevocationReason.KEY_COMPROMISE, emptyAttributes()));
|
RevocationReason.KEY_COMPROMISE, emptyAttributes()));
|
||||||
int submissionsBeforeRejections = runtime.submittedSignCount();
|
int submissionsBeforeRejections = runtime.submittedSignCount();
|
||||||
@@ -230,7 +231,7 @@ public final class PkiCoreE2eTest {
|
|||||||
|
|
||||||
assertEquals(submissionsBeforeRejections, runtime.submittedSignCount());
|
assertEquals(submissionsBeforeRejections, runtime.submittedSignCount());
|
||||||
assertTrue(runtime.store().getCredential(rootCredentialId).isPresent());
|
assertTrue(runtime.store().getCredential(rootCredentialId).isPresent());
|
||||||
assertTrue(runtime.caService().getCa(intermediateCaId).caCredentials().size() == 1);
|
assertTrue(runtime.caService().getCa(intermediateCaId).credentialIds().size() == 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,7 +321,7 @@ public final class PkiCoreE2eTest {
|
|||||||
.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootCaId,
|
.createIntermediate(new IntermediateCreateCommand(runtime.framework().formatId(), rootCaId,
|
||||||
new SubjectRef("CN=Rejected Intermediate"), "intermediate-ca",
|
new SubjectRef("CN=Rejected Intermediate"), "intermediate-ca",
|
||||||
Optional.of(intermediateKeyRef), emptyAttributes()));
|
Optional.of(intermediateKeyRef), emptyAttributes()));
|
||||||
Credential rootCredential = runtime.caService().getCa(rootCaId).caCredentials().get(0);
|
Credential rootCredential = runtime.caCredential(runtime.caService().getCa(rootCaId), 0);
|
||||||
EffectiveCredentialStatusResolver resolver = scriptedResolver(credential -> status, resolutionFailure);
|
EffectiveCredentialStatusResolver resolver = scriptedResolver(credential -> status, resolutionFailure);
|
||||||
CountingIssuerBackend backend = new CountingIssuerBackend(runtime.issuerBackend());
|
CountingIssuerBackend backend = new CountingIssuerBackend(runtime.issuerBackend());
|
||||||
IssuanceService issuance = runtime.issuanceService(backend, resolver);
|
IssuanceService issuance = runtime.issuanceService(backend, resolver);
|
||||||
@@ -332,7 +333,7 @@ public final class PkiCoreE2eTest {
|
|||||||
int signCount = runtime.submittedSignCount();
|
int signCount = runtime.submittedSignCount();
|
||||||
int caCount = runtime.store().listCas().size();
|
int caCount = runtime.store().listCas().size();
|
||||||
int statusCount = runtime.store().listStatusObjects(rootCaId).size();
|
int statusCount = runtime.store().listStatusObjects(rootCaId).size();
|
||||||
int intermediateCredentialCount = runtime.caService().getCa(intermediateCaId).caCredentials().size();
|
int intermediateCredentialCount = runtime.caService().getCa(intermediateCaId).credentialIds().size();
|
||||||
|
|
||||||
assertThrows(PkiException.class, () -> issuance
|
assertThrows(PkiException.class, () -> issuance
|
||||||
.issueEndEntity(new IssueEndEntityCommand(rootCaId, leafRequest, "default", Optional.empty())));
|
.issueEndEntity(new IssueEndEntityCommand(rootCaId, leafRequest, "default", Optional.empty())));
|
||||||
@@ -355,7 +356,7 @@ public final class PkiCoreE2eTest {
|
|||||||
assertEquals(caCount, runtime.store().listCas().size());
|
assertEquals(caCount, runtime.store().listCas().size());
|
||||||
assertEquals(statusCount, runtime.store().listStatusObjects(rootCaId).size());
|
assertEquals(statusCount, runtime.store().listStatusObjects(rootCaId).size());
|
||||||
assertEquals(intermediateCredentialCount,
|
assertEquals(intermediateCredentialCount,
|
||||||
runtime.caService().getCa(intermediateCaId).caCredentials().size());
|
runtime.caService().getCa(intermediateCaId).credentialIds().size());
|
||||||
assertTrue(runtime.store().getCredential(rootCredential.credentialId()).isPresent());
|
assertTrue(runtime.store().getCredential(rootCredential.credentialId()).isPresent());
|
||||||
assertFalse(runtime.auditSink().snapshot().toString().contains("DO_NOT_EXPOSE_REVOCATION_SENTINEL"));
|
assertFalse(runtime.auditSink().snapshot().toString().contains("DO_NOT_EXPOSE_REVOCATION_SENTINEL"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -472,7 +472,7 @@ final class PkiProofGateE2eTest {
|
|||||||
new SubjectRef("CN=Intermediate"), "intermediate-ca", Optional.of(intermediateKeyRef),
|
new SubjectRef("CN=Intermediate"), "intermediate-ca", Optional.of(intermediateKeyRef),
|
||||||
approved));
|
approved));
|
||||||
|
|
||||||
Credential first = runtime.caService().getCa(intermediateCaId).caCredentials().get(0);
|
Credential first = runtime.caCredential(runtime.caService().getCa(intermediateCaId), 0);
|
||||||
X509CertificateHolder firstHolder = new X509CertificateHolder(runtime.credentialBytes(first));
|
X509CertificateHolder firstHolder = new X509CertificateHolder(runtime.credentialBytes(first));
|
||||||
assertEquals("CN=Root", firstHolder.getIssuer().toString());
|
assertEquals("CN=Root", firstHolder.getIssuer().toString());
|
||||||
assertEquals("CN=Intermediate", firstHolder.getSubject().toString());
|
assertEquals("CN=Intermediate", firstHolder.getSubject().toString());
|
||||||
@@ -538,7 +538,7 @@ final class PkiProofGateE2eTest {
|
|||||||
runtime.framework().formatId(), rootCaId, intermediateCaId, "intermediate-ca",
|
runtime.framework().formatId(), rootCaId, intermediateCaId, "intermediate-ca",
|
||||||
Optional.empty(), new SimpleAttributeSet())));
|
Optional.empty(), new SimpleAttributeSet())));
|
||||||
assertEquals(7, runtime.submittedSignCount());
|
assertEquals(7, runtime.submittedSignCount());
|
||||||
assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size());
|
assertEquals(1, runtime.caService().getCa(intermediateCaId).credentialIds().size());
|
||||||
assertTrue(runtime.store().listWorkflowStates().isEmpty());
|
assertTrue(runtime.store().listWorkflowStates().isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,13 +551,13 @@ final class PkiProofGateE2eTest {
|
|||||||
PkiId rootCaId = source.caService().createRoot(new CaCreateCommand(source.framework().formatId(),
|
PkiId rootCaId = source.caService().createRoot(new CaCreateCommand(source.framework().formatId(),
|
||||||
new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef), new SimpleAttributeSet()));
|
new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef), new SimpleAttributeSet()));
|
||||||
rootCertificate = source
|
rootCertificate = source
|
||||||
.credentialBytes(source.caService().getCa(rootCaId).caCredentials().get(0)).clone();
|
.credentialBytes(source.caCredential(source.caService().getCa(rootCaId), 0)).clone();
|
||||||
PkiId intermediateCaId = source.caService()
|
PkiId intermediateCaId = source.caService()
|
||||||
.createIntermediate(new IntermediateCreateCommand(source.framework().formatId(), rootCaId,
|
.createIntermediate(new IntermediateCreateCommand(source.framework().formatId(), rootCaId,
|
||||||
new SubjectRef("CN=Intermediate"), "intermediate-ca", Optional.of(intermediateKeyRef),
|
new SubjectRef("CN=Intermediate"), "intermediate-ca", Optional.of(intermediateKeyRef),
|
||||||
new SimpleAttributeSet()));
|
new SimpleAttributeSet()));
|
||||||
intermediateCertificate = source
|
intermediateCertificate = source
|
||||||
.credentialBytes(source.caService().getCa(intermediateCaId).caCredentials().get(0)).clone();
|
.credentialBytes(source.caCredential(source.caService().getCa(intermediateCaId), 0)).clone();
|
||||||
ParsedCertificationRequest leaf = parse(source, makeCsr(subjectKey, subjectKey, "CN=Leaf"));
|
ParsedCertificationRequest leaf = parse(source, makeCsr(subjectKey, subjectKey, "CN=Leaf"));
|
||||||
leafCertificate = source.credentialBytes(source.issuanceService()
|
leafCertificate = source.credentialBytes(source.issuanceService()
|
||||||
.issueEndEntity(new IssueEndEntityCommand(rootCaId, leaf, "default", Optional.empty()))
|
.issueEndEntity(new IssueEndEntityCommand(rootCaId, leaf, "default", Optional.empty()))
|
||||||
@@ -585,10 +585,10 @@ final class PkiProofGateE2eTest {
|
|||||||
.importRoot(new CaImportCommand(target.framework().formatId(), new SubjectRef("CN=Root"), "root-ca",
|
.importRoot(new CaImportCommand(target.framework().formatId(), new SubjectRef("CN=Root"), "root-ca",
|
||||||
rootKeyRef, target.stageCredential(callerOwnedCertificate),
|
rootKeyRef, target.stageCredential(callerOwnedCertificate),
|
||||||
new SimpleAttributeSet()));
|
new SimpleAttributeSet()));
|
||||||
assertTrue(target.caService().getCa(importedCaId).caCredentials().get(0)
|
assertTrue(target.caCredential(target.caService().getCa(importedCaId), 0)
|
||||||
.profileBinding() instanceof CaProfileBinding);
|
.profileBinding() instanceof CaProfileBinding);
|
||||||
assertArrayEquals(expectedImportedCertificate,
|
assertArrayEquals(expectedImportedCertificate,
|
||||||
target.credentialBytes(target.caService().getCa(importedCaId).caCredentials().get(0)));
|
target.credentialBytes(target.caCredential(target.caService().getCa(importedCaId), 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
assertInvalidRootImport(tempDir.resolve("import-leaf"), rootKeyRef, rootKey, leafCertificate, "CN=Leaf");
|
assertInvalidRootImport(tempDir.resolve("import-leaf"), rootKeyRef, rootKey, leafCertificate, "CN=Leaf");
|
||||||
@@ -787,12 +787,14 @@ final class PkiProofGateE2eTest {
|
|||||||
runtime.store().getCredential(returned.credential().credentialId()).orElseThrow()));
|
runtime.store().getCredential(returned.credential().credentialId()).orElseThrow()));
|
||||||
|
|
||||||
CaRecord root = runtime.caService().getCa(rootCaId);
|
CaRecord root = runtime.caService().getCa(rootCaId);
|
||||||
Credential original = root.caCredentials().get(0);
|
Credential original = runtime.caCredential(root, 0);
|
||||||
Credential revoked = new Credential(original.credentialId(), original.formatId(), original.issuerRef(),
|
Credential revoked = new Credential(new PkiId(original.credentialId().value() + ":revoked"),
|
||||||
|
original.formatId(), original.issuerRef(),
|
||||||
original.subjectRef(), original.validity(), original.serialOrUniqueId(), original.publicKeyId(),
|
original.subjectRef(), original.validity(), original.serialOrUniqueId(), original.publicKeyId(),
|
||||||
original.profileBinding(), CredentialStatus.REVOKED, original.content(), original.attributes());
|
original.profileBinding(), CredentialStatus.REVOKED, original.content(), original.attributes());
|
||||||
|
runtime.store().putCredential(revoked);
|
||||||
runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(),
|
runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(),
|
||||||
root.subjectRef(), List.of(revoked)));
|
root.subjectRef(), List.of(revoked.credentialId())));
|
||||||
int before = runtime.submittedSignCount();
|
int before = runtime.submittedSignCount();
|
||||||
assertThrows(PkiException.class, () -> runtime.issuanceService()
|
assertThrows(PkiException.class, () -> runtime.issuanceService()
|
||||||
.issueEndEntity(new IssueEndEntityCommand(rootCaId, subject, "default", Optional.empty())));
|
.issueEndEntity(new IssueEndEntityCommand(rootCaId, subject, "default", Optional.empty())));
|
||||||
@@ -800,17 +802,19 @@ final class PkiProofGateE2eTest {
|
|||||||
|
|
||||||
Validity expiredValidity = new Validity(Instant.now().minus(Duration.ofDays(2)),
|
Validity expiredValidity = new Validity(Instant.now().minus(Duration.ofDays(2)),
|
||||||
Instant.now().minus(Duration.ofDays(1)));
|
Instant.now().minus(Duration.ofDays(1)));
|
||||||
Credential expired = new Credential(original.credentialId(), original.formatId(), original.issuerRef(),
|
Credential expired = new Credential(new PkiId(original.credentialId().value() + ":expired"),
|
||||||
original.subjectRef(), expiredValidity, original.serialOrUniqueId(), original.publicKeyId(),
|
original.formatId(), original.issuerRef(), original.subjectRef(), expiredValidity,
|
||||||
original.profileBinding(), CredentialStatus.ISSUED, original.content(), original.attributes());
|
original.serialOrUniqueId(), original.publicKeyId(), original.profileBinding(),
|
||||||
|
CredentialStatus.ISSUED, original.content(), original.attributes());
|
||||||
|
runtime.store().putCredential(expired);
|
||||||
runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(),
|
runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(),
|
||||||
root.subjectRef(), List.of(expired)));
|
root.subjectRef(), List.of(expired.credentialId())));
|
||||||
assertThrows(PkiException.class, () -> runtime.issuanceService()
|
assertThrows(PkiException.class, () -> runtime.issuanceService()
|
||||||
.issueEndEntity(new IssueEndEntityCommand(rootCaId, subject, "default", Optional.empty())));
|
.issueEndEntity(new IssueEndEntityCommand(rootCaId, subject, "default", Optional.empty())));
|
||||||
assertEquals(before, runtime.submittedSignCount());
|
assertEquals(before, runtime.submittedSignCount());
|
||||||
|
|
||||||
runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(),
|
runtime.store().putCa(new CaRecord(root.caId(), root.kind(), root.state(), root.issuerKeyRef(),
|
||||||
root.subjectRef(), List.of(original)));
|
root.subjectRef(), List.of(original.credentialId())));
|
||||||
ParsedCertificationRequest missing = withAttributes(subject, new SimpleAttributeSet());
|
ParsedCertificationRequest missing = withAttributes(subject, new SimpleAttributeSet());
|
||||||
AttributeSet hostileAttributes = new AttributeSet() {
|
AttributeSet hostileAttributes = new AttributeSet() {
|
||||||
@Override
|
@Override
|
||||||
@@ -857,7 +861,7 @@ final class PkiProofGateE2eTest {
|
|||||||
Map.of(rootKeyRef, rootKey, intermediateKeyRef, intermediateKey))) {
|
Map.of(rootKeyRef, rootKey, intermediateKeyRef, intermediateKey))) {
|
||||||
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
|
PkiId rootCaId = runtime.caService().createRoot(new CaCreateCommand(runtime.framework().formatId(),
|
||||||
new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef), new SimpleAttributeSet()));
|
new SubjectRef("CN=Root"), "root-ca", Optional.of(rootKeyRef), new SimpleAttributeSet()));
|
||||||
assertTrue(runtime.caService().getCa(rootCaId).caCredentials().get(0)
|
assertTrue(runtime.caCredential(runtime.caService().getCa(rootCaId), 0)
|
||||||
.profileBinding() instanceof CaProfileBinding);
|
.profileBinding() instanceof CaProfileBinding);
|
||||||
CredentialIssuerBackend delegate = runtime.issuerBackend();
|
CredentialIssuerBackend delegate = runtime.issuerBackend();
|
||||||
for (BindingVariantMutation mutation : BindingVariantMutation.values()) {
|
for (BindingVariantMutation mutation : BindingVariantMutation.values()) {
|
||||||
@@ -910,7 +914,7 @@ final class PkiProofGateE2eTest {
|
|||||||
Optional.empty(), new SimpleAttributeSet())),
|
Optional.empty(), new SimpleAttributeSet())),
|
||||||
mutation.name());
|
mutation.name());
|
||||||
assertTrue(rejected.getMessage().contains("CREDENTIAL_PROFILE_BINDING_MISMATCH"), mutation.name());
|
assertTrue(rejected.getMessage().contains("CREDENTIAL_PROFILE_BINDING_MISMATCH"), mutation.name());
|
||||||
assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size(), mutation.name());
|
assertEquals(1, runtime.caService().getCa(intermediateCaId).credentialIds().size(), mutation.name());
|
||||||
if (produced.get() != null) {
|
if (produced.get() != null) {
|
||||||
assertTrue(runtime.store().getCredential(produced.get().credentialId()).isEmpty(), mutation.name());
|
assertTrue(runtime.store().getCredential(produced.get().credentialId()).isEmpty(), mutation.name());
|
||||||
}
|
}
|
||||||
@@ -935,7 +939,7 @@ final class PkiProofGateE2eTest {
|
|||||||
() -> wrongSubjectService.issueIntermediateCertificate(
|
() -> wrongSubjectService.issueIntermediateCertificate(
|
||||||
new IntermediateCertIssueCommand(runtime.framework().formatId(), rootCaId, intermediateCaId,
|
new IntermediateCertIssueCommand(runtime.framework().formatId(), rootCaId, intermediateCaId,
|
||||||
"intermediate-ca", Optional.empty(), new SimpleAttributeSet())));
|
"intermediate-ca", Optional.empty(), new SimpleAttributeSet())));
|
||||||
assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size());
|
assertEquals(1, runtime.caService().getCa(intermediateCaId).credentialIds().size());
|
||||||
|
|
||||||
CredentialIssuerBackend invalidSignatureBackend = new CredentialIssuerBackend() {
|
CredentialIssuerBackend invalidSignatureBackend = new CredentialIssuerBackend() {
|
||||||
@Override
|
@Override
|
||||||
@@ -960,7 +964,7 @@ final class PkiProofGateE2eTest {
|
|||||||
() -> invalidSignatureService.issueIntermediateCertificate(
|
() -> invalidSignatureService.issueIntermediateCertificate(
|
||||||
new IntermediateCertIssueCommand(runtime.framework().formatId(), rootCaId, intermediateCaId,
|
new IntermediateCertIssueCommand(runtime.framework().formatId(), rootCaId, intermediateCaId,
|
||||||
"intermediate-ca", Optional.empty(), new SimpleAttributeSet())));
|
"intermediate-ca", Optional.empty(), new SimpleAttributeSet())));
|
||||||
assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size());
|
assertEquals(1, runtime.caService().getCa(intermediateCaId).credentialIds().size());
|
||||||
|
|
||||||
for (IntermediateExtensionVariant variant : IntermediateExtensionVariant.values()) {
|
for (IntermediateExtensionVariant variant : IntermediateExtensionVariant.values()) {
|
||||||
CaService maliciousExtensionService = runtime
|
CaService maliciousExtensionService = runtime
|
||||||
@@ -970,7 +974,7 @@ final class PkiProofGateE2eTest {
|
|||||||
runtime.framework().formatId(), rootCaId, intermediateCaId, "intermediate-ca",
|
runtime.framework().formatId(), rootCaId, intermediateCaId, "intermediate-ca",
|
||||||
Optional.empty(), new SimpleAttributeSet())),
|
Optional.empty(), new SimpleAttributeSet())),
|
||||||
variant.name());
|
variant.name());
|
||||||
assertEquals(1, runtime.caService().getCa(intermediateCaId).caCredentials().size(), variant.name());
|
assertEquals(1, runtime.caService().getCa(intermediateCaId).credentialIds().size(), variant.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
AtomicReference<Credential> rawCredential = new AtomicReference<>();
|
AtomicReference<Credential> rawCredential = new AtomicReference<>();
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ final class DefaultStatusObjectServiceCrlTest {
|
|||||||
try (PkiTestRuntime runtime = PkiTestRuntime.create(root, root.resolve("bus.log"),
|
try (PkiTestRuntime runtime = PkiTestRuntime.create(root, root.resolve("bus.log"),
|
||||||
Map.of(rootKeyRef, rootKey))) {
|
Map.of(rootKeyRef, rootKey))) {
|
||||||
PkiId caId = createRoot(runtime, rootKeyRef, "CRL Generator Root");
|
PkiId caId = createRoot(runtime, rootKeyRef, "CRL Generator Root");
|
||||||
Credential issuer = runtime.caService().getCa(caId).caCredentials().get(0);
|
Credential issuer = runtime.caCredential(runtime.caService().getCa(caId), 0);
|
||||||
StatusObjectGenerateCommand command = crlCommand(runtime, caId, issuer, rootKeyRef);
|
StatusObjectGenerateCommand command = crlCommand(runtime, caId, issuer, rootKeyRef);
|
||||||
List<RevocationReason> reasons = activeReasons();
|
List<RevocationReason> reasons = activeReasons();
|
||||||
List<BigInteger> serials = serials(reasons.size());
|
List<BigInteger> serials = serials(reasons.size());
|
||||||
@@ -188,7 +188,7 @@ final class DefaultStatusObjectServiceCrlTest {
|
|||||||
try (PkiTestRuntime runtime = PkiTestRuntime.create(root, root.resolve("bus.log"),
|
try (PkiTestRuntime runtime = PkiTestRuntime.create(root, root.resolve("bus.log"),
|
||||||
Map.of(rootKeyRef, rootKey))) {
|
Map.of(rootKeyRef, rootKey))) {
|
||||||
PkiId caId = createRoot(runtime, rootKeyRef, "CRL Failure Root");
|
PkiId caId = createRoot(runtime, rootKeyRef, "CRL Failure Root");
|
||||||
Credential template = runtime.caService().getCa(caId).caCredentials().get(0);
|
Credential template = runtime.caCredential(runtime.caService().getCa(caId), 0);
|
||||||
StatusObjectGenerateCommand command = new StatusObjectGenerateCommand(caId, StatusObjectType.CRL,
|
StatusObjectGenerateCommand command = new StatusObjectGenerateCommand(caId, StatusObjectType.CRL,
|
||||||
runtime.framework().formatId(), emptyAttributes());
|
runtime.framework().formatId(), emptyAttributes());
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
package zeroecho.pki.impl.fs;
|
package zeroecho.pki.impl.fs;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
@@ -218,7 +219,7 @@ public final class FilesystemPkiStoreTest {
|
|||||||
store.putCa(ca1);
|
store.putCa(ca1);
|
||||||
|
|
||||||
CaRecord ca2 = new CaRecord(ca1.caId(), ca1.kind(), CaState.DISABLED, ca1.issuerKeyRef(), ca1.subjectRef(),
|
CaRecord ca2 = new CaRecord(ca1.caId(), ca1.kind(), CaState.DISABLED, ca1.issuerKeyRef(), ca1.subjectRef(),
|
||||||
ca1.caCredentials());
|
ca1.credentialIds());
|
||||||
store.putCa(ca2);
|
store.putCa(ca2);
|
||||||
|
|
||||||
Optional<CaRecord> loaded = store.getCa(ca1.caId());
|
Optional<CaRecord> loaded = store.getCa(ca1.caId());
|
||||||
@@ -232,6 +233,216 @@ public final class FilesystemPkiStoreTest {
|
|||||||
System.out.println("caHistoryCreatesCurrentAndHistory...ok");
|
System.out.println("caHistoryCreatesCurrentAndHistory...ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void caReferencesRequireValidStandaloneCredentialsAndPreserveOrder() throws Exception {
|
||||||
|
System.out.println("caReferencesRequireValidStandaloneCredentialsAndPreserveOrder");
|
||||||
|
Path root = tmp.resolve("store-ca-authority");
|
||||||
|
try (FilesystemPkiStore store = new FilesystemPkiStore(root, FsPkiStoreOptions.defaults())) {
|
||||||
|
Credential first = TestObjects.minimalCredential(store, "SERIAL-FIRST", "profile-ca");
|
||||||
|
Credential second = TestObjects.minimalCredential(store, "SERIAL-SECOND", "profile-ca");
|
||||||
|
store.putCredential(first);
|
||||||
|
store.putCredential(second);
|
||||||
|
CaRecord ordered = new CaRecord(new PkiId("ca-ordered"), CaKind.ROOT, CaState.ACTIVE,
|
||||||
|
new KeyRef("key-ordered"), new SubjectRef("CN=Ordered"),
|
||||||
|
List.of(second.credentialId(), first.credentialId()));
|
||||||
|
store.putCa(ordered);
|
||||||
|
|
||||||
|
assertEquals(List.of(second.credentialId(), first.credentialId()),
|
||||||
|
store.getCa(ordered.caId()).orElseThrow().credentialIds());
|
||||||
|
CaRecord missing = new CaRecord(new PkiId("ca-missing"), CaKind.ROOT, CaState.ACTIVE,
|
||||||
|
new KeyRef("key-missing"), new SubjectRef("CN=Missing"),
|
||||||
|
List.of(new PkiId("credential-missing")));
|
||||||
|
assertThrows(IllegalStateException.class, () -> store.putCa(missing));
|
||||||
|
|
||||||
|
Files.write(root.resolve("staged-content").resolve(first.content().contentId() + ".content"),
|
||||||
|
new byte[] { 9, 9, 9 });
|
||||||
|
assertThrows(IllegalStateException.class, () -> store.getCa(ordered.caId()));
|
||||||
|
assertThrows(IllegalStateException.class, store::listCas);
|
||||||
|
}
|
||||||
|
System.out.println("caReferencesRequireValidStandaloneCredentialsAndPreserveOrder...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void oldStoreVersionIsRejected() throws Exception {
|
||||||
|
System.out.println("oldStoreVersionIsRejected");
|
||||||
|
Path root = tmp.resolve("store-old-version");
|
||||||
|
Files.createDirectories(root);
|
||||||
|
Files.writeString(root.resolve(FsPaths.VERSION_FILE), "v2");
|
||||||
|
|
||||||
|
assertThrows(IllegalStateException.class,
|
||||||
|
() -> new FilesystemPkiStore(root, FsPkiStoreOptions.defaults()));
|
||||||
|
System.out.println("oldStoreVersionIsRejected...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void snapshotReconstructsSharedCredentialOnceWithNewTargetReference() throws Exception {
|
||||||
|
System.out.println("snapshotReconstructsSharedCredentialOnceWithNewTargetReference");
|
||||||
|
Path root = tmp.resolve("store-snapshot-authority");
|
||||||
|
Path snapshot = tmp.resolve("snapshot-authority");
|
||||||
|
FsPkiStoreOptions options = nonStrictSnapshotOptions();
|
||||||
|
PkiId credentialId;
|
||||||
|
String sourceContentId;
|
||||||
|
try (FilesystemPkiStore source = new FilesystemPkiStore(root, options)) {
|
||||||
|
Credential shared = TestObjects.minimalCredential(source, "SERIAL-SHARED", "profile-ca");
|
||||||
|
source.putCredential(shared);
|
||||||
|
credentialId = shared.credentialId();
|
||||||
|
sourceContentId = shared.content().contentId();
|
||||||
|
source.putCa(new CaRecord(new PkiId("ca-shared-one"), CaKind.ROOT, CaState.ACTIVE,
|
||||||
|
new KeyRef("key-shared-one"), new SubjectRef("CN=Shared One"), List.of(credentialId)));
|
||||||
|
source.putCa(new CaRecord(new PkiId("ca-shared-two"), CaKind.ROOT, CaState.ACTIVE,
|
||||||
|
new KeyRef("key-shared-two"), new SubjectRef("CN=Shared Two"), List.of(credentialId)));
|
||||||
|
source.exportSnapshot(snapshot, Instant.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
try (FilesystemPkiStore restored = new FilesystemPkiStore(snapshot, options)) {
|
||||||
|
assertEquals(2, restored.listCas().size());
|
||||||
|
Credential credential = restored.getCredential(credentialId).orElseThrow();
|
||||||
|
assertFalse(sourceContentId.equals(credential.content().contentId()));
|
||||||
|
assertEquals(List.of(credentialId),
|
||||||
|
restored.getCa(new PkiId("ca-shared-one")).orElseThrow().credentialIds());
|
||||||
|
}
|
||||||
|
try (java.util.stream.Stream<Path> records = Files.list(snapshot.resolve("credentials").resolve("by-id"))) {
|
||||||
|
assertEquals(1, records.filter(Files::isRegularFile).count());
|
||||||
|
}
|
||||||
|
assertFalse(Files.exists(snapshot.resolve("staged-content").resolve(sourceContentId + ".content")));
|
||||||
|
assertFalse(Files.exists(snapshot.resolve("staged-content").resolve(sourceContentId + ".meta")));
|
||||||
|
assertFalse(Files.exists(snapshot.resolve("staged-content").resolve(sourceContentId + ".owners")));
|
||||||
|
System.out.println("snapshotReconstructsSharedCredentialOnceWithNewTargetReference...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void snapshotPreservesStandaloneCredentialWithNewReferenceAndResolvableCa() throws Exception {
|
||||||
|
System.out.println("snapshotPreservesStandaloneCredentialWithNewReferenceAndResolvableCa");
|
||||||
|
Path root = tmp.resolve("store-snapshot-standalone");
|
||||||
|
Path snapshot = tmp.resolve("snapshot-standalone");
|
||||||
|
FsPkiStoreOptions options = nonStrictSnapshotOptions();
|
||||||
|
PkiId standaloneId;
|
||||||
|
String standaloneSourceContentId;
|
||||||
|
PkiId caId;
|
||||||
|
try (FilesystemPkiStore source = new FilesystemPkiStore(root, options)) {
|
||||||
|
Credential standalone = TestObjects.minimalCredential(source, "SERIAL-END-ENTITY", "profile-leaf");
|
||||||
|
source.putCredential(standalone);
|
||||||
|
standaloneId = standalone.credentialId();
|
||||||
|
standaloneSourceContentId = standalone.content().contentId();
|
||||||
|
CaRecord ca = TestObjects.minimalCaRecord(source, "ca-with-authority", CaState.ACTIVE);
|
||||||
|
source.putCa(ca);
|
||||||
|
caId = ca.caId();
|
||||||
|
source.exportSnapshot(snapshot, Instant.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
try (FilesystemPkiStore restored = new FilesystemPkiStore(snapshot, options)) {
|
||||||
|
Credential standalone = restored.getCredential(standaloneId).orElseThrow();
|
||||||
|
assertFalse(standaloneSourceContentId.equals(standalone.content().contentId()));
|
||||||
|
CaRecord ca = restored.getCa(caId).orElseThrow();
|
||||||
|
assertTrue(restored.getCredential(ca.credentialIds().get(0)).isPresent());
|
||||||
|
}
|
||||||
|
System.out.println("snapshotPreservesStandaloneCredentialWithNewReferenceAndResolvableCa...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void snapshotPreservesSharedStatusContentWithoutStaleCredentialOwner() throws Exception {
|
||||||
|
System.out.println("snapshotPreservesSharedStatusContentWithoutStaleCredentialOwner");
|
||||||
|
Path root = tmp.resolve("store-snapshot-shared-status");
|
||||||
|
Path snapshot = tmp.resolve("snapshot-shared-status");
|
||||||
|
FsPkiStoreOptions options = nonStrictSnapshotOptions();
|
||||||
|
PkiId credentialId;
|
||||||
|
PkiId statusId = new PkiId("status-shared-content");
|
||||||
|
String sourceContentId;
|
||||||
|
byte[] expected = new byte[] { 1, 2, 3 };
|
||||||
|
try (FilesystemPkiStore source = new FilesystemPkiStore(root, options)) {
|
||||||
|
Credential credential = TestObjects.minimalCredential(source, "SERIAL-SHARED-STATUS", "profile-ca");
|
||||||
|
source.putCredential(credential);
|
||||||
|
credentialId = credential.credentialId();
|
||||||
|
sourceContentId = credential.content().contentId();
|
||||||
|
StatusObject status = new StatusObject(statusId, credential.formatId(), credential.issuerRef().caId(),
|
||||||
|
StatusObjectType.CRL, Instant.EPOCH, Optional.empty(), credential.content(),
|
||||||
|
TestObjects.emptyAttributes());
|
||||||
|
source.putStatusObject(status);
|
||||||
|
source.exportSnapshot(snapshot, Instant.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
try (FilesystemPkiStore restored = new FilesystemPkiStore(snapshot, options)) {
|
||||||
|
Credential credential = restored.getCredential(credentialId).orElseThrow();
|
||||||
|
StatusObject status = restored.getStatusObject(statusId).orElseThrow();
|
||||||
|
assertFalse(sourceContentId.equals(credential.content().contentId()));
|
||||||
|
assertEquals(sourceContentId, status.content().contentId());
|
||||||
|
assertArrayEquals(expected, zeroecho.pki.testkit.PkiTestRuntime.readContent(restored, credential.content()));
|
||||||
|
assertArrayEquals(expected, zeroecho.pki.testkit.PkiTestRuntime.readContent(restored, status.content()));
|
||||||
|
}
|
||||||
|
assertTrue(Files.exists(snapshot.resolve("staged-content").resolve(sourceContentId + ".content")));
|
||||||
|
assertTrue(Files.exists(snapshot.resolve("staged-content").resolve(sourceContentId + ".meta")));
|
||||||
|
assertFalse(Files.exists(snapshot.resolve("staged-content").resolve(sourceContentId + ".owners")));
|
||||||
|
System.out.println("snapshotPreservesSharedStatusContentWithoutStaleCredentialOwner...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void snapshotBuildFailureCleansOwnedTemporaryRootAndPublishesNothing() throws Exception {
|
||||||
|
System.out.println("snapshotBuildFailureCleansOwnedTemporaryRootAndPublishesNothing");
|
||||||
|
Path root = tmp.resolve("store-snapshot-build-failure");
|
||||||
|
Path snapshot = tmp.resolve("snapshot-build-failure");
|
||||||
|
FsPkiStoreOptions options = nonStrictSnapshotOptions();
|
||||||
|
try (FilesystemPkiStore source = new FilesystemPkiStore(root, options)) {
|
||||||
|
Credential standalone = TestObjects.minimalCredential(source, "SERIAL-BUILD-FAIL", "profile-leaf");
|
||||||
|
source.putCredential(standalone);
|
||||||
|
Files.delete(root.resolve("SIGNING_TIME_WATERMARK"));
|
||||||
|
|
||||||
|
assertThrows(IllegalStateException.class, () -> source.exportSnapshot(snapshot, Instant.now()));
|
||||||
|
assertFalse(Files.exists(snapshot));
|
||||||
|
}
|
||||||
|
try (java.util.stream.Stream<Path> siblings = Files.list(tmp)) {
|
||||||
|
assertFalse(siblings.anyMatch(path -> path.getFileName().toString().startsWith(".zeroecho-snapshot-")));
|
||||||
|
}
|
||||||
|
System.out.println("snapshotBuildFailureCleansOwnedTemporaryRootAndPublishesNothing...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void snapshotRejectsExistingTargetWithoutChangingIt() throws Exception {
|
||||||
|
System.out.println("snapshotRejectsExistingTargetWithoutChangingIt");
|
||||||
|
Path root = tmp.resolve("store-snapshot-existing-target");
|
||||||
|
Path snapshot = tmp.resolve("snapshot-existing-target");
|
||||||
|
Path sentinel = snapshot.resolve("sentinel.txt");
|
||||||
|
Files.createDirectories(snapshot);
|
||||||
|
Files.writeString(sentinel, "owner data");
|
||||||
|
|
||||||
|
try (FilesystemPkiStore source = new FilesystemPkiStore(root, nonStrictSnapshotOptions())) {
|
||||||
|
assertThrows(IllegalStateException.class, () -> source.exportSnapshot(snapshot, Instant.now()));
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("owner data", Files.readString(sentinel));
|
||||||
|
try (java.util.stream.Stream<Path> siblings = Files.list(tmp)) {
|
||||||
|
assertFalse(siblings.anyMatch(path -> path.getFileName().toString().startsWith(".zeroecho-snapshot-")));
|
||||||
|
}
|
||||||
|
System.out.println("snapshotRejectsExistingTargetWithoutChangingIt...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void snapshotStrictFailsAndNonStrictOmitsOnlyInconsistentCa() throws Exception {
|
||||||
|
System.out.println("snapshotStrictFailsAndNonStrictOmitsOnlyInconsistentCa");
|
||||||
|
Path root = tmp.resolve("store-snapshot-inconsistent");
|
||||||
|
Path strictSnapshot = tmp.resolve("snapshot-inconsistent-strict");
|
||||||
|
Path nonStrictSnapshot = tmp.resolve("snapshot-inconsistent-nonstrict");
|
||||||
|
FsPkiStoreOptions nonStrict = nonStrictSnapshotOptions();
|
||||||
|
try (FilesystemPkiStore source = new FilesystemPkiStore(root, nonStrict)) {
|
||||||
|
CaRecord valid = TestObjects.minimalCaRecord(source, "ca-valid", CaState.ACTIVE);
|
||||||
|
CaRecord inconsistent = TestObjects.minimalCaRecord(source, "ca-inconsistent", CaState.ACTIVE);
|
||||||
|
source.putCa(valid);
|
||||||
|
source.putCa(inconsistent);
|
||||||
|
Files.delete(new FsPaths(root).credentialPath(inconsistent.credentialIds().get(0)));
|
||||||
|
|
||||||
|
FsPkiStoreOptions strict = strictSnapshotOptions();
|
||||||
|
assertThrows(IllegalStateException.class,
|
||||||
|
() -> new FsSnapshotExporter(strict).exportSnapshot(source, strictSnapshot, Instant.now()));
|
||||||
|
assertFalse(Files.exists(strictSnapshot));
|
||||||
|
|
||||||
|
source.exportSnapshot(nonStrictSnapshot, Instant.now());
|
||||||
|
}
|
||||||
|
try (FilesystemPkiStore restored = new FilesystemPkiStore(nonStrictSnapshot, nonStrict)) {
|
||||||
|
assertEquals(List.of("ca-valid"), restored.listCas().stream()
|
||||||
|
.map(ca -> ca.caId().value()).toList());
|
||||||
|
}
|
||||||
|
System.out.println("snapshotStrictFailsAndNonStrictOmitsOnlyInconsistentCa...ok");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void revocationJournalPersistsLegalTransitions() throws Exception {
|
void revocationJournalPersistsLegalTransitions() throws Exception {
|
||||||
System.out.println("revocationJournalPersistsLegalTransitions");
|
System.out.println("revocationJournalPersistsLegalTransitions");
|
||||||
@@ -553,9 +764,9 @@ public final class FilesystemPkiStoreTest {
|
|||||||
SubjectRef subjectRef = new SubjectRef("CN=" + caId);
|
SubjectRef subjectRef = new SubjectRef("CN=" + caId);
|
||||||
|
|
||||||
Credential cred = minimalCredential(store, "CA-" + caId, "profile-ca");
|
Credential cred = minimalCredential(store, "CA-" + caId, "profile-ca");
|
||||||
List<Credential> caCredentials = List.of(cred);
|
store.putCredential(cred);
|
||||||
|
return new CaRecord(id, CaKind.ROOT, state, issuerKeyRef, subjectRef,
|
||||||
return new CaRecord(id, CaKind.ROOT, state, issuerKeyRef, subjectRef, caCredentials);
|
List.of(cred.credentialId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
static CertificateProfile minimalProfile(String profileId) {
|
static CertificateProfile minimalProfile(String profileId) {
|
||||||
|
|||||||
@@ -59,12 +59,16 @@ import zeroecho.pki.api.EncodedObject;
|
|||||||
import zeroecho.pki.api.Encoding;
|
import zeroecho.pki.api.Encoding;
|
||||||
import zeroecho.pki.api.FormatId;
|
import zeroecho.pki.api.FormatId;
|
||||||
import zeroecho.pki.api.IssuerRef;
|
import zeroecho.pki.api.IssuerRef;
|
||||||
|
import zeroecho.pki.api.KeyRef;
|
||||||
import zeroecho.pki.api.PkiId;
|
import zeroecho.pki.api.PkiId;
|
||||||
import zeroecho.pki.api.SubjectRef;
|
import zeroecho.pki.api.SubjectRef;
|
||||||
import zeroecho.pki.api.Validity;
|
import zeroecho.pki.api.Validity;
|
||||||
import zeroecho.pki.api.attr.AttributeId;
|
import zeroecho.pki.api.attr.AttributeId;
|
||||||
import zeroecho.pki.api.attr.AttributeSet;
|
import zeroecho.pki.api.attr.AttributeSet;
|
||||||
import zeroecho.pki.api.attr.AttributeValue;
|
import zeroecho.pki.api.attr.AttributeValue;
|
||||||
|
import zeroecho.pki.api.ca.CaKind;
|
||||||
|
import zeroecho.pki.api.ca.CaRecord;
|
||||||
|
import zeroecho.pki.api.ca.CaState;
|
||||||
import zeroecho.pki.api.credential.CaProfileBinding;
|
import zeroecho.pki.api.credential.CaProfileBinding;
|
||||||
import zeroecho.pki.api.credential.Credential;
|
import zeroecho.pki.api.credential.Credential;
|
||||||
import zeroecho.pki.api.credential.CredentialProfileBinding;
|
import zeroecho.pki.api.credential.CredentialProfileBinding;
|
||||||
@@ -242,6 +246,30 @@ final class FsCodecTest {
|
|||||||
assertInvalid(encoded);
|
assertInvalid(encoded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void caRecordRoundTripsOnlyOrderedCredentialIdentifiersAndRejectsEmbeddedValueTag() {
|
||||||
|
System.out.println("caRecordRoundTripsOnlyOrderedCredentialIdentifiersAndRejectsEmbeddedValueTag");
|
||||||
|
PkiId first = new PkiId("credential-first");
|
||||||
|
PkiId second = new PkiId("credential-second");
|
||||||
|
CaRecord original = new CaRecord(new PkiId("ca-codec"), CaKind.ROOT, CaState.ACTIVE,
|
||||||
|
new KeyRef("key-codec"), new SubjectRef("CN=Codec"), List.of(first, second));
|
||||||
|
|
||||||
|
byte[] encoded = FsCodec.encode(FsCodec.CA_RECORD, original);
|
||||||
|
CaRecord decoded = FsCodec.decode(FsCodec.CA_RECORD, encoded);
|
||||||
|
assertEquals(original, decoded);
|
||||||
|
assertEquals(List.of(first, second), decoded.credentialIds());
|
||||||
|
|
||||||
|
byte[] embeddedValueTag = encoded.clone();
|
||||||
|
int firstIdentifier = indexOf(embeddedValueTag, first.value().getBytes(StandardCharsets.UTF_8));
|
||||||
|
assertTrue(firstIdentifier > 0);
|
||||||
|
int pkiIdTag = findPrevious(embeddedValueTag, firstIdentifier, (byte) 20);
|
||||||
|
assertTrue(pkiIdTag >= 0);
|
||||||
|
embeddedValueTag[pkiIdTag] = 34;
|
||||||
|
assertThrows(IllegalStateException.class,
|
||||||
|
() -> FsCodec.decode(FsCodec.CA_RECORD, embeddedValueTag));
|
||||||
|
System.out.println("caRecordRoundTripsOnlyOrderedCredentialIdentifiersAndRejectsEmbeddedValueTag...ok");
|
||||||
|
}
|
||||||
|
|
||||||
private static int indexOf(byte[] source, byte[] target) {
|
private static int indexOf(byte[] source, byte[] target) {
|
||||||
for (int index = 0; index <= source.length - target.length; index++) {
|
for (int index = 0; index <= source.length - target.length; index++) {
|
||||||
boolean matches = true;
|
boolean matches = true;
|
||||||
@@ -258,6 +286,15 @@ final class FsCodecTest {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int findPrevious(byte[] source, int startExclusive, byte value) {
|
||||||
|
for (int index = startExclusive - 1; index >= 0; index--) {
|
||||||
|
if (source[index] == value) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
private static ParsedCertificationRequest roundTripRequest(AttributeSet attributes) {
|
private static ParsedCertificationRequest roundTripRequest(AttributeSet attributes) {
|
||||||
byte[] encoded = FsCodec.encode(FsCodec.PARSED_REQUEST, request(attributes));
|
byte[] encoded = FsCodec.encode(FsCodec.PARSED_REQUEST, request(attributes));
|
||||||
return FsCodec.decode(FsCodec.PARSED_REQUEST, encoded);
|
return FsCodec.decode(FsCodec.PARSED_REQUEST, encoded);
|
||||||
|
|||||||
@@ -59,10 +59,12 @@ import zeroecho.pki.api.EncodedObject;
|
|||||||
import zeroecho.pki.api.Encoding;
|
import zeroecho.pki.api.Encoding;
|
||||||
import zeroecho.pki.api.IssuanceService;
|
import zeroecho.pki.api.IssuanceService;
|
||||||
import zeroecho.pki.api.KeyRef;
|
import zeroecho.pki.api.KeyRef;
|
||||||
|
import zeroecho.pki.api.PkiId;
|
||||||
import zeroecho.pki.api.ProfileService;
|
import zeroecho.pki.api.ProfileService;
|
||||||
import zeroecho.pki.api.RevocationService;
|
import zeroecho.pki.api.RevocationService;
|
||||||
import zeroecho.pki.api.StatusObjectService;
|
import zeroecho.pki.api.StatusObjectService;
|
||||||
import zeroecho.pki.api.content.DurableContentReference;
|
import zeroecho.pki.api.content.DurableContentReference;
|
||||||
|
import zeroecho.pki.api.credential.Credential;
|
||||||
import zeroecho.pki.api.credential.EffectiveCredentialStatusResolver;
|
import zeroecho.pki.api.credential.EffectiveCredentialStatusResolver;
|
||||||
import zeroecho.pki.api.profile.BuiltInCertificateProfileCatalog;
|
import zeroecho.pki.api.profile.BuiltInCertificateProfileCatalog;
|
||||||
import zeroecho.pki.impl.audit.InMemoryAuditSink;
|
import zeroecho.pki.impl.audit.InMemoryAuditSink;
|
||||||
@@ -206,6 +208,11 @@ public final class PkiTestRuntime implements AutoCloseable {
|
|||||||
return readContent(store, credential.content());
|
return readContent(store, credential.content());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Credential caCredential(zeroecho.pki.api.ca.CaRecord ca, int index) {
|
||||||
|
PkiId credentialId = ca.credentialIds().get(index);
|
||||||
|
return store.getCredential(credentialId).orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
private record UntrustedReference(String storeId, String contentId, Encoding encoding, long length, String sha256,
|
private record UntrustedReference(String storeId, String contentId, Encoding encoding, long length, String sha256,
|
||||||
DurableContentReference.Lifecycle lifecycle) implements DurableContentReference {
|
DurableContentReference.Lifecycle lifecycle) implements DurableContentReference {
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user