feat: introduce SignatureWorkflow SPI and zeroecho-lib implementation
- add SignatureWorkflow SPI for asynchronous sign/verify operations - define audit-friendly, exception-free failure model - introduce stable OperationStatus, State and OperationResult semantics - document trust boundaries, lifecycle, and audit constraints in SPI JavaDoc - add ZeroEchoLibSignatureWorkflow backed by KeyringStore and ZeroEcho lib - enforce opaque KeyRef handling and provider-local parsing - add deterministic detail codes and UNKNOWN_OPERATION handling - integrate workflow provider into ServiceLoader bootstrap - align PkiBootstrap logging and defaults with crypto workflow SPI - add comprehensive JUnit tests for validation and status semantics Signed-off-by: Leo Galambos <lg@hq.egothor.org>
This commit is contained in:
@@ -0,0 +1,573 @@
|
||||
/*******************************************************************************
|
||||
* 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 java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.context.SignatureContext;
|
||||
import zeroecho.core.io.TailStrippingInputStream;
|
||||
import zeroecho.core.storage.KeyringStore;
|
||||
import zeroecho.pki.api.EncodedObject;
|
||||
import zeroecho.pki.api.Encoding;
|
||||
import zeroecho.pki.api.KeyRef;
|
||||
import zeroecho.pki.api.PkiId;
|
||||
import zeroecho.pki.spi.crypto.SignatureWorkflow;
|
||||
|
||||
/**
|
||||
* ZeroEcho-lib backed {@link SignatureWorkflow}.
|
||||
*
|
||||
* <h2>Failure model</h2>
|
||||
* <p>
|
||||
* Validation and policy failures must not surface as uncaught exceptions.
|
||||
* Instead, this provider always returns an operation id and exposes failures
|
||||
* via {@link #status(PkiId)} as {@link State#FAILED} with a stable detail code.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Security</h2>
|
||||
* <ul>
|
||||
* <li>Never logs {@link KeyRef} values.</li>
|
||||
* <li>Never logs payload bytes or signature bytes.</li>
|
||||
* <li>Never returns private key material; private keys are materialized only
|
||||
* within this boundary.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow { // NOPMD
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(ZeroEchoLibSignatureWorkflow.class.getName());
|
||||
|
||||
// Stable detail codes (non-secret), intended for audit + transport mapping.
|
||||
public static final String DC_SUBMITTED = "SUBMITTED";
|
||||
public static final String DC_SIGNED = "SIGNED";
|
||||
public static final String DC_VERIFIED = "VERIFIED";
|
||||
public static final String DC_REJECTED = "REJECTED";
|
||||
public static final String DC_CANCELLED = "CANCELLED";
|
||||
|
||||
public static final String DC_INVALID_KEYREF_SUFFIX = "INVALID_KEYREF_SUFFIX";
|
||||
public static final String DC_INVALID_KEYREF_PREFIX = "INVALID_KEYREF_PREFIX";
|
||||
public static final String DC_UNSUPPORTED_PUBLICKEY_FORM = "UNSUPPORTED_PUBLICKEY_FORM"; // NOPMD
|
||||
public static final String DC_INVALID_ALGORITHM_ID = "INVALID_ALGORITHM_ID";
|
||||
public static final String DC_ALGORITHM_MISMATCH = "ALGORITHM_MISMATCH";
|
||||
public static final String DC_KEYRING_IO_ERROR = "KEYRING_IO_ERROR";
|
||||
public static final String DC_KEY_NOT_FOUND = "KEY_NOT_FOUND";
|
||||
public static final String DC_CRYPTO_FAILURE = "CRYPTO_FAILURE";
|
||||
public static final String DC_UNSUPPORTED_SIGNATURE_ENCODING = "UNSUPPORTED_SIGNATURE_ENCODING"; // NOPMD
|
||||
public static final String DC_UNKNOWN_OPERATION = "UNKNOWN_OPERATION";
|
||||
|
||||
private static final OperationStatus UNKNOWN_OPERATION_STATUS = new OperationStatus(State.FAILED, Instant.EPOCH,
|
||||
Optional.of(DC_UNKNOWN_OPERATION), Optional.empty());
|
||||
|
||||
private final String id;
|
||||
private final java.nio.file.Path keyringPath;
|
||||
private final String keyRefPrefix;
|
||||
private final boolean requireComponentSuffix;
|
||||
|
||||
private final Map<PkiId, OperationStatus> statuses;
|
||||
private final Map<PkiId, NotificationSink> sinks;
|
||||
|
||||
private volatile KeyringStore keyringOrNull; // NOPMD
|
||||
|
||||
/* default */ ZeroEchoLibSignatureWorkflow(String id, java.nio.file.Path keyringPath, String keyRefPrefix,
|
||||
boolean requireComponentSuffix) {
|
||||
if (id == null || id.isBlank()) {
|
||||
throw new IllegalArgumentException("id must not be blank");
|
||||
}
|
||||
if (keyringPath == null) {
|
||||
throw new IllegalArgumentException("keyringPath must not be null");
|
||||
}
|
||||
if (keyRefPrefix == null) {
|
||||
throw new IllegalArgumentException("keyRefPrefix must not be null");
|
||||
}
|
||||
this.id = id;
|
||||
this.keyringPath = keyringPath;
|
||||
this.keyRefPrefix = keyRefPrefix;
|
||||
this.requireComponentSuffix = requireComponentSuffix;
|
||||
|
||||
this.statuses = Collections.synchronizedMap(new HashMap<>());
|
||||
this.sinks = Collections.synchronizedMap(new HashMap<>());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String id() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> supportedAlgorithms() {
|
||||
// Informational only; runtime policy/key availability may still reject.
|
||||
return CryptoAlgorithms.available();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PkiId submitSign(SignRequest request) {
|
||||
if (request == null) {
|
||||
throw new IllegalArgumentException("request must not be null");
|
||||
}
|
||||
|
||||
PkiId opId = newOperationId();
|
||||
putStatus(opId, new OperationStatus(State.RUNNING, Instant.now(), Optional.of(DC_SUBMITTED), Optional.empty()));
|
||||
|
||||
try {
|
||||
KeyRefParts parts = parseKeyRefOrThrow(request.keyRef(), true);
|
||||
|
||||
if (request.algorithmId() == null || request.algorithmId().isBlank()) {
|
||||
throw new InvalidRequestException(DC_INVALID_ALGORITHM_ID);
|
||||
}
|
||||
|
||||
KeyringStore ks = requireKeyringOrThrow();
|
||||
|
||||
KeyringStore.PrivateWithId prv;
|
||||
KeyringStore.PublicWithId pub;
|
||||
try {
|
||||
prv = ks.getPrivateWithId(parts.privateAlias);
|
||||
pub = ks.getPublicWithId(parts.publicAlias);
|
||||
} catch (GeneralSecurityException missing) {
|
||||
throw new InvalidRequestException(DC_KEY_NOT_FOUND, missing);
|
||||
}
|
||||
|
||||
enforceAlgorithmMatchOrThrow(request.algorithmId(), prv.algorithm());
|
||||
|
||||
byte[] signatureBytes = signStreaming(request.algorithmId(), prv.key(), pub.key(),
|
||||
request.payload().bytes());
|
||||
|
||||
Encoding outEnc = request.preferredSignatureEncoding().orElse(Encoding.BINARY);
|
||||
EncodedObject signature = encodeSignatureOrThrow(outEnc, signatureBytes);
|
||||
|
||||
OperationResult result = new OperationResult(Optional.of(signature), Optional.empty());
|
||||
putStatus(opId,
|
||||
new OperationStatus(State.SUCCEEDED, Instant.now(), Optional.of(DC_SIGNED), Optional.of(result)));
|
||||
return opId;
|
||||
|
||||
} catch (InvalidRequestException inv) { // NOPMD
|
||||
putStatus(opId,
|
||||
new OperationStatus(State.FAILED, Instant.now(), Optional.of(inv.detailCode), Optional.empty()));
|
||||
return opId;
|
||||
|
||||
} catch (IOException io) {
|
||||
putStatus(opId, new OperationStatus(State.FAILED, Instant.now(), Optional.of(DC_KEYRING_IO_ERROR),
|
||||
Optional.empty()));
|
||||
if (LOG.isLoggable(Level.FINE)) {
|
||||
LOG.log(Level.FINE, "Sign failed due to IO (details suppressed)", io);
|
||||
}
|
||||
return opId;
|
||||
|
||||
} catch (GeneralSecurityException sec) {
|
||||
putStatus(opId,
|
||||
new OperationStatus(State.FAILED, Instant.now(), Optional.of(DC_CRYPTO_FAILURE), Optional.empty()));
|
||||
if (LOG.isLoggable(Level.FINE)) {
|
||||
LOG.log(Level.FINE, "Sign failed due to security exception (details suppressed)", sec);
|
||||
}
|
||||
return opId;
|
||||
|
||||
} catch (RuntimeException ex) { // NOPMD
|
||||
putStatus(opId,
|
||||
new OperationStatus(State.FAILED, Instant.now(), Optional.of(DC_CRYPTO_FAILURE), Optional.empty()));
|
||||
if (LOG.isLoggable(Level.FINE)) {
|
||||
LOG.log(Level.FINE, "Sign failed due to runtime exception (details suppressed)", ex);
|
||||
}
|
||||
return opId;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PkiId submitVerify(VerifyRequest request) {
|
||||
if (request == null) {
|
||||
throw new IllegalArgumentException("request must not be null");
|
||||
}
|
||||
|
||||
PkiId opId = newOperationId();
|
||||
putStatus(opId, new OperationStatus(State.RUNNING, Instant.now(), Optional.of(DC_SUBMITTED), Optional.empty()));
|
||||
|
||||
try {
|
||||
if (request.algorithmId() == null || request.algorithmId().isBlank()) {
|
||||
throw new InvalidRequestException(DC_INVALID_ALGORITHM_ID);
|
||||
}
|
||||
|
||||
// Provider choice: full PQC/hybrid support requires KeyRef for public keys.
|
||||
// Encoded public keys are not supported here; treat as FAILED (Variant 1).
|
||||
if (request.publicKeyRef().isEmpty() && request.publicKeyEncoded().isPresent()) {
|
||||
throw new InvalidRequestException(DC_UNSUPPORTED_PUBLICKEY_FORM);
|
||||
}
|
||||
|
||||
PublicKey pub = resolvePublicKeyOrThrow(request);
|
||||
|
||||
byte[] sigBytes = decodeSignatureOrThrow(request.signature());
|
||||
boolean ok = verifyStreaming(request.algorithmId(), pub, request.payload().bytes(), sigBytes);
|
||||
|
||||
OperationResult result = new OperationResult(Optional.empty(), Optional.of(ok));
|
||||
String dc = ok ? DC_VERIFIED : DC_REJECTED;
|
||||
putStatus(opId, new OperationStatus(State.SUCCEEDED, Instant.now(), Optional.of(dc), Optional.of(result)));
|
||||
return opId;
|
||||
|
||||
} catch (InvalidRequestException inv) { // NOPMD
|
||||
putStatus(opId,
|
||||
new OperationStatus(State.FAILED, Instant.now(), Optional.of(inv.detailCode), Optional.empty()));
|
||||
return opId;
|
||||
|
||||
} catch (IOException io) {
|
||||
putStatus(opId, new OperationStatus(State.FAILED, Instant.now(), Optional.of(DC_KEYRING_IO_ERROR),
|
||||
Optional.empty()));
|
||||
if (LOG.isLoggable(Level.FINE)) {
|
||||
LOG.log(Level.FINE, "Verify failed due to IO (details suppressed)", io);
|
||||
}
|
||||
return opId;
|
||||
|
||||
} catch (GeneralSecurityException sec) {
|
||||
putStatus(opId,
|
||||
new OperationStatus(State.FAILED, Instant.now(), Optional.of(DC_CRYPTO_FAILURE), Optional.empty()));
|
||||
if (LOG.isLoggable(Level.FINE)) {
|
||||
LOG.log(Level.FINE, "Verify failed due to security exception (details suppressed)", sec);
|
||||
}
|
||||
return opId;
|
||||
|
||||
} catch (RuntimeException ex) { // NOPMD
|
||||
putStatus(opId,
|
||||
new OperationStatus(State.FAILED, Instant.now(), Optional.of(DC_CRYPTO_FAILURE), Optional.empty()));
|
||||
if (LOG.isLoggable(Level.FINE)) {
|
||||
LOG.log(Level.FINE, "Verify failed due to runtime exception (details suppressed)", ex);
|
||||
}
|
||||
return opId;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public OperationStatus status(PkiId operationId) {
|
||||
if (operationId == null) {
|
||||
throw new IllegalArgumentException("operationId must not be null");
|
||||
}
|
||||
OperationStatus st = this.statuses.get(operationId);
|
||||
if (st == null) {
|
||||
return UNKNOWN_OPERATION_STATUS;
|
||||
}
|
||||
return st;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancel(PkiId operationId, String reason) {
|
||||
if (operationId == null) {
|
||||
throw new IllegalArgumentException("operationId must not be null");
|
||||
}
|
||||
if (reason == null || reason.isBlank()) {
|
||||
throw new IllegalArgumentException("reason must not be blank");
|
||||
}
|
||||
OperationStatus st = this.statuses.get(operationId);
|
||||
if (st == null || st.isTerminal()) {
|
||||
return false;
|
||||
}
|
||||
putStatus(operationId,
|
||||
new OperationStatus(State.CANCELLED, Instant.now(), Optional.of(DC_CANCELLED), Optional.empty()));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Registration register(NotificationSink sink) {
|
||||
if (sink == null) {
|
||||
throw new IllegalArgumentException("sink must not be null");
|
||||
}
|
||||
PkiId regId = newOperationId();
|
||||
this.sinks.put(regId, sink);
|
||||
return new Registration() {
|
||||
@Override
|
||||
public void close() {
|
||||
sinks.remove(regId);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
this.statuses.clear();
|
||||
this.sinks.clear();
|
||||
this.keyringOrNull = null;
|
||||
}
|
||||
|
||||
private KeyringStore requireKeyringOrThrow() throws IOException {
|
||||
KeyringStore ks = this.keyringOrNull;
|
||||
if (ks != null) {
|
||||
return ks;
|
||||
}
|
||||
KeyringStore loaded = KeyringStore.load(this.keyringPath);
|
||||
this.keyringOrNull = loaded;
|
||||
return loaded;
|
||||
}
|
||||
|
||||
private static void enforceAlgorithmMatchOrThrow(String requested, String stored) throws InvalidRequestException {
|
||||
if (!requested.equals(stored)) {
|
||||
throw new InvalidRequestException(DC_ALGORITHM_MISMATCH);
|
||||
}
|
||||
}
|
||||
|
||||
private KeyRefParts parseKeyRefOrThrow(KeyRef keyRef, boolean forSigning) throws InvalidRequestException {
|
||||
String raw = keyRef.value();
|
||||
if (!raw.startsWith(this.keyRefPrefix)) {
|
||||
throw new InvalidRequestException(DC_INVALID_KEYREF_PREFIX);
|
||||
}
|
||||
|
||||
String v = raw.substring(this.keyRefPrefix.length());
|
||||
boolean hasPrv = v.endsWith(".prv");
|
||||
boolean hasPub = v.endsWith(".pub");
|
||||
|
||||
if (this.requireComponentSuffix) {
|
||||
if (forSigning && !hasPrv) {
|
||||
throw new InvalidRequestException(DC_INVALID_KEYREF_SUFFIX);
|
||||
}
|
||||
if (!forSigning && !hasPub) {
|
||||
throw new InvalidRequestException(DC_INVALID_KEYREF_SUFFIX);
|
||||
}
|
||||
}
|
||||
|
||||
if (forSigning) {
|
||||
String privateAlias = hasPrv ? v : (v + ".prv");
|
||||
String base = hasPrv ? v.substring(0, v.length() - 4) : v;
|
||||
String publicAlias = base + ".pub";
|
||||
return new KeyRefParts(privateAlias, publicAlias);
|
||||
}
|
||||
|
||||
String publicAlias = hasPub ? v : (v + ".pub");
|
||||
return new KeyRefParts(publicAlias, publicAlias);
|
||||
}
|
||||
|
||||
private PublicKey resolvePublicKeyOrThrow(VerifyRequest request)
|
||||
throws InvalidRequestException, IOException, GeneralSecurityException {
|
||||
|
||||
if (request.publicKeyRef().isPresent()) {
|
||||
KeyRefParts parts = parseKeyRefOrThrow(request.publicKeyRef().get(), false);
|
||||
|
||||
KeyringStore ks = requireKeyringOrThrow();
|
||||
|
||||
KeyringStore.PublicWithId pub;
|
||||
try {
|
||||
pub = ks.getPublicWithId(parts.publicAlias);
|
||||
} catch (GeneralSecurityException missing) {
|
||||
throw new InvalidRequestException(DC_KEY_NOT_FOUND, missing);
|
||||
}
|
||||
|
||||
enforceAlgorithmMatchOrThrow(request.algorithmId(), pub.algorithm());
|
||||
return pub.key();
|
||||
}
|
||||
|
||||
throw new InvalidRequestException(DC_UNSUPPORTED_PUBLICKEY_FORM);
|
||||
}
|
||||
|
||||
private static byte[] signStreaming(String algorithmId, PrivateKey prv, PublicKey pub, byte[] msg)
|
||||
throws GeneralSecurityException, IOException {
|
||||
|
||||
int sigLen;
|
||||
try (SignatureContext verifier = CryptoAlgorithms.create(algorithmId, KeyUsage.VERIFY, pub)) {
|
||||
sigLen = verifier.tagLength();
|
||||
}
|
||||
|
||||
try (SignatureContext signer = CryptoAlgorithms.create(algorithmId, KeyUsage.SIGN, prv)) {
|
||||
final byte[][] sigHolder = new byte[1][];
|
||||
try (InputStream in = new TailStrippingInputStream(signer.wrap(new ByteArrayInputStream(msg)), sigLen,
|
||||
8192) {
|
||||
@Override
|
||||
protected void processTail(byte[] tail) throws IOException {
|
||||
sigHolder[0] = (tail == null) ? null : tail.clone();
|
||||
}
|
||||
}) {
|
||||
in.transferTo(OutputStream.nullOutputStream());
|
||||
}
|
||||
|
||||
byte[] sig = sigHolder[0];
|
||||
if (sig == null || sig.length == 0) {
|
||||
throw new GeneralSecurityException("Signature trailer missing.");
|
||||
}
|
||||
return sig;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean verifyStreaming(String algorithmId, PublicKey pub, byte[] msg, byte[] signature)
|
||||
throws GeneralSecurityException, IOException {
|
||||
|
||||
try (SignatureContext verifier = CryptoAlgorithms.create(algorithmId, KeyUsage.VERIFY, pub)) {
|
||||
verifier.setExpectedTag(signature);
|
||||
try (InputStream in = verifier.wrap(new ByteArrayInputStream(msg))) {
|
||||
in.transferTo(OutputStream.nullOutputStream());
|
||||
}
|
||||
return true;
|
||||
} catch (Exception mismatch) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static EncodedObject encodeSignatureOrThrow(Encoding encoding, byte[] sigBytes)
|
||||
throws InvalidRequestException {
|
||||
if (encoding == Encoding.BINARY || encoding == Encoding.DER) {
|
||||
return new EncodedObject(encoding, sigBytes);
|
||||
}
|
||||
if (encoding == Encoding.PEM) {
|
||||
String pem = pemWrap("ZEROECHO SIGNATURE", sigBytes);
|
||||
return new EncodedObject(Encoding.PEM, pem.getBytes(java.nio.charset.StandardCharsets.US_ASCII));
|
||||
}
|
||||
throw new InvalidRequestException(DC_UNSUPPORTED_SIGNATURE_ENCODING);
|
||||
}
|
||||
|
||||
private static byte[] decodeSignatureOrThrow(EncodedObject signature) throws InvalidRequestException {
|
||||
if (signature.encoding() == Encoding.BINARY || signature.encoding() == Encoding.DER) {
|
||||
return signature.bytes().clone();
|
||||
}
|
||||
if (signature.encoding() == Encoding.PEM) {
|
||||
String text = new String(signature.bytes(), java.nio.charset.StandardCharsets.US_ASCII);
|
||||
return pemUnwrap(text);
|
||||
}
|
||||
throw new InvalidRequestException(DC_UNSUPPORTED_SIGNATURE_ENCODING);
|
||||
}
|
||||
|
||||
private static String pemWrap(String label, byte[] data) {
|
||||
String b64 = Base64.getMimeEncoder(64, new byte[] { '\n' }).encodeToString(data);
|
||||
return "-----BEGIN " + label + "-----\n" + b64 + "\n-----END " + label + "-----\n";
|
||||
}
|
||||
|
||||
private static byte[] pemUnwrap(String pem) {
|
||||
String[] lines = pem.replace("\r", "").split("\n");
|
||||
StringBuilder b64 = new StringBuilder();
|
||||
for (String line : lines) {
|
||||
if (line.startsWith("-----BEGIN ") || line.startsWith("-----END ")) {
|
||||
continue;
|
||||
}
|
||||
String t = line.trim();
|
||||
if (!t.isEmpty()) {
|
||||
b64.append(t);
|
||||
}
|
||||
}
|
||||
return Base64.getDecoder().decode(b64.toString());
|
||||
}
|
||||
|
||||
private void putStatus(PkiId id, OperationStatus st) {
|
||||
this.statuses.put(id, st);
|
||||
for (NotificationSink sink : this.sinks.values()) {
|
||||
try {
|
||||
sink.onStatusChanged(id, st);
|
||||
} catch (Throwable ignore) { // NOPMD
|
||||
// sink must not break provider
|
||||
LOG.log(Level.FINE, "cannot put status, ignoring", ignore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static PkiId newOperationId() {
|
||||
return new PkiId(UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsed, provider-local representation of a {@link KeyRef}.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Security</h2>
|
||||
* <ul>
|
||||
* <li>This type must never contain private key material; it only carries alias
|
||||
* strings.</li>
|
||||
* <li>Instances must not be logged in production, as aliases can be sensitive
|
||||
* operational metadata.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* <p>
|
||||
* 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}.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Design constraints</h2>
|
||||
* <ul>
|
||||
* <li>{@code detailCode} must be non-sensitive and stable, suitable for audit
|
||||
* logs and transport-level APIs.</li>
|
||||
* <li>The exception message is set to {@code detailCode} for diagnostics only;
|
||||
* callers must not depend on it.</li>
|
||||
* <li>{@code cause} may be attached for local diagnostics but must not be used
|
||||
* to derive user-facing output.</li>
|
||||
* </ul>
|
||||
*/
|
||||
private static final class InvalidRequestException extends Exception {
|
||||
private static final long serialVersionUID = 2058759559902131220L;
|
||||
private final String detailCode;
|
||||
|
||||
private InvalidRequestException(String detailCode) {
|
||||
super(detailCode);
|
||||
this.detailCode = detailCode;
|
||||
}
|
||||
|
||||
private InvalidRequestException(String detailCode, Throwable cause) {
|
||||
super(detailCode, cause);
|
||||
this.detailCode = detailCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*******************************************************************************
|
||||
* 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 java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
import zeroecho.pki.spi.ProviderConfig;
|
||||
import zeroecho.pki.spi.crypto.SignatureWorkflow;
|
||||
import zeroecho.pki.spi.crypto.SignatureWorkflowProvider;
|
||||
|
||||
/**
|
||||
* Production provider bridging PKI signature workflow to ZeroEcho lib based on
|
||||
* {@code KeyringStore}.
|
||||
*
|
||||
* <p>
|
||||
* Configuration keys:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@code keyringPath} (required): filesystem path to the KeyringStore
|
||||
* file</li>
|
||||
* <li>{@code keyRefPrefix} (optional, default {@code "zeroecho-lib:"})</li>
|
||||
* <li>{@code requireComponentSuffix} (optional, default {@code true})</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Security</h2>
|
||||
* <p>
|
||||
* Configuration values may be sensitive and must not be logged. This provider
|
||||
* performs no value logging.
|
||||
* </p>
|
||||
*/
|
||||
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<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -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}.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* <h2>KeyRef mapping</h2>
|
||||
* <p>
|
||||
* This provider supports simple mappings intended for practical deployments:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@code zeroecho-lib:<alias>.prv} for signing</li>
|
||||
* <li>{@code zeroecho-lib:<alias>.pub} for verification</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Configuration</h2>
|
||||
* <ul>
|
||||
* <li>{@code keyringPath} (required): path to the KeyringStore text file</li>
|
||||
* <li>{@code keyRefPrefix} (optional, default {@code "zeroecho-lib:"})</li>
|
||||
* <li>{@code requireComponentSuffix} (optional, default {@code true}): require
|
||||
* {@code .prv}/{@code .pub}</li>
|
||||
* </ul>
|
||||
*/
|
||||
package zeroecho.pki.impl.crypto.zeroecholib;
|
||||
@@ -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.).
|
||||
* </p>
|
||||
*
|
||||
* <h2>System property conventions</h2>
|
||||
*
|
||||
* <ul>
|
||||
* <li>Select store provider: {@code -Dzeroecho.pki.store=<id>}</li>
|
||||
* <li>Configure store provider:
|
||||
* {@code -Dzeroecho.pki.store.<key>=<value>}</li>
|
||||
* <li>Select audit provider: {@code -Dzeroecho.pki.audit=<id>}</li>
|
||||
* <li>Configure audit provider:
|
||||
* {@code -Dzeroecho.pki.audit.<key>=<value>}</li>
|
||||
* <li>Select crypto workflow provider:
|
||||
* {@code -Dzeroecho.pki.crypto.workflow=<id>}</li>
|
||||
* <li>Configure crypto workflow provider:
|
||||
* {@code -Dzeroecho.pki.crypto.workflow.<key>=<value>}</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* Provider selection is deterministic and fail-fast:
|
||||
@@ -112,19 +123,8 @@ public final class PkiBootstrap {
|
||||
* {@code "pki-store"} relative to the working directory.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* <p>
|
||||
* Selection and configuration follow the same conventions as
|
||||
@@ -177,11 +177,14 @@ public final class PkiBootstrap {
|
||||
* {@code zeroecho.pki.audit.} prefixed properties.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* @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<String, String> 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.
|
||||
*
|
||||
* <p>
|
||||
* Conventions:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>Select provider: {@code -Dzeroecho.pki.crypto.workflow=<id>}</li>
|
||||
* <li>Provider config:
|
||||
* {@code -Dzeroecho.pki.crypto.workflow.<key>=<value>}</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Security note: configuration values may be sensitive and must not be logged.
|
||||
* This bootstrap logs only provider ids and configuration keys.
|
||||
* </p>
|
||||
*
|
||||
* @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<String, String> 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);
|
||||
}
|
||||
}
|
||||
|
||||
435
pki/src/main/java/zeroecho/pki/spi/crypto/SignatureWorkflow.java
Normal file
435
pki/src/main/java/zeroecho/pki/spi/crypto/SignatureWorkflow.java
Normal file
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Trust boundary</h2>
|
||||
* <ul>
|
||||
* <li><strong>PKI (caller):</strong> works only with {@link KeyRef}, algorithm
|
||||
* id and payload bytes; it must never parse {@code KeyRef} nor access private
|
||||
* key bytes.</li>
|
||||
* <li><strong>Provider (callee):</strong> resolves {@code KeyRef} into an
|
||||
* internal runtime key handle, enforces policy (including multi-hop approvals),
|
||||
* and performs cryptographic operations.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Failure model (normative - Variant 1)</h2>
|
||||
* <p>
|
||||
* This workflow is designed to be audit-friendly and transport-friendly (e.g.,
|
||||
* future HTTP APIs). Implementations must follow these rules:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>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()}.</li>
|
||||
* <li>{@link IllegalArgumentException} may be thrown only for programmer errors
|
||||
* such as {@code request == null} or {@code operationId == null}. These are not
|
||||
* business/policy failures.</li>
|
||||
* <li>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.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public interface SignatureWorkflow extends Closeable {
|
||||
|
||||
/**
|
||||
* Provider identifier for diagnostics and audit correlation.
|
||||
*
|
||||
* @return stable provider id (never blank)
|
||||
*/
|
||||
String id();
|
||||
|
||||
/**
|
||||
* Submits a signing request.
|
||||
*
|
||||
* <p>
|
||||
* Providers must never return private key material. The output is a signature
|
||||
* (encoded) on success.
|
||||
* </p>
|
||||
*
|
||||
* <h4>Failure model (normative)</h4>
|
||||
* <ul>
|
||||
* <li>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}.</li>
|
||||
* <li>May throw {@link IllegalArgumentException} only for programmer errors
|
||||
* (e.g., {@code request == null}).</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param request request (never {@code null})
|
||||
* @return operation id (never {@code null})
|
||||
*/
|
||||
PkiId submitSign(SignRequest request);
|
||||
|
||||
/**
|
||||
* Submits a verification request.
|
||||
*
|
||||
* <p>
|
||||
* Verification typically uses public key material. For maximal portability
|
||||
* across PQC/hybrid algorithms, verification requests can either:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>reference a public key via {@link KeyRef} (preferred), or</li>
|
||||
* <li>provide encoded public key bytes (provider-defined format; may be
|
||||
* unsupported by some providers).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h4>Failure model (normative)</h4>
|
||||
* <ul>
|
||||
* <li>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}.</li>
|
||||
* <li>May throw {@link IllegalArgumentException} only for programmer errors
|
||||
* (e.g., {@code request == null}).</li>
|
||||
* </ul>
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* <p>
|
||||
* Implementations may invoke the sink from provider-owned threads. The sink
|
||||
* must not throw.
|
||||
* </p>
|
||||
*
|
||||
* @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<String> supportedAlgorithms();
|
||||
|
||||
@Override
|
||||
void close();
|
||||
|
||||
/**
|
||||
* Signing request.
|
||||
*
|
||||
* <p>
|
||||
* 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)}.
|
||||
* </p>
|
||||
*
|
||||
* @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<Encoding> preferredSignatureEncoding, Optional<Instant> 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.
|
||||
*
|
||||
* <p>
|
||||
* 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)}.
|
||||
* </p>
|
||||
*
|
||||
* @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<KeyRef> publicKeyRef, Optional<EncodedObject> publicKeyEncoded,
|
||||
Optional<Instant> 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.
|
||||
*
|
||||
* <p>
|
||||
* 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)}.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Failure and audit model</h2>
|
||||
* <ul>
|
||||
* <li>Operational, validation, or policy failures are reported via
|
||||
* {@link State#FAILED}, not via uncaught exceptions.</li>
|
||||
* <li>{@link #detailCode()} provides a stable, non-sensitive classification of
|
||||
* the outcome and is intended for audit logs, diagnostics, and transport-level
|
||||
* APIs.</li>
|
||||
* <li>{@link #detailCode()} must never contain secrets, key identifiers,
|
||||
* payload data, or provider-internal state.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Terminal states</h2>
|
||||
* <p>
|
||||
* Once an operation reaches a terminal state (see {@link #isTerminal()}), its
|
||||
* {@code OperationStatus} must no longer change.
|
||||
* </p>
|
||||
*
|
||||
* @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<String> detailCode,
|
||||
Optional<OperationResult> 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.
|
||||
*
|
||||
* <p>
|
||||
* Terminal states are final and must not transition to any other state.
|
||||
* </p>
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* <p>
|
||||
* The state machine is intentionally minimal to accommodate both local
|
||||
* (in-process) and remote or multi-hop providers.
|
||||
* </p>
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link #PENDING} – operation accepted but not yet actively processed</li>
|
||||
* <li>{@link #RUNNING} – operation is currently being processed</li>
|
||||
* <li>{@link #WAITING_APPROVAL} – operation is suspended awaiting external
|
||||
* approval (e.g. human review, policy gate, HSM quorum)</li>
|
||||
* <li>{@link #SUCCEEDED} – operation completed successfully</li>
|
||||
* <li>{@link #FAILED} – operation permanently failed due to validation, policy,
|
||||
* or execution error</li>
|
||||
* <li>{@link #CANCELLED} – operation was explicitly cancelled by the
|
||||
* caller</li>
|
||||
* <li>{@link #EXPIRED} – operation expired before completion</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Providers must document which transitions they support and under which
|
||||
* conditions a given state may be observed.
|
||||
* </p>
|
||||
*/
|
||||
enum State {
|
||||
PENDING, RUNNING, WAITING_APPROVAL, SUCCEEDED, FAILED, CANCELLED, EXPIRED
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminal result of a signature workflow operation.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* <ul>
|
||||
* <li>For sign operations, {@link #signature()} contains the generated
|
||||
* signature.</li>
|
||||
* <li>For verify operations, {@link #verified()} indicates verification
|
||||
* outcome.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Exactly one of {@code signature} or {@code verified} is typically present,
|
||||
* depending on the operation type.
|
||||
* </p>
|
||||
*
|
||||
* @param signature optional signature artifact for successful sign operations
|
||||
* @param verified optional verification result for successful verify
|
||||
* operations
|
||||
*/
|
||||
record OperationResult(Optional<EncodedObject> signature, Optional<Boolean> verified) {
|
||||
|
||||
public OperationResult {
|
||||
Objects.requireNonNull(signature, "signature");
|
||||
Objects.requireNonNull(verified, "verified");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback interface for receiving asynchronous status updates.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* 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)}.
|
||||
* </p>
|
||||
*/
|
||||
@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}.
|
||||
*
|
||||
* <p>
|
||||
* Closing the registration unregisters the associated sink. The operation is
|
||||
* idempotent and must not throw.
|
||||
* </p>
|
||||
*/
|
||||
@SuppressWarnings("PMD.ImplicitFunctionalInterface")
|
||||
interface Registration extends AutoCloseable {
|
||||
|
||||
/**
|
||||
* Unregisters the associated {@link NotificationSink}.
|
||||
*/
|
||||
@Override
|
||||
void close();
|
||||
}
|
||||
}
|
||||
@@ -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<SignatureWorkflow> {
|
||||
// marker
|
||||
}
|
||||
64
pki/src/main/java/zeroecho/pki/spi/crypto/package-info.java
Normal file
64
pki/src/main/java/zeroecho/pki/spi/crypto/package-info.java
Normal file
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Trust boundary</h2>
|
||||
* <ul>
|
||||
* <li><strong>PKI (caller):</strong> submits sign/verify requests using
|
||||
* {@code KeyRef}, algorithm id, and payload bytes. It must not parse nor
|
||||
* interpret {@code KeyRef}.</li>
|
||||
* <li><strong>Crypto provider (callee):</strong> 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).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Security constraints</h2>
|
||||
* <ul>
|
||||
* <li>{@code KeyRef} is sensitive metadata and must not be logged.</li>
|
||||
* <li>Payload bytes and signature bytes must not be logged.</li>
|
||||
* <li>Implementations must avoid persisting secrets; audit evidence must
|
||||
* contain only non-secret metadata.</li>
|
||||
* </ul>
|
||||
*/
|
||||
package zeroecho.pki.spi.crypto;
|
||||
@@ -0,0 +1 @@
|
||||
zeroecho.pki.impl.crypto.zeroecholib.ZeroEchoLibSignatureWorkflowProvider
|
||||
@@ -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("<none>"));
|
||||
|
||||
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("<none>"));
|
||||
|
||||
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("<none>"));
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user