diff --git a/pki/.classpath b/pki/.classpath
index 000d5a0..8ce7b51 100644
--- a/pki/.classpath
+++ b/pki/.classpath
@@ -6,26 +6,27 @@
- * Audit events may represent high-level PKI operations (issuance, revocation, - * publication, backup) and attribute access governance outcomes. - * Implementations must ensure no secrets appear in {@code details}. + * An {@code AuditEvent} is an immutable, structured record describing a + * security-relevant action or outcome. Typical event categories include + * high-level PKI operations (issuance, revocation, publication, backup) and + * attribute-level governance decisions (read/export attempts and outcomes). *
* - * @param time event time (server time) - * @param category non-empty category (e.g., "ISSUANCE", "REVOCATION", - * "ATTRIBUTE_ACCESS") - * @param action non-empty action string (e.g., "ISSUE_END_ENTITY", "REVOKE", - * "READ") - * @param principal actor responsible for the event - * @param purpose purpose of the operation/access - * @param objectId optional subject object id (credential id, request id, etc.) - * @param formatId optional format id related to the object - * @param details additional non-sensitive key/value details + *+ * The static comparator returned by {@link #auditOrder()} defines a + * deterministic order for presentation and query results. It does not imply + * causality; it is purely a stable sort key. + *
+ * + *+ * This record enforces basic invariants: time is non-null, {@code category} and + * {@code action} are non-blank, {@code principal} and {@code purpose} are + * non-null, and optional containers/maps are non-null. + *
+ * + * @param time event time (server time), never {@code null} + * @param category non-blank category (e.g., {@code "ISSUANCE"}, + * {@code "REVOCATION"}, {@code "ATTRIBUTE_ACCESS"}) + * @param action non-blank action string (e.g., {@code "ISSUE_END_ENTITY"}, + * {@code "REVOKE"}, {@code "READ"}) + * @param principal actor responsible for the event, never {@code null} + * @param purpose declared purpose of the operation/access, never {@code null} + * @param objectId optional subject object id (credential id, request id, + * attribute id, etc.), never {@code null} + * @param formatId optional format id related to the object (e.g., encoding), + * never {@code null} + * @param details additional non-sensitive key/value details, never + * {@code null} */ public record AuditEvent(Instant time, String category, String action, Principal principal, Purpose purpose, Optional+ * Note that this constructor does not (and cannot) automatically prove the + * absence of secrets in {@link #details()}. The responsibility to redact and + * constrain details is on the event producer. + *
+ * + * @throws IllegalArgumentException if {@code time} is {@code null}, + * {@code category/action} are blank, + * {@code principal/purpose} are {@code null}, + * or any of {@code objectId/formatId/details} + * are {@code null} */ public AuditEvent { if (time == null) { @@ -96,4 +136,78 @@ public record AuditEvent(Instant time, String category, String action, Principal throw new IllegalArgumentException("details must not be null"); } } + + /** + * Returns a deterministic comparator for audit events. + * + *+ * The comparator orders events by: {@code time}, {@code category}, + * {@code action}, {@code principal.type}, {@code principal.name}. + *
+ * + *+ * This ordering is intended for consistent presentation and stable test + * assertions. It is not a security primitive. + *
+ * + * @return deterministic comparator, never {@code null} + */ + public static Comparator+ * Matching semantics: + *
+ *+ * This method is side-effect free and deterministic. + *
+ * + * @param query query constraints, must not be {@code null} + * @return {@code true} if this event matches all present constraints; + * {@code false} otherwise + * @throws NullPointerException if {@code query} is {@code null} + */ + public boolean matches(AuditQuery query) { + Objects.requireNonNull(query, "query"); + + if (query.category().isPresent() && !query.category().get().equals(category())) { + return false; + } + if (query.action().isPresent() && !query.action().get().equals(action())) { + return false; + } + if (query.after().isPresent() && time().isBefore(query.after().get())) { + return false; + } + if (query.before().isPresent() && time().isAfter(query.before().get())) { + return false; + } + if (query.objectId().isPresent() && !query.objectId().get().equals(objectId().orElse(null))) { + return false; + } + if (query.principalName().isPresent() && !query.principalName().get().equals(principal().name())) { // NOPMD + return false; + } + return true; + } } diff --git a/pki/src/main/java/zeroecho/pki/impl/audit/AuditEventCodec.java b/pki/src/main/java/zeroecho/pki/impl/audit/AuditEventCodec.java new file mode 100644 index 0000000..8e3f7bf --- /dev/null +++ b/pki/src/main/java/zeroecho/pki/impl/audit/AuditEventCodec.java @@ -0,0 +1,285 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.pki.impl.audit; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Instant; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; + +import zeroecho.pki.api.FormatId; +import zeroecho.pki.api.PkiId; +import zeroecho.pki.api.audit.AuditEvent; +import zeroecho.pki.api.audit.Principal; +import zeroecho.pki.api.audit.Purpose; + +/** + * Internal codec for deterministic audit event identification and line-based + * persistence encoding. + * + *+ * {@link #eventId(AuditEvent)} computes SHA-256 over canonical bytes from + * {@link #canonicalBytes(AuditEvent)}. Canonicalization sorts + * {@link AuditEvent#details()} keys to ensure deterministic output. + *
+ * + *+ * This component acts as a Policy Enforcement Point (PEP). For attribute + * operations it: + *
+ *+ * This class logs operational failures (e.g., PDP throws) and unexpected + * states, but never logs attribute values. + *
+ */ +public final class DefaultAttributeGovernanceService implements AttributeGovernanceService { + + private static final Logger LOGGER = Logger.getLogger(DefaultAttributeGovernanceService.class.getName()); + private static final String CATEGORY = "ATTRIBUTE_GOVERNANCE"; + + private final AttributeAccessController accessController; + private final AuditService auditService; + private final Clock clock; + + /** + * Creates a governance service. + * + * @param accessController decision point (PDP) + * @param auditService audit service for recording decisions + * @param clock time source + * @throws IllegalArgumentException if any argument is null + */ + public DefaultAttributeGovernanceService(AttributeAccessController accessController, AuditService auditService, + Clock clock) { + if (accessController == null) { + throw new IllegalArgumentException("accessController must not be null"); + } + if (auditService == null) { + throw new IllegalArgumentException("auditService must not be null"); + } + if (clock == null) { + throw new IllegalArgumentException("clock must not be null"); + } + this.accessController = accessController; + this.auditService = auditService; + this.clock = clock; + + LOGGER.log(Level.INFO, "DefaultAttributeGovernanceService initialized."); + } + + @Override + public Optional+ * Only metadata is recorded. No attribute values are included. + *
+ * + * @param definitionOrNull attribute definition or null if unknown attribute + * @param id attribute id + * @param action access action + * @param context access context + * @param decision access decision + */ + private void auditIfConfigured(AttributeDefinition definitionOrNull, AttributeId id, AccessAction action, + AccessContext context, AccessDecision decision) { + + boolean audit = shouldAudit(definitionOrNull, decision); + if (!audit) { + return; + } + + Instant now = clock.instant(); + + Map+ * This sink supports {@link java.util.ServiceLoader} by providing a public + * no-arg constructor. + *
+ * + *+ * baseDir/ + * YYYY/ + * MM/ + * YYYY-MM-DD.audit.log.gz + *+ * + *
+ * Each event is encoded as a single UTF-8 line and appended as its own gzip + * member. Members are concatenated to enable append-only writes. + *
+ * + *+ * {@link java.util.zip.GZIPInputStream#close()} closes the wrapped stream. When + * scanning concatenated gzip members, we must keep the outer stream open and + * only close the inflater state of each member. + *
+ */ + private static final class CloseShieldInputStream extends FilterInputStream { + + private CloseShieldInputStream(InputStream in) { + super(in); + } + + @Override + public void close() throws IOException { + // Intentionally do nothing. + } + } +} diff --git a/pki/src/main/java/zeroecho/pki/impl/audit/FileAuditSinkFiles.java b/pki/src/main/java/zeroecho/pki/impl/audit/FileAuditSinkFiles.java new file mode 100644 index 0000000..e001fe9 --- /dev/null +++ b/pki/src/main/java/zeroecho/pki/impl/audit/FileAuditSinkFiles.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.pki.impl.audit; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * Internal utilities for enumerating persisted audit files. + */ +final class FileAuditSinkFiles { + + private FileAuditSinkFiles() { + } + + /** + * Lists all {@code *.audit.log.gz} files under {@code baseDir} + * deterministically. + * + * @param baseDir base directory (must not be null) + * @return immutable, lexicographically sorted list of paths + * @throws IllegalArgumentException if {@code baseDir} is null + * @throws IllegalStateException if filesystem traversal fails + */ + /* default */ static List+ * This sink is bounded by {@code maxEvents}. When the capacity is exceeded, the + * oldest event is dropped (ring-buffer policy). This prevents unbounded memory + * growth in long-running processes. + *
+ * + *+ * This implementation is synchronized and safe for concurrent use. + *
+ */ +public final class InMemoryAuditSink implements AuditSink, SearchableAuditSink { + + private static final Logger LOG = Logger.getLogger(InMemoryAuditSink.class.getName()); + + /* default */ static final int DEFAULT_MAX_EVENTS = 10_000; + + private final int maxEvents; + private final Deque+ * Defaults to 10_000 events. + *
+ */ + public InMemoryAuditSink() { + this(DEFAULT_MAX_EVENTS); + } + + /** + * Creates a sink with an explicit maximum number of events. + * + * @param maxEvents maximum number of retained events (must be positive) + * @throws IllegalArgumentException if {@code maxEvents <= 0} + */ + public InMemoryAuditSink(int maxEvents) { + if (maxEvents <= 0) { + throw new IllegalArgumentException("maxEvents must be positive"); + } + this.maxEvents = maxEvents; + this.events = new ArrayDeque<>(Math.min(maxEvents, 1024)); + this.overflowLogged = false; + + LOG.log(Level.INFO, "InMemoryAuditSink initialized; maxEvents={0}", maxEvents); + } + + @Override + public synchronized void record(AuditEvent event) { // NOPMD + if (event == null) { + throw new IllegalArgumentException("event must not be null"); + } + + if (events.size() >= maxEvents) { + events.removeFirst(); + if (!overflowLogged) { + overflowLogged = true; + LOG.log(Level.WARNING, "InMemoryAuditSink capacity exceeded; dropping oldest events (maxEvents={0})", + maxEvents); + } + } + + events.addLast(event); + } + + /** + * Returns an immutable snapshot of all currently retained events in insertion + * order (oldest to newest). + * + * @return immutable snapshot + */ + public synchronized List+ * The public SPI {@code zeroecho.pki.spi.AuditSink} is record-only. Sinks may + * also implement this internal interface to support + * {@link zeroecho.pki.api.audit.AuditService#search(AuditQuery)} and + * {@link zeroecho.pki.api.audit.AuditService#get(PkiId)}. + *
+ */ +interface SearchableAuditSink { + + /** + * Searches persisted events by query constraints. + * + * @param query query constraints (must not be null) + * @return immutable list of matching events (never null) + * @throws IllegalArgumentException if {@code query} is null + * @throws RuntimeException on storage failure + */ + List+ * Timestamp is rendered in ISO-8601 UTC, and the output line layout is stable. + *
+ * + *+ * This sink is suitable for development and demonstrations. It is not intended + * as a production persistence backend. + *
+ */ +public final class StdoutAuditSink implements AuditSink { + + private static final DateTimeFormatter TS = DateTimeFormatter.ISO_INSTANT.withZone(ZoneOffset.UTC); + private static final Logger LOG = Logger.getLogger(StdoutAuditSink.class.getName()); + + /** + * Public no-arg constructor required for {@link java.util.ServiceLoader}. + */ + public StdoutAuditSink() { + LOG.info("initialized"); + } + + @Override + public void record(AuditEvent event) { + Objects.requireNonNull(event, "event"); + + String line = TS.format(event.time()) + " category=" + safe(event.category()) + " action=" + + safe(event.action()) + " principalType=" + safe(event.principal().type()) + " principalName=" + + safe(event.principal().name()) + " purpose=" + safe(event.purpose().value()); + + System.out.println(line); + } + + private static String safe(String s) { + if (s == null) { + return ""; + } + return s.replace('\n', ' ').replace('\r', ' '); + } +} diff --git a/pki/src/main/java/zeroecho/pki/impl/audit/StdoutAuditSinkProvider.java b/pki/src/main/java/zeroecho/pki/impl/audit/StdoutAuditSinkProvider.java new file mode 100644 index 0000000..0ab3828 --- /dev/null +++ b/pki/src/main/java/zeroecho/pki/impl/audit/StdoutAuditSinkProvider.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.pki.impl.audit; + +import java.util.Collections; +import java.util.Set; + +import zeroecho.pki.spi.ProviderConfig; +import zeroecho.pki.spi.audit.AuditSink; +import zeroecho.pki.spi.audit.AuditSinkProvider; + +/** + * + */ +public class StdoutAuditSinkProvider implements AuditSinkProvider { + @Override + public String id() { + return "stdout"; + } + + @Override + public Set+ * This package provides concrete implementations of the audit runtime, based on + * the public API {@code zeroecho.pki.api.audit.*} and the SPI + * {@code zeroecho.pki.spi.AuditSink}. + *
+ * + *+ * {@link zeroecho.pki.impl.audit.FileAuditSink} provides a baseline persistent + * store using daily gzip files and sequential scanning. It is suitable as an + * initial production-grade implementation without introducing a database. + *
+ */ +package zeroecho.pki.impl.audit; diff --git a/pki/src/main/java/zeroecho/pki/impl/fs/FilesystemPkiStore.java b/pki/src/main/java/zeroecho/pki/impl/fs/FilesystemPkiStore.java index 7873ecc..cb0d400 100644 --- a/pki/src/main/java/zeroecho/pki/impl/fs/FilesystemPkiStore.java +++ b/pki/src/main/java/zeroecho/pki/impl/fs/FilesystemPkiStore.java @@ -147,6 +147,8 @@ public final class FilesystemPkiStore implements PkiStore, Closeable { ensureVersionFile(); this.historySeq = new AtomicLong(0L); + LOG.log(Level.INFO, "running in {0}", root); + } catch (IOException e) { throw new IllegalStateException("failed to open filesystem store at " + root, e); } diff --git a/pki/src/main/java/zeroecho/pki/impl/fs/FilesystemPkiStoreProvider.java b/pki/src/main/java/zeroecho/pki/impl/fs/FilesystemPkiStoreProvider.java new file mode 100644 index 0000000..9aea6b3 --- /dev/null +++ b/pki/src/main/java/zeroecho/pki/impl/fs/FilesystemPkiStoreProvider.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.pki.impl.fs; + +import java.nio.file.Path; +import java.util.Objects; +import java.util.Set; + +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}. + * + *+ * Supported configuration keys: + *
+ *+ * {@link java.util.ServiceLoader} instantiates providers using a public no-arg + * constructor. However, the produced instances (stores, audit sinks, + * publishers, framework adapters) typically require backend-specific + * configuration (filesystem root, JDBC URL, remote endpoints, credentials, + * etc.). This SPI standardizes that configuration handoff. + *
+ * + *+ * {@link #id()} must return a stable identifier that is unique among providers + * of the same service type. Identifiers are used for deterministic selection + * (e.g. via system properties) and must not change across releases once + * published. + *
+ * + *+ * 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. + *
+ * + *+ * Configuration values may contain sensitive material (passwords, tokens, + * endpoints). Providers and bootstraps must never log configuration values. If + * logging is required, log provider id and configuration keys only. + *
+ * + * @param+ * Keys are informational (diagnostics/validation/help). Unknown keys must be + * ignored by implementations for forward compatibility. + *
+ * + * @return supported configuration keys (never {@code null}) + */ + Set+ * Implementations must validate required keys and throw + * {@link IllegalArgumentException} if configuration is incomplete or invalid. + *
+ * + * @param config configuration (never {@code null}) + * @return new instance (never {@code null}) + * @throws IllegalArgumentException if required configuration is missing/invalid + * @throws RuntimeException if opening fails + */ + T allocate(ProviderConfig config); +} diff --git a/pki/src/main/java/zeroecho/pki/spi/ProviderConfig.java b/pki/src/main/java/zeroecho/pki/spi/ProviderConfig.java new file mode 100644 index 0000000..a07ef5e --- /dev/null +++ b/pki/src/main/java/zeroecho/pki/spi/ProviderConfig.java @@ -0,0 +1,109 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.pki.spi; + +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** + * Backend configuration for {@link ConfigurableProvider} instances. + * + *+ * Values are stringly-typed by design to keep the SPI generic and stable across + * different backends (filesystem, JDBC, remote services). Providers interpret + * keys relevant to their backend and must ignore unknown keys for forward + * compatibility. + *
+ * + *+ * The {@code backendId} is the selected provider identifier + * ({@link ConfigurableProvider#id()}), used for diagnostics and selection + * correlation. It is not a URL, driver name, nor any secret. + *
+ * + *+ * This record is immutable and safe to share. Configuration values may be + * sensitive and must not be logged. If diagnostics are needed, log keys only. + *
+ * + * @param backendId selected backend id (never blank) + * @param properties backend properties (never {@code null}) + */ +public record ProviderConfig(String backendId, Map+ * {@link java.util.ServiceLoader} cannot construct {@link AuditSink} instances + * directly because stores typically require backend-specific configuration + * (filesystem root, etc.). This SPI provides a uniform instantiation mechanism. + *
+ * + *+ * Implementations must provide a public no-arg constructor to be discoverable + * via {@link java.util.ServiceLoader}. + *
+ * + *+ * Security note: providers and bootstraps must never log configuration values + * because they can contain sensitive material (passwords, tokens, endpoints). + *
+ */ +public interface AuditSinkProvider extends ConfigurableProvider+ * Implementations must validate required keys and throw + * {@link IllegalArgumentException} if configuration is incomplete or invalid. + *
+ * + * @param config configuration (never {@code null}) + * @return opened store (never {@code null}) + * @throws IllegalArgumentException if required configuration is missing/invalid + * @throws RuntimeException if opening fails + */ + @Override + AuditSink allocate(ProviderConfig config); +} diff --git a/pki/src/main/java/zeroecho/pki/spi/audit/package-info.java b/pki/src/main/java/zeroecho/pki/spi/audit/package-info.java new file mode 100644 index 0000000..9677219 --- /dev/null +++ b/pki/src/main/java/zeroecho/pki/spi/audit/package-info.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * Service Provider Interfaces (SPI) for PKI audit event persistence. + * + *+ * This package defines the SPI contracts that allow the PKI subsystem to + * delegate audit event persistence to pluggable, backend-specific + * implementations discovered via {@link java.util.ServiceLoader}. + *
+ * + *+ * Audit functionality in the PKI layer is intentionally decoupled from any + * concrete storage mechanism. Implementations may persist audit events to + * filesystems, databases, remote services, or in-memory buffers, depending on + * deployment requirements. + *
+ * + *+ * The SPI is split into two roles: + *
+ *+ * {@link java.util.ServiceLoader} is used only to discover + * {@code AuditSinkProvider} implementations. Direct discovery of + * {@code AuditSink} instances is intentionally avoided because most audit + * backends require configuration parameters (such as paths, connection strings, + * credentials, or limits). + *
+ * + *+ * Providers must expose a public no-argument constructor and create + * {@code AuditSink} instances exclusively through the + * {@link zeroecho.pki.spi.audit.AuditSinkProvider#allocate allocate} method, + * using a {@link zeroecho.pki.spi.ProviderConfig} supplied by the bootstrap or + * runtime environment. + *
+ * + *+ * Implementations of this SPI are security-critical. The following rules are + * mandatory: + *
+ *+ * Unless explicitly documented otherwise by a concrete implementation, + * {@code AuditSink} instances are expected to be thread-safe and usable by + * multiple concurrent PKI operations. + *
+ * + *+ * Lifecycle management (creation, activation, shutdown) is controlled by the + * PKI bootstrap and runtime layers; this SPI does not define explicit close or + * shutdown semantics unless required by a specific implementation. + *
+ * + *+ * This package is intended for infrastructure-level integration and must not be + * used directly by application code. Consumers should interact with audit + * functionality exclusively through the public PKI API. + *
+ */ +package zeroecho.pki.spi.audit; diff --git a/pki/src/main/java/zeroecho/pki/spi/bootstrap/PkiBootstrap.java b/pki/src/main/java/zeroecho/pki/spi/bootstrap/PkiBootstrap.java new file mode 100644 index 0000000..5cccaab --- /dev/null +++ b/pki/src/main/java/zeroecho/pki/spi/bootstrap/PkiBootstrap.java @@ -0,0 +1,217 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.pki.spi.bootstrap; + +import java.nio.file.Path; +import java.util.Map; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +import zeroecho.pki.spi.ConfigurableProvider; +import zeroecho.pki.spi.ProviderConfig; +import zeroecho.pki.spi.audit.AuditSink; +import zeroecho.pki.spi.audit.AuditSinkProvider; +import zeroecho.pki.spi.store.PkiStore; +import zeroecho.pki.spi.store.PkiStoreProvider; + +/** + * PKI bootstrap utilities for ServiceLoader-based components. + * + *+ * 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, + * etc.). + *
+ * + *+ * Security note: configuration values may be sensitive and must not be logged. + * This bootstrap logs only provider ids and configuration keys. + *
+ */ +public final class PkiBootstrap { + + private static final Logger LOG = Logger.getLogger(PkiBootstrap.class.getName()); + + private static final String PROP_STORE_BACKEND = "zeroecho.pki.store"; + private static final String PROP_STORE_PREFIX = "zeroecho.pki.store."; + + private static final String PROP_AUDIT_BACKEND = "zeroecho.pki.audit"; + private static final String PROP_AUDIT_PREFIX = "zeroecho.pki.audit."; + + private PkiBootstrap() { + throw new AssertionError("No instances."); + } + + /** + * Opens a {@link PkiStore} using {@link PkiStoreProvider} discovered via + * {@link java.util.ServiceLoader}. + * + *+ * Provider selection is deterministic and fail-fast: + *
+ *+ * Configuration properties are read from {@link System#getProperties()} using + * the prefix {@code zeroecho.pki.store.}. Values are treated as sensitive and + * are never logged; only keys may be logged. + *
+ * + *+ * Defaulting rules implemented by this bootstrap (policy, not SPI requirement): + * for {@code fs} provider, if {@code root} is not specified, defaults to + * {@code "pki-store"} relative to the working directory. + *
+ * + *+ * This method is suitable for CLI and server deployments. In DI containers + * (Spring, Micronaut, etc.), invoke it from managed components and close the + * returned store using the container lifecycle. + *
+ * + * @return opened store (never {@code null}) + * @throws IllegalStateException if provider selection is ambiguous or no + * provider is available + * @throws IllegalArgumentException if required configuration is missing/invalid + * @throws RuntimeException if allocation fails + */ + + public static PkiStore openStore() { + String requestedId = System.getProperty(PROP_STORE_BACKEND); + + PkiStoreProvider provider = SpiSelector.select(PkiStoreProvider.class, requestedId, + new SpiSelector.ProviderId<>() { + @Override + public String id(PkiStoreProvider p) { + return p.id(); + } + }); + + Map+ * This method is safe: it does not log configuration values. + *
+ * + * @param provider provider (never {@code null}) + */ + public static+ * Selection and configuration follow the same conventions as + * {@link #openStore()}, using {@code -Dzeroecho.pki.audit=<id>} and + * {@code zeroecho.pki.audit.} prefixed properties. + *
+ * + * @return opened audit sink (never {@code null}) + * @throws IllegalStateException if provider selection is ambiguous or no + * provider is available + * @throws IllegalArgumentException if required configuration is missing/invalid + * @throws RuntimeException if allocation fails + */ + public static AuditSink openAudit() { + String requestedId = System.getProperty(PROP_AUDIT_BACKEND); + + if (requestedId == null) { + requestedId = "stdout"; + } + + AuditSinkProvider provider = SpiSelector.select(AuditSinkProvider.class, requestedId, + new SpiSelector.ProviderId<>() { + + @Override + public String id(AuditSinkProvider p) { + return p.id(); + } + }); + + Map+ * This class centralizes deterministic provider-selection rules: + *
+ *+ * This utility is intended to be used by multiple bootstraps (store, audit, + * publish, framework components). It is deliberately small and has no logging + * side-effects. + *
+ * + *+ * Note: provider iteration order from {@link java.util.ServiceLoader} must not + * be relied upon. Determinism is achieved by explicit id selection or by + * requiring a single available provider. + *
+ */ +public final class SpiSelector { + + private SpiSelector() { + throw new AssertionError("No instances."); + } + + /** + * Provider id accessor used by the selector. + * + * @param+ * Convention: + *
+ *+ * Values may be sensitive and must not be logged. Callers may log keys only. + *
+ */ +public final class SpiSystemProperties { + + private SpiSystemProperties() { + throw new AssertionError("No instances."); + } + + /** + * Reads all string system properties with the given prefix. + * + *+ * The returned map is a snapshot of {@link System#getProperties()} at the time + * of invocation. Values are returned as-is (no trimming/normalization) to keep + * behavior explicit. + *
+ * + * @param prefix property prefix including trailing dot, e.g. + * {@code "zeroecho.pki.store."} + * @return map of subKey -> value (never {@code null}) + */ + public static Map