From de55ea909f6b97a94108fb2c458961bf56372342 Mon Sep 17 00:00:00 2001
From: Leo Galambos
+ * Supported configuration keys: + *
+ *+ * Supported configuration keys: + *
+ *+ * 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. * * *+ * 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. * * ** 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: + *
+ *+ * 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. * * *+ * 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; *@@ -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: + *
+ *+ * 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* 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
+ * 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