feat(pki): add typed CLI operation foundation

Add a reusable PKI session and typed synchronous operation executor
shared by direct CLI commands and versioned sequential batch plans.

Provide deterministic references, structured output, failure policies and
safe lifecycle handling without introducing a scripting language or runtime.
This commit is contained in:
2026-08-04 01:13:49 +02:00
parent 64af4519f0
commit 3de6cd7a34
34 changed files with 3779 additions and 17 deletions

View File

@@ -0,0 +1,269 @@
/*******************************************************************************
* 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.application;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import zeroecho.core.io.CancellationSignal;
import zeroecho.pki.api.PkiException;
import zeroecho.pki.api.RevocationService;
import zeroecho.pki.api.credential.CaProfileBinding;
import zeroecho.pki.api.credential.Credential;
import zeroecho.pki.api.credential.EndEntityProfileBinding;
import zeroecho.pki.api.profile.CertificateProfileDefinition;
import zeroecho.pki.api.profile.CertificateProfileDocumentCodec;
import zeroecho.pki.api.profile.CertificateProfileRef;
import zeroecho.pki.api.publication.PublicationRecord;
import zeroecho.pki.api.revocation.RevocationCommand;
import zeroecho.pki.api.revocation.RevocationRecord;
import zeroecho.pki.api.revocation.RevocationTransition;
import zeroecho.pki.impl.core.attr.SimpleAttributeSet;
import zeroecho.pki.spi.store.PkiStore;
import zeroecho.pki.spi.store.RevocationHistory;
/** Explicit non-reflective executor used by one backend session. */
final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
private final PkiSessionConfiguration configuration;
private final PkiStore store;
private final RevocationService revocations;
private final Runnable openCheck;
/* default */ DefaultPkiOperationExecutor(PkiSessionConfiguration configuration, PkiStore store,
RevocationService revocations, Runnable openCheck) {
this.configuration = Objects.requireNonNull(configuration, "configuration");
this.store = Objects.requireNonNull(store, "store");
this.revocations = Objects.requireNonNull(revocations, "revocations");
this.openCheck = Objects.requireNonNull(openCheck, "openCheck");
}
@Override
public PkiOperationOutcome execute(PkiOperation operation, CancellationSignal cancellation) {
openCheck.run();
PkiOperation exact = Objects.requireNonNull(operation, "operation");
CancellationSignal signal = Objects.requireNonNull(cancellation, "cancellation");
try {
signal.throwIfCancelled();
PkiOperationResult result = switch (exact) {
case PkiOperation.ValidateConfiguration ignored -> validateConfiguration();
case PkiOperation.ValidateProfile request -> validateProfile(request);
case PkiOperation.InspectCredential request -> inspectCredential(request);
case PkiOperation.RevokeCredential request -> revokeCredential(request);
case PkiOperation.ReadRevocationHistory request -> readHistory(request, signal);
case PkiOperation.InspectPublication request -> inspectPublication(request);
};
return new PkiOperationOutcome.Success(result);
} catch (InterruptedIOException failure) {
return failure(exact.name(), PkiOperationFailure.CANCELLED, "OPERATION_CANCELLED");
} catch (IOException failure) {
return failure(exact.name(), PkiOperationFailure.RESOURCE_FAILURE, "OPERATION_RESOURCE_FAILED");
} catch (RuntimeException failure) { // NOPMD - normalize provider failures at the application boundary.
return classify(exact.name(), failure);
}
}
private PkiOperationResult validateConfiguration() {
Map<String, PkiOperationValue> fields = fields();
fields.put("version", integer(configuration.version()));
fields.put("storeProvider", text(configuration.store().backendId()));
fields.put("auditProvider", text(configuration.audit().backendId()));
return result(PkiOperation.ValidateConfiguration.NAME, fields);
}
private static PkiOperationResult validateProfile(PkiOperation.ValidateProfile request) {
CertificateProfileDefinition definition = CertificateProfileDocumentCodec.parse(request.document());
Map<String, PkiOperationValue> fields = fields();
fields.put("profileId", text(definition.profileId()));
fields.put("profileVersion", integer(definition.profileVersion()));
fields.put("certificateType", text(definition.certificateType().name()));
fields.put("formatId", text(definition.formatId().value()));
return result(request.name(), fields);
}
private PkiOperationResult inspectCredential(PkiOperation.InspectCredential request) {
Credential credential = store.getCredential(request.credentialId())
.orElseThrow(() -> new MissingObjectException());
CertificateProfileRef profile = profileReference(credential);
Map<String, PkiOperationValue> fields = fields();
fields.put("credentialId", text(credential.credentialId().value()));
fields.put("formatId", text(credential.formatId().value()));
fields.put("issuerId", text(credential.issuerRef().caId().value()));
fields.put("publicKeyId", text(credential.publicKeyId().value()));
fields.put("profileId", text(profile.profileId()));
fields.put("profileVersion", integer(profile.profileVersion()));
fields.put("status", text(credential.status().name()));
fields.put("notBefore", text(credential.validity().notBefore().toString()));
fields.put("notAfter", text(credential.validity().notAfter().toString()));
return result(request.name(), fields);
}
private PkiOperationResult revokeCredential(PkiOperation.RevokeCredential request) {
RevocationCommand.RevokePermanently command = new RevocationCommand.RevokePermanently(request.credentialId(),
request.reason(), new SimpleAttributeSet());
RevocationRecord record = revocations.revokePermanently(command);
return revocationResult(request.name(), record);
}
private PkiOperationResult readHistory(PkiOperation.ReadRevocationHistory request, CancellationSignal signal)
throws IOException {
List<PkiOperationValue> transitions = new ArrayList<>();
boolean truncated;
try (RevocationHistory history = revocations.history(request.credentialId())) {
while (transitions.size() < request.limit() && history.next(signal)) {
transitions.add(transitionValue(history.current()));
}
truncated = transitions.size() == request.limit() && history.next(signal);
}
Map<String, PkiOperationValue> fields = fields();
fields.put("credentialId", text(request.credentialId().value()));
fields.put("count", integer(transitions.size()));
fields.put("truncated", bool(truncated));
fields.put("transitions", new PkiOperationValue.ListValue(transitions));
return result(request.name(), fields);
}
private PkiOperationResult inspectPublication(PkiOperation.InspectPublication request) {
PublicationRecord record = store.getPublicationRecord(request.publicationId())
.orElseThrow(() -> new MissingObjectException());
Map<String, PkiOperationValue> fields = fields();
fields.put("publicationId", text(record.publicationId().value()));
fields.put("sourceType", text(record.sourceType().name()));
fields.put("sourceId", text(record.sourceId().value()));
fields.put("status", text(record.status().name()));
fields.put("attemptNumber", integer(record.attemptNumber()));
fields.put("createdAt", text(record.createdAt().toString()));
fields.put("updatedAt", text(record.updatedAt().toString()));
record.failure().ifPresent(value -> fields.put("failure", text(value.name())));
record.evidence().ifPresent(value -> fields.put("evidence", text(value.name())));
return result(request.name(), fields);
}
private static PkiOperationResult revocationResult(String operation, RevocationRecord record) {
Map<String, PkiOperationValue> fields = fields();
fields.put("credentialId", text(record.credentialId().value()));
appendTransition(fields, record.transition());
return result(operation, fields);
}
private static PkiOperationValue transitionValue(RevocationTransition transition) {
Map<String, PkiOperationValue> fields = fields();
appendTransition(fields, transition);
return new PkiOperationValue.ObjectValue(fields);
}
private static void appendTransition(Map<String, PkiOperationValue> fields, RevocationTransition transition) {
fields.put("revision", integer(transition.revision()));
fields.put("state", text(transition.state().name()));
fields.put("time", text(transition.time().toString()));
transition.permanentReason().ifPresent(reason -> fields.put("reason", text(reason.name())));
}
private static CertificateProfileRef profileReference(Credential credential) {
if (credential.profileBinding() instanceof EndEntityProfileBinding binding) {
return binding.reference();
}
if (credential.profileBinding() instanceof CaProfileBinding binding) {
return binding.reference();
}
throw new IllegalStateException("Credential profile binding is unsupported");
}
private static PkiOperationOutcome classify(String operation, RuntimeException failure) {
if (failure instanceof MissingObjectException) {
return failure(operation, PkiOperationFailure.NOT_FOUND, "OBJECT_NOT_FOUND");
}
String message = Optional.ofNullable(failure.getMessage()).orElse("");
if (message.contains("RECOVERY_REQUIRED") || message.contains("DURABILITY_UNCONFIRMED")) {
return failure(operation, PkiOperationFailure.RECOVERY_REQUIRED, "RECOVERY_REQUIRED");
}
if (message.contains("OUTCOME_UNKNOWN") || message.contains("RESULT_UNCONFIRMED")) {
return failure(operation, PkiOperationFailure.EXTERNAL_OUTCOME_UNKNOWN, "EXTERNAL_OUTCOME_UNKNOWN");
}
if (message.contains("CREDENTIAL_NOT_FOUND")) {
return failure(operation, PkiOperationFailure.NOT_FOUND, "OBJECT_NOT_FOUND");
}
if (message.contains("CONFLICT") || message.contains("changed concurrently")) {
return failure(operation, PkiOperationFailure.CONFLICT, "OPERATION_CONFLICT");
}
if (message.contains("TRANSITION_ILLEGAL") || message.contains("REVOCATION_TERMINAL")) {
return failure(operation, PkiOperationFailure.POLICY_REJECTION, "PKI_POLICY_REJECTED");
}
if (failure instanceof IllegalArgumentException) {
return failure(operation, PkiOperationFailure.VALIDATION_FAILURE, "OPERATION_INPUT_INVALID");
}
if (failure instanceof PkiException) {
return failure(operation, PkiOperationFailure.POLICY_REJECTION, "PKI_POLICY_REJECTED");
}
if (failure instanceof IllegalStateException) {
return failure(operation, PkiOperationFailure.RESOURCE_FAILURE, "PKI_RESOURCE_FAILED");
}
return failure(operation, PkiOperationFailure.INTERNAL_FAILURE, "INTERNAL_FAILURE");
}
private static PkiOperationOutcome failure(String operation, PkiOperationFailure classification, String code) {
return new PkiOperationOutcome.Failure(operation, classification, code);
}
private static PkiOperationResult result(String operation, Map<String, PkiOperationValue> fields) {
return new PkiOperationResult(operation, fields);
}
private static Map<String, PkiOperationValue> fields() {
return new LinkedHashMap<>();
}
private static PkiOperationValue text(String value) {
return new PkiOperationValue.Text(value);
}
private static PkiOperationValue integer(long value) {
return new PkiOperationValue.IntegerValue(value);
}
private static PkiOperationValue bool(boolean value) {
return new PkiOperationValue.BooleanValue(value);
}
/** Internal marker normalized to the public NOT_FOUND classification. */
private static final class MissingObjectException extends RuntimeException {
private static final long serialVersionUID = 1L;
}
}

View File

@@ -0,0 +1,209 @@
/*******************************************************************************
* 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.application;
import java.time.Clock;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import zeroecho.pki.api.ProfileService;
import zeroecho.pki.api.RevocationService;
import zeroecho.pki.impl.core.DefaultProfileService;
import zeroecho.pki.impl.core.DefaultRevocationService;
import zeroecho.pki.spi.ProviderConfig;
import zeroecho.pki.spi.audit.AuditSink;
import zeroecho.pki.spi.bootstrap.PkiBootstrap;
import zeroecho.pki.spi.store.PkiStore;
/** Default synchronous session composition. */
final class DefaultPkiSession implements PkiSession {
private final PkiStore store;
private final AuditSink audit;
private final ProfileService profiles;
private final RevocationService revocations;
private final PkiOperationExecutor operations;
private final AtomicBoolean closed = new AtomicBoolean();
private DefaultPkiSession(PkiSessionConfiguration configuration, PkiStore store, AuditSink audit, Clock clock) {
this.store = store;
this.audit = audit;
this.profiles = new DefaultProfileService(store, clock, audit);
this.revocations = new DefaultRevocationService(store, clock, audit);
this.operations = new DefaultPkiOperationExecutor(configuration, store, revocations,
this::requireOpen);
}
/* default */ static PkiSession open(PkiSessionConfiguration configuration) {
return open(configuration, Clock.systemUTC(), ProductionBootstrap.INSTANCE);
}
@SuppressWarnings("PMD.AvoidCatchingGenericException")
/* default */ static PkiSession open(PkiSessionConfiguration configuration, Clock clock, Bootstrap bootstrap) {
PkiSessionConfiguration exact = Objects.requireNonNull(configuration, "configuration");
Objects.requireNonNull(clock, "clock");
Objects.requireNonNull(bootstrap, "bootstrap");
bootstrap.validateStore(exact.store());
bootstrap.validateAudit(exact.audit());
PkiStore store = null;
AuditSink audit = null;
try {
store = Objects.requireNonNull(bootstrap.openStore(exact.store()), "opened store");
audit = Objects.requireNonNull(bootstrap.openAudit(exact.audit()), "opened audit sink");
return new DefaultPkiSession(exact, store, audit, clock);
} catch (RuntimeException | Error primary) {
closeAfterConstructionFailure(audit, store, primary);
throw primary;
}
}
@Override
public ProfileService profiles() {
requireOpen();
return profiles;
}
@Override
public RevocationService revocations() {
requireOpen();
return revocations;
}
@Override
public PkiOperationExecutor operations() {
requireOpen();
return operations;
}
@Override
public void close() throws Exception {
if (!closed.compareAndSet(false, true)) {
return;
}
Throwable primary = null;
try {
audit.close();
} catch (Throwable failure) { // NOPMD - preserve Error and checked close failures.
primary = failure;
}
try {
store.close();
} catch (Throwable failure) { // NOPMD - preserve Error and checked close failures.
if (primary == null) {
primary = failure;
} else {
primary.addSuppressed(failure);
}
}
rethrow(primary);
}
private void requireOpen() {
if (closed.get()) {
throw new IllegalStateException("PKI session is closed");
}
}
private static void closeAfterConstructionFailure(AuditSink audit, PkiStore store, Throwable primary) {
if (audit != null) {
try {
audit.close();
} catch (Throwable failure) { // NOPMD - construction failure remains primary.
primary.addSuppressed(failure);
}
}
if (store != null) {
try {
store.close();
} catch (Throwable failure) { // NOPMD - construction failure remains primary.
primary.addSuppressed(failure);
}
}
}
@SuppressWarnings("PMD.SignatureDeclareThrowsException")
private static void rethrow(Throwable failure) throws Exception {
if (failure == null) {
return;
}
if (failure instanceof Exception exception) {
throw exception;
}
if (failure instanceof Error error) {
throw error;
}
throw new IllegalStateException("PKI session close failed", failure);
}
/** Resource bootstrap seam used for deterministic lifecycle verification. */
/* default */ interface Bootstrap {
/** Validates store configuration without allocating a store. */
void validateStore(ProviderConfig configuration);
/** Validates audit configuration without allocating a sink. */
void validateAudit(ProviderConfig configuration);
/** Opens the configured store. */
PkiStore openStore(ProviderConfig configuration);
/** Opens the configured audit sink. */
AuditSink openAudit(ProviderConfig configuration);
}
/** Production provider bootstrap implementation. */
private enum ProductionBootstrap implements Bootstrap {
INSTANCE;
@Override
public void validateStore(ProviderConfig configuration) {
PkiBootstrap.validateStoreConfiguration(configuration);
}
@Override
public void validateAudit(ProviderConfig configuration) {
PkiBootstrap.validateAuditConfiguration(configuration);
}
@Override
public PkiStore openStore(ProviderConfig configuration) {
return PkiBootstrap.openStore(configuration);
}
@Override
public AuditSink openAudit(ProviderConfig configuration) {
return PkiBootstrap.openAudit(configuration);
}
}
}

View File

@@ -0,0 +1,158 @@
/*******************************************************************************
* 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.application;
import java.util.Objects;
import zeroecho.pki.api.PkiId;
import zeroecho.pki.api.revocation.RevocationReason;
/**
* Closed set of typed synchronous PKI operations available to transports.
*
* <p>Names are stable external semantic identities. Implementations dispatch by
* explicit type and never by reflection or Java class name.</p>
*/
public sealed interface PkiOperation permits PkiOperation.ValidateConfiguration, PkiOperation.ValidateProfile,
PkiOperation.InspectCredential, PkiOperation.RevokeCredential, PkiOperation.ReadRevocationHistory,
PkiOperation.InspectPublication {
/** @return stable semantic operation name */
String name();
/** Validates the already-open session configuration. */
record ValidateConfiguration() implements PkiOperation {
/** Stable operation name. */
public static final String NAME = "configuration.validate";
@Override
public String name() {
return NAME;
}
}
/** Validates one bounded certificate-profile JSON document without persisting it. */
record ValidateProfile(byte[] document) implements PkiOperation {
/** Stable operation name. */
public static final String NAME = "profile.validate";
/** Defensively snapshots the document. */
public ValidateProfile {
document = Objects.requireNonNull(document, "document").clone();
}
@Override
public byte[] document() {
return document.clone();
}
@Override
public String name() {
return NAME;
}
}
/** Reads safe metadata for one committed credential. */
record InspectCredential(PkiId credentialId) implements PkiOperation {
/** Stable operation name. */
public static final String NAME = "credential.inspect";
/** Validates the identifier. */
public InspectCredential {
Objects.requireNonNull(credentialId, "credentialId");
}
@Override
public String name() {
return NAME;
}
}
/** Permanently revokes one committed credential. */
record RevokeCredential(PkiId credentialId, RevocationReason reason) implements PkiOperation {
/** Stable operation name. */
public static final String NAME = "credential.revoke";
/** Validates permanent-revocation inputs. */
public RevokeCredential {
Objects.requireNonNull(credentialId, "credentialId");
Objects.requireNonNull(reason, "reason");
if (reason == RevocationReason.CERTIFICATE_HOLD || reason == RevocationReason.REMOVE_FROM_CRL) {
throw new IllegalArgumentException("reason must be permanent");
}
}
@Override
public String name() {
return NAME;
}
}
/** Reads a bounded prefix from one closeable authoritative history cursor. */
record ReadRevocationHistory(PkiId credentialId, int limit) implements PkiOperation {
/** Stable operation name. */
public static final String NAME = "revocation.history";
/** Maximum finite result entries for one invocation. */
public static final int MAXIMUM_RESULT_ENTRIES = 1_000;
/** Validates the identifier and finite result limit. */
public ReadRevocationHistory {
Objects.requireNonNull(credentialId, "credentialId");
if (limit < 1 || limit > MAXIMUM_RESULT_ENTRIES) {
throw new IllegalArgumentException("limit must be between 1 and 1000");
}
}
@Override
public String name() {
return NAME;
}
}
/** Reads safe durable state for one publication operation. */
record InspectPublication(PkiId publicationId) implements PkiOperation {
/** Stable operation name. */
public static final String NAME = "publication.inspect";
/** Validates the identifier. */
public InspectPublication {
Objects.requireNonNull(publicationId, "publicationId");
}
@Override
public String name() {
return NAME;
}
}
}

View File

@@ -0,0 +1,57 @@
/*******************************************************************************
* 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.application;
import zeroecho.core.io.CancellationSignal;
/**
* Synchronous transport-neutral executor for the closed PKI operation model.
*
* <p>Implementations execute on the caller thread, perform no hidden retry and
* create no thread or executor. A returned cancellation or failure never implies
* rollback of an already committed backend transition.</p>
*/
@FunctionalInterface
public interface PkiOperationExecutor {
/**
* Executes one typed operation synchronously.
*
* @param operation validated typed operation
* @param cancellation cooperative cancellation signal
* @return successful result or safely classified failure
* @throws IllegalStateException if the owning session is closed
*/
PkiOperationOutcome execute(PkiOperation operation, CancellationSignal cancellation);
}

View File

@@ -0,0 +1,56 @@
/*******************************************************************************
* 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.application;
/** Stable safe classifications for synchronous PKI operation failures. */
public enum PkiOperationFailure {
/** Typed input or operation precondition is malformed. */
VALIDATION_FAILURE,
/** A valid request is rejected by PKI policy. */
POLICY_REJECTION,
/** The selected authoritative object does not exist. */
NOT_FOUND,
/** The requested state conflicts with committed state. */
CONFLICT,
/** Durable recovery is required before the operation can continue. */
RECOVERY_REQUIRED,
/** An external operation may have completed and must be reconciled. */
EXTERNAL_OUTCOME_UNKNOWN,
/** Filesystem, channel or another required resource failed. */
RESOURCE_FAILURE,
/** Cooperative cancellation was observed. */
CANCELLED,
/** An unexpected implementation defect prevented a safe result. */
INTERNAL_FAILURE
}

View File

@@ -0,0 +1,60 @@
/*******************************************************************************
* 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.application;
import java.util.Objects;
/** Closed success-or-safe-failure outcome returned by the operation executor. */
public sealed interface PkiOperationOutcome permits PkiOperationOutcome.Success, PkiOperationOutcome.Failure {
/** Successful operation outcome. */
record Success(PkiOperationResult result) implements PkiOperationOutcome {
/** Validates the result. */
public Success {
Objects.requireNonNull(result, "result");
}
}
/** Safely classified operation failure without a throwable graph. */
record Failure(String operationName, PkiOperationFailure classification, String code)
implements PkiOperationOutcome {
/** Validates stable, non-sensitive failure data. */
public Failure {
if (operationName == null || operationName.isBlank() || code == null || !code.matches("[A-Z0-9_]{3,64}")) {
throw new IllegalArgumentException("Operation failure identity or code is invalid");
}
Objects.requireNonNull(classification, "classification");
}
}
}

View File

@@ -0,0 +1,71 @@
/*******************************************************************************
* 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.application;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
/**
* Finite structured result of one successful typed PKI operation.
*
* @param operationName stable operation identity
* @param fields ordered allowlisted result fields
*/
public record PkiOperationResult(String operationName, Map<String, PkiOperationValue> fields) {
/** Validates and snapshots the result. */
public PkiOperationResult {
if (operationName == null || operationName.isBlank()) {
throw new IllegalArgumentException("operationName must not be blank");
}
Objects.requireNonNull(fields, "fields");
fields = Collections.unmodifiableMap(new LinkedHashMap<>(fields));
if (fields.containsKey(null) || fields.containsValue(null)) {
throw new IllegalArgumentException("result fields must not contain null");
}
}
/**
* Returns one declared safe field.
*
* @param name field name
* @return field value when declared
*/
public Optional<PkiOperationValue> field(String name) {
return Optional.ofNullable(fields.get(Objects.requireNonNull(name, "name")));
}
}

View File

@@ -0,0 +1,94 @@
/*******************************************************************************
* 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.application;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Closed transport-neutral value model used by safe operation results and plan
* arguments.
*
* <p>The model deliberately excludes byte arrays and arbitrary Java objects, so
* private keys, unrestricted DER and provider exception graphs cannot become
* operation output accidentally.</p>
*/
public sealed interface PkiOperationValue permits PkiOperationValue.Text, PkiOperationValue.IntegerValue,
PkiOperationValue.BooleanValue, PkiOperationValue.ObjectValue, PkiOperationValue.ListValue {
/** Text scalar. */
record Text(String value) implements PkiOperationValue {
/** Validates the value. */
public Text {
Objects.requireNonNull(value, "value");
}
}
/** Signed 64-bit integer scalar. */
record IntegerValue(long value) implements PkiOperationValue {
}
/** Boolean scalar. */
record BooleanValue(boolean value) implements PkiOperationValue {
}
/** Deterministically ordered object value. */
record ObjectValue(Map<String, PkiOperationValue> fields) implements PkiOperationValue {
/** Validates names and snapshots insertion order. */
public ObjectValue {
Objects.requireNonNull(fields, "fields");
Map<String, PkiOperationValue> copy = new LinkedHashMap<>();
for (Map.Entry<String, PkiOperationValue> entry : fields.entrySet()) {
String name = Objects.requireNonNull(entry.getKey(), "field name");
if (name.isBlank() || copy.putIfAbsent(name, Objects.requireNonNull(entry.getValue(), "field value"))
!= null) {
throw new IllegalArgumentException("Operation object field names must be unique and nonblank");
}
}
fields = Collections.unmodifiableMap(copy);
}
}
/** Immutable ordered list value. */
record ListValue(List<PkiOperationValue> values) implements PkiOperationValue {
/** Snapshots the list and rejects null entries. */
public ListValue {
Objects.requireNonNull(values, "values");
values = List.copyOf(values);
}
}
}

View File

@@ -0,0 +1,78 @@
/*******************************************************************************
* 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.application;
import zeroecho.pki.api.ProfileService;
import zeroecho.pki.api.RevocationService;
/**
* Lifecycle-owned synchronous PKI backend session.
*
* <p>A session is constructed from one validated immutable configuration and is
* suitable for one CLI plan or for the complete lifetime of a future server.
* It creates no scheduler, thread or executor. Implementations reject service
* access after close.</p>
*/
public interface PkiSession extends AutoCloseable {
/**
* Opens a production session.
*
* @param configuration validated immutable configuration
* @return opened session
* @throws IllegalArgumentException if configuration is invalid
* @throws RuntimeException if resource construction fails
*/
static PkiSession open(PkiSessionConfiguration configuration) {
return DefaultPkiSession.open(configuration);
}
/** @return profile lifecycle service owned by this session */
ProfileService profiles();
/** @return revocation service owned by this session */
RevocationService revocations();
/** @return shared typed operation executor owned by this session */
PkiOperationExecutor operations();
/**
* Closes services and backend resources in reverse construction order.
* Repeated calls are harmless; primary and suppressed failures are preserved.
*
* @throws Exception if resource closure fails
*/
@Override
void close() throws Exception;
}

View File

@@ -0,0 +1,68 @@
/*******************************************************************************
* 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.application;
import java.util.Objects;
import zeroecho.pki.spi.ProviderConfig;
/**
* Immutable, versioned configuration for one synchronous PKI backend session.
*
* <p>Provider values may be sensitive. Callers must not render or log this
* object. Validation of provider identities and closed property sets occurs
* before backend allocation.</p>
*
* @param version configuration schema version
* @param store store-provider configuration
* @param audit audit-provider configuration
*/
public record PkiSessionConfiguration(int version, ProviderConfig store, ProviderConfig audit) {
/** Current configuration schema version. */
public static final int CURRENT_VERSION = 1;
/** Validates and snapshots the configuration. */
public PkiSessionConfiguration {
if (version != CURRENT_VERSION) {
throw new IllegalArgumentException("Unsupported PKI session configuration version");
}
store = snapshot(Objects.requireNonNull(store, "store"));
audit = snapshot(Objects.requireNonNull(audit, "audit"));
}
private static ProviderConfig snapshot(ProviderConfig config) {
return new ProviderConfig(config.backendId(), config.properties());
}
}

View File

@@ -0,0 +1,36 @@
/*******************************************************************************
* 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.
******************************************************************************/
/** Lifecycle-owned synchronous PKI sessions and transport-neutral typed operations. */
package zeroecho.pki.application;

View File

@@ -33,7 +33,6 @@
******************************************************************************/
package zeroecho.pki.spi;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -76,7 +75,7 @@ public record ProviderConfig(String backendId, Map<String, String> properties) {
if (backendId.isBlank()) {
throw new IllegalArgumentException("backendId must not be blank");
}
properties = Collections.unmodifiableMap(properties);
properties = Map.copyOf(properties);
}
/**

View File

@@ -44,7 +44,7 @@ import zeroecho.pki.api.audit.AuditEvent;
* </p>
*/
@FunctionalInterface
public interface AuditSink {
public interface AuditSink extends AutoCloseable {
/**
* Persists an audit event.
@@ -54,4 +54,16 @@ public interface AuditSink {
* @throws RuntimeException if persistence fails
*/
void record(AuditEvent event);
/**
* Releases lifecycle-owned sink resources.
*
* <p>The default implementation owns no closeable resource. Stateful
* providers may override this method; repeated close calls should be
* harmless.</p>
*/
@Override
default void close() {
// Most current sinks open resources only for the duration of record(...).
}
}

View File

@@ -140,27 +140,47 @@ public final class PkiBootstrap {
*/
public static PkiStore openStore() {
String requestedId = System.getProperty(PROP_STORE_BACKEND);
PkiStoreProvider provider = SpiSelector.select(PkiStoreProvider.class, requestedId,
new SpiSelector.ProviderId<>() {
@Override
public String id(PkiStoreProvider p) {
return p.id();
}
});
Map<String, String> props = SpiSystemProperties.readPrefixed(PROP_STORE_PREFIX);
PkiStoreProvider provider = selectStoreProvider(requestedId);
if ("fs".equals(provider.id()) && !props.containsKey("root")) {
props.put("root", Path.of("pki-store").toString());
}
ProviderConfig config = new ProviderConfig(provider.id(), props);
if (LOG.isLoggable(Level.INFO)) {
LOG.info("Selected store provider: " + provider.id() + " (keys: " + props.keySet() + ")");
}
return provider.allocate(config);
}
/**
* Validates an explicit store configuration without allocating a store.
*
* <p>This entry point is intended for lifecycle composition that must reject
* every invalid configuration before exposing or allocating backend
* resources. Unknown provider properties are rejected because an explicit
* application configuration is versioned and closed.</p>
*
* @param config immutable store-provider configuration
* @throws NullPointerException if {@code config} is {@code null}
* @throws IllegalArgumentException if the provider or configuration is invalid
*/
public static void validateStoreConfiguration(ProviderConfig config) {
PkiStoreProvider provider = selectStoreProvider(Objects.requireNonNull(config, "config").backendId());
requireKnownKeys(provider, config);
provider.validateConfig(config);
}
/**
* Opens a store from an explicit, strictly validated configuration.
*
* @param config immutable store-provider configuration
* @return opened store owned by the caller
* @throws IllegalArgumentException if the configuration is invalid
* @throws RuntimeException if store allocation fails
*/
public static PkiStore openStore(ProviderConfig config) {
validateStoreConfiguration(config);
PkiStoreProvider provider = selectStoreProvider(config.backendId());
return provider.allocate(config);
}
@@ -200,6 +220,32 @@ public final class PkiBootstrap {
return provider.allocate(config);
}
/**
* Validates an explicit audit configuration without allocating a sink.
*
* @param config immutable audit-provider configuration
* @throws NullPointerException if {@code config} is {@code null}
* @throws IllegalArgumentException if the provider or configuration is invalid
*/
public static void validateAuditConfiguration(ProviderConfig config) {
AuditSinkProvider provider = selectAuditProvider(Objects.requireNonNull(config, "config").backendId());
requireKnownKeys(provider, config);
provider.validateConfig(config);
}
/**
* Opens an audit sink from an explicit, strictly validated configuration.
*
* @param config immutable audit-provider configuration
* @return configured audit sink
* @throws IllegalArgumentException if the configuration is invalid
* @throws RuntimeException if sink allocation fails
*/
public static AuditSink openAudit(ProviderConfig config) {
validateAuditConfiguration(config);
return selectAuditProvider(config.backendId()).allocate(config);
}
/**
* Opens a {@link SignatureWorkflow} using {@link SignatureWorkflowProvider}
* discovered via ServiceLoader.
@@ -349,4 +395,30 @@ public final class PkiBootstrap {
LOG.fine("Provider '" + provider.id() + "' supports keys: " + provider.supportedKeys());
}
}
private static PkiStoreProvider selectStoreProvider(String requestedId) {
return SpiSelector.select(PkiStoreProvider.class, requestedId, new SpiSelector.ProviderId<>() {
@Override
public String id(PkiStoreProvider provider) {
return provider.id();
}
});
}
private static AuditSinkProvider selectAuditProvider(String requestedId) {
return SpiSelector.select(AuditSinkProvider.class, requestedId, new SpiSelector.ProviderId<>() {
@Override
public String id(AuditSinkProvider provider) {
return provider.id();
}
});
}
private static void requireKnownKeys(ConfigurableProvider<?> provider, ProviderConfig config) {
for (String key : config.properties().keySet()) {
if (!provider.supportedKeys().contains(key)) {
throw new IllegalArgumentException("Unknown provider configuration key: " + key);
}
}
}
}

View File

@@ -90,7 +90,7 @@ import zeroecho.pki.api.status.StatusObject;
* {@link IllegalStateException} when an operation cannot be completed safely.
* </p>
*/
public interface PkiStore extends SignWorkflowStore {
public interface PkiStore extends SignWorkflowStore, AutoCloseable {
/**
* Returns the runtime-owned durable streaming-content store.
@@ -430,4 +430,17 @@ public interface PkiStore extends SignWorkflowStore {
* @throws IllegalStateException if listing fails
*/
List<WorkflowStateRecord> listWorkflowStates();
/**
* Closes this store and releases all lifecycle-owned resources.
*
* <p>After this method returns, implementations must reject new operations.
* Repeated close calls must follow the implementation's documented idempotence
* contract. A caller that coordinates multiple resources must preserve the
* primary close failure and attach later failures as suppressed exceptions.</p>
*
* @throws Exception if a lifecycle-owned resource cannot be closed
*/
@Override
void close() throws Exception;
}

View File

@@ -0,0 +1,206 @@
/*******************************************************************************
* 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.application;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.lang.reflect.Proxy;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
import zeroecho.core.io.CancellationSignal;
import zeroecho.pki.api.PkiId;
import zeroecho.pki.api.RevocationService;
import zeroecho.pki.api.profile.BuiltInCertificateProfileCatalog;
import zeroecho.pki.api.revocation.RevocationCommand;
import zeroecho.pki.api.revocation.RevocationQuery;
import zeroecho.pki.api.revocation.RevocationReason;
import zeroecho.pki.api.revocation.RevocationRecord;
import zeroecho.pki.api.revocation.RevocationState;
import zeroecho.pki.api.revocation.RevocationTransition;
import zeroecho.pki.impl.core.attr.SimpleAttributeSet;
import zeroecho.pki.spi.ProviderConfig;
import zeroecho.pki.spi.store.PkiStore;
import zeroecho.pki.spi.store.RevocationHistory;
import zeroecho.pki.spi.store.RevocationSnapshot;
class PkiOperationExecutorTest {
private static final PkiId CREDENTIAL_ID = new PkiId("credential:test");
@Test
void validatesConfigurationAndProfilesWithSafeTypedResults() {
System.out.println("validatesConfigurationAndProfilesWithSafeTypedResults");
DefaultPkiOperationExecutor executor = executor(new AtomicInteger());
PkiOperationOutcome configuration = executor.execute(new PkiOperation.ValidateConfiguration(),
CancellationSignal.NONE);
byte[] profile = BuiltInCertificateProfileCatalog.load(getClass().getClassLoader()).get(0).canonicalJson();
PkiOperationOutcome validated = executor.execute(new PkiOperation.ValidateProfile(profile),
CancellationSignal.NONE);
PkiOperationOutcome malformed = executor.execute(new PkiOperation.ValidateProfile(new byte[] { 1 }),
CancellationSignal.NONE);
System.out.println("...profileBytes=" + profile.length);
assertInstanceOf(PkiOperationOutcome.Success.class, configuration);
PkiOperationResult profileResult = ((PkiOperationOutcome.Success) validated).result();
assertTrue(profileResult.field("profileId").isPresent());
assertEquals(PkiOperationFailure.POLICY_REJECTION,
((PkiOperationOutcome.Failure) malformed).classification());
System.out.println("...ok");
}
@Test
void delegatesMutationAndStreamsBoundedHistory() {
System.out.println("delegatesMutationAndStreamsBoundedHistory");
AtomicInteger revocations = new AtomicInteger();
DefaultPkiOperationExecutor executor = executor(revocations);
PkiOperationOutcome mutation = executor.execute(
new PkiOperation.RevokeCredential(CREDENTIAL_ID, RevocationReason.KEY_COMPROMISE),
CancellationSignal.NONE);
PkiOperationOutcome history = executor.execute(new PkiOperation.ReadRevocationHistory(CREDENTIAL_ID, 1),
CancellationSignal.NONE);
System.out.println("...mutations=" + revocations.get());
assertInstanceOf(PkiOperationOutcome.Success.class, mutation);
PkiOperationResult historyResult = ((PkiOperationOutcome.Success) history).result();
assertEquals(new PkiOperationValue.BooleanValue(true), historyResult.field("truncated").orElseThrow());
assertEquals(1, revocations.get());
System.out.println("...ok");
}
@Test
void classifiesMissingObjectsAndCancellationWithoutThrowableDetails() {
System.out.println("classifiesMissingObjectsAndCancellationWithoutThrowableDetails");
DefaultPkiOperationExecutor executor = executor(new AtomicInteger());
PkiOperationOutcome missing = executor.execute(new PkiOperation.InspectPublication(new PkiId("missing")),
CancellationSignal.NONE);
PkiOperationOutcome cancelled = executor.execute(new PkiOperation.InspectCredential(CREDENTIAL_ID),
() -> true);
assertEquals(PkiOperationFailure.NOT_FOUND, ((PkiOperationOutcome.Failure) missing).classification());
PkiOperationOutcome.Failure cancelledFailure = (PkiOperationOutcome.Failure) cancelled;
assertEquals(PkiOperationFailure.CANCELLED, cancelledFailure.classification());
assertEquals("OPERATION_CANCELLED", cancelledFailure.code());
System.out.println("...ok");
}
private static DefaultPkiOperationExecutor executor(AtomicInteger mutations) {
PkiSessionConfiguration configuration = new PkiSessionConfiguration(1,
new ProviderConfig("fs", Map.of("root", "unused")), new ProviderConfig("memory", Map.of()));
PkiStore store = (PkiStore) Proxy.newProxyInstance(PkiStore.class.getClassLoader(),
new Class<?>[] { PkiStore.class }, (proxy, method, arguments) -> switch (method.getName()) {
case "getCredential", "getPublicationRecord" -> Optional.empty();
case "close" -> null;
default -> throw new UnsupportedOperationException(method.getName());
});
return new DefaultPkiOperationExecutor(configuration, store, revocations(mutations), () -> {
});
}
private static RevocationService revocations(AtomicInteger mutations) {
return new RevocationService() {
@Override
public RevocationRecord hold(RevocationCommand.Hold command) {
throw new UnsupportedOperationException();
}
@Override
public RevocationRecord unhold(RevocationCommand.Unhold command) {
throw new UnsupportedOperationException();
}
@Override
public RevocationRecord revokePermanently(RevocationCommand.RevokePermanently command) {
mutations.incrementAndGet();
return new RevocationRecord(command.credentialId(), transitions().get(1));
}
@Override
public Optional<RevocationRecord> get(PkiId credentialId) {
return Optional.empty();
}
@Override
public RevocationHistory history(PkiId credentialId) {
return historyCursor(credentialId);
}
@Override
public RevocationSnapshot search(RevocationQuery query) {
throw new UnsupportedOperationException();
}
};
}
private static RevocationHistory historyCursor(PkiId credentialId) {
return new RevocationHistory() {
private int index = -1;
@Override
public PkiId credentialId() {
return credentialId;
}
@Override
public boolean next(CancellationSignal cancellation) throws IOException {
cancellation.throwIfCancelled();
index++;
return index < transitions().size();
}
@Override
public RevocationTransition current() {
return transitions().get(index);
}
@Override
public void close() {
// no-op
}
};
}
private static List<RevocationTransition> transitions() {
return List.of(new RevocationTransition(1L, RevocationState.HELD, Instant.parse("2026-08-04T00:00:00Z"),
Optional.empty(), new SimpleAttributeSet()),
new RevocationTransition(2L, RevocationState.PERMANENTLY_REVOKED,
Instant.parse("2026-08-04T00:01:00Z"), Optional.of(RevocationReason.KEY_COMPROMISE),
new SimpleAttributeSet()));
}
}

View File

@@ -0,0 +1,190 @@
/*******************************************************************************
* 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.application;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.IOException;
import java.lang.reflect.Proxy;
import java.nio.file.Path;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import zeroecho.core.io.CancellationSignal;
import zeroecho.pki.spi.ProviderConfig;
import zeroecho.pki.spi.audit.AuditSink;
import zeroecho.pki.spi.store.PkiStore;
class PkiSessionLifecycleTest {
private static final Clock CLOCK = Clock.fixed(Instant.parse("2026-08-04T00:00:00Z"), ZoneOffset.UTC);
@TempDir
Path temporaryDirectory;
@Test
void opensReusesAndDeterministicallyClosesOneSession() throws Exception {
System.out.println("opensReusesAndDeterministicallyClosesOneSession");
PkiSessionConfiguration configuration = configuration(temporaryDirectory.resolve("store"));
PkiSession session = PkiSession.open(configuration);
PkiOperationOutcome first = session.operations().execute(new PkiOperation.ValidateConfiguration(),
CancellationSignal.NONE);
PkiOperationOutcome second = session.operations().execute(new PkiOperation.ValidateConfiguration(),
CancellationSignal.NONE);
System.out.println("...executions=2");
assertInstanceOf(PkiOperationOutcome.Success.class, first);
assertInstanceOf(PkiOperationOutcome.Success.class, second);
session.close();
session.close();
assertThrows(IllegalStateException.class, session::operations);
assertThrows(UnsupportedOperationException.class,
() -> ((PkiOperationOutcome.Success) first).result().fields().put("x", new PkiOperationValue.Text("y")));
System.out.println("...ok");
}
@Test
void validatesBeforeAllocationAndCleansPartialConstruction() {
System.out.println("validatesBeforeAllocationAndCleansPartialConstruction");
PkiSessionConfiguration configuration = configuration(temporaryDirectory.resolve("unused"));
AtomicInteger storesOpened = new AtomicInteger();
AtomicInteger storesClosed = new AtomicInteger();
DefaultPkiSession.Bootstrap invalid = bootstrap(storesOpened, storesClosed, true, false, false, false);
assertThrows(IllegalArgumentException.class,
() -> DefaultPkiSession.open(configuration, CLOCK, invalid));
assertEquals(0, storesOpened.get());
DefaultPkiSession.Bootstrap partial = bootstrap(storesOpened, storesClosed, false, true, false, false);
assertThrows(IllegalStateException.class,
() -> DefaultPkiSession.open(configuration, CLOCK, partial));
System.out.println("...partialStoreCloses=" + storesClosed.get());
assertEquals(1, storesClosed.get());
System.out.println("...ok");
}
@Test
void preservesPrimaryAndSuppressedCloseFailures() throws Exception {
System.out.println("preservesPrimaryAndSuppressedCloseFailures");
PkiSessionConfiguration configuration = configuration(temporaryDirectory.resolve("unused-close"));
DefaultPkiSession.Bootstrap bootstrap = bootstrap(new AtomicInteger(), new AtomicInteger(), false, false,
true, true);
PkiSession session = DefaultPkiSession.open(configuration, CLOCK, bootstrap);
Exception failure = assertThrows(Exception.class, session::close);
System.out.println("...suppressed=" + failure.getSuppressed().length);
assertEquals("audit-close", failure.getMessage());
assertEquals(1, failure.getSuppressed().length);
assertInstanceOf(IOException.class, failure.getSuppressed()[0]);
session.close();
System.out.println("...ok");
}
private static PkiSessionConfiguration configuration(Path storeRoot) {
return new PkiSessionConfiguration(1, new ProviderConfig("fs", Map.of("root", storeRoot.toString())),
new ProviderConfig("memory", Map.of("size", "16")));
}
private static DefaultPkiSession.Bootstrap bootstrap(AtomicInteger storesOpened, AtomicInteger storesClosed,
boolean invalidAudit, boolean failAuditOpen, boolean failAuditClose, boolean failStoreClose) {
return new DefaultPkiSession.Bootstrap() {
@Override
public void validateStore(ProviderConfig configuration) {
// valid
}
@Override
public void validateAudit(ProviderConfig configuration) {
if (invalidAudit) {
throw new IllegalArgumentException("invalid");
}
}
@Override
public PkiStore openStore(ProviderConfig configuration) {
storesOpened.incrementAndGet();
return proxyStore(() -> {
storesClosed.incrementAndGet();
if (failStoreClose) {
throw new IOException("store-close");
}
});
}
@Override
public AuditSink openAudit(ProviderConfig configuration) {
if (failAuditOpen) {
throw new IllegalStateException("audit-open");
}
return new AuditSink() {
@Override
public void record(zeroecho.pki.api.audit.AuditEvent event) {
// no-op
}
@Override
public void close() {
if (failAuditClose) {
throw new IllegalStateException("audit-close");
}
}
};
}
};
}
private static PkiStore proxyStore(CloseAction close) {
return (PkiStore) Proxy.newProxyInstance(PkiStore.class.getClassLoader(), new Class<?>[] { PkiStore.class },
(proxy, method, arguments) -> {
if ("close".equals(method.getName())) {
close.close();
return null;
}
if ("toString".equals(method.getName())) {
return "test-store";
}
throw new UnsupportedOperationException(method.getName());
});
}
@FunctionalInterface
private interface CloseAction {
void close() throws Exception;
}
}