+ * This is an internal helper that translates an opaque {@link KeyRef} into
+ * concrete keyring aliases. The mapping is intentionally provider-specific: PKI
+ * must treat {@link KeyRef} as an opaque identifier and must never rely on
+ * parsing rules.
+ *
+ *
+ *
+ * For signing requests, {@link #privateAlias} is expected to address the
+ * private component (e.g. {@code *.prv}) and {@link #publicAlias} the
+ * corresponding public component (e.g. {@code *.pub}). For verification,
+ * {@link #privateAlias} is unused and {@link #publicAlias} addresses the public
+ * component.
+ *
+ */
+ private static final class KeyRefParts {
+ private final String privateAlias;
+ private final String publicAlias;
+
+ private KeyRefParts(String privateAlias, String publicAlias) {
+ this.privateAlias = privateAlias;
+ this.publicAlias = publicAlias;
+ }
+ }
+
+ /**
+ * Internal exception used to signal validation/policy failures that must be
+ * surfaced via operation status.
+ *
+ *
+ * This exception must never escape the public boundary of
+ * {@link ZeroEchoLibSignatureWorkflow}. It is caught by
+ * {@code submitSign}/{@code submitVerify} and converted to a deterministic
+ * status: {@link State#FAILED} with a stable {@code detailCode}.
+ *
+ *
+ *
+ * Configuration values may be sensitive and must not be logged. This provider
+ * performs no value logging.
+ *
+ */
+public final class ZeroEchoLibSignatureWorkflowProvider implements SignatureWorkflowProvider {
+
+ private static final String KEY_KEYRING_PATH = "keyringPath";
+ private static final String KEY_KEYREF_PREFIX = "keyRefPrefix";
+ private static final String KEY_REQUIRE_SUFFIX = "requireComponentSuffix";
+
+ @Override
+ public String id() {
+ return "zeroecho-lib";
+ }
+
+ @Override
+ public Set supportedKeys() {
+ return Set.of(KEY_KEYRING_PATH, KEY_KEYREF_PREFIX, KEY_REQUIRE_SUFFIX);
+ }
+
+ @Override
+ public SignatureWorkflow allocate(ProviderConfig config) {
+ if (config == null) {
+ throw new IllegalArgumentException("config must not be null");
+ }
+
+ // Defensive hardening: fail fast if miswired config reaches this provider.
+ if (!id().equals(config.backendId())) {
+ throw new IllegalArgumentException("ProviderConfig backendId mismatch.");
+ }
+
+ String keyringPath = config.require(KEY_KEYRING_PATH);
+ String prefix = config.get(KEY_KEYREF_PREFIX).orElse("zeroecho-lib:");
+ boolean requireSuffix = config.get(KEY_REQUIRE_SUFFIX).map(Boolean::parseBoolean).orElse(Boolean.TRUE);
+
+ return new ZeroEchoLibSignatureWorkflow(id(), Path.of(keyringPath), prefix, requireSuffix);
+ }
+}
diff --git a/pki/src/main/java/zeroecho/pki/impl/crypto/zeroecholib/package-info.java b/pki/src/main/java/zeroecho/pki/impl/crypto/zeroecholib/package-info.java
new file mode 100644
index 0000000..781366e
--- /dev/null
+++ b/pki/src/main/java/zeroecho/pki/impl/crypto/zeroecholib/package-info.java
@@ -0,0 +1,69 @@
+/*******************************************************************************
+ * Copyright (C) 2025, 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.
+ ******************************************************************************/
+/**
+ * Reference crypto boundary provider backed by ZeroEcho lib and
+ * {@code KeyringStore}.
+ *
+ *
+ * This provider resolves {@link zeroecho.pki.api.KeyRef} values to key aliases
+ * managed by {@code zeroecho.core.storage.KeyringStore}. Private key material
+ * is never returned to PKI; it is materialized only inside the provider
+ * implementation to perform signing.
+ *
+ *
+ * KeyRef mapping
+ *
+ * This provider supports simple mappings intended for practical deployments:
+ *
+ *
+ * - {@code zeroecho-lib:<alias>.prv} for signing
+ * - {@code zeroecho-lib:<alias>.pub} for verification
+ *
+ *
+ *
+ * The alias component may be a UUID string (recommended). The provider does not
+ * require UUIDs by default, but a stricter mode can be enabled by
+ * configuration.
+ *
+ *
+ * Configuration
+ *
+ * - {@code keyringPath} (required): path to the KeyringStore text file
+ * - {@code keyRefPrefix} (optional, default {@code "zeroecho-lib:"})
+ * - {@code requireComponentSuffix} (optional, default {@code true}): require
+ * {@code .prv}/{@code .pub}
+ *
+ */
+package zeroecho.pki.impl.crypto.zeroecholib;
diff --git a/pki/src/main/java/zeroecho/pki/spi/bootstrap/PkiBootstrap.java b/pki/src/main/java/zeroecho/pki/spi/bootstrap/PkiBootstrap.java
index 5cccaab..f4cac7c 100644
--- a/pki/src/main/java/zeroecho/pki/spi/bootstrap/PkiBootstrap.java
+++ b/pki/src/main/java/zeroecho/pki/spi/bootstrap/PkiBootstrap.java
@@ -44,6 +44,8 @@ import zeroecho.pki.spi.ConfigurableProvider;
import zeroecho.pki.spi.ProviderConfig;
import zeroecho.pki.spi.audit.AuditSink;
import zeroecho.pki.spi.audit.AuditSinkProvider;
+import zeroecho.pki.spi.crypto.SignatureWorkflow;
+import zeroecho.pki.spi.crypto.SignatureWorkflowProvider;
import zeroecho.pki.spi.store.PkiStore;
import zeroecho.pki.spi.store.PkiStoreProvider;
@@ -54,15 +56,21 @@ import zeroecho.pki.spi.store.PkiStoreProvider;
* This class provides deterministic selection and instantiation rules for
* components discovered via {@link java.util.ServiceLoader}. It is designed to
* scale as more SPIs are introduced (audit, publish, framework integrations,
- * etc.).
+ * crypto/workflows, etc.).
*
*
* System property conventions
- *
*
* - Select store provider: {@code -Dzeroecho.pki.store=<id>}
* - Configure store provider:
* {@code -Dzeroecho.pki.store.<key>=<value>}
+ * - Select audit provider: {@code -Dzeroecho.pki.audit=<id>}
+ * - Configure audit provider:
+ * {@code -Dzeroecho.pki.audit.<key>=<value>}
+ * - Select crypto workflow provider:
+ * {@code -Dzeroecho.pki.crypto.workflow=<id>}
+ * - Configure crypto workflow provider:
+ * {@code -Dzeroecho.pki.crypto.workflow.<key>=<value>}
*
*
*
@@ -80,13 +88,16 @@ public final class PkiBootstrap {
private static final String PROP_AUDIT_BACKEND = "zeroecho.pki.audit";
private static final String PROP_AUDIT_PREFIX = "zeroecho.pki.audit.";
+ private static final String PROP_CRYPTO_WORKFLOW_BACKEND = "zeroecho.pki.crypto.workflow";
+ private static final String PROP_CRYPTO_WORKFLOW_PREFIX = "zeroecho.pki.crypto.workflow.";
+
private PkiBootstrap() {
throw new AssertionError("No instances.");
}
/**
* Opens a {@link PkiStore} using {@link PkiStoreProvider} discovered via
- * {@link java.util.ServiceLoader}.
+ * ServiceLoader.
*
*
* Provider selection is deterministic and fail-fast:
@@ -112,19 +123,8 @@ public final class PkiBootstrap {
* {@code "pki-store"} relative to the working directory.
*
*
- *
- * This method is suitable for CLI and server deployments. In DI containers
- * (Spring, Micronaut, etc.), invoke it from managed components and close the
- * returned store using the container lifecycle.
- *
- *
* @return opened store (never {@code null})
- * @throws IllegalStateException if provider selection is ambiguous or no
- * provider is available
- * @throws IllegalArgumentException if required configuration is missing/invalid
- * @throws RuntimeException if allocation fails
*/
-
public static PkiStore openStore() {
String requestedId = System.getProperty(PROP_STORE_BACKEND);
@@ -169,7 +169,7 @@ public final class PkiBootstrap {
/**
* Opens an {@link AuditSink} using {@link AuditSinkProvider} discovered via
- * {@link java.util.ServiceLoader}.
+ * ServiceLoader.
*
*
* Selection and configuration follow the same conventions as
@@ -177,11 +177,14 @@ public final class PkiBootstrap {
* {@code zeroecho.pki.audit.} prefixed properties.
*
*
+ *
+ * Defaulting rules implemented by this bootstrap (policy, not SPI requirement):
+ * if no audit backend is specified, defaults to {@code stdout}. For
+ * {@code file} provider, if {@code root} is not specified, defaults to
+ * {@code "pki-audit"} relative to the working directory.
+ *
+ *
* @return opened audit sink (never {@code null})
- * @throws IllegalStateException if provider selection is ambiguous or no
- * provider is available
- * @throws IllegalArgumentException if required configuration is missing/invalid
- * @throws RuntimeException if allocation fails
*/
public static AuditSink openAudit() {
String requestedId = System.getProperty(PROP_AUDIT_BACKEND);
@@ -192,7 +195,6 @@ public final class PkiBootstrap {
AuditSinkProvider provider = SpiSelector.select(AuditSinkProvider.class, requestedId,
new SpiSelector.ProviderId<>() {
-
@Override
public String id(AuditSinkProvider p) {
return p.id();
@@ -202,16 +204,56 @@ public final class PkiBootstrap {
Map props = SpiSystemProperties.readPrefixed(PROP_AUDIT_PREFIX);
if ("file".equals(provider.id()) && !props.containsKey("root")) {
- props.put("root", Path.of("pki-store").toString());
+ props.put("root", Path.of("pki-audit").toString());
}
ProviderConfig config = new ProviderConfig(provider.id(), props);
if (LOG.isLoggable(Level.INFO)) {
- LOG.info("Selected store provider: " + provider.id() + " (keys: " + props.keySet() + ")");
+ LOG.info("Selected audit provider: " + provider.id() + " (keys: " + props.keySet() + ")");
}
return provider.allocate(config);
+ }
+ /**
+ * Opens a {@link SignatureWorkflow} using {@link SignatureWorkflowProvider}
+ * discovered via ServiceLoader.
+ *
+ *
+ * Conventions:
+ *
+ *
+ * - Select provider: {@code -Dzeroecho.pki.crypto.workflow=<id>}
+ * - Provider config:
+ * {@code -Dzeroecho.pki.crypto.workflow.<key>=<value>}
+ *
+ *
+ *
+ * Security note: configuration values may be sensitive and must not be logged.
+ * This bootstrap logs only provider ids and configuration keys.
+ *
+ *
+ * @return opened signature workflow (never {@code null})
+ */
+ public static SignatureWorkflow openSignatureWorkflow() {
+ String requestedId = System.getProperty(PROP_CRYPTO_WORKFLOW_BACKEND);
+
+ SignatureWorkflowProvider provider = SpiSelector.select(SignatureWorkflowProvider.class, requestedId,
+ new SpiSelector.ProviderId<>() {
+ @Override
+ public String id(SignatureWorkflowProvider p) {
+ return p.id();
+ }
+ });
+
+ Map props = SpiSystemProperties.readPrefixed(PROP_CRYPTO_WORKFLOW_PREFIX);
+ ProviderConfig config = new ProviderConfig(provider.id(), props);
+
+ if (LOG.isLoggable(Level.INFO)) {
+ LOG.info("Selected crypto workflow provider: " + provider.id() + " (keys: " + props.keySet() + ")");
+ }
+
+ return provider.allocate(config);
}
}
diff --git a/pki/src/main/java/zeroecho/pki/spi/crypto/SignatureWorkflow.java b/pki/src/main/java/zeroecho/pki/spi/crypto/SignatureWorkflow.java
new file mode 100644
index 0000000..380fb60
--- /dev/null
+++ b/pki/src/main/java/zeroecho/pki/spi/crypto/SignatureWorkflow.java
@@ -0,0 +1,435 @@
+/*******************************************************************************
+ * Copyright (C) 2025, 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.spi.crypto;
+
+import java.io.Closeable;
+import java.time.Instant;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+import zeroecho.pki.api.EncodedObject;
+import zeroecho.pki.api.Encoding;
+import zeroecho.pki.api.KeyRef;
+import zeroecho.pki.api.PkiId;
+import zeroecho.pki.api.audit.AccessContext;
+
+/**
+ * Asynchronous signature workflow boundary for PKI.
+ *
+ *
+ * This SPI is intentionally asynchronous to support ultra-secure deployments
+ * where signing can be routed across multiple hops and may require human
+ * approval. A local provider may complete operations immediately; a
+ * remote/hop-based provider may keep them pending for extended periods.
+ *
+ *
+ * Trust boundary
+ *
+ * - PKI (caller): works only with {@link KeyRef}, algorithm
+ * id and payload bytes; it must never parse {@code KeyRef} nor access private
+ * key bytes.
+ * - Provider (callee): resolves {@code KeyRef} into an
+ * internal runtime key handle, enforces policy (including multi-hop approvals),
+ * and performs cryptographic operations.
+ *
+ *
+ * Failure model (normative - Variant 1)
+ *
+ * This workflow is designed to be audit-friendly and transport-friendly (e.g.,
+ * future HTTP APIs). Implementations must follow these rules:
+ *
+ *
+ * - Validation and policy failures must not be surfaced as uncaught
+ * exceptions. Instead, the provider must return an operation id and expose the
+ * failure via {@link #status(PkiId)} using {@link OperationStatus#state()} ==
+ * {@link State#FAILED} and a stable {@link OperationStatus#detailCode()}.
+ * - {@link IllegalArgumentException} may be thrown only for programmer errors
+ * such as {@code request == null} or {@code operationId == null}. These are not
+ * business/policy failures.
+ * - Providers must ensure {@link OperationStatus#detailCode()} does not
+ * contain secrets, key material, or payload fragments; it must be safe for
+ * audit logs and API responses.
+ *
+ */
+public interface SignatureWorkflow extends Closeable {
+
+ /**
+ * Provider identifier for diagnostics and audit correlation.
+ *
+ * @return stable provider id (never blank)
+ */
+ String id();
+
+ /**
+ * Submits a signing request.
+ *
+ *
+ * Providers must never return private key material. The output is a signature
+ * (encoded) on success.
+ *
+ *
+ * Failure model (normative)
+ *
+ * - For validation and policy failures: do not throw; return operation id and
+ * expose failure via {@link #status(PkiId)} with {@code FAILED} and stable
+ * {@code detailCode}.
+ * - May throw {@link IllegalArgumentException} only for programmer errors
+ * (e.g., {@code request == null}).
+ *
+ *
+ * @param request request (never {@code null})
+ * @return operation id (never {@code null})
+ */
+ PkiId submitSign(SignRequest request);
+
+ /**
+ * Submits a verification request.
+ *
+ *
+ * Verification typically uses public key material. For maximal portability
+ * across PQC/hybrid algorithms, verification requests can either:
+ *
+ *
+ * - reference a public key via {@link KeyRef} (preferred), or
+ * - provide encoded public key bytes (provider-defined format; may be
+ * unsupported by some providers).
+ *
+ *
+ * Failure model (normative)
+ *
+ * - For validation and policy failures: do not throw; return operation id and
+ * expose failure via {@link #status(PkiId)} with {@code FAILED} and stable
+ * {@code detailCode}.
+ * - May throw {@link IllegalArgumentException} only for programmer errors
+ * (e.g., {@code request == null}).
+ *
+ *
+ * @param request request (never {@code null})
+ * @return operation id (never {@code null})
+ */
+ PkiId submitVerify(VerifyRequest request);
+
+ /**
+ * Reads current status of an operation.
+ *
+ * @param operationId operation id (never {@code null})
+ * @return status (never {@code null})
+ */
+ OperationStatus status(PkiId operationId);
+
+ /**
+ * Best-effort cancellation.
+ *
+ * @param operationId operation id (never {@code null})
+ * @param reason non-sensitive reason (never blank)
+ * @return true if cancellation was accepted; false if already terminal/unknown
+ */
+ boolean cancel(PkiId operationId, String reason);
+
+ /**
+ * Registers a notification sink for status changes.
+ *
+ *
+ * Implementations may invoke the sink from provider-owned threads. The sink
+ * must not throw.
+ *
+ *
+ * @param sink sink (never {@code null})
+ * @return registration handle (never {@code null})
+ */
+ Registration register(NotificationSink sink);
+
+ /**
+ * Returns supported algorithm identifiers.
+ *
+ * @return supported algorithm ids (never {@code null})
+ */
+ Set supportedAlgorithms();
+
+ @Override
+ void close();
+
+ /**
+ * Signing request.
+ *
+ *
+ * Note: this record performs basic structural validation and may throw
+ * {@link IllegalArgumentException} at construction time. Callers building
+ * requests for external transports are expected to catch such exceptions and
+ * convert them to an appropriate error state before invoking
+ * {@link #submitSign(SignRequest)}.
+ *
+ *
+ * @param accessContext audit/governance context (never
+ * {@code null})
+ * @param keyRef opaque reference to the private key (never
+ * {@code null})
+ * @param algorithmId requested signature algorithm id (never
+ * blank)
+ * @param payload payload bytes (never {@code null})
+ * @param preferredSignatureEncoding preferred signature encoding (optional)
+ * @param deadline optional absolute deadline
+ */
+ record SignRequest(AccessContext accessContext, KeyRef keyRef, String algorithmId, EncodedObject payload,
+ Optional preferredSignatureEncoding, Optional deadline) {
+
+ public SignRequest {
+ Objects.requireNonNull(accessContext, "accessContext");
+ Objects.requireNonNull(keyRef, "keyRef");
+ Objects.requireNonNull(algorithmId, "algorithmId");
+ Objects.requireNonNull(payload, "payload");
+ Objects.requireNonNull(preferredSignatureEncoding, "preferredSignatureEncoding");
+ Objects.requireNonNull(deadline, "deadline");
+ if (algorithmId.isBlank()) {
+ throw new IllegalArgumentException("algorithmId must not be blank");
+ }
+ }
+ }
+
+ /**
+ * Verification request.
+ *
+ *
+ * Note: this record performs basic structural validation and may throw
+ * {@link IllegalArgumentException} at construction time. Callers building
+ * requests for external transports are expected to catch such exceptions and
+ * convert them to an appropriate error state before invoking
+ * {@link #submitVerify(VerifyRequest)}.
+ *
+ *
+ * @param accessContext audit/governance context (never {@code null})
+ * @param algorithmId requested signature algorithm id (never blank)
+ * @param payload signed payload bytes (never {@code null})
+ * @param signature signature bytes (never {@code null})
+ * @param publicKeyRef optional public key reference (preferred)
+ * @param publicKeyEncoded optional encoded public key bytes (provider-specific;
+ * may be unsupported)
+ * @param deadline optional absolute deadline
+ */
+ record VerifyRequest(AccessContext accessContext, String algorithmId, EncodedObject payload,
+ EncodedObject signature, Optional publicKeyRef, Optional publicKeyEncoded,
+ Optional deadline) {
+
+ public VerifyRequest {
+ Objects.requireNonNull(accessContext, "accessContext");
+ Objects.requireNonNull(algorithmId, "algorithmId");
+ Objects.requireNonNull(payload, "payload");
+ Objects.requireNonNull(signature, "signature");
+ Objects.requireNonNull(publicKeyRef, "publicKeyRef");
+ Objects.requireNonNull(publicKeyEncoded, "publicKeyEncoded");
+ Objects.requireNonNull(deadline, "deadline");
+ if (algorithmId.isBlank()) {
+ throw new IllegalArgumentException("algorithmId must not be blank");
+ }
+ if (publicKeyRef.isEmpty() && publicKeyEncoded.isEmpty()) {
+ throw new IllegalArgumentException("Either publicKeyRef or publicKeyEncoded must be provided");
+ }
+ }
+ }
+
+ /**
+ * Status of a long-running or asynchronous signature workflow operation.
+ *
+ *
+ * An {@code OperationStatus} represents the authoritative, audit-relevant state
+ * of an operation previously submitted via {@code submitSign} or
+ * {@code submitVerify}. Providers must ensure that status transitions are
+ * monotonic and observable through repeated calls to
+ * {@link SignatureWorkflow#status(PkiId)}.
+ *
+ *
+ * Failure and audit model
+ *
+ * - Operational, validation, or policy failures are reported via
+ * {@link State#FAILED}, not via uncaught exceptions.
+ * - {@link #detailCode()} provides a stable, non-sensitive classification of
+ * the outcome and is intended for audit logs, diagnostics, and transport-level
+ * APIs.
+ * - {@link #detailCode()} must never contain secrets, key identifiers,
+ * payload data, or provider-internal state.
+ *
+ *
+ * Terminal states
+ *
+ * Once an operation reaches a terminal state (see {@link #isTerminal()}), its
+ * {@code OperationStatus} must no longer change.
+ *
+ *
+ * @param state current lifecycle state of the operation (never
+ * {@code null})
+ * @param updatedAt timestamp of the last state transition (never
+ * {@code null}); providers should use a deterministic value
+ * where possible for non-operational states (e.g. unknown
+ * operation)
+ * @param detailCode optional stable, non-sensitive code describing the outcome
+ * or reason for the current state
+ * @param result optional terminal result; present only for
+ * {@link State#SUCCEEDED} and never for non-terminal states
+ */
+ record OperationStatus(State state, Instant updatedAt, Optional detailCode,
+ Optional result) {
+
+ public OperationStatus {
+ Objects.requireNonNull(state, "state");
+ Objects.requireNonNull(updatedAt, "updatedAt");
+ Objects.requireNonNull(detailCode, "detailCode");
+ Objects.requireNonNull(result, "result");
+ }
+
+ /**
+ * Indicates whether this status represents a terminal lifecycle state.
+ *
+ *
+ * Terminal states are final and must not transition to any other state.
+ *
+ *
+ * @return {@code true} if the operation is terminal, {@code false} otherwise
+ */
+ public boolean isTerminal() {
+ return state == State.SUCCEEDED || state == State.FAILED || state == State.CANCELLED
+ || state == State.EXPIRED;
+ }
+ }
+
+ /**
+ * Lifecycle states of a signature workflow operation.
+ *
+ *
+ * The state machine is intentionally minimal to accommodate both local
+ * (in-process) and remote or multi-hop providers.
+ *
+ *
+ *
+ * - {@link #PENDING} – operation accepted but not yet actively processed
+ * - {@link #RUNNING} – operation is currently being processed
+ * - {@link #WAITING_APPROVAL} – operation is suspended awaiting external
+ * approval (e.g. human review, policy gate, HSM quorum)
+ * - {@link #SUCCEEDED} – operation completed successfully
+ * - {@link #FAILED} – operation permanently failed due to validation, policy,
+ * or execution error
+ * - {@link #CANCELLED} – operation was explicitly cancelled by the
+ * caller
+ * - {@link #EXPIRED} – operation expired before completion
+ *
+ *
+ *
+ * Providers must document which transitions they support and under which
+ * conditions a given state may be observed.
+ *
+ */
+ enum State {
+ PENDING, RUNNING, WAITING_APPROVAL, SUCCEEDED, FAILED, CANCELLED, EXPIRED
+ }
+
+ /**
+ * Terminal result of a signature workflow operation.
+ *
+ *
+ * An {@code OperationResult} is present only when an operation reaches
+ * {@link State#SUCCEEDED}. It represents the semantic outcome of the operation
+ * without exposing any sensitive key material.
+ *
+ *
+ *
+ * - For sign operations, {@link #signature()} contains the generated
+ * signature.
+ * - For verify operations, {@link #verified()} indicates verification
+ * outcome.
+ *
+ *
+ *
+ * Exactly one of {@code signature} or {@code verified} is typically present,
+ * depending on the operation type.
+ *
+ *
+ * @param signature optional signature artifact for successful sign operations
+ * @param verified optional verification result for successful verify
+ * operations
+ */
+ record OperationResult(Optional signature, Optional verified) {
+
+ public OperationResult {
+ Objects.requireNonNull(signature, "signature");
+ Objects.requireNonNull(verified, "verified");
+ }
+ }
+
+ /**
+ * Callback interface for receiving asynchronous status updates.
+ *
+ *
+ * Providers may invoke this callback from provider-managed threads.
+ * Implementations must therefore be thread-safe and must not throw exceptions.
+ * Any exception thrown by a {@code NotificationSink} must be ignored by the
+ * provider.
+ *
+ *
+ *
+ * The callback must be treated as a best-effort notification mechanism and must
+ * not be relied upon as the sole source of truth; callers should always be able
+ * to query the authoritative state via {@link SignatureWorkflow#status(PkiId)}.
+ *
+ */
+ @FunctionalInterface
+ interface NotificationSink {
+
+ /**
+ * Notifies about a change in operation status.
+ *
+ * @param operationId identifier of the affected operation (never {@code null})
+ * @param status new status of the operation (never {@code null})
+ */
+ void onStatusChanged(PkiId operationId, OperationStatus status);
+ }
+
+ /**
+ * Handle representing a registered {@link NotificationSink}.
+ *
+ *
+ * Closing the registration unregisters the associated sink. The operation is
+ * idempotent and must not throw.
+ *
+ */
+ @SuppressWarnings("PMD.ImplicitFunctionalInterface")
+ interface Registration extends AutoCloseable {
+
+ /**
+ * Unregisters the associated {@link NotificationSink}.
+ */
+ @Override
+ void close();
+ }
+}
diff --git a/pki/src/main/java/zeroecho/pki/spi/crypto/SignatureWorkflowProvider.java b/pki/src/main/java/zeroecho/pki/spi/crypto/SignatureWorkflowProvider.java
new file mode 100644
index 0000000..26694b9
--- /dev/null
+++ b/pki/src/main/java/zeroecho/pki/spi/crypto/SignatureWorkflowProvider.java
@@ -0,0 +1,44 @@
+/*******************************************************************************
+ * Copyright (C) 2025, 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.spi.crypto;
+
+import zeroecho.pki.spi.ConfigurableProvider;
+
+/**
+ * ServiceLoader provider for {@link SignatureWorkflow}.
+ */
+public interface SignatureWorkflowProvider extends ConfigurableProvider {
+ // marker
+}
\ No newline at end of file
diff --git a/pki/src/main/java/zeroecho/pki/spi/crypto/package-info.java b/pki/src/main/java/zeroecho/pki/spi/crypto/package-info.java
new file mode 100644
index 0000000..567bd11
--- /dev/null
+++ b/pki/src/main/java/zeroecho/pki/spi/crypto/package-info.java
@@ -0,0 +1,64 @@
+/*******************************************************************************
+ * Copyright (C) 2025, 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.
+ ******************************************************************************/
+/**
+ * Crypto boundary SPI for the PKI module.
+ *
+ *
+ * The PKI module must never store, expose, nor directly handle private key
+ * material. It operates exclusively on opaque {@link zeroecho.pki.api.KeyRef}
+ * values resolved at runtime by a selected
+ * {@link java.util.ServiceLoader}-driven provider.
+ *
+ *
+ * Trust boundary
+ *
+ * - PKI (caller): submits sign/verify requests using
+ * {@code KeyRef}, algorithm id, and payload bytes. It must not parse nor
+ * interpret {@code KeyRef}.
+ * - Crypto provider (callee): resolves {@code KeyRef} to an
+ * internal key handle, enforces policy (including multi-hop approvals in other
+ * implementations), performs crypto, and returns only non-secret outputs
+ * (signature bytes or verification decision).
+ *
+ *
+ * Security constraints
+ *
+ * - {@code KeyRef} is sensitive metadata and must not be logged.
+ * - Payload bytes and signature bytes must not be logged.
+ * - Implementations must avoid persisting secrets; audit evidence must
+ * contain only non-secret metadata.
+ *
+ */
+package zeroecho.pki.spi.crypto;
diff --git a/pki/src/main/resources/META-INF/services/zeroecho.pki.spi.crypto.SignatureWorkflowProvider b/pki/src/main/resources/META-INF/services/zeroecho.pki.spi.crypto.SignatureWorkflowProvider
new file mode 100644
index 0000000..5e3e6ce
--- /dev/null
+++ b/pki/src/main/resources/META-INF/services/zeroecho.pki.spi.crypto.SignatureWorkflowProvider
@@ -0,0 +1 @@
+zeroecho.pki.impl.crypto.zeroecholib.ZeroEchoLibSignatureWorkflowProvider
diff --git a/pki/src/test/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibKeyRefParsingTest.java b/pki/src/test/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibKeyRefParsingTest.java
new file mode 100644
index 0000000..2a1ae03
--- /dev/null
+++ b/pki/src/test/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibKeyRefParsingTest.java
@@ -0,0 +1,131 @@
+/*******************************************************************************
+ * Copyright (C) 2025, 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.impl.crypto.zeroecholib;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.nio.file.Path;
+import java.time.Instant;
+import java.util.Optional;
+
+import org.junit.jupiter.api.Test;
+
+import zeroecho.pki.api.EncodedObject;
+import zeroecho.pki.api.Encoding;
+import zeroecho.pki.api.KeyRef;
+import zeroecho.pki.api.PkiId;
+import zeroecho.pki.api.audit.AccessContext;
+import zeroecho.pki.api.audit.Principal;
+import zeroecho.pki.api.audit.Purpose;
+import zeroecho.pki.spi.crypto.SignatureWorkflow;
+
+public final class ZeroEchoLibKeyRefParsingTest {
+
+ @Test
+ void signing_requires_prv_suffix_in_strict_mode_ok() {
+ System.out.println("signing_requires_prv_suffix_in_strict_mode_ok");
+
+ try (ZeroEchoLibSignatureWorkflow wf = new ZeroEchoLibSignatureWorkflow("zeroecho-lib", Path.of("nonexistent"),
+ "zeroecho-lib:", true)) {
+
+ AccessContext ctx = new AccessContext(new Principal("TEST", "unit"), new Purpose("UNIT_TEST"),
+ Optional.empty(), Optional.empty());
+
+ SignatureWorkflow.SignRequest req = new SignatureWorkflow.SignRequest(ctx, new KeyRef("zeroecho-lib:abc"), // missing
+ // .prv
+ "ECDSA", new EncodedObject(Encoding.BINARY, new byte[] { 0x01 }), Optional.of(Encoding.BINARY),
+ Optional.of(Instant.now()));
+
+ PkiId opId = wf.submitSign(req);
+ SignatureWorkflow.OperationStatus st = wf.status(opId);
+
+ System.out.println("...state=" + st.state());
+ System.out.println("...detailCode=" + st.detailCode().orElse(""));
+
+ assertEquals(SignatureWorkflow.State.FAILED, st.state());
+ assertEquals(ZeroEchoLibSignatureWorkflow.DC_INVALID_KEYREF_SUFFIX, st.detailCode().orElse(""));
+ }
+ System.out.println("...ok");
+ }
+
+ @Test
+ void verify_with_publicKeyEncoded_is_rejected_ok() {
+ System.out.println("verify_with_publicKeyEncoded_is_rejected_ok");
+
+ try (ZeroEchoLibSignatureWorkflow wf = new ZeroEchoLibSignatureWorkflow("zeroecho-lib", Path.of("nonexistent"),
+ "zeroecho-lib:", true)) {
+
+ AccessContext ctx = new AccessContext(new Principal("TEST", "unit"), new Purpose("UNIT_TEST"),
+ Optional.empty(), Optional.empty());
+
+ SignatureWorkflow.VerifyRequest req = new SignatureWorkflow.VerifyRequest(ctx, "ECDSA",
+ new EncodedObject(Encoding.BINARY, new byte[] { 0x01 }),
+ new EncodedObject(Encoding.BINARY, new byte[] { 0x02 }), Optional.empty(),
+ Optional.of(new EncodedObject(Encoding.BINARY, new byte[] { 0x03 })), // unsupported form
+ Optional.of(Instant.now()));
+
+ PkiId opId = wf.submitVerify(req);
+ SignatureWorkflow.OperationStatus st = wf.status(opId);
+
+ System.out.println("...state=" + st.state());
+ System.out.println("...detailCode=" + st.detailCode().orElse(""));
+
+ assertEquals(SignatureWorkflow.State.FAILED, st.state());
+ assertEquals(ZeroEchoLibSignatureWorkflow.DC_UNSUPPORTED_PUBLICKEY_FORM, st.detailCode().orElse(""));
+ }
+ System.out.println("...ok");
+ }
+
+ @Test
+ void status_unknown_operation_is_deterministic_ok() {
+ System.out.println("status_unknown_operation_is_deterministic_ok");
+
+ try (ZeroEchoLibSignatureWorkflow wf = new ZeroEchoLibSignatureWorkflow("zeroecho-lib", Path.of("nonexistent"),
+ "zeroecho-lib:", true)) {
+
+ PkiId unknown = new PkiId("00000000-0000-0000-0000-000000000000");
+ SignatureWorkflow.OperationStatus st = wf.status(unknown);
+
+ System.out.println("...state=" + st.state());
+ System.out.println("...detailCode=" + st.detailCode().orElse(""));
+ System.out.println("...updatedAt=" + st.updatedAt());
+
+ assertEquals(SignatureWorkflow.State.FAILED, st.state());
+ assertEquals(ZeroEchoLibSignatureWorkflow.DC_UNKNOWN_OPERATION, st.detailCode().orElse(""));
+ assertEquals(Instant.EPOCH, st.updatedAt());
+ }
+ System.out.println("...ok");
+ }
+}
diff --git a/pki/src/test/java/zeroecho/pki/spi/bootstrap/PkiBootstrapTest.java b/pki/src/test/java/zeroecho/pki/spi/bootstrap/PkiBootstrapTest.java
index c3c1b2c..7366764 100644
--- a/pki/src/test/java/zeroecho/pki/spi/bootstrap/PkiBootstrapTest.java
+++ b/pki/src/test/java/zeroecho/pki/spi/bootstrap/PkiBootstrapTest.java
@@ -116,7 +116,6 @@ public final class PkiBootstrapTest {
// Per bootstrap: if no -Dzeroecho.pki.audit is set, default is "stdout".
// This must work even if multiple audit providers exist.
- // :contentReference[oaicite:1]{index=1}
AuditSink sink = PkiBootstrap.openAudit();
assertNotNull(sink);