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:
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user