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:
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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=<id>}</li>
|
||||
* <li>Configure async bus provider:
|
||||
* {@code -Dzeroecho.pki.async.<key>=<value>}</li>
|
||||
* <li>Select credential framework provider:
|
||||
* {@code -Dzeroecho.pki.framework=<id>}</li>
|
||||
* <li>Configure credential framework provider:
|
||||
* {@code -Dzeroecho.pki.framework.<key>=<value>}</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=<id>} 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.
|
||||
*
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
zeroecho.pki.impl.framework.x509.bc.BcX509CredentialFrameworkProvider
|
||||
@@ -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()) {
|
||||
|
||||
Reference in New Issue
Block a user