diff --git a/pki/src/main/java/zeroecho/pki/impl/async/FileBackedAsyncBusProvider.java b/pki/src/main/java/zeroecho/pki/impl/async/FileBackedAsyncBusProvider.java index 54afa73..600b1e9 100644 --- a/pki/src/main/java/zeroecho/pki/impl/async/FileBackedAsyncBusProvider.java +++ b/pki/src/main/java/zeroecho/pki/impl/async/FileBackedAsyncBusProvider.java @@ -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; *

*/ 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 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 allocate(ProviderConfig config) { Objects.requireNonNull(config, "config"); - - if (!id().equals(config.backendId())) { - throw new IllegalArgumentException("ProviderConfig backendId mismatch."); - } + validateConfig(config); Map props = config.properties(); String logPath = props.getOrDefault(KEY_LOG_PATH, Path.of("pki-async").resolve("async.log").toString()); diff --git a/pki/src/main/java/zeroecho/pki/impl/audit/FileAuditSinkProvider.java b/pki/src/main/java/zeroecho/pki/impl/audit/FileAuditSinkProvider.java index 2bea65c..62c8228 100644 --- a/pki/src/main/java/zeroecho/pki/impl/audit/FileAuditSinkProvider.java +++ b/pki/src/main/java/zeroecho/pki/impl/audit/FileAuditSinkProvider.java @@ -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. + * + *

+ * Supported configuration keys: + *

+ *
    + *
  • {@code root} - audit storage root directory (required)
  • + *
*/ 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); diff --git a/pki/src/main/java/zeroecho/pki/impl/audit/InMemoryAuditSinkProvider.java b/pki/src/main/java/zeroecho/pki/impl/audit/InMemoryAuditSinkProvider.java index 552027a..ef7a37e 100644 --- a/pki/src/main/java/zeroecho/pki/impl/audit/InMemoryAuditSinkProvider.java +++ b/pki/src/main/java/zeroecho/pki/impl/audit/InMemoryAuditSinkProvider.java @@ -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. + * + *

+ * Supported configuration keys: + *

+ *
    + *
  • {@code size} - maximum retained event count (optional, positive + * integer)
  • + *
*/ 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 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 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); } } } diff --git a/pki/src/main/java/zeroecho/pki/impl/audit/StdoutAuditSinkProvider.java b/pki/src/main/java/zeroecho/pki/impl/audit/StdoutAuditSinkProvider.java index 3a300cc..8e33a04 100644 --- a/pki/src/main/java/zeroecho/pki/impl/audit/StdoutAuditSinkProvider.java +++ b/pki/src/main/java/zeroecho/pki/impl/audit/StdoutAuditSinkProvider.java @@ -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. + * + *

+ * This provider accepts no provider-specific configuration keys. + *

*/ 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(); } } diff --git a/pki/src/main/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflowProvider.java b/pki/src/main/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflowProvider.java index e191f2e..b66afc7 100644 --- a/pki/src/main/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflowProvider.java +++ b/pki/src/main/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflowProvider.java @@ -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; *

*/ 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); diff --git a/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509CredentialFrameworkProvider.java b/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509CredentialFrameworkProvider.java index 4298de6..78491cd 100644 --- a/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509CredentialFrameworkProvider.java +++ b/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509CredentialFrameworkProvider.java @@ -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. *

* *

Thread-safety

@@ -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. + * + *

+ * 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()}. + *

+ * + * @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(); } } diff --git a/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/package-info.java b/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/package-info.java index 37ec20c..eb5684c 100644 --- a/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/package-info.java +++ b/pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/package-info.java @@ -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. *

* *

Security considerations

diff --git a/pki/src/main/java/zeroecho/pki/impl/fs/FilesystemPkiStoreProvider.java b/pki/src/main/java/zeroecho/pki/impl/fs/FilesystemPkiStoreProvider.java index 660ab49..16a590e 100644 --- a/pki/src/main/java/zeroecho/pki/impl/fs/FilesystemPkiStoreProvider.java +++ b/pki/src/main/java/zeroecho/pki/impl/fs/FilesystemPkiStoreProvider.java @@ -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}. * *

* Supported configuration keys: @@ -53,6 +54,7 @@ import zeroecho.pki.spi.store.PkiStoreProvider; * */ 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. + * + *

+ * Required configuration: + *

+ *
    + *
  • {@code root}
  • + *
+ * + *

+ * Unknown keys are ignored for forward compatibility and are reported by key + * name only. + *

+ * + * @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); diff --git a/pki/src/main/java/zeroecho/pki/spi/ConfigurableProvider.java b/pki/src/main/java/zeroecho/pki/spi/ConfigurableProvider.java index d37120b..30e2bc5 100644 --- a/pki/src/main/java/zeroecho/pki/spi/ConfigurableProvider.java +++ b/pki/src/main/java/zeroecho/pki/spi/ConfigurableProvider.java @@ -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. *

* *

Security

@@ -94,6 +97,37 @@ public interface ConfigurableProvider { */ Set supportedKeys(); + /** + * Validates the provided configuration before allocation. + * + *

+ * 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. + *

+ * + *

+ * 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. + *

+ * + * @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. * diff --git a/pki/src/main/java/zeroecho/pki/spi/bootstrap/PkiBootstrap.java b/pki/src/main/java/zeroecho/pki/spi/bootstrap/PkiBootstrap.java index 96b36db..10b3499 100644 --- a/pki/src/main/java/zeroecho/pki/spi/bootstrap/PkiBootstrap.java +++ b/pki/src/main/java/zeroecho/pki/spi/bootstrap/PkiBootstrap.java @@ -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; *

* 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.). *

* @@ -77,6 +79,10 @@ import zeroecho.pki.util.async.AsyncBus; *
  • Select async bus provider: {@code -Dzeroecho.pki.async=<id>}
  • *
  • Configure async bus provider: * {@code -Dzeroecho.pki.async.<key>=<value>}
  • + *
  • Select credential framework provider: + * {@code -Dzeroecho.pki.framework=<id>}
  • + *
  • Configure credential framework provider: + * {@code -Dzeroecho.pki.framework.<key>=<value>}
  • * * *

    @@ -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. + * + *

    + * Provider selection is deterministic and fail-fast: + *

    + *
      + *
    • If {@code -Dzeroecho.pki.framework=<id>} is specified, the provider + * with the matching id is selected.
    • + *
    • If no id is specified and exactly one provider exists, that provider is + * selected.
    • + *
    • If multiple providers exist and no id is specified, bootstrap fails as + * configuration is ambiguous.
    • + *
    + * + *

    + * 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. + *

    + * + * @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 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. * diff --git a/pki/src/main/java/zeroecho/pki/spi/bootstrap/package-info.java b/pki/src/main/java/zeroecho/pki/spi/bootstrap/package-info.java index 3b516ed..4ba5c18 100644 --- a/pki/src/main/java/zeroecho/pki/spi/bootstrap/package-info.java +++ b/pki/src/main/java/zeroecho/pki/spi/bootstrap/package-info.java @@ -36,8 +36,8 @@ * *

    * 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). *

    * *

    diff --git a/pki/src/main/java/zeroecho/pki/spi/framework/CredentialFrameworkProvider.java b/pki/src/main/java/zeroecho/pki/spi/framework/CredentialFrameworkProvider.java index acff3d4..0da1c9b 100644 --- a/pki/src/main/java/zeroecho/pki/spi/framework/CredentialFrameworkProvider.java +++ b/pki/src/main/java/zeroecho/pki/spi/framework/CredentialFrameworkProvider.java @@ -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; *

    * 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 * 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. *

    * * @param config provider configuration (never {@code null}) @@ -78,24 +79,4 @@ public interface CredentialFrameworkProvider extends ConfigurableProvider - * This helper is intended for defensive checks inside provider implementations. - *

    - * - * @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."); - } - } } diff --git a/pki/src/main/java/zeroecho/pki/spi/framework/package-info.java b/pki/src/main/java/zeroecho/pki/spi/framework/package-info.java index 961ad21..fb506ce 100644 --- a/pki/src/main/java/zeroecho/pki/spi/framework/package-info.java +++ b/pki/src/main/java/zeroecho/pki/spi/framework/package-info.java @@ -40,5 +40,13 @@ * framework-specific fields. Framework-specific types must not leak into the * public API. *

    + * + *

    + * 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. + *

    + * */ package zeroecho.pki.spi.framework; diff --git a/pki/src/main/java/zeroecho/pki/spi/package-info.java b/pki/src/main/java/zeroecho/pki/spi/package-info.java index c7de0d5..9f33b4c 100644 --- a/pki/src/main/java/zeroecho/pki/spi/package-info.java +++ b/pki/src/main/java/zeroecho/pki/spi/package-info.java @@ -36,9 +36,9 @@ * *

    * 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}. *

    * *

    diff --git a/pki/src/main/resources/META-INF/services/zeroecho.pki.spi.framework.CredentialFrameworkProvider b/pki/src/main/resources/META-INF/services/zeroecho.pki.spi.framework.CredentialFrameworkProvider new file mode 100644 index 0000000..d6c1a74 --- /dev/null +++ b/pki/src/main/resources/META-INF/services/zeroecho.pki.spi.framework.CredentialFrameworkProvider @@ -0,0 +1 @@ +zeroecho.pki.impl.framework.x509.bc.BcX509CredentialFrameworkProvider diff --git a/pki/src/test/java/zeroecho/pki/spi/bootstrap/PkiBootstrapTest.java b/pki/src/test/java/zeroecho/pki/spi/bootstrap/PkiBootstrapTest.java index 50e8197..5cd249b 100644 --- a/pki/src/test/java/zeroecho/pki/spi/bootstrap/PkiBootstrapTest.java +++ b/pki/src/test/java/zeroecho/pki/spi/bootstrap/PkiBootstrapTest.java @@ -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 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()) {