Integrate CredentialFrameworkProvider bootstrap SPI and harden provider config validation

feat: add SPI-based CredentialFrameworkProvider resolution to PkiBootstrap via ServiceLoader
feat: add PkiBootstrap.openCredentialFramework() for provider-driven credential framework initialization
feat: register BcX509CredentialFrameworkProvider in META-INF/services
feat: introduce ConfigurableProvider.validateConfig(ProviderConfig) as a standard provider-side validation hook
fix: move generic backendId consistency validation into the default ConfigurableProvider validation routine
fix: enforce provider-local configuration validation from allocate() so direct provider use remains safe outside bootstrap
fix: add provider-specific validateConfig implementations for bootstrap-managed providers based on consumed configuration keys
fix: report unknown provider configuration keys through provider-local JUL warning logs without exposing values
fix: fail fast on malformed consumed configuration values instead of silently falling back where invalid input would mask operator error
fix: extend PkiBootstrapTest to cover CredentialFrameworkProvider bootstrap path
fix: extend PkiBootstrapTest to cover async and crypto.workflow initialization paths whose prefixed properties are cleared in test setup
fix: add negative bootstrap/provider validation coverage for backend mismatch and invalid configured values
docs: expand JavaDoc and package-level documentation for CredentialFrameworkProvider bootstrap wiring, ServiceLoader usage, and configuration validation behavior
chore: keep PkiBootstrap independent from implementation-specific BC framework classes and preserve provider autonomy over validation and diagnostics

Closes #3 spent @2h
This commit is contained in:
2026-04-06 01:51:15 +02:00
parent a66c115a80
commit de55ea909f
16 changed files with 480 additions and 65 deletions

View File

@@ -37,6 +37,8 @@ import java.nio.file.Path;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.pki.api.PkiId;
import zeroecho.pki.api.audit.Principal;
@@ -71,6 +73,7 @@ import zeroecho.pki.util.async.impl.DurableAsyncBus;
* </p>
*/
public final class FileBackedAsyncBusProvider implements AsyncBusProvider {
private static final Logger LOG = Logger.getLogger(FileBackedAsyncBusProvider.class.getName());
/**
* Configuration key for the log file path.
@@ -87,13 +90,29 @@ public final class FileBackedAsyncBusProvider implements AsyncBusProvider {
return Set.of(KEY_LOG_PATH);
}
/**
* Validates configuration for the durable file-backed async bus provider.
*
* @param config provider configuration (never {@code null})
* @throws IllegalArgumentException if the configuration is invalid
*/
@Override
public void validateConfig(final ProviderConfig config) {
AsyncBusProvider.super.validateConfig(config);
Map<String, String> props = config.properties();
String logPath = props.getOrDefault(KEY_LOG_PATH, Path.of("pki-async").resolve("async.log").toString());
Path.of(logPath);
for (String key : props.keySet()) {
if (!supportedKeys().contains(key) && LOG.isLoggable(Level.WARNING)) {
LOG.warning("Ignoring unknown async bus configuration key: " + key);
}
}
}
@Override
public AsyncBus<PkiId, Principal, String, Object> allocate(ProviderConfig config) {
Objects.requireNonNull(config, "config");
if (!id().equals(config.backendId())) {
throw new IllegalArgumentException("ProviderConfig backendId mismatch.");
}
validateConfig(config);
Map<String, String> props = config.properties();
String logPath = props.getOrDefault(KEY_LOG_PATH, Path.of("pki-async").resolve("async.log").toString());

View File

@@ -36,15 +36,26 @@ package zeroecho.pki.impl.audit;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.pki.spi.ProviderConfig;
import zeroecho.pki.spi.audit.AuditSink;
import zeroecho.pki.spi.audit.AuditSinkProvider;
/**
* {@link AuditSinkProvider} for the file-backed audit sink.
*
* <p>
* Supported configuration keys:
* </p>
* <ul>
* <li>{@code root} - audit storage root directory (required)</li>
* </ul>
*/
public class FileAuditSinkProvider implements AuditSinkProvider {
private static final Logger LOG = Logger.getLogger(FileAuditSinkProvider.class.getName());
@Override
public String id() {
return "file";
@@ -55,9 +66,29 @@ public class FileAuditSinkProvider implements AuditSinkProvider {
return Set.of("root");
}
/**
* Validates configuration for the file-backed audit sink provider.
*
* @param config provider configuration (never {@code null})
* @throws IllegalArgumentException if the configuration is incomplete or
* invalid
*/
@Override
public void validateConfig(final ProviderConfig config) {
AuditSinkProvider.super.validateConfig(config);
String rootString = config.require("root");
Path.of(rootString);
for (String key : config.properties().keySet()) {
if (!supportedKeys().contains(key) && LOG.isLoggable(Level.WARNING)) {
LOG.warning("Ignoring unknown audit sink configuration key: " + key);
}
}
}
@Override
public AuditSink allocate(ProviderConfig config) {
Objects.requireNonNull(config, "config");
validateConfig(config);
String rootString = config.require("root");
Path root = Path.of(rootString);

View File

@@ -35,15 +35,27 @@ package zeroecho.pki.impl.audit;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.pki.spi.ProviderConfig;
import zeroecho.pki.spi.audit.AuditSink;
import zeroecho.pki.spi.audit.AuditSinkProvider;
/**
* {@link AuditSinkProvider} for the in-memory audit sink.
*
* <p>
* Supported configuration keys:
* </p>
* <ul>
* <li>{@code size} - maximum retained event count (optional, positive
* integer)</li>
* </ul>
*/
public class InMemoryAuditSinkProvider implements AuditSinkProvider {
private static final Logger LOG = Logger.getLogger(InMemoryAuditSinkProvider.class.getName());
@Override
public String id() {
return "memory";
@@ -54,25 +66,54 @@ public class InMemoryAuditSinkProvider implements AuditSinkProvider {
return Set.of("size");
}
/**
* Validates configuration for the in-memory audit sink provider.
*
* @param config provider configuration (never {@code null})
* @throws IllegalArgumentException if the configuration contains an invalid
* {@code size} value
*/
@Override
public AuditSink allocate(ProviderConfig config) {
public void validateConfig(final ProviderConfig config) {
AuditSinkProvider.super.validateConfig(config);
Optional<String> sizeStr = config.get("size");
if (sizeStr.isPresent()) {
return new InMemoryAuditSink(parseIntOrDefault(sizeStr.get(), InMemoryAuditSink.DEFAULT_MAX_EVENTS));
int size = parseInt(sizeStr.get(), "size");
if (size <= 0) {
throw new IllegalArgumentException("Configuration key 'size' must be positive.");
}
}
for (String key : config.properties().keySet()) {
if (!supportedKeys().contains(key) && LOG.isLoggable(Level.WARNING)) {
LOG.warning("Ignoring unknown audit sink configuration key: " + key);
}
}
}
@Override
public AuditSink allocate(ProviderConfig config) {
validateConfig(config);
Optional<String> sizeStr = config.get("size");
if (sizeStr.isPresent()) {
return new InMemoryAuditSink(parseInt(sizeStr.get(), "size"));
} else {
return new InMemoryAuditSink();
}
}
private static int parseIntOrDefault(String value, int defaultValue) {
if (value == null) {
return defaultValue;
}
/**
* Parses an integer configuration value.
*
* @param value raw configuration value
* @param keyName configuration key name
* @return parsed integer value
* @throws IllegalArgumentException if the value is not a valid integer
*/
private static int parseInt(final String value, final String keyName) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException ex) {
return defaultValue;
throw new IllegalArgumentException("Configuration key '" + keyName + "' must be an integer.", ex);
}
}
}

View File

@@ -35,15 +35,23 @@ package zeroecho.pki.impl.audit;
import java.util.Collections;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.pki.spi.ProviderConfig;
import zeroecho.pki.spi.audit.AuditSink;
import zeroecho.pki.spi.audit.AuditSinkProvider;
/**
* {@link AuditSinkProvider} for the standard-output audit sink.
*
* <p>
* This provider accepts no provider-specific configuration keys.
* </p>
*/
public class StdoutAuditSinkProvider implements AuditSinkProvider {
private static final Logger LOG = Logger.getLogger(StdoutAuditSinkProvider.class.getName());
@Override
public String id() {
return "stdout";
@@ -54,8 +62,24 @@ public class StdoutAuditSinkProvider implements AuditSinkProvider {
return Collections.emptySet();
}
/**
* Validates configuration for the standard-output audit sink provider.
*
* @param config provider configuration (never {@code null})
*/
@Override
public void validateConfig(final ProviderConfig config) {
AuditSinkProvider.super.validateConfig(config);
for (String key : config.properties().keySet()) {
if (!supportedKeys().contains(key) && LOG.isLoggable(Level.WARNING)) {
LOG.warning("Ignoring unknown audit sink configuration key: " + key);
}
}
}
@Override
public AuditSink allocate(ProviderConfig config) {
validateConfig(config);
return new StdoutAuditSink();
}
}

View File

@@ -35,6 +35,8 @@ package zeroecho.pki.impl.crypto.zeroecholib;
import java.nio.file.Path;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.pki.spi.ProviderConfig;
import zeroecho.pki.spi.crypto.SignatureWorkflow;
@@ -61,6 +63,7 @@ import zeroecho.pki.spi.crypto.SignatureWorkflowProvider;
* </p>
*/
public final class ZeroEchoLibSignatureWorkflowProvider implements SignatureWorkflowProvider {
private static final Logger LOG = Logger.getLogger(ZeroEchoLibSignatureWorkflowProvider.class.getName());
private static final String KEY_KEYRING_PATH = "keyringPath";
private static final String KEY_KEYREF_PREFIX = "keyRefPrefix";
@@ -76,17 +79,39 @@ public final class ZeroEchoLibSignatureWorkflowProvider implements SignatureWork
return Set.of(KEY_KEYRING_PATH, KEY_KEYREF_PREFIX, KEY_REQUIRE_SUFFIX);
}
/**
* Validates configuration for the ZeroEcho-lib signature workflow provider.
*
* @param config provider configuration (never {@code null})
* @throws IllegalArgumentException if the configuration is incomplete or
* invalid
*/
@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.");
public void validateConfig(final ProviderConfig config) {
SignatureWorkflowProvider.super.validateConfig(config);
String keyringPath = config.require(KEY_KEYRING_PATH);
Path.of(keyringPath);
config.get(KEY_KEYREF_PREFIX).ifPresent(value -> {
if (value.isBlank()) {
throw new IllegalArgumentException("Configuration key '" + KEY_KEYREF_PREFIX + "' must not be blank.");
}
});
config.get(KEY_REQUIRE_SUFFIX).ifPresent(value -> {
if (!("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value))) {
throw new IllegalArgumentException(
"Configuration key '" + KEY_REQUIRE_SUFFIX + "' must be 'true' or 'false'.");
}
});
for (String key : config.properties().keySet()) {
if (!supportedKeys().contains(key) && LOG.isLoggable(Level.WARNING)) {
LOG.warning("Ignoring unknown signature workflow configuration key: " + key);
}
}
}
@Override
public SignatureWorkflow allocate(final ProviderConfig config) {
validateConfig(config);
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);

View File

@@ -34,6 +34,8 @@
package zeroecho.pki.impl.framework.x509.bc;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.pki.spi.ProviderConfig;
import zeroecho.pki.spi.framework.CredentialFramework;
@@ -73,7 +75,9 @@ import zeroecho.pki.spi.framework.CredentialFrameworkProvider;
* The current implementation does not consume any provider-specific
* configuration keys beyond backend selection through
* {@link ProviderConfig#backendId()}. Consequently, {@link #supportedKeys()}
* returns an empty set.
* returns an empty set and bootstrap will reject any
* {@code zeroecho.pki.framework.*} configuration keys other than the backend
* selector itself.
* </p>
*
* <h2>Thread-safety</h2>
@@ -83,6 +87,8 @@ import zeroecho.pki.spi.framework.CredentialFrameworkProvider;
*/
public final class BcX509CredentialFrameworkProvider implements CredentialFrameworkProvider {
private static final Logger LOG = Logger.getLogger(BcX509CredentialFrameworkProvider.class.getName());
/**
* Returns the stable provider identifier used to select this X.509 framework
* backend.
@@ -110,6 +116,32 @@ public final class BcX509CredentialFrameworkProvider implements CredentialFramew
return Set.of();
}
/**
* Validates configuration for the Bouncy Castle backed X.509 credential
* framework provider.
*
* <p>
* This provider currently defines no provider-specific keys. Unknown keys are
* ignored for forward compatibility and are reported only by name through JUL
* warning output. The backend id must match {@link #id()}.
* </p>
*
* @param config provider configuration used to select this backend; must not be
* {@code null}
* @throws IllegalArgumentException if the backend id does not match this
* provider identifier
*/
@Override
public void validateConfig(final ProviderConfig config) {
CredentialFrameworkProvider.super.validateConfig(config);
for (String key : config.properties().keySet()) {
if (!supportedKeys().contains(key) && LOG.isLoggable(Level.WARNING)) {
LOG.warning("Ignoring unknown credential framework configuration key: " + key);
}
}
}
/**
* Allocates a new Bouncy Castle backed X.509 credential framework instance.
*
@@ -129,12 +161,7 @@ public final class BcX509CredentialFrameworkProvider implements CredentialFramew
*/
@Override
public CredentialFramework allocate(ProviderConfig config) {
if (config == null) {
throw new IllegalArgumentException("config must not be null");
}
if (!id().equals(config.backendId())) {
throw new IllegalArgumentException("ProviderConfig backendId mismatch");
}
validateConfig(config);
return new BcX509CredentialFramework();
}
}

View File

@@ -91,7 +91,9 @@
* issuance and status-object generation remain intentionally unsupported until
* concrete backend components are supplied. This allows bootstrap and runtime
* composition to separate framework selection from full cryptographic and PKI
* service wiring.
* service wiring. The package is exposed to runtime composition only through
* the framework SPI and its ServiceLoader provider; bootstrap code must not
* depend on these implementation classes directly.
* </p>
*
* <h2>Security considerations</h2>

View File

@@ -36,14 +36,15 @@ package zeroecho.pki.impl.fs;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.pki.spi.ProviderConfig;
import zeroecho.pki.spi.audit.AuditSinkProvider;
import zeroecho.pki.spi.store.PkiStore;
import zeroecho.pki.spi.store.PkiStoreProvider;
/**
* {@link AuditSinkProvider} for the filesystem-backed {@link PkiStore}.
* {@link PkiStoreProvider} for the filesystem-backed {@link PkiStore}.
*
* <p>
* Supported configuration keys:
@@ -53,6 +54,7 @@ import zeroecho.pki.spi.store.PkiStoreProvider;
* </ul>
*/
public final class FilesystemPkiStoreProvider implements PkiStoreProvider {
private static final Logger LOG = Logger.getLogger(FilesystemPkiStoreProvider.class.getName());
/**
* Public no-arg constructor required by {@link java.util.ServiceLoader}.
@@ -71,9 +73,41 @@ public final class FilesystemPkiStoreProvider implements PkiStoreProvider {
return Set.of("root");
}
/**
* Validates configuration for the filesystem-backed PKI store provider.
*
* <p>
* Required configuration:
* </p>
* <ul>
* <li>{@code root}</li>
* </ul>
*
* <p>
* Unknown keys are ignored for forward compatibility and are reported by key
* name only.
* </p>
*
* @param config provider configuration (never {@code null})
* @throws IllegalArgumentException if the configuration is incomplete or
* invalid
*/
@Override
public PkiStore allocate(ProviderConfig config) {
public void validateConfig(final ProviderConfig config) {
PkiStoreProvider.super.validateConfig(config);
String rootString = config.require("root");
Path.of(rootString);
for (String key : config.properties().keySet()) {
if (!supportedKeys().contains(key) && LOG.isLoggable(Level.WARNING)) {
LOG.warning("Ignoring unknown PKI store configuration key: " + key);
}
}
}
@Override
public PkiStore allocate(final ProviderConfig config) {
Objects.requireNonNull(config, "config");
validateConfig(config);
String rootString = config.require("root");
Path root = Path.of(rootString);

View File

@@ -33,6 +33,7 @@
******************************************************************************/
package zeroecho.pki.spi;
import java.util.Objects;
import java.util.Set;
/**
@@ -59,8 +60,10 @@ import java.util.Set;
* Configuration is provided as string properties through
* {@link ProviderConfig}. Providers must validate required keys and reject
* invalid configurations with {@link IllegalArgumentException}. Unknown keys
* must be ignored for forward compatibility. {@link #supportedKeys()} is
* informational and may be used for diagnostics or help output.
* must be ignored for forward compatibility, but may be reported through
* provider-local diagnostic logging that names keys only.
* {@link #supportedKeys()} is informational and may be used for diagnostics or
* help output.
* </p>
*
* <h2>Security</h2>
@@ -94,6 +97,37 @@ public interface ConfigurableProvider<T> {
*/
Set<String> supportedKeys();
/**
* Validates the provided configuration before allocation.
*
* <p>
* Implementations should use this hook to enforce backend-specific invariants
* such as required keys and key formats. The default implementation already
* verifies that {@link ProviderConfig#backendId()} matches {@link #id()}.
* Unknown keys should normally be ignored for forward compatibility, but may be
* reported through provider-local logging that names keys only and never logs
* values.
* </p>
*
* <p>
* The default implementation performs a null check and verifies that the
* supplied backend id matches {@link #id()}. Providers with additional
* requirements should override this method, invoke the default implementation,
* and then validate their own keys.
* </p>
*
* @param config configuration to validate (never {@code null})
* @throws NullPointerException if {@code config} is {@code null}
* @throws IllegalArgumentException if the configuration is invalid
*/
default void validateConfig(final ProviderConfig config) {
Objects.requireNonNull(config, "config");
if (!id().equals(config.backendId())) {
throw new IllegalArgumentException("ProviderConfig backendId mismatch.");
}
}
/**
* Allocates a new instance using the provided configuration.
*

View File

@@ -48,6 +48,8 @@ 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.framework.CredentialFramework;
import zeroecho.pki.spi.framework.CredentialFrameworkProvider;
import zeroecho.pki.spi.store.PkiStore;
import zeroecho.pki.spi.store.PkiStoreProvider;
import zeroecho.pki.util.async.AsyncBus;
@@ -58,7 +60,7 @@ import zeroecho.pki.util.async.AsyncBus;
* <p>
* 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,
* scale as more SPIs are introduced (audit, publish, credential frameworks,
* crypto/workflows, async orchestration, etc.).
* </p>
*
@@ -77,6 +79,10 @@ import zeroecho.pki.util.async.AsyncBus;
* <li>Select async bus provider: {@code -Dzeroecho.pki.async=&lt;id&gt;}</li>
* <li>Configure async bus provider:
* {@code -Dzeroecho.pki.async.&lt;key&gt;=&lt;value&gt;}</li>
* <li>Select credential framework provider:
* {@code -Dzeroecho.pki.framework=&lt;id&gt;}</li>
* <li>Configure credential framework provider:
* {@code -Dzeroecho.pki.framework.&lt;key&gt;=&lt;value&gt;}</li>
* </ul>
*
* <p>
@@ -100,6 +106,9 @@ public final class PkiBootstrap {
private static final String PROP_ASYNC_BACKEND = "zeroecho.pki.async";
private static final String PROP_ASYNC_PREFIX = "zeroecho.pki.async.";
private static final String PROP_FRAMEWORK_BACKEND = "zeroecho.pki.framework";
private static final String PROP_FRAMEWORK_PREFIX = "zeroecho.pki.framework.";
private PkiBootstrap() {
throw new AssertionError("No instances.");
}
@@ -264,6 +273,54 @@ public final class PkiBootstrap {
return provider.allocate(config);
}
/**
* Opens a {@link CredentialFramework} using {@link CredentialFrameworkProvider}
* discovered via ServiceLoader.
*
* <p>
* Provider selection is deterministic and fail-fast:
* </p>
* <ul>
* <li>If {@code -Dzeroecho.pki.framework=&lt;id&gt;} is specified, the provider
* with the matching id is selected.</li>
* <li>If no id is specified and exactly one provider exists, that provider is
* selected.</li>
* <li>If multiple providers exist and no id is specified, bootstrap fails as
* configuration is ambiguous.</li>
* </ul>
*
* <p>
* Configuration properties are read from {@link System#getProperties()} using
* the prefix {@code zeroecho.pki.framework.}. Values are treated as sensitive
* and are never logged; only keys may be logged.
* </p>
*
* @return credential framework (never {@code null})
* @throws IllegalArgumentException if unsupported configuration keys are
* provided
* @throws IllegalStateException if provider selection fails
*/
public static CredentialFramework openCredentialFramework() {
String requestedId = System.getProperty(PROP_FRAMEWORK_BACKEND);
CredentialFrameworkProvider provider = SpiSelector.select(CredentialFrameworkProvider.class, requestedId,
new SpiSelector.ProviderId<>() {
@Override
public String id(CredentialFrameworkProvider p) {
return p.id();
}
});
Map<String, String> props = SpiSystemProperties.readPrefixed(PROP_FRAMEWORK_PREFIX);
ProviderConfig config = new ProviderConfig(provider.id(), props);
if (LOG.isLoggable(Level.INFO)) {
LOG.info("Selected credential framework provider: " + provider.id() + " (keys: " + props.keySet() + ")");
}
return provider.allocate(config);
}
/**
* Logs provider help information (supported keys) for diagnostics.
*

View File

@@ -36,8 +36,8 @@
*
* <p>
* This package centralizes deterministic provider selection and a minimal
* configuration convention for multiple SPIs (store, audit, publication, and
* future components).
* configuration convention for multiple SPIs (store, audit, publication,
* credential frameworks, and future components).
* </p>
*
* <p>

View File

@@ -33,8 +33,6 @@
******************************************************************************/
package zeroecho.pki.spi.framework;
import java.util.Objects;
import zeroecho.pki.spi.ConfigurableProvider;
import zeroecho.pki.spi.ProviderConfig;
@@ -44,6 +42,7 @@ import zeroecho.pki.spi.ProviderConfig;
* <p>
* The PKI runtime selects a framework provider using
* {@link java.util.ServiceLoader} and instantiates it through
* {@link #validateConfig(ProviderConfig)} and
* {@link #allocate(ProviderConfig)}. Provider selection is performed by
* {@code PkiBootstrap} using configuration properties (similarly to store,
* audit, async bus, and signature workflow providers).
@@ -65,7 +64,9 @@ public interface CredentialFrameworkProvider extends ConfigurableProvider<Creden
* <p>
* Implementations must validate that {@link ProviderConfig#backendId()} matches
* {@link #id()}. A mismatch must be reported as
* {@link IllegalArgumentException}.
* {@link IllegalArgumentException}. Implementations should invoke
* {@link #validateConfig(ProviderConfig)} from this method so that the same
* enforcement applies even when a provider is used outside bootstrap.
* </p>
*
* @param config provider configuration (never {@code null})
@@ -78,24 +79,4 @@ public interface CredentialFrameworkProvider extends ConfigurableProvider<Creden
@Override
CredentialFramework allocate(ProviderConfig config);
/**
* Enforces that the provided configuration is intended for this provider.
*
* <p>
* This helper is intended for defensive checks inside provider implementations.
* </p>
*
* @param provider provider instance (never {@code null})
* @param config provider configuration (never {@code null})
* @throws NullPointerException if any argument is {@code null}
* @throws IllegalArgumentException if the backend id does not match
*/
static void requireIdMatch(final CredentialFrameworkProvider provider, final ProviderConfig config) {
Objects.requireNonNull(provider, "provider");
Objects.requireNonNull(config, "config");
if (!provider.id().equals(config.backendId())) {
throw new IllegalArgumentException("ProviderConfig backendId mismatch.");
}
}
}

View File

@@ -40,5 +40,13 @@
* framework-specific fields. Framework-specific types must not leak into the
* public API.
* </p>
*
* <p>
* Runtime selection and allocation of concrete framework implementations is
* performed through {@link java.util.ServiceLoader} and the
* {@code CredentialFrameworkProvider} SPI. Bootstrap configuration follows the
* same deterministic, fail-fast conventions as the other PKI SPIs.
* </p>
*
*/
package zeroecho.pki.spi.framework;

View File

@@ -36,9 +36,9 @@
*
* <p>
* SPIs are used to plug in persistence backends, audit sinks, publishing
* targets, and framework integrations while keeping the public PKI API
* framework-agnostic. Implementations are typically discovered via
* {@link java.util.ServiceLoader}.
* targets, credential frameworks, and other integrations while keeping the
* public PKI API framework-agnostic. Implementations are typically discovered
* via {@link java.util.ServiceLoader}.
* </p>
*
* <p>

View File

@@ -0,0 +1 @@
zeroecho.pki.impl.framework.x509.bc.BcX509CredentialFrameworkProvider

View File

@@ -35,6 +35,7 @@ package zeroecho.pki.spi.bootstrap;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.nio.file.Path;
import java.util.Properties;
@@ -44,8 +45,13 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import zeroecho.pki.api.PkiId;
import zeroecho.pki.api.audit.Principal;
import zeroecho.pki.spi.audit.AuditSink;
import zeroecho.pki.spi.crypto.SignatureWorkflow;
import zeroecho.pki.spi.framework.CredentialFramework;
import zeroecho.pki.spi.store.PkiStore;
import zeroecho.pki.util.async.AsyncBus;
/**
* JUnit 5 tests for {@link PkiBootstrap}.
@@ -75,9 +81,15 @@ public final class PkiBootstrapTest {
clearPrefix("zeroecho.pki.store.");
clearPrefix("zeroecho.pki.audit.");
clearPrefix("zeroecho.pki.framework.");
clearPrefix("zeroecho.pki.async.");
clearPrefix("zeroecho.pki.crypto.workflow.");
System.clearProperty("zeroecho.pki.store");
System.clearProperty("zeroecho.pki.audit");
System.clearProperty("zeroecho.pki.framework");
System.clearProperty("zeroecho.pki.async");
System.clearProperty("zeroecho.pki.crypto.workflow");
System.out.println("...tempDir=" + this.tempDir);
System.out.println("...ok");
@@ -162,6 +174,125 @@ public final class PkiBootstrapTest {
System.out.println("...ok");
}
@Test
public void openAudit_memory_rejectsInvalidSize() {
System.out.println("openAudit_memory_rejectsInvalidSize");
System.setProperty("zeroecho.pki.audit", "memory");
System.setProperty("zeroecho.pki.audit.size", "abc");
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> PkiBootstrap.openAudit());
System.out.println("...message=" + exception.getMessage());
assertEquals("Configuration key 'size' must be an integer.", exception.getMessage());
System.out.println("...ok");
}
@Test
public void openCredentialFramework_explicitX509BcProvider() {
System.out.println("openCredentialFramework_explicitX509BcProvider");
System.setProperty("zeroecho.pki.framework", "x509-bc");
CredentialFramework framework = PkiBootstrap.openCredentialFramework();
assertNotNull(framework);
String frameworkClassName = framework.getClass().getName();
System.out.println("...frameworkClass=" + frameworkClassName);
assertEquals("zeroecho.pki.impl.framework.x509.bc.BcX509CredentialFramework", frameworkClassName);
System.out.println("...ok");
}
@Test
public void openCredentialFramework_x509Bc_byDefault() {
System.out.println("openCredentialFramework_x509Bc_byDefault");
CredentialFramework framework = PkiBootstrap.openCredentialFramework();
assertNotNull(framework);
String frameworkClassName = framework.getClass().getName();
System.out.println("...frameworkClass=" + frameworkClassName);
assertEquals("zeroecho.pki.impl.framework.x509.bc.BcX509CredentialFramework", frameworkClassName);
System.out.println("...ok");
}
@Test
public void openCredentialFramework_x509Bc_ignoresUnknownKey() {
System.out.println("openCredentialFramework_x509Bc_ignoresUnknownKey");
System.setProperty("zeroecho.pki.framework", "x509-bc");
System.setProperty("zeroecho.pki.framework.unused", "value");
CredentialFramework framework = PkiBootstrap.openCredentialFramework();
assertNotNull(framework);
String frameworkClassName = framework.getClass().getName();
System.out.println("...frameworkClass=" + frameworkClassName);
assertEquals("zeroecho.pki.impl.framework.x509.bc.BcX509CredentialFramework", frameworkClassName);
System.out.println("...ok");
}
@Test
public void openAsync_file_usesTempLogPath() {
System.out.println("openAsync_file_usesTempLogPath");
System.setProperty("zeroecho.pki.async", "file");
System.setProperty("zeroecho.pki.async.logPath", this.tempDir.resolve("async").resolve("async.log").toString());
AsyncBus<PkiId, Principal, String, Object> bus = PkiBootstrap.openAsyncBus();
assertNotNull(bus);
String busClassName = bus.getClass().getName();
System.out.println("...asyncClass=" + busClassName);
assertEquals("zeroecho.pki.util.async.impl.DurableAsyncBus", busClassName);
System.out.println("...ok");
}
@Test
public void openSignatureWorkflow_zeroEchoLib_usesConfiguredKeyringPath() {
System.out.println("openSignatureWorkflow_zeroEchoLib_usesConfiguredKeyringPath");
System.setProperty("zeroecho.pki.crypto.workflow", "zeroecho-lib");
System.setProperty("zeroecho.pki.crypto.workflow.keyringPath",
this.tempDir.resolve("workflow").resolve("keyring.zek").toString());
System.setProperty("zeroecho.pki.crypto.workflow.keyRefPrefix", "test-prefix:");
System.setProperty("zeroecho.pki.crypto.workflow.requireComponentSuffix", "false");
SignatureWorkflow workflow = PkiBootstrap.openSignatureWorkflow();
assertNotNull(workflow);
String workflowClassName = workflow.getClass().getName();
System.out.println("...workflowClass=" + workflowClassName);
assertEquals("zeroecho.pki.impl.crypto.zeroecholib.ZeroEchoLibSignatureWorkflow", workflowClassName);
System.out.println("...ok");
}
@Test
public void providerValidation_rejectsBackendIdMismatch() {
System.out.println("providerValidation_rejectsBackendIdMismatch");
zeroecho.pki.impl.fs.FilesystemPkiStoreProvider provider = new zeroecho.pki.impl.fs.FilesystemPkiStoreProvider();
zeroecho.pki.spi.ProviderConfig config = new zeroecho.pki.spi.ProviderConfig("other",
java.util.Map.of("root", this.tempDir.resolve("store").toString()));
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> provider.validateConfig(config));
System.out.println("...message=" + exception.getMessage());
assertEquals("ProviderConfig backendId mismatch.", exception.getMessage());
System.out.println("...ok");
}
private static void clearPrefix(String prefix) {
Properties props = System.getProperties();
for (Object keyObj : props.keySet().toArray()) {