security(lib,storage): enforce single-use encryption contexts, encrypt keyring and harden key import
This commit is contained in:
@@ -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 {
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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' });
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user