security(lib,storage): enforce single-use encryption contexts, encrypt keyring and harden key import

This commit is contained in:
2026-07-29 23:18:22 +02:00
parent 9bbcab7522
commit 8b2f3df41f
64 changed files with 7473 additions and 1799 deletions

View File

@@ -91,6 +91,8 @@ import zeroecho.core.io.TailStrippingInputStream;
import zeroecho.core.spec.AlgorithmKeySpec;
import zeroecho.core.spec.ContextSpec;
import zeroecho.core.storage.KeyringStore;
import zeroecho.core.storage.KeyringPassword;
import zeroecho.core.spi.KeyringUnlockProvider;
import zeroecho.sdk.ZeroEchoSession;
import zeroecho.pki.api.EncodedObject;
import zeroecho.pki.api.Encoding;
@@ -275,6 +277,7 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
private final String keyRefPrefix;
private final boolean requireComponentSuffix;
private final ZeroEchoSession session;
private final KeyringUnlockProvider keyringUnlockProvider;
private final ConcurrentMap<PkiId, OperationStatus> statuses;
private final ConcurrentMap<PkiId, String> fingerprints;
@@ -288,19 +291,33 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
private final ReentrantLock timeWatermarkLock;
private final AtomicReference<String> boundNamespace;
private final ReentrantLock domainLock;
private final ReentrantLock keyringLifecycleLock;
private final BiConsumer<String, byte[]> cleanupObserver;
private volatile KeyringStore keyringOrNull; // NOPMD
private boolean closed;
/* default */ ZeroEchoLibSignatureWorkflow(String id, Path keyringPath, Path operationRoot, Clock clock,
Duration operationHorizon, String keyRefPrefix, boolean requireComponentSuffix) {
Duration operationHorizon, String keyRefPrefix, boolean requireComponentSuffix,
KeyringUnlockProvider keyringUnlockProvider) {
this(id, keyringPath, operationRoot, clock, operationHorizon, keyRefPrefix, requireComponentSuffix,
keyringUnlockProvider,
(category, cleared) -> {
});
}
/* default */ ZeroEchoLibSignatureWorkflow(String id, Path keyringPath, Path operationRoot, Clock clock,
Duration operationHorizon, String keyRefPrefix, boolean requireComponentSuffix,
KeyringUnlockProvider keyringUnlockProvider, KeyringStore keyring) {
this(id, keyringPath, operationRoot, clock, operationHorizon, keyRefPrefix,
requireComponentSuffix, keyringUnlockProvider);
this.keyringOrNull = java.util.Objects.requireNonNull(
keyring, "keyring must not be null");
}
/* default */ ZeroEchoLibSignatureWorkflow(String id, Path keyringPath, Path operationRoot, Clock clock,
Duration operationHorizon, String keyRefPrefix, boolean requireComponentSuffix,
KeyringUnlockProvider keyringUnlockProvider,
BiConsumer<String, byte[]> cleanupObserver) {
if (id == null || id.isBlank()) {
throw new IllegalArgumentException("id must not be blank");
@@ -323,6 +340,9 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
if (cleanupObserver == null) {
throw new IllegalArgumentException("cleanupObserver must not be null");
}
if (keyringUnlockProvider == null) {
throw new IllegalArgumentException("keyringUnlockProvider must not be null");
}
this.id = id;
this.keyringPath = keyringPath;
this.operationRoot = operationRoot.toAbsolutePath().normalize();
@@ -331,6 +351,7 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
this.keyRefPrefix = keyRefPrefix;
this.requireComponentSuffix = requireComponentSuffix;
this.cleanupObserver = cleanupObserver;
this.keyringUnlockProvider = keyringUnlockProvider;
this.session = new ZeroEchoSession();
this.statuses = new ConcurrentHashMap<>();
@@ -340,6 +361,7 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
this.sinks = new ConcurrentHashMap<>();
this.operationLocks = new ConcurrentHashMap<>();
this.domainLock = new ReentrantLock();
this.keyringLifecycleLock = new ReentrantLock();
this.timeWatermarkLock = new ReentrantLock();
try {
Files.createDirectories(this.operationRoot);
@@ -451,6 +473,7 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
* @throws IllegalArgumentException if {@code request} is {@code null}
*/
@Override
@SuppressWarnings("PMD.CloseResource")
public PkiId submitSign(SignRequest request) {
if (request == null) {
throw new IllegalArgumentException("request must not be null");
@@ -837,13 +860,28 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
* </p>
*/
@Override
@SuppressWarnings("PMD.CloseResource")
public void close() {
KeyringStore keyring;
keyringLifecycleLock.lock();
try {
if (closed) {
return;
}
closed = true;
keyring = this.keyringOrNull;
this.keyringOrNull = null;
} finally {
keyringLifecycleLock.unlock();
}
this.statuses.clear();
this.fingerprints.clear();
this.fences.clear();
this.requests.clear();
this.sinks.clear();
this.keyringOrNull = null;
if (keyring != null) {
keyring.close();
}
try {
this.ownershipLock.release();
this.ownershipChannel.close();
@@ -852,14 +890,37 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
}
}
private KeyringStore requireKeyringOrThrow() throws IOException {
KeyringStore ks = this.keyringOrNull;
if (ks != null) {
return ks;
@SuppressWarnings("PMD.CloseResource")
private KeyringStore requireKeyringOrThrow() throws IOException, GeneralSecurityException {
keyringLifecycleLock.lock();
try {
if (closed) {
throw new IOException("Signature workflow is closed");
}
KeyringStore ks = this.keyringOrNull;
if (ks != null) {
return ks;
}
try (KeyringPassword password = acquireKeyringPassword()) {
KeyringStore loaded = KeyringStore.open(this.keyringPath, password);
if (closed) {
loaded.close();
throw new IOException("Signature workflow is closed");
}
this.keyringOrNull = loaded;
return loaded;
}
} finally {
keyringLifecycleLock.unlock();
}
KeyringStore loaded = KeyringStore.load(this.session, this.keyringPath);
this.keyringOrNull = loaded;
return loaded;
}
private KeyringPassword acquireKeyringPassword() throws IOException {
KeyringPassword password = this.keyringUnlockProvider.acquire();
if (password == null) {
throw new IOException("Keyring unlock provider returned no password");
}
return password;
}
private static void enforceAlgorithmMatchOrThrow(String requested, String stored) throws InvalidRequestException {
@@ -900,6 +961,7 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
return new KeyRefParts(publicAlias, publicAlias);
}
@SuppressWarnings("PMD.CloseResource")
private PublicKey resolvePublicKeyOrThrow(VerifyRequest request)
throws InvalidRequestException, IOException, GeneralSecurityException {

View File

@@ -33,16 +33,23 @@
******************************************************************************/
package zeroecho.pki.impl.crypto.zeroecholib;
import java.io.IOException;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.time.Clock;
import java.time.Duration;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.core.spi.KeyringUnlockProvider;
import zeroecho.core.storage.KeyringPassword;
import zeroecho.core.storage.KeyringStore;
import zeroecho.pki.api.PkiException;
import zeroecho.pki.spi.ProviderConfig;
import zeroecho.pki.spi.crypto.SignatureWorkflow;
import zeroecho.pki.spi.crypto.SignatureWorkflowProvider;
import zeroecho.pki.spi.crypto.SignatureWorkflowRuntimeDependencies;
/**
* Production provider bridging PKI signature workflow to ZeroEcho lib based on
@@ -69,6 +76,15 @@ import zeroecho.pki.spi.crypto.SignatureWorkflowProvider;
* </p>
*/
public final class ZeroEchoLibSignatureWorkflowProvider implements SignatureWorkflowProvider {
/** Stable failure code for a missing explicit keyring unlock provider. */
public static final String DC_KEYRING_UNLOCK_PROVIDER_REQUIRED =
"KEYRING_UNLOCK_PROVIDER_REQUIRED";
/** Stable failure code for an unlock-provider acquisition failure. */
public static final String DC_KEYRING_UNLOCK_PROVIDER_FAILED =
"KEYRING_UNLOCK_PROVIDER_FAILED";
/** Stable failure code for an I/O failure while opening the keyring. */
public static final String DC_KEYRING_OPEN_FAILED = "KEYRING_OPEN_FAILED";
private static final Logger LOG = Logger.getLogger(ZeroEchoLibSignatureWorkflowProvider.class.getName());
private static final String KEY_KEYRING_PATH = "keyringPath";
@@ -76,6 +92,29 @@ public final class ZeroEchoLibSignatureWorkflowProvider implements SignatureWork
private static final String KEY_OPERATION_HORIZON = "operationHorizon";
private static final String KEY_KEYREF_PREFIX = "keyRefPrefix";
private static final String KEY_REQUIRE_SUFFIX = "requireComponentSuffix";
private final KeyringUnlockProvider keyringUnlockProvider;
/**
* Creates a service-loadable provider without unlock material.
*
* <p>{@link #allocate(ProviderConfig)} fails until an explicitly injected
* provider instance is used. Service configuration text can never contain
* an unlock secret.</p>
*/
public ZeroEchoLibSignatureWorkflowProvider() {
this.keyringUnlockProvider = null;
}
/**
* Creates a provider with an explicit headless unlock source.
*
* @param keyringUnlockProvider provider returning a fresh destroyable
* password for each keyring open
*/
public ZeroEchoLibSignatureWorkflowProvider(KeyringUnlockProvider keyringUnlockProvider) {
this.keyringUnlockProvider = java.util.Objects.requireNonNull(
keyringUnlockProvider, "keyringUnlockProvider must not be null");
}
@Override
public String id() {
@@ -127,6 +166,38 @@ public final class ZeroEchoLibSignatureWorkflowProvider implements SignatureWork
@Override
public SignatureWorkflow allocate(final ProviderConfig config) {
KeyringUnlockProvider provider = keyringUnlockProvider;
if (provider == null) {
throw new PkiException(DC_KEYRING_UNLOCK_PROVIDER_REQUIRED);
}
return allocate(config, provider);
}
/**
* Allocates a workflow using explicit runtime dependencies.
*
* @param config structural provider configuration
* @param dependencies explicit process-local runtime dependencies
* @return opened workflow owning an unlocked keyring
* @throws PkiException if the keyring unlock provider is absent or fails
* @throws RuntimeException if workflow allocation otherwise fails
*/
@Override
public SignatureWorkflow allocate(final ProviderConfig config,
SignatureWorkflowRuntimeDependencies dependencies) {
java.util.Objects.requireNonNull(dependencies, "dependencies must not be null");
KeyringUnlockProvider provider = dependencies.keyringUnlockProvider()
.orElse(keyringUnlockProvider);
if (provider == null) {
throw new PkiException(DC_KEYRING_UNLOCK_PROVIDER_REQUIRED);
}
return allocate(config, provider);
}
// Cleanup must cover every constructor failure, including unchecked failures.
@SuppressWarnings("PMD.AvoidCatchingGenericException")
private SignatureWorkflow allocate(final ProviderConfig config,
KeyringUnlockProvider unlockProvider) {
validateConfig(config);
String keyringPath = config.require(KEY_KEYRING_PATH);
Path operationRoot = Path.of(config.require(KEY_OPERATION_ROOT));
@@ -134,7 +205,42 @@ public final class ZeroEchoLibSignatureWorkflowProvider implements SignatureWork
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), operationRoot, Clock.systemUTC(),
operationHorizon, prefix, requireSuffix);
KeyringStore keyring = openKeyring(Path.of(keyringPath), unlockProvider);
try {
return new ZeroEchoLibSignatureWorkflow(id(), Path.of(keyringPath), operationRoot,
Clock.systemUTC(), operationHorizon, prefix, requireSuffix,
unlockProvider, keyring);
} catch (RuntimeException | Error failure) {
keyring.close();
throw failure;
}
}
/*
* The unlock provider is arbitrary application code. Its throwable message
* and cause are intentionally removed at this security boundary.
*/
@SuppressWarnings({
"PMD.AvoidCatchingGenericException",
"PMD.PreserveStackTrace"
})
private static KeyringStore openKeyring(Path keyringPath,
KeyringUnlockProvider unlockProvider) {
KeyringPassword password;
try {
password = unlockProvider.acquire();
} catch (IOException | RuntimeException failure) {
throw new PkiException(DC_KEYRING_UNLOCK_PROVIDER_FAILED);
}
if (password == null) {
throw new PkiException(DC_KEYRING_UNLOCK_PROVIDER_FAILED);
}
try (password) {
try {
return KeyringStore.open(keyringPath, password);
} catch (IOException | GeneralSecurityException failure) {
throw new PkiException(DC_KEYRING_OPEN_FAILED);
}
}
}
}

View File

@@ -48,6 +48,7 @@ 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.crypto.SignatureWorkflowRuntimeDependencies;
import zeroecho.pki.spi.framework.CredentialFramework;
import zeroecho.pki.spi.framework.CredentialFrameworkProvider;
import zeroecho.pki.spi.store.PkiStore;
@@ -203,9 +204,19 @@ public final class PkiBootstrap {
* Opens a {@link SignatureWorkflow} using {@link SignatureWorkflowProvider}
* discovered via ServiceLoader.
*
* <p>Runtime capabilities are supplied explicitly and are not represented
* in system properties or {@link ProviderConfig}. Providers that do not use
* a software keyring ignore an absent keyring dependency; a keyring-backed
* provider rejects it before allocating a workflow.</p>
*
* @param dependencies explicit process-local runtime dependencies
* @return signature workflow (never {@code null})
* @throws NullPointerException if {@code dependencies} is {@code null}
* @throws RuntimeException if provider selection or workflow allocation fails
*/
public static SignatureWorkflow openSignatureWorkflow() {
public static SignatureWorkflow openSignatureWorkflow(
SignatureWorkflowRuntimeDependencies dependencies) {
Objects.requireNonNull(dependencies, "dependencies must not be null");
String requestedId = System.getProperty(PROP_CRYPTO_WORKFLOW_BACKEND);
SignatureWorkflowProvider provider = SpiSelector.select(SignatureWorkflowProvider.class, requestedId,
@@ -224,7 +235,7 @@ public final class PkiBootstrap {
LOG.info("Selected crypto workflow provider: " + provider.id() + " (keys: " + props.keySet() + ")");
}
return provider.allocate(config);
return provider.allocate(config, dependencies);
}
/**

View File

@@ -33,11 +33,37 @@
******************************************************************************/
package zeroecho.pki.spi.crypto;
import java.util.Objects;
import zeroecho.pki.spi.ProviderConfig;
import zeroecho.pki.spi.ConfigurableProvider;
/**
* ServiceLoader provider for {@link SignatureWorkflow}.
*
* <p>Service loading discovers provider factories only. Runtime capabilities,
* including keyring unlock providers, are supplied explicitly through
* {@link #allocate(ProviderConfig, SignatureWorkflowRuntimeDependencies)} and
* are never stored in textual provider configuration.</p>
*/
public interface SignatureWorkflowProvider extends ConfigurableProvider<SignatureWorkflow> {
// marker
/**
* Allocates a workflow using explicit process-local runtime dependencies.
*
* <p>The default implementation supports providers that need no additional
* runtime capability. A provider requiring a software keyring must override
* this method and reject an absent unlock provider before opening any
* workflow resource.</p>
*
* @param config structural provider configuration
* @param dependencies explicit process-local runtime dependencies
* @return allocated workflow
* @throws NullPointerException if {@code dependencies} is {@code null}
* @throws RuntimeException if allocation fails
*/
default SignatureWorkflow allocate(ProviderConfig config,
SignatureWorkflowRuntimeDependencies dependencies) {
Objects.requireNonNull(dependencies, "dependencies must not be null");
return allocate(config);
}
}

View File

@@ -0,0 +1,65 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
******************************************************************************/
package zeroecho.pki.spi.crypto;
import java.util.Objects;
import java.util.Optional;
import zeroecho.core.spi.KeyringUnlockProvider;
/**
* Immutable runtime dependencies supplied when opening a signature workflow.
*
* <p>These dependencies are process-local capabilities. They are never encoded
* in {@link zeroecho.pki.spi.ProviderConfig}, persisted, or discovered through
* {@link java.util.ServiceLoader}. An absent keyring unlock provider is valid
* only for workflow implementations that do not use a software keyring.</p>
*
* <p>Instances are immutable and safe for concurrent use. This object does not
* acquire or retain password material.</p>
*/
public final class SignatureWorkflowRuntimeDependencies {
private static final SignatureWorkflowRuntimeDependencies NONE =
new SignatureWorkflowRuntimeDependencies(null);
private final KeyringUnlockProvider keyringUnlockProvider;
private SignatureWorkflowRuntimeDependencies(KeyringUnlockProvider keyringUnlockProvider) {
this.keyringUnlockProvider = keyringUnlockProvider;
}
/**
* Returns dependencies without a software-keyring unlock provider.
*
* @return immutable empty runtime dependencies
*/
public static SignatureWorkflowRuntimeDependencies none() {
return NONE;
}
/**
* Creates dependencies containing an explicit software-keyring unlock
* provider.
*
* @param provider provider returning a fresh destroyable password for each
* keyring open
* @return immutable runtime dependencies containing {@code provider}
* @throws NullPointerException if {@code provider} is {@code null}
*/
public static SignatureWorkflowRuntimeDependencies withKeyringUnlockProvider(
KeyringUnlockProvider provider) {
return new SignatureWorkflowRuntimeDependencies(
Objects.requireNonNull(provider, "provider must not be null"));
}
/**
* Returns the explicitly supplied software-keyring unlock provider.
*
* @return provider when one was supplied, otherwise an empty optional
*/
public Optional<KeyringUnlockProvider> keyringUnlockProvider() {
return Optional.ofNullable(keyringUnlockProvider);
}
}

View File

@@ -0,0 +1,14 @@
package zeroecho.pki.impl.crypto.zeroecholib;
import zeroecho.core.spi.KeyringUnlockProvider;
import zeroecho.core.storage.KeyringPassword;
public final class TestKeyringUnlocks {
private TestKeyringUnlocks() {
}
public static KeyringUnlockProvider provider() {
return () -> new KeyringPassword(
new char[] { 'p', 'k', 'i', '-', 't', 'e', 's', 't', '-', 'k', 'e', 'y' });
}
}

View File

@@ -67,7 +67,7 @@ public final class ZeroEchoLibKeyRefParsingTest {
System.out.println("signing_requires_prv_suffix_in_strict_mode_ok");
try (ZeroEchoLibSignatureWorkflow wf = new ZeroEchoLibSignatureWorkflow("zeroecho-lib", Path.of("nonexistent"),
tempDir.resolve("operations-1"), Clock.systemUTC(), Duration.ofDays(90), "zeroecho-lib:", true)) {
tempDir.resolve("operations-1"), Clock.systemUTC(), Duration.ofDays(90), "zeroecho-lib:", true, TestKeyringUnlocks.provider())) {
AccessContext ctx = new AccessContext(new Principal("TEST", "unit"), new Purpose("UNIT_TEST"),
Optional.empty(), Optional.empty());
@@ -95,7 +95,7 @@ public final class ZeroEchoLibKeyRefParsingTest {
System.out.println("verify_with_publicKeyEncoded_invalid_spki_fails_with_crypto_failure_ok");
try (ZeroEchoLibSignatureWorkflow wf = new ZeroEchoLibSignatureWorkflow("zeroecho-lib", Path.of("nonexistent"),
tempDir.resolve("operations-2"), Clock.systemUTC(), Duration.ofDays(90), "zeroecho-lib:", true)) {
tempDir.resolve("operations-2"), Clock.systemUTC(), Duration.ofDays(90), "zeroecho-lib:", true, TestKeyringUnlocks.provider())) {
AccessContext ctx = new AccessContext(new Principal("TEST", "unit"), new Purpose("UNIT_TEST"),
Optional.empty(), Optional.empty());
@@ -123,7 +123,7 @@ public final class ZeroEchoLibKeyRefParsingTest {
System.out.println("status_unknown_operation_is_deterministic_ok");
try (ZeroEchoLibSignatureWorkflow wf = new ZeroEchoLibSignatureWorkflow("zeroecho-lib", Path.of("nonexistent"),
tempDir.resolve("operations-3"), Clock.systemUTC(), Duration.ofDays(90), "zeroecho-lib:", true)) {
tempDir.resolve("operations-3"), Clock.systemUTC(), Duration.ofDays(90), "zeroecho-lib:", true, TestKeyringUnlocks.provider())) {
PkiId unknown = new PkiId("00000000-0000-0000-0000-000000000000");
SignatureWorkflow.OperationStatus st = wf.status(unknown);

View File

@@ -90,10 +90,12 @@ final class ZeroEchoLibSignatureWorkflowPersistenceTest {
Instant now = Instant.parse("2026-02-03T04:05:06.789Z");
Path keyring = root.resolve("keyring.txt");
KeyPair pair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
KeyringStore keyringStore = new KeyringStore(new zeroecho.sdk.ZeroEchoSession());
keyringStore.putPrivate("test.prv", "RSA", new RsaPrivateKeySpec(pair.getPrivate().getEncoded()));
keyringStore.putPublic("test.pub", "RSA", new RsaPublicKeySpec(pair.getPublic().getEncoded()));
keyringStore.save(keyring);
try (zeroecho.core.storage.KeyringPassword password =
TestKeyringUnlocks.provider().acquire();
KeyringStore keyringStore = KeyringStore.create(keyring, password)) {
keyringStore.putPrivate("test.prv", "RSA", pair.getPrivate());
keyringStore.putPublic("test.pub", "RSA", pair.getPublic());
}
List<ObservedBuffer> cleared = new ArrayList<>();
List<LogRecord> records = new ArrayList<>();
@@ -120,7 +122,7 @@ final class ZeroEchoLibSignatureWorkflowPersistenceTest {
logger.addHandler(handler);
try (ZeroEchoLibSignatureWorkflow workflow = new ZeroEchoLibSignatureWorkflow("zeroecho-lib", keyring,
root.resolve("cleanup-operations"), Clock.fixed(now, ZoneOffset.UTC), Duration.ofDays(90),
"zeroecho-lib:", true, (category, bytes) -> cleared.add(new ObservedBuffer(category, bytes)));
"zeroecho-lib:", true, TestKeyringUnlocks.provider(), (category, bytes) -> cleared.add(new ObservedBuffer(category, bytes)));
SignatureWorkflow.Registration registration = workflow.register((operationId, status) -> {
throw new IllegalStateException("DO_NOT_LOG_SIGNATURE_SENTINEL");
})) {
@@ -194,10 +196,12 @@ final class ZeroEchoLibSignatureWorkflowPersistenceTest {
Instant base = Instant.parse("2026-02-03T04:05:06Z");
Path keyring = root.resolve("keyring.txt");
KeyPair pair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
KeyringStore keyringStore = new KeyringStore(new zeroecho.sdk.ZeroEchoSession());
keyringStore.putPrivate("test.prv", "RSA", new RsaPrivateKeySpec(pair.getPrivate().getEncoded()));
keyringStore.putPublic("test.pub", "RSA", new RsaPublicKeySpec(pair.getPublic().getEncoded()));
keyringStore.save(keyring);
try (zeroecho.core.storage.KeyringPassword password =
TestKeyringUnlocks.provider().acquire();
KeyringStore keyringStore = KeyringStore.create(keyring, password)) {
keyringStore.putPrivate("test.prv", "RSA", pair.getPrivate());
keyringStore.putPublic("test.pub", "RSA", pair.getPublic());
}
PkiId onTimeId = SigningSubmissionId.create(NAMESPACE, base, new SecureRandom()).id();
Clock onTimeClock = Clock.fixed(base, ZoneOffset.UTC);
@@ -360,7 +364,7 @@ final class ZeroEchoLibSignatureWorkflowPersistenceTest {
private static ZeroEchoLibSignatureWorkflow workflow(Path root, Path operations, Path keyring, Clock clock) {
return new ZeroEchoLibSignatureWorkflow("zeroecho-lib", keyring, operations, clock,
Duration.ofDays(90), "zeroecho-lib:", true);
Duration.ofDays(90), "zeroecho-lib:", true, TestKeyringUnlocks.provider());
}
private static SignatureWorkflow.SignRequest request(PkiId id, long fence, byte[] payload) {

View File

@@ -71,11 +71,14 @@ public final class ZeroEchoLibSignatureWorkflowVerifyEncodedEcdsaTest {
System.out.println("verifyFromSpkiDerEcdsaSucceeds");
Path keyring = tempDir.resolve("keyring.txt");
KeyringStore ks = new KeyringStore(new zeroecho.sdk.ZeroEchoSession());
ks.save(keyring);
try (zeroecho.core.storage.KeyringPassword password =
TestKeyringUnlocks.provider().acquire();
KeyringStore ignored = KeyringStore.create(keyring, password)) {
// Empty keyring is sufficient for encoded-key verification.
}
try (ZeroEchoLibSignatureWorkflow wf = new ZeroEchoLibSignatureWorkflow("wf", keyring,
tempDir.resolve("operations"), Clock.systemUTC(), Duration.ofDays(90), "zeroecho-lib:", true)) {
tempDir.resolve("operations"), Clock.systemUTC(), Duration.ofDays(90), "zeroecho-lib:", true, TestKeyringUnlocks.provider())) {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
kpg.initialize(new ECGenParameterSpec("secp256r1"));
KeyPair kp = kpg.generateKeyPair();

View File

@@ -66,11 +66,14 @@ public final class ZeroEchoLibSignatureWorkflowVerifyEncodedTest {
System.out.println("verifyFromSpkiDerSucceeds");
Path keyring = tempDir.resolve("keyring.txt");
KeyringStore ks = new KeyringStore(new zeroecho.sdk.ZeroEchoSession());
ks.save(keyring);
try (zeroecho.core.storage.KeyringPassword password =
TestKeyringUnlocks.provider().acquire();
KeyringStore ignored = KeyringStore.create(keyring, password)) {
// Empty keyring is sufficient for encoded-key verification.
}
try (ZeroEchoLibSignatureWorkflow wf = new ZeroEchoLibSignatureWorkflow("wf", keyring,
tempDir.resolve("operations"), Clock.systemUTC(), Duration.ofDays(90), "zeroecho-lib:", true)) {
tempDir.resolve("operations"), Clock.systemUTC(), Duration.ofDays(90), "zeroecho-lib:", true, TestKeyringUnlocks.provider())) {
KeyPair kp = KeyPairGenerator.getInstance("RSA").generateKeyPair();
byte[] payload = "pqc-ready".getBytes(java.nio.charset.StandardCharsets.UTF_8);

View File

@@ -48,6 +48,8 @@ import org.bouncycastle.asn1.x500.X500Name;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import zeroecho.core.storage.KeyringPassword;
import zeroecho.core.storage.KeyringStore;
import zeroecho.pki.api.EncodedObject;
import zeroecho.pki.api.Encoding;
import zeroecho.pki.api.issuance.VerificationPolicy;
@@ -68,8 +70,12 @@ public final class WorkflowProofOfPossessionVerifierTest {
public void verifyRsaCsrViaWorkflow_ok() throws Exception {
System.out.println("verifyRsaCsrViaWorkflow_ok");
Path keyringPath = tempDir.resolve("keyring.txt");
java.nio.file.Files.writeString(keyringPath, "", java.nio.charset.StandardCharsets.UTF_8);
Path keyringPath = tempDir.resolve("keyring.zek");
try (KeyringPassword password =
zeroecho.pki.impl.crypto.zeroecholib.TestKeyringUnlocks.provider().acquire();
KeyringStore ignored = KeyringStore.create(keyringPath, password)) {
// Encoded-key verification needs no persisted key entry.
}
System.out.println("...keyringPath=" + keyringPath.getFileName());
KeyPair kp = KeyPairGenerator.getInstance("RSA").generateKeyPair();
@@ -86,7 +92,9 @@ public final class WorkflowProofOfPossessionVerifierTest {
ParsedCertificationRequest parsed = new BcX509CertificationRequestParser().parse(req);
ZeroEchoLibSignatureWorkflowProvider provider = new ZeroEchoLibSignatureWorkflowProvider();
ZeroEchoLibSignatureWorkflowProvider provider =
new ZeroEchoLibSignatureWorkflowProvider(
zeroecho.pki.impl.crypto.zeroecholib.TestKeyringUnlocks.provider());
ProviderConfig cfg = new ProviderConfig(provider.id(), Map.of("keyringPath", keyringPath.toString(),
"operationRoot", tempDir.resolve("signing-operations").toString()));
SignatureWorkflow wf = provider.allocate(cfg);

View File

@@ -34,21 +34,52 @@
package zeroecho.pki.spi.bootstrap;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import zeroecho.core.spi.KeyringUnlockProvider;
import zeroecho.core.storage.KeyringPassword;
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.api.audit.Principal;
import zeroecho.pki.api.audit.AccessContext;
import zeroecho.pki.api.audit.Purpose;
import zeroecho.pki.api.orch.SigningSubmissionId;
import zeroecho.pki.api.PkiException;
import zeroecho.pki.impl.crypto.zeroecholib.ZeroEchoLibSignatureWorkflowProvider;
import zeroecho.pki.spi.ProviderConfig;
import zeroecho.pki.spi.audit.AuditSink;
import zeroecho.pki.spi.crypto.SignatureWorkflow;
import zeroecho.pki.spi.crypto.SignatureWorkflowRuntimeDependencies;
import zeroecho.pki.spi.framework.CredentialFramework;
import zeroecho.pki.spi.store.PkiStore;
import zeroecho.pki.util.async.AsyncBus;
@@ -68,6 +99,10 @@ import zeroecho.pki.util.async.AsyncBus;
* </p>
*/
public final class PkiBootstrapTest {
private static final char[] KEYRING_PASSWORD =
{ 'b', 'o', 'o', 't', 's', 't', 'r', 'a', 'p', '-', 't', 'e', 's', 't' };
private static final String SIGNING_NAMESPACE =
"0123456789abcdef0123456789abcdef.zeroecho-lib";
@TempDir
private Path tempDir;
@@ -257,24 +292,192 @@ public final class PkiBootstrapTest {
}
@Test
public void openSignatureWorkflow_zeroEchoLib_usesConfiguredKeyringPath() {
public void openSignatureWorkflow_zeroEchoLib_usesConfiguredKeyringPath()
throws Exception {
System.out.println("openSignatureWorkflow_zeroEchoLib_usesConfiguredKeyringPath");
Path keyringPath = this.tempDir.resolve("workflow").resolve("keyring.zek");
createSigningKeyring(keyringPath);
System.setProperty("zeroecho.pki.crypto.workflow", "zeroecho-lib");
System.setProperty("zeroecho.pki.crypto.workflow.keyringPath",
this.tempDir.resolve("workflow").resolve("keyring.zek").toString());
keyringPath.toString());
System.setProperty("zeroecho.pki.crypto.workflow.operationRoot",
this.tempDir.resolve("workflow").resolve("operations").toString());
System.setProperty("zeroecho.pki.crypto.workflow.keyRefPrefix", "test-prefix:");
System.setProperty("zeroecho.pki.crypto.workflow.requireComponentSuffix", "false");
SignatureWorkflow workflow = PkiBootstrap.openSignatureWorkflow();
assertNotNull(workflow);
AtomicInteger acquisitions = new AtomicInteger();
AtomicReference<KeyringPassword> suppliedPassword = new AtomicReference<>();
KeyringUnlockProvider unlockProvider = () -> {
acquisitions.incrementAndGet();
KeyringPassword password = password();
suppliedPassword.set(password);
return password;
};
SignatureWorkflowRuntimeDependencies dependencies =
SignatureWorkflowRuntimeDependencies.withKeyringUnlockProvider(unlockProvider);
String workflowClassName = workflow.getClass().getName();
System.out.println("...workflowClass=" + workflowClassName);
try (SignatureWorkflow workflow = PkiBootstrap.openSignatureWorkflow(dependencies)) {
assertNotNull(workflow);
String workflowClassName = workflow.getClass().getName();
System.out.println("...workflowClass=" + workflowClassName);
System.out.println("...unlockAcquisitions=" + acquisitions.get());
assertEquals("zeroecho.pki.impl.crypto.zeroecholib.ZeroEchoLibSignatureWorkflow", workflowClassName);
assertEquals("zeroecho.pki.impl.crypto.zeroecholib.ZeroEchoLibSignatureWorkflow",
workflowClassName);
assertEquals(1, acquisitions.get());
assertTrue(suppliedPassword.get().isDestroyed());
byte[] payload = { 1, 2, 3, 4 };
AccessContext access = new AccessContext(new Principal("TEST", "bootstrap"),
new Purpose("BOOTSTRAP_TEST"), Optional.empty(), Optional.empty());
PkiId submissionId = SigningSubmissionId.create(SIGNING_NAMESPACE,
Instant.now(), new SecureRandom()).id();
SignatureWorkflow.SignRequest request = SignatureWorkflow.SignRequest.create(
submissionId, SIGNING_NAMESPACE, 1L, access,
new KeyRef("test-prefix:bootstrap"), "SHA256withRSA",
new EncodedObject(Encoding.BINARY, payload),
Optional.of(Encoding.BINARY), Optional.empty());
workflow.submitSign(request);
assertEquals(SignatureWorkflow.State.SUCCEEDED,
workflow.status(submissionId).state());
assertTrue(workflow.status(submissionId).result().orElseThrow()
.signature().orElseThrow().bytes().length > 0);
}
try (KeyringPassword password = password();
KeyringStore reopened = KeyringStore.open(keyringPath, password)) {
assertTrue(reopened.contains("bootstrap.prv"));
}
System.out.println("...ok");
}
@Test
public void openSignatureWorkflow_zeroEchoLib_requiresExplicitUnlockProvider() {
System.out.println("openSignatureWorkflow_zeroEchoLib_requiresExplicitUnlockProvider");
configureWorkflow(this.tempDir.resolve("missing-provider"));
PkiException exception = assertThrows(PkiException.class,
() -> PkiBootstrap.openSignatureWorkflow(
SignatureWorkflowRuntimeDependencies.none()));
System.out.println("...code=" + exception.getMessage());
assertEquals(ZeroEchoLibSignatureWorkflowProvider.DC_KEYRING_UNLOCK_PROVIDER_REQUIRED,
exception.getMessage());
assertNull(exception.getCause());
assertEquals(0, exception.getSuppressed().length);
System.out.println("...ok");
}
@Test
public void openSignatureWorkflow_zeroEchoLib_sanitizesUnlockProviderFailure() {
System.out.println("openSignatureWorkflow_zeroEchoLib_sanitizesUnlockProviderFailure");
String sentinel = "DO_NOT_LOG_KEYRING_UNLOCK_SENTINEL";
Path root = this.tempDir.resolve("provider-failure");
Path keyringPath = root.resolve("keyring.zek");
createSigningKeyring(keyringPath);
configureWorkflow(root);
List<LogRecord> records = new ArrayList<>();
Logger bootstrapLogger = Logger.getLogger(PkiBootstrap.class.getName());
Level oldLevel = bootstrapLogger.getLevel();
Handler handler = collectingHandler(records);
bootstrapLogger.addHandler(handler);
bootstrapLogger.setLevel(Level.ALL);
try {
KeyringUnlockProvider failing = () -> {
throw new IOException(sentinel);
};
PkiException exception = assertThrows(PkiException.class,
() -> PkiBootstrap.openSignatureWorkflow(
SignatureWorkflowRuntimeDependencies
.withKeyringUnlockProvider(failing)));
System.out.println("...code=" + exception.getMessage());
assertEquals(ZeroEchoLibSignatureWorkflowProvider
.DC_KEYRING_UNLOCK_PROVIDER_FAILED, exception.getMessage());
assertNull(exception.getCause());
assertEquals(0, exception.getSuppressed().length);
assertFalse(exception.toString().contains(sentinel));
assertTrue(records.stream().noneMatch(record ->
String.valueOf(record.getMessage()).contains(sentinel)));
try (SignatureWorkflow workflow = PkiBootstrap.openSignatureWorkflow(
SignatureWorkflowRuntimeDependencies.withKeyringUnlockProvider(
() -> password()))) {
assertNotNull(workflow);
}
} finally {
bootstrapLogger.removeHandler(handler);
bootstrapLogger.setLevel(oldLevel);
}
System.out.println("...ok");
}
@Test
public void openSignatureWorkflow_zeroEchoLib_wrongPasswordFailsUniformly()
throws Exception {
System.out.println("openSignatureWorkflow_zeroEchoLib_wrongPasswordFailsUniformly");
Path root = this.tempDir.resolve("wrong-password");
createSigningKeyring(root.resolve("keyring.zek"));
configureWorkflow(root);
AtomicReference<KeyringPassword> suppliedPassword = new AtomicReference<>();
KeyringUnlockProvider wrongProvider = () -> {
KeyringPassword password =
new KeyringPassword(new char[] { 'w', 'r', 'o', 'n', 'g' });
suppliedPassword.set(password);
return password;
};
PkiException exception = assertThrows(PkiException.class,
() -> PkiBootstrap.openSignatureWorkflow(
SignatureWorkflowRuntimeDependencies
.withKeyringUnlockProvider(wrongProvider)));
assertEquals(ZeroEchoLibSignatureWorkflowProvider.DC_KEYRING_OPEN_FAILED,
exception.getMessage());
assertNull(exception.getCause());
assertEquals(0, exception.getSuppressed().length);
assertTrue(suppliedPassword.get().isDestroyed());
try (SignatureWorkflow workflow = PkiBootstrap.openSignatureWorkflow(
SignatureWorkflowRuntimeDependencies.withKeyringUnlockProvider(
() -> password()))) {
assertNotNull(workflow);
}
System.out.println("...ok");
}
@Test
public void signatureWorkflowBootstrapApi_requiresRuntimeDependencies() {
System.out.println("signatureWorkflowBootstrapApi_requiresRuntimeDependencies");
java.lang.reflect.Method[] methods = PkiBootstrap.class.getDeclaredMethods();
long workflowOpeners = java.util.Arrays.stream(methods)
.filter(method -> "openSignatureWorkflow".equals(method.getName()))
.count();
java.lang.reflect.Method opener = java.util.Arrays.stream(methods)
.filter(method -> "openSignatureWorkflow".equals(method.getName()))
.findFirst().orElseThrow();
System.out.println("...workflowOpeners=" + workflowOpeners);
assertEquals(1L, workflowOpeners);
assertEquals(List.of(SignatureWorkflowRuntimeDependencies.class),
List.of(opener.getParameterTypes()));
assertTrue(java.util.Arrays.stream(PkiBootstrap.class.getDeclaredFields())
.noneMatch(field -> KeyringUnlockProvider.class.equals(field.getType())));
assertTrue(ProviderConfig.class.getRecordComponents()[1].getType()
.equals(Map.class));
assertTrue(java.util.Arrays.stream(PkiBootstrap.class.getMethods())
.noneMatch(method -> java.util.Arrays.stream(method.getParameterTypes())
.anyMatch(String.class::equals)
&& "openSignatureWorkflow".equals(method.getName())));
System.out.println("...ok");
}
@@ -306,4 +509,51 @@ public final class PkiBootstrapTest {
}
}
}
private void configureWorkflow(Path root) {
System.setProperty("zeroecho.pki.crypto.workflow", "zeroecho-lib");
System.setProperty("zeroecho.pki.crypto.workflow.keyringPath",
root.resolve("keyring.zek").toString());
System.setProperty("zeroecho.pki.crypto.workflow.operationRoot",
root.resolve("operations").toString());
System.setProperty("zeroecho.pki.crypto.workflow.keyRefPrefix", "test-prefix:");
System.setProperty("zeroecho.pki.crypto.workflow.requireComponentSuffix", "false");
}
private static void createSigningKeyring(Path keyringPath) {
try {
KeyPair pair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
try (KeyringPassword password = password();
KeyringStore keyring = KeyringStore.create(keyringPath, password)) {
keyring.putPrivate("bootstrap.prv", "RSA", pair.getPrivate());
keyring.putPublic("bootstrap.pub", "RSA", pair.getPublic());
}
} catch (Exception failure) {
throw new IllegalStateException("Unable to create bootstrap test keyring",
failure);
}
}
private static KeyringPassword password() {
return new KeyringPassword(KEYRING_PASSWORD);
}
private static Handler collectingHandler(List<LogRecord> records) {
return new Handler() {
@Override
public void publish(LogRecord record) {
records.add(record);
}
@Override
public void flush() {
// No buffered output.
}
@Override
public void close() {
// No owned resource.
}
};
}
}