feat(pki-server): add multi-CA security foundation

Add the durable multi-authority realm, scoped default-deny authorization,
approval and break-glass workflows, auditor views and disclosure policy.

Enforce all administration through the transport-neutral secured operation
gateway while preserving immutable PKI authority and future HTTP reuse.
This commit is contained in:
2026-08-04 18:46:00 +02:00
parent e997996316
commit 8a5cbb61b3
30 changed files with 4791 additions and 6 deletions

View File

@@ -0,0 +1,77 @@
/*******************************************************************************
* Copyright (C) 2026, 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.application;
import java.util.Objects;
import java.util.Optional;
import zeroecho.pki.api.PkiId;
import zeroecho.pki.api.publication.PublicationRecord;
import zeroecho.pki.api.publication.PublicationSourceType;
import zeroecho.pki.api.status.StatusObject;
import zeroecho.pki.spi.store.PkiStore;
/** Store-backed scope resolver that exposes identifiers only. */
final class DefaultPkiResourceScopeResolver implements PkiResourceScopeResolver {
private final PkiStore store;
private final Runnable openCheck;
/* default */ DefaultPkiResourceScopeResolver(PkiStore store, Runnable openCheck) {
this.store = Objects.requireNonNull(store, "store");
this.openCheck = Objects.requireNonNull(openCheck, "openCheck");
}
@Override
public Optional<PkiId> credentialAuthority(PkiId credentialId) {
openCheck.run();
return store.getCredential(Objects.requireNonNull(credentialId, "credentialId"))
.map(credential -> credential.issuerRef().caId());
}
@Override
public Optional<PkiId> statusObjectAuthority(PkiId statusObjectId) {
openCheck.run();
return store.getStatusObject(Objects.requireNonNull(statusObjectId, "statusObjectId"))
.map(StatusObject::issuerCaId);
}
@Override
public Optional<PkiId> publicationAuthority(PkiId publicationId) {
openCheck.run();
Optional<PublicationRecord> record = store.getPublicationRecord(
Objects.requireNonNull(publicationId, "publicationId"));
return record.flatMap(value -> value.sourceType() == PublicationSourceType.CREDENTIAL
? credentialAuthority(value.sourceId()) : statusObjectAuthority(value.sourceId()));
}
}

View File

@@ -114,6 +114,7 @@ final class DefaultPkiSession implements PkiSession {
private final Optional<PkiSigningBus> signingBus;
private final Optional<SignatureWorkflow> signatureWorkflow;
private final X509AlgorithmBindingRegistry algorithmBindings;
private final PkiResourceScopeResolver resourceScopes;
private final PkiOperationExecutor operations;
private final AtomicBoolean closed = new AtomicBoolean();
@@ -132,6 +133,7 @@ final class DefaultPkiSession implements PkiSession {
this.signingBus = graph.signingBus();
this.signatureWorkflow = graph.signatureWorkflow();
this.algorithmBindings = Objects.requireNonNull(algorithmBindings, "algorithmBindings");
this.resourceScopes = new DefaultPkiResourceScopeResolver(store, this::requireOpen);
this.operations = new DefaultPkiOperationExecutor(configuration, store, profiles, revocations, authorities,
requests, issuance, statusObjects, publications, algorithmBindings, this::requireOpen);
}
@@ -175,7 +177,8 @@ final class DefaultPkiSession implements PkiSession {
ServiceGraph graph = ServiceGraph.empty();
try {
store = Objects.requireNonNull(bootstrap.openStore(exact.store()), "opened store");
audit = Objects.requireNonNull(bootstrap.openAudit(exact.audit()), "opened audit sink");
audit = dependencies.auditSink().orElseGet(
() -> Objects.requireNonNull(bootstrap.openAudit(exact.audit()), "opened audit sink"));
ProfileService profiles = new DefaultProfileService(store, clock, audit, algorithmBindings,
exact.signing().isPresent(),
exact.signing().flatMap(PkiSessionConfiguration.SigningConfiguration::certificateSignatureBinding),
@@ -240,6 +243,12 @@ final class DefaultPkiSession implements PkiSession {
return algorithmBindings;
}
@Override
public PkiResourceScopeResolver resourceScopes() {
requireOpen();
return resourceScopes;
}
@Override
public PkiOperationExecutor operations() {
requireOpen();

View File

@@ -0,0 +1,56 @@
/*******************************************************************************
* Copyright (C) 2026, 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.application;
import java.util.Optional;
import zeroecho.pki.api.PkiId;
/**
* Read-only transport-neutral resolver for authorization scope cross-checks.
*
* <p>Resolved identifiers are metadata, not access authority. Callers must still
* authorize the requested action and must not reveal an empty result to an
* unauthorized principal.</p>
*/
public interface PkiResourceScopeResolver {
/** Returns the exact logical issuer authority of one committed credential. */
Optional<PkiId> credentialAuthority(PkiId credentialId);
/** Returns the exact logical issuer authority of one committed status object. */
Optional<PkiId> statusObjectAuthority(PkiId statusObjectId);
/** Returns the authority of the immutable source behind one publication operation. */
Optional<PkiId> publicationAuthority(PkiId publicationId);
}

View File

@@ -115,6 +115,23 @@ public interface PkiSession extends AutoCloseable {
*/
X509AlgorithmBindingRegistry algorithmBindings();
/**
* Returns the read-only authoritative identifier resolver used to cross-check
* realm and authority scopes before protected operations.
*
* @return session-owned resource-scope resolver
*/
default PkiResourceScopeResolver resourceScopes() {
return new PkiResourceScopeResolver() {
@Override public Optional<zeroecho.pki.api.PkiId> credentialAuthority(
zeroecho.pki.api.PkiId credentialId) { return Optional.empty(); }
@Override public Optional<zeroecho.pki.api.PkiId> statusObjectAuthority(
zeroecho.pki.api.PkiId statusObjectId) { return Optional.empty(); }
@Override public Optional<zeroecho.pki.api.PkiId> publicationAuthority(
zeroecho.pki.api.PkiId publicationId) { return Optional.empty(); }
};
}
/** @return shared typed operation executor owned by this session */
PkiOperationExecutor operations();

View File

@@ -37,18 +37,31 @@ import java.util.Objects;
import java.util.Optional;
import zeroecho.core.spi.KeyringUnlockProvider;
import zeroecho.pki.spi.audit.AuditSink;
/** Immutable process-local capabilities for one PKI session composition. */
public record PkiSessionRuntimeDependencies(Optional<KeyringUnlockProvider> keyringUnlockProvider) {
/**
* Immutable process-local capabilities for one PKI session composition.
*
* @param keyringUnlockProvider optional secure key-unlock capability
* @param auditSink optional precomposed lifecycle-owned audit sink
*/
public record PkiSessionRuntimeDependencies(Optional<KeyringUnlockProvider> keyringUnlockProvider,
Optional<AuditSink> auditSink) {
/** Creates dependencies without an externally composed audit sink. */
public PkiSessionRuntimeDependencies(Optional<KeyringUnlockProvider> keyringUnlockProvider) {
this(keyringUnlockProvider, Optional.empty());
}
/** Validates the optional capability container. */
public PkiSessionRuntimeDependencies {
keyringUnlockProvider = Objects.requireNonNull(keyringUnlockProvider, "keyringUnlockProvider");
auditSink = Objects.requireNonNull(auditSink, "auditSink");
}
/** @return an immutable dependency set without key-unlock access */
public static PkiSessionRuntimeDependencies none() {
return new PkiSessionRuntimeDependencies(Optional.empty());
return new PkiSessionRuntimeDependencies(Optional.empty(), Optional.empty());
}
/**
@@ -58,6 +71,22 @@ public record PkiSessionRuntimeDependencies(Optional<KeyringUnlockProvider> keyr
* @return immutable runtime dependencies
*/
public static PkiSessionRuntimeDependencies withKeyringUnlockProvider(KeyringUnlockProvider provider) {
return new PkiSessionRuntimeDependencies(Optional.of(Objects.requireNonNull(provider, "provider")));
return new PkiSessionRuntimeDependencies(Optional.of(Objects.requireNonNull(provider, "provider")),
Optional.empty());
}
/**
* Returns a dependency set using one lifecycle-owned shared audit sink.
*
* <p>The opened session assumes ownership and closes the sink. This enables
* transport-neutral compositions to share one audit authority with adjacent
* control services without exposing it through {@link PkiSession}.</p>
*
* @param sink configured audit sink transferred to the session
* @return immutable dependencies preserving any key-unlock capability
*/
public PkiSessionRuntimeDependencies withAuditSink(AuditSink sink) {
return new PkiSessionRuntimeDependencies(keyringUnlockProvider,
Optional.of(Objects.requireNonNull(sink, "sink")));
}
}

View File

@@ -99,6 +99,31 @@ class PkiSessionLifecycleTest {
System.out.println("...ok");
}
@Test
void ownsOneExplicitlyPrecomposedAuditSink() throws Exception {
System.out.println("ownsOneExplicitlyPrecomposedAuditSink");
AtomicInteger closes = new AtomicInteger();
AuditSink shared = new AuditSink() {
@Override
public void record(zeroecho.pki.api.audit.AuditEvent event) {
// The lifecycle assertion does not require emitting an event.
}
@Override
public void close() {
closes.incrementAndGet();
}
};
PkiSessionRuntimeDependencies dependencies = PkiSessionRuntimeDependencies.none().withAuditSink(shared);
PkiSession session = PkiSession.open(configuration(temporaryDirectory.resolve("shared-audit-store")),
dependencies);
session.close();
session.close();
assertEquals(1, closes.get());
System.out.println("...shared-audit-closes=" + closes.get());
System.out.println("...ok");
}
@Test
void explicitlyActivatesAuthorizedDeployerBindingProvider() throws Exception {
System.out.println("explicitlyActivatesAuthorizedDeployerBindingProvider");