security(pki): enforce proof gates and durable signing workflows

This commit is contained in:
2026-07-29 14:20:21 +02:00
parent 49dc080c65
commit 07e04e0eed
53 changed files with 9628 additions and 950 deletions

View File

@@ -36,10 +36,12 @@ package zeroecho.pki.testkit;
import java.io.IOException;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.PublicKey;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import zeroecho.pki.api.CaService;
import zeroecho.pki.api.CertificationRequestService;
@@ -56,6 +58,7 @@ import zeroecho.pki.impl.core.DefaultRevocationService;
import zeroecho.pki.impl.core.DefaultStatusObjectService;
import zeroecho.pki.impl.core.async.PkiSigningBus;
import zeroecho.pki.impl.core.attr.SimpleAttributeSet;
import zeroecho.pki.impl.audit.InMemoryAuditSink;
import zeroecho.pki.impl.framework.x509.bc.BcX509CredentialFramework;
import zeroecho.pki.impl.framework.x509.bc.BcX509CredentialIssuerBackend;
import zeroecho.pki.impl.framework.x509.bc.BcX509StatusObjectGenerator;
@@ -63,6 +66,8 @@ import zeroecho.pki.impl.fs.FilesystemPkiStore;
import zeroecho.pki.impl.fs.FsPkiStoreOptions;
import zeroecho.pki.spi.crypto.SignatureWorkflow;
import zeroecho.pki.spi.framework.CredentialFramework;
import zeroecho.pki.spi.framework.CredentialIssuerBackend;
import zeroecho.pki.spi.framework.ProofOfPossessionVerifier;
import zeroecho.pki.spi.store.PkiStore;
/**
@@ -79,8 +84,10 @@ public final class PkiTestRuntime implements AutoCloseable {
private final FilesystemPkiStore store;
private final PkiSigningBus signingBus;
private final SignatureWorkflow signatureWorkflow;
private final InMemoryAuditSink auditSink;
private final CredentialFramework framework;
private final CredentialIssuerBackend issuerBackend;
private final CaService caService;
private final CertificationRequestService certificationRequestService;
@@ -88,23 +95,29 @@ public final class PkiTestRuntime implements AutoCloseable {
private final RevocationService revocationService;
private final StatusObjectService statusObjectService;
private final Map<String, KeyPair> keyPairsByKeyRef;
private final Map<String, PublicKey> publicKeysByKeyRef;
private Runnable publicKeyResolveHook;
private PkiTestRuntime(FilesystemPkiStore store, PkiSigningBus signingBus, SignatureWorkflow signatureWorkflow,
CredentialFramework framework, Map<String, KeyPair> keyPairsByKeyRef) {
CredentialFramework framework, CredentialIssuerBackend issuerBackend,
Map<String, PublicKey> publicKeysByKeyRef, Duration signingTtl) {
this.store = store;
this.signingBus = signingBus;
this.signatureWorkflow = signatureWorkflow;
this.auditSink = new InMemoryAuditSink();
this.framework = framework;
this.keyPairsByKeyRef = keyPairsByKeyRef;
this.issuerBackend = issuerBackend;
this.publicKeysByKeyRef = publicKeysByKeyRef;
this.publicKeyResolveHook = () -> {
};
this.certificationRequestService = new DefaultCertificationRequestService(store, framework);
this.issuanceService = new DefaultIssuanceService(store, framework);
this.issuanceService = new DefaultIssuanceService(store, framework, issuerBackend, auditSink);
this.revocationService = new DefaultRevocationService(store);
this.statusObjectService = new DefaultStatusObjectService(store, framework);
this.caService = new DefaultCaService(store, framework, this::resolvePublicKeyInfo, signingBus, "SHA256withRSA",
Duration.ofSeconds(2));
this.caService = new DefaultCaService(store, framework, issuerBackend, this::resolvePublicKeyInfo, signingBus,
auditSink, "SHA256withRSA", signingTtl);
}
/**
@@ -116,9 +129,36 @@ public final class PkiTestRuntime implements AutoCloseable {
* @return runtime
*/
public static PkiTestRuntime create(Path rootDir, Path busFile, Map<KeyRef, KeyPair> keyPairs) {
Map<KeyRef, PublicKey> publicKeys = new HashMap<>();
for (Map.Entry<KeyRef, KeyPair> entry : keyPairs.entrySet()) {
publicKeys.put(entry.getKey(), entry.getValue().getPublic());
}
return create(rootDir, busFile, keyPairs, publicKeys, Optional.empty());
}
/**
* Creates a test runtime with independently controlled signing keys, resolved
* public keys, and proof verifier.
*
* @param rootDir working root for the filesystem store
* @param busFile durable bus line store file path
* @param signingKeys signing workflow key pairs indexed by key reference
* @param resolvedKeys public keys returned by managed-key resolution
* @param proofVerifier proof verifier used by the credential framework
* @return runtime
*/
public static PkiTestRuntime create(Path rootDir, Path busFile, Map<KeyRef, KeyPair> signingKeys,
Map<KeyRef, PublicKey> resolvedKeys, ProofOfPossessionVerifier proofVerifier) {
return create(rootDir, busFile, signingKeys, resolvedKeys, Optional.of(proofVerifier));
}
private static PkiTestRuntime create(Path rootDir, Path busFile, Map<KeyRef, KeyPair> keyPairs,
Map<KeyRef, PublicKey> resolvedKeys, Optional<ProofOfPossessionVerifier> proofVerifier) {
Objects.requireNonNull(rootDir, "rootDir");
Objects.requireNonNull(busFile, "busFile");
Objects.requireNonNull(keyPairs, "keyPairs");
Objects.requireNonNull(resolvedKeys, "resolvedKeys");
Objects.requireNonNull(proofVerifier, "proofVerifier");
FsPkiStoreOptions opts = FsPkiStoreOptions.defaults();
@@ -129,6 +169,10 @@ public final class PkiTestRuntime implements AutoCloseable {
for (Map.Entry<KeyRef, KeyPair> e : keyPairs.entrySet()) {
byRef.put(e.getKey().value(), e.getValue());
}
Map<String, PublicKey> publicByRef = new HashMap<>();
for (Map.Entry<KeyRef, PublicKey> entry : resolvedKeys.entrySet()) {
publicByRef.put(entry.getKey().value(), entry.getValue());
}
SignatureWorkflow signer = new InMemorySignatureWorkflow(byRef);
PkiSigningBus signingBus = new PkiSigningBus(store, signer, busFile);
@@ -137,17 +181,43 @@ public final class PkiTestRuntime implements AutoCloseable {
Duration.ofSeconds(2));
BcX509StatusObjectGenerator statusGen = new BcX509StatusObjectGenerator(signingBus, "SHA256withRSA",
Duration.ofSeconds(2));
CredentialFramework framework = new BcX509CredentialFramework().wired(issuerBackend, statusGen);
BcX509CredentialFramework baseFramework = new BcX509CredentialFramework();
CredentialFramework framework = proofVerifier
.map(verifier -> baseFramework.wired(statusGen, verifier))
.orElseGet(() -> baseFramework.wired(statusGen));
return new PkiTestRuntime(store, signingBus, signer, framework, byRef);
return new PkiTestRuntime(store, signingBus, signer, framework, issuerBackend, publicByRef,
Duration.ofSeconds(2));
}
public static PkiTestRuntime createWithPendingSigner(Path rootDir, Path busFile, Map<KeyRef, KeyPair> keyPairs,
Duration signingTtl) {
Objects.requireNonNull(signingTtl, "signingTtl");
FsPkiStoreOptions opts = FsPkiStoreOptions.defaults();
FilesystemPkiStore store = new FilesystemPkiStore(rootDir.resolve("store"), opts);
Map<String, KeyPair> byRef = new HashMap<>();
Map<String, PublicKey> publicByRef = new HashMap<>();
for (Map.Entry<KeyRef, KeyPair> entry : keyPairs.entrySet()) {
byRef.put(entry.getKey().value(), entry.getValue());
publicByRef.put(entry.getKey().value(), entry.getValue().getPublic());
}
SignatureWorkflow signer = new InMemorySignatureWorkflow(byRef, false);
PkiSigningBus signingBus = new PkiSigningBus(store, signer, busFile);
BcX509CredentialIssuerBackend issuerBackend = new BcX509CredentialIssuerBackend(signingBus,
"SHA256withRSA", signingTtl);
BcX509StatusObjectGenerator statusGen = new BcX509StatusObjectGenerator(signingBus, "SHA256withRSA",
signingTtl);
CredentialFramework framework = new BcX509CredentialFramework().wired(statusGen);
return new PkiTestRuntime(store, signingBus, signer, framework, issuerBackend, publicByRef, signingTtl);
}
private EncodedObject resolvePublicKeyInfo(KeyRef keyRef) {
KeyPair kp = keyPairsByKeyRef.get(keyRef.value());
if (kp == null) {
publicKeyResolveHook.run();
PublicKey publicKey = publicKeysByKeyRef.get(keyRef.value());
if (publicKey == null) {
throw new IllegalArgumentException("Unknown keyRef");
}
return new EncodedObject(Encoding.DER, kp.getPublic().getEncoded());
return new EncodedObject(Encoding.DER, publicKey.getEncoded());
}
public PkiStore store() {
@@ -162,14 +232,65 @@ public final class PkiTestRuntime implements AutoCloseable {
return signatureWorkflow;
}
public int submittedSignCount() {
return ((InMemorySignatureWorkflow) signatureWorkflow).submittedSignCount();
}
public void replaceManagedKey(KeyRef keyRef, KeyPair keyPair) {
publicKeysByKeyRef.put(keyRef.value(), keyPair.getPublic());
((InMemorySignatureWorkflow) signatureWorkflow).putKeyPair(keyRef, keyPair);
}
/**
* Replaces only the public key returned by the managed-key resolver.
*
* @param keyRef managed key reference
* @param publicKey replacement resolved public key
*/
public void replaceResolvedKey(KeyRef keyRef, PublicKey publicKey) {
publicKeysByKeyRef.put(keyRef.value(), Objects.requireNonNull(publicKey, "publicKey"));
}
public void onPublicKeyResolve(Runnable hook) {
this.publicKeyResolveHook = Objects.requireNonNull(hook, "hook");
}
public boolean hasRunningSignatureOperations() {
return ((InMemorySignatureWorkflow) signatureWorkflow).hasRunningOperations();
}
/**
* Returns the deterministic audit sink shared by the test runtime services.
*
* @return in-memory audit sink
*/
public InMemoryAuditSink auditSink() {
return auditSink;
}
public CredentialFramework framework() {
return framework;
}
public CredentialIssuerBackend issuerBackend() {
return issuerBackend;
}
public CaService caService() {
return caService;
}
public CaService caService(CredentialFramework credentialFramework) {
return new DefaultCaService(store, Objects.requireNonNull(credentialFramework, "credentialFramework"),
issuerBackend, this::resolvePublicKeyInfo, signingBus, auditSink, "SHA256withRSA",
Duration.ofSeconds(2));
}
public CaService caService(CredentialIssuerBackend backend) {
return new DefaultCaService(store, framework, Objects.requireNonNull(backend, "backend"),
this::resolvePublicKeyInfo, signingBus, auditSink, "SHA256withRSA", Duration.ofSeconds(2));
}
public CertificationRequestService certificationRequestService() {
return certificationRequestService;
}
@@ -186,6 +307,11 @@ public final class PkiTestRuntime implements AutoCloseable {
return statusObjectService;
}
/**
* Returns a new empty attribute set suitable for test commands.
*
* @return empty attributes
*/
public SimpleAttributeSet emptyAttributes() {
return new SimpleAttributeSet();
}