feat(pki): complete PKI administration CLI
Complete the transport-neutral PKI session composition and expose all currently implemented administration capabilities through typed direct and batch operations. Preserve explicit optional capabilities, KeyRef confinement, deterministic lifecycle handling and reuse by the future persistent server.
This commit is contained in:
47
README.md
47
README.md
@@ -41,6 +41,53 @@ Whether you're working on secure file storage, encrypted communication, or priva
|
||||
|
||||
- CLI Tools & Keystore Management
|
||||
|
||||
## PKI administration CLI
|
||||
|
||||
The application distribution provides one synchronous PKI administration entry
|
||||
point. Direct commands and JSON plans use the same typed backend operation
|
||||
executor and one lifecycle-owned `PkiSession`:
|
||||
|
||||
```text
|
||||
zeroecho pki --help
|
||||
zeroecho pki configuration.validate --config pki-config.json
|
||||
zeroecho pki run administration-plan.json --config pki-config.json --output json
|
||||
```
|
||||
|
||||
The administration surface covers every administratively meaningful capability
|
||||
implemented by the current backend: configuration validation; persisted profile
|
||||
validation/import/inspection/activation; CA creation, inspection, listing, and
|
||||
lifecycle transitions; strict CSR import and proof verification; credential
|
||||
issuance and inspection; revocation inspection and bounded history/snapshot
|
||||
presentation; signed status/CRL generation and inspection; and explicit
|
||||
publication registration, processing, retry, reconciliation, inspection, and
|
||||
listing. Presentation limits bound terminal output only; backend revocation and
|
||||
publication cursors remain streaming and do not acquire an aggregate population
|
||||
limit.
|
||||
|
||||
Plans are versioned JSON documents with ordered, uniquely named operations.
|
||||
References use exact whole values from earlier successful steps. Plans support
|
||||
only `FAIL_FAST` and `CONTINUE_INDEPENDENT`; they have no scripts, expressions,
|
||||
loops, implicit retries, or rollback across already committed operations. Binary
|
||||
artifacts and secrets are never emitted in terminal JSON.
|
||||
|
||||
Version-two session configuration enables capabilities explicitly. A signing
|
||||
section selects one signature-workflow provider, one X.509 framework, a durable
|
||||
signing-bus location, an algorithm, a finite signing deadline, and the name of an
|
||||
environment variable supplying the process-local keyring unlock capability.
|
||||
Publisher entries select configured destinations independently, so publication
|
||||
can be administered without enabling signing. Values of unlock variables,
|
||||
private keys, and publisher credentials are never operation arguments or output.
|
||||
Read-only version-one configurations remain valid and allocate neither signing
|
||||
nor publisher capabilities.
|
||||
|
||||
The CLI and a future server share the same transport-neutral `PkiSession` and
|
||||
typed operations; batch execution opens that service graph once. Backup and
|
||||
import/export commands remain unavailable because the current backend has no
|
||||
production service implementation for them. Final backend release readiness is
|
||||
intentionally deferred until the planned extensible X.509 algorithm/OID binding
|
||||
work and its CLI integration are complete; this CLI does not anticipate or add
|
||||
placeholders for that future work.
|
||||
|
||||
|
||||
## Development Status
|
||||
|
||||
|
||||
@@ -56,6 +56,18 @@ public final class PkiCli {
|
||||
private static final String CONFIG = "--config";
|
||||
private static final String OUTPUT = "--output";
|
||||
private static final String RUN = "run";
|
||||
private static final Map<String, String> OPERATION_ARGUMENT_NAMES = Map.ofEntries(
|
||||
Map.entry("--profile-file", "profileFile"), Map.entry("--profile-id", "profileId"),
|
||||
Map.entry("--profile-version", "profileVersion"), Map.entry("--ca-id", "caId"),
|
||||
Map.entry("--issuer-ca-id", "issuerCaId"), Map.entry("--format-id", "formatId"),
|
||||
Map.entry("--subject-ref", "subjectRef"), Map.entry("--key-ref", "keyRef"),
|
||||
Map.entry("--state", "state"), Map.entry("--request-id", "requestId"),
|
||||
Map.entry("--request-file", "requestFile"), Map.entry("--encoding", "encoding"),
|
||||
Map.entry("--credential-id", "credentialId"), Map.entry("--status-object-id", "statusObjectId"),
|
||||
Map.entry("--type", "type"), Map.entry("--publication-id", "publicationId"),
|
||||
Map.entry("--source-type", "sourceType"), Map.entry("--source-id", "sourceId"),
|
||||
Map.entry("--target-type", "targetType"), Map.entry("--target-id", "targetId"),
|
||||
Map.entry("--reason", "reason"), Map.entry("--limit", "limit"));
|
||||
private static final int RUN_TOKEN_COUNT = 2;
|
||||
|
||||
private PkiCli() {
|
||||
@@ -195,8 +207,8 @@ public final class PkiCli {
|
||||
for (int index = 0; index < tokens.size(); index += 2) {
|
||||
String name = argumentName(tokens.get(index));
|
||||
String raw = tokens.get(index + 1);
|
||||
PkiOperationValue value = "limit".equals(name) ? new PkiOperationValue.IntegerValue(parseLong(raw))
|
||||
: new PkiOperationValue.Text(raw);
|
||||
PkiOperationValue value = "limit".equals(name) || "profileVersion".equals(name)
|
||||
? new PkiOperationValue.IntegerValue(parseLong(raw)) : new PkiOperationValue.Text(raw);
|
||||
if (arguments.putIfAbsent(name, value) != null) {
|
||||
throw new IllegalArgumentException("Operation argument is duplicated");
|
||||
}
|
||||
@@ -205,14 +217,11 @@ public final class PkiCli {
|
||||
}
|
||||
|
||||
private static String argumentName(String option) {
|
||||
return switch (option) {
|
||||
case "--profile-file" -> "profileFile";
|
||||
case "--credential-id" -> "credentialId";
|
||||
case "--publication-id" -> "publicationId";
|
||||
case "--reason" -> "reason";
|
||||
case "--limit" -> "limit";
|
||||
default -> throw new IllegalArgumentException("Unknown PKI operation argument");
|
||||
};
|
||||
String name = OPERATION_ARGUMENT_NAMES.get(option);
|
||||
if (name == null) {
|
||||
throw new IllegalArgumentException("Unknown PKI operation argument");
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
private static PkiPlan singlePlan(Invocation invocation, PkiOperationRegistry registry) throws IOException {
|
||||
@@ -256,10 +265,33 @@ public final class PkiCli {
|
||||
writer.println("Operations:");
|
||||
writer.println(" configuration.validate");
|
||||
writer.println(" profile.validate --profile-file <file>");
|
||||
writer.println(" profile.register --profile-file <file>");
|
||||
writer.println(" profile.inspect --profile-id <id> --profile-version <version>");
|
||||
writer.println(" profile.list --profile-id <id>");
|
||||
writer.println(" profile.activate --profile-id <id> --profile-version <version>");
|
||||
writer.println(" ca.inspect --ca-id <id>");
|
||||
writer.println(" ca.list --limit <1..1000>");
|
||||
writer.println(" ca.create --format-id <id> --subject-ref <subject> --profile-id <id> --key-ref <ref>");
|
||||
writer.println(" ca.transition --ca-id <id> --state <state> --reason <reason>");
|
||||
writer.println(" request.inspect --request-id <id>");
|
||||
writer.println(" request.import --format-id <id> --encoding DER --request-file <file>");
|
||||
writer.println(" request.verify --request-id <id>");
|
||||
writer.println(" credential.inspect --credential-id <id>");
|
||||
writer.println(" credential.issue --issuer-ca-id <id> --request-id <id> --profile-id <id>");
|
||||
writer.println(" credential.revoke --credential-id <id> --reason <permanent-reason>");
|
||||
writer.println(" revocation.inspect --credential-id <id>");
|
||||
writer.println(" revocation.history --credential-id <id> --limit <1..1000>");
|
||||
writer.println(" revocation.snapshot --limit <1..1000>");
|
||||
writer.println(" status.inspect --status-object-id <id>");
|
||||
writer.println(" status.generate --issuer-ca-id <id> --type <type> --format-id <id>");
|
||||
writer.println(" inventory.status --issuer-ca-id <id> --limit <1..1000>");
|
||||
writer.println(" publication.inspect --publication-id <id>");
|
||||
writer.println(" publication.list --limit <1..1000>");
|
||||
writer.println(" publication.register --publication-id <id> --source-type <type> --source-id <id>");
|
||||
writer.println(" --target-type <type> --target-id <id>");
|
||||
writer.println(" publication.process --publication-id <id>");
|
||||
writer.println(" publication.retry --publication-id <id>");
|
||||
writer.println(" publication.reconcile --publication-id <id>");
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
package zeroecho.pki.cli;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -45,8 +47,12 @@ import zeroecho.pki.spi.ProviderConfig;
|
||||
/** Strict versioned CLI configuration loader. */
|
||||
final class PkiCliConfiguration {
|
||||
|
||||
private static final Set<String> ROOT_FIELDS = Set.of("version", "store", "audit");
|
||||
private static final int VERSION_ONE = 1;
|
||||
private static final Set<String> ROOT_FIELDS_V1 = Set.of("version", "store", "audit");
|
||||
private static final Set<String> ROOT_FIELDS_V2 = Set.of("version", "store", "audit", "signing", "publishers");
|
||||
private static final Set<String> PROVIDER_FIELDS = Set.of("provider", "properties");
|
||||
private static final Set<String> SIGNING_FIELDS = Set.of("workflow", "framework", "busPath",
|
||||
"signatureAlgorithm", "signingTtlSeconds", "unlockEnvironmentVariable");
|
||||
private static final String STDOUT_PROVIDER = "stdout";
|
||||
|
||||
private PkiCliConfiguration() {
|
||||
@@ -55,15 +61,43 @@ final class PkiCliConfiguration {
|
||||
/* default */ static PkiSessionConfiguration read(Path path) throws java.io.IOException {
|
||||
byte[] document = PkiCliFiles.readRegularFile(path, PkiCliJson.MAXIMUM_DOCUMENT_BYTES);
|
||||
PkiOperationValue.ObjectValue root = object(PkiCliJson.parse(document));
|
||||
requireExactFields(root.fields(), ROOT_FIELDS);
|
||||
int version = Math.toIntExact(integer(required(root, "version")));
|
||||
if (version == VERSION_ONE) {
|
||||
requireExactFields(root.fields(), ROOT_FIELDS_V1);
|
||||
} else {
|
||||
requireVersionTwoFields(root.fields());
|
||||
}
|
||||
ProviderConfig store = provider(required(root, "store"));
|
||||
ProviderConfig audit = provider(required(root, "audit"));
|
||||
if (STDOUT_PROVIDER.equals(audit.backendId())) {
|
||||
throw new IllegalArgumentException("CLI audit provider must not write to standard output");
|
||||
}
|
||||
if (version == VERSION_ONE) {
|
||||
return new PkiSessionConfiguration(version, store, audit);
|
||||
}
|
||||
java.util.Optional<PkiSessionConfiguration.SigningConfiguration> signing = java.util.Optional
|
||||
.ofNullable(root.fields().get("signing")).map(PkiCliConfiguration::signing);
|
||||
PkiOperationValue publisherValue = root.fields().get("publishers");
|
||||
List<ProviderConfig> publishers = publisherValue == null ? List.of()
|
||||
: list(publisherValue).values().stream().map(PkiCliConfiguration::provider).toList();
|
||||
return new PkiSessionConfiguration(version, store, audit, signing, publishers);
|
||||
}
|
||||
|
||||
private static void requireVersionTwoFields(Map<String, PkiOperationValue> actual) {
|
||||
if (!actual.keySet().containsAll(ROOT_FIELDS_V1) || !ROOT_FIELDS_V2.containsAll(actual.keySet())) {
|
||||
throw new IllegalArgumentException("CLI document fields are invalid");
|
||||
}
|
||||
}
|
||||
|
||||
private static PkiSessionConfiguration.SigningConfiguration signing(PkiOperationValue value) {
|
||||
PkiOperationValue.ObjectValue object = object(value);
|
||||
requireExactFields(object.fields(), SIGNING_FIELDS);
|
||||
return new PkiSessionConfiguration.SigningConfiguration(provider(required(object, "workflow")),
|
||||
provider(required(object, "framework")), text(required(object, "busPath")),
|
||||
text(required(object, "signatureAlgorithm")),
|
||||
Duration.ofSeconds(integer(required(object, "signingTtlSeconds"))),
|
||||
java.util.Optional.of(text(required(object, "unlockEnvironmentVariable"))));
|
||||
}
|
||||
|
||||
private static ProviderConfig provider(PkiOperationValue value) {
|
||||
PkiOperationValue.ObjectValue object = object(value);
|
||||
@@ -92,6 +126,13 @@ final class PkiCliConfiguration {
|
||||
throw new IllegalArgumentException("CLI field has the wrong type");
|
||||
}
|
||||
|
||||
private static PkiOperationValue.ListValue list(PkiOperationValue value) {
|
||||
if (value instanceof PkiOperationValue.ListValue list) {
|
||||
return list;
|
||||
}
|
||||
throw new IllegalArgumentException("CLI field has the wrong type");
|
||||
}
|
||||
|
||||
/* default */ static String text(PkiOperationValue value) {
|
||||
if (value instanceof PkiOperationValue.Text text) {
|
||||
return text.value();
|
||||
|
||||
@@ -39,9 +39,19 @@ import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import zeroecho.pki.api.EncodedObject;
|
||||
import zeroecho.pki.api.Encoding;
|
||||
import zeroecho.pki.api.FormatId;
|
||||
import zeroecho.pki.api.KeyRef;
|
||||
import zeroecho.pki.api.PkiId;
|
||||
import zeroecho.pki.api.SubjectRef;
|
||||
import zeroecho.pki.api.ca.CaState;
|
||||
import zeroecho.pki.api.profile.CertificateProfileDocumentCodec;
|
||||
import zeroecho.pki.api.publication.PublicationSourceType;
|
||||
import zeroecho.pki.api.publication.PublicationTarget;
|
||||
import zeroecho.pki.api.publication.PublicationTargetType;
|
||||
import zeroecho.pki.api.revocation.RevocationReason;
|
||||
import zeroecho.pki.api.status.StatusObjectType;
|
||||
import zeroecho.pki.application.PkiOperation;
|
||||
import zeroecho.pki.application.PkiOperationValue;
|
||||
|
||||
@@ -50,6 +60,15 @@ import zeroecho.pki.application.PkiOperationValue;
|
||||
final class PkiOperationRegistry {
|
||||
|
||||
private static final String CREDENTIAL_ID = "credentialId";
|
||||
private static final String FORMAT_ID = "formatId";
|
||||
private static final String ISSUER_CA_ID = "issuerCaId";
|
||||
private static final String LIMIT = "limit";
|
||||
private static final String PUBLICATION_ID = "publicationId";
|
||||
private static final String PROFILE_FILE = "profileFile";
|
||||
private static final String PROFILE_ID = "profileId";
|
||||
private static final String PROFILE_VERSION = "profileVersion";
|
||||
private static final String REQUEST_ID = "requestId";
|
||||
private static final int MAXIMUM_REQUEST_BYTES = 1024 * 1024;
|
||||
|
||||
private final Map<String, Binding> bindings;
|
||||
|
||||
@@ -57,10 +76,32 @@ final class PkiOperationRegistry {
|
||||
Map<String, Binding> configured = new LinkedHashMap<>();
|
||||
add(configured, PkiOperation.ValidateConfiguration.NAME, this::configuration);
|
||||
add(configured, PkiOperation.ValidateProfile.NAME, this::profile);
|
||||
add(configured, PkiOperation.RegisterProfile.NAME, this::registerProfile);
|
||||
add(configured, PkiOperation.InspectProfile.NAME, this::inspectProfile);
|
||||
add(configured, PkiOperation.ListProfileVersions.NAME, this::listProfiles);
|
||||
add(configured, PkiOperation.ActivateProfile.NAME, this::activateProfile);
|
||||
add(configured, PkiOperation.InspectAuthority.NAME, this::inspectAuthority);
|
||||
add(configured, PkiOperation.ListAuthorities.NAME, this::listAuthorities);
|
||||
add(configured, PkiOperation.CreateAuthority.NAME, this::createAuthority);
|
||||
add(configured, PkiOperation.TransitionAuthority.NAME, this::transitionAuthority);
|
||||
add(configured, PkiOperation.InspectRequest.NAME, this::inspectRequest);
|
||||
add(configured, PkiOperation.ImportRequest.NAME, this::importRequest);
|
||||
add(configured, PkiOperation.VerifyRequest.NAME, this::verifyRequest);
|
||||
add(configured, PkiOperation.InspectCredential.NAME, this::credential);
|
||||
add(configured, PkiOperation.IssueCredential.NAME, this::issueCredential);
|
||||
add(configured, PkiOperation.RevokeCredential.NAME, this::revoke);
|
||||
add(configured, PkiOperation.InspectRevocation.NAME, this::inspectRevocation);
|
||||
add(configured, PkiOperation.ReadRevocationHistory.NAME, this::history);
|
||||
add(configured, PkiOperation.SnapshotRevocations.NAME, this::snapshotRevocations);
|
||||
add(configured, PkiOperation.InspectStatus.NAME, this::inspectStatus);
|
||||
add(configured, PkiOperation.ListStatus.NAME, this::listStatus);
|
||||
add(configured, PkiOperation.GenerateStatus.NAME, this::generateStatus);
|
||||
add(configured, PkiOperation.InspectPublication.NAME, this::publication);
|
||||
add(configured, PkiOperation.ListPublications.NAME, this::listPublications);
|
||||
add(configured, PkiOperation.RegisterPublication.NAME, this::registerPublication);
|
||||
add(configured, PkiOperation.ProcessPublication.NAME, this::processPublication);
|
||||
add(configured, PkiOperation.RetryPublication.NAME, this::retryPublication);
|
||||
add(configured, PkiOperation.ReconcilePublication.NAME, this::reconcilePublication);
|
||||
bindings = Map.copyOf(configured);
|
||||
}
|
||||
|
||||
@@ -83,17 +124,88 @@ final class PkiOperationRegistry {
|
||||
}
|
||||
|
||||
private PkiOperation profile(PkiOperationValue.ObjectValue arguments, Path baseDirectory) throws IOException {
|
||||
requireFields(arguments, Set.of("profileFile"));
|
||||
Path path = resolve(baseDirectory, text(arguments, "profileFile"));
|
||||
requireFields(arguments, Set.of(PROFILE_FILE));
|
||||
Path path = resolve(baseDirectory, text(arguments, PROFILE_FILE));
|
||||
byte[] document = PkiCliFiles.readRegularFile(path, CertificateProfileDocumentCodec.MAXIMUM_DOCUMENT_BYTES);
|
||||
return new PkiOperation.ValidateProfile(document);
|
||||
}
|
||||
|
||||
private PkiOperation registerProfile(PkiOperationValue.ObjectValue arguments, Path baseDirectory)
|
||||
throws IOException {
|
||||
requireFields(arguments, Set.of(PROFILE_FILE));
|
||||
Path path = resolve(baseDirectory, text(arguments, PROFILE_FILE));
|
||||
byte[] document = PkiCliFiles.readRegularFile(path, CertificateProfileDocumentCodec.MAXIMUM_DOCUMENT_BYTES);
|
||||
return new PkiOperation.RegisterProfile(document);
|
||||
}
|
||||
|
||||
private PkiOperation inspectProfile(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
requireFields(arguments, Set.of(PROFILE_ID, PROFILE_VERSION));
|
||||
return new PkiOperation.InspectProfile(text(arguments, PROFILE_ID), integer(arguments, PROFILE_VERSION));
|
||||
}
|
||||
|
||||
private PkiOperation listProfiles(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
requireFields(arguments, Set.of(PROFILE_ID));
|
||||
return new PkiOperation.ListProfileVersions(text(arguments, PROFILE_ID));
|
||||
}
|
||||
|
||||
private PkiOperation activateProfile(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
requireFields(arguments, Set.of(PROFILE_ID, PROFILE_VERSION));
|
||||
return new PkiOperation.ActivateProfile(text(arguments, PROFILE_ID), integer(arguments, PROFILE_VERSION));
|
||||
}
|
||||
|
||||
private PkiOperation inspectAuthority(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
requireFields(arguments, Set.of("caId"));
|
||||
return new PkiOperation.InspectAuthority(new PkiId(text(arguments, "caId")));
|
||||
}
|
||||
|
||||
private PkiOperation listAuthorities(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
requireFields(arguments, Set.of(LIMIT));
|
||||
return new PkiOperation.ListAuthorities(limit(arguments));
|
||||
}
|
||||
|
||||
private PkiOperation createAuthority(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
requireFields(arguments, Set.of(FORMAT_ID, "subjectRef", PROFILE_ID, "keyRef"));
|
||||
return new PkiOperation.CreateAuthority(new FormatId(text(arguments, FORMAT_ID)),
|
||||
new SubjectRef(text(arguments, "subjectRef")), text(arguments, PROFILE_ID),
|
||||
new KeyRef(text(arguments, "keyRef")));
|
||||
}
|
||||
|
||||
private PkiOperation transitionAuthority(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
requireFields(arguments, Set.of("caId", "state", "reason"));
|
||||
return new PkiOperation.TransitionAuthority(new PkiId(text(arguments, "caId")),
|
||||
CaState.valueOf(text(arguments, "state")), text(arguments, "reason"));
|
||||
}
|
||||
|
||||
private PkiOperation inspectRequest(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
requireFields(arguments, Set.of(REQUEST_ID));
|
||||
return new PkiOperation.InspectRequest(new PkiId(text(arguments, REQUEST_ID)));
|
||||
}
|
||||
|
||||
private PkiOperation importRequest(PkiOperationValue.ObjectValue arguments, Path baseDirectory)
|
||||
throws IOException {
|
||||
requireFields(arguments, Set.of(FORMAT_ID, "encoding", "requestFile"));
|
||||
Path path = resolve(baseDirectory, text(arguments, "requestFile"));
|
||||
byte[] document = PkiCliFiles.readRegularFile(path, MAXIMUM_REQUEST_BYTES);
|
||||
return new PkiOperation.ImportRequest(new FormatId(text(arguments, FORMAT_ID)),
|
||||
new EncodedObject(Encoding.valueOf(text(arguments, "encoding")), document));
|
||||
}
|
||||
|
||||
private PkiOperation verifyRequest(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
requireFields(arguments, Set.of(REQUEST_ID));
|
||||
return new PkiOperation.VerifyRequest(new PkiId(text(arguments, REQUEST_ID)));
|
||||
}
|
||||
|
||||
private PkiOperation credential(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
requireFields(arguments, Set.of(CREDENTIAL_ID));
|
||||
return new PkiOperation.InspectCredential(new PkiId(text(arguments, CREDENTIAL_ID)));
|
||||
}
|
||||
|
||||
private PkiOperation issueCredential(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
requireFields(arguments, Set.of(ISSUER_CA_ID, REQUEST_ID, PROFILE_ID));
|
||||
return new PkiOperation.IssueCredential(new PkiId(text(arguments, ISSUER_CA_ID)),
|
||||
new PkiId(text(arguments, REQUEST_ID)), text(arguments, PROFILE_ID));
|
||||
}
|
||||
|
||||
private PkiOperation revoke(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
requireFields(arguments, Set.of(CREDENTIAL_ID, "reason"));
|
||||
RevocationReason reason = RevocationReason.valueOf(text(arguments, "reason"));
|
||||
@@ -101,14 +213,71 @@ final class PkiOperationRegistry {
|
||||
}
|
||||
|
||||
private PkiOperation history(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
requireFields(arguments, Set.of(CREDENTIAL_ID, "limit"));
|
||||
int limit = Math.toIntExact(integer(arguments, "limit"));
|
||||
return new PkiOperation.ReadRevocationHistory(new PkiId(text(arguments, CREDENTIAL_ID)), limit);
|
||||
requireFields(arguments, Set.of(CREDENTIAL_ID, LIMIT));
|
||||
return new PkiOperation.ReadRevocationHistory(new PkiId(text(arguments, CREDENTIAL_ID)), limit(arguments));
|
||||
}
|
||||
|
||||
private PkiOperation inspectRevocation(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
requireFields(arguments, Set.of(CREDENTIAL_ID));
|
||||
return new PkiOperation.InspectRevocation(new PkiId(text(arguments, CREDENTIAL_ID)));
|
||||
}
|
||||
|
||||
private PkiOperation snapshotRevocations(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
requireFields(arguments, Set.of(LIMIT));
|
||||
return new PkiOperation.SnapshotRevocations(limit(arguments));
|
||||
}
|
||||
|
||||
private PkiOperation inspectStatus(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
requireFields(arguments, Set.of("statusObjectId"));
|
||||
return new PkiOperation.InspectStatus(new PkiId(text(arguments, "statusObjectId")));
|
||||
}
|
||||
|
||||
private PkiOperation listStatus(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
requireFields(arguments, Set.of(ISSUER_CA_ID, LIMIT));
|
||||
return new PkiOperation.ListStatus(new PkiId(text(arguments, ISSUER_CA_ID)), limit(arguments));
|
||||
}
|
||||
|
||||
private PkiOperation generateStatus(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
requireFields(arguments, Set.of(ISSUER_CA_ID, "type", FORMAT_ID));
|
||||
return new PkiOperation.GenerateStatus(new PkiId(text(arguments, ISSUER_CA_ID)),
|
||||
StatusObjectType.valueOf(text(arguments, "type")), new FormatId(text(arguments, FORMAT_ID)));
|
||||
}
|
||||
|
||||
private PkiOperation publication(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
requireFields(arguments, Set.of("publicationId"));
|
||||
return new PkiOperation.InspectPublication(new PkiId(text(arguments, "publicationId")));
|
||||
requireFields(arguments, Set.of(PUBLICATION_ID));
|
||||
return new PkiOperation.InspectPublication(new PkiId(text(arguments, PUBLICATION_ID)));
|
||||
}
|
||||
|
||||
private PkiOperation listPublications(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
requireFields(arguments, Set.of(LIMIT));
|
||||
return new PkiOperation.ListPublications(limit(arguments));
|
||||
}
|
||||
|
||||
private PkiOperation registerPublication(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
requireFields(arguments,
|
||||
Set.of(PUBLICATION_ID, "sourceType", "sourceId", "targetType", "targetId"));
|
||||
PublicationTarget target = new PublicationTarget(PublicationTargetType.valueOf(text(arguments, "targetType")),
|
||||
text(arguments, "targetId"));
|
||||
return new PkiOperation.RegisterPublication(new PkiId(text(arguments, PUBLICATION_ID)),
|
||||
PublicationSourceType.valueOf(text(arguments, "sourceType")),
|
||||
new PkiId(text(arguments, "sourceId")), target);
|
||||
}
|
||||
|
||||
private PkiOperation processPublication(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
return new PkiOperation.ProcessPublication(publicationId(arguments));
|
||||
}
|
||||
|
||||
private PkiOperation retryPublication(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
return new PkiOperation.RetryPublication(publicationId(arguments));
|
||||
}
|
||||
|
||||
private PkiOperation reconcilePublication(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||
return new PkiOperation.ReconcilePublication(publicationId(arguments));
|
||||
}
|
||||
|
||||
private static PkiId publicationId(PkiOperationValue.ObjectValue arguments) {
|
||||
requireFields(arguments, Set.of(PUBLICATION_ID));
|
||||
return new PkiId(text(arguments, PUBLICATION_ID));
|
||||
}
|
||||
|
||||
private static void add(Map<String, Binding> target, String name, Binding binding) {
|
||||
@@ -131,6 +300,10 @@ final class PkiOperationRegistry {
|
||||
return PkiCliConfiguration.integer(PkiCliConfiguration.required(arguments, name));
|
||||
}
|
||||
|
||||
private static int limit(PkiOperationValue.ObjectValue arguments) {
|
||||
return Math.toIntExact(integer(arguments, LIMIT));
|
||||
}
|
||||
|
||||
private static Path resolve(Path baseDirectory, String value) {
|
||||
Path supplied = Path.of(value);
|
||||
return (supplied.isAbsolute() ? supplied : baseDirectory.resolve(supplied)).toAbsolutePath().normalize();
|
||||
|
||||
@@ -51,6 +51,8 @@ import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import zeroecho.pki.api.ProfileService;
|
||||
import zeroecho.pki.api.RevocationService;
|
||||
import zeroecho.pki.api.profile.BuiltInCertificateProfileCatalog;
|
||||
import zeroecho.pki.api.profile.BuiltInCertificateProfileTemplate;
|
||||
import zeroecho.pki.application.PkiOperation;
|
||||
import zeroecho.pki.application.PkiOperationFailure;
|
||||
import zeroecho.pki.application.PkiOperationOutcome;
|
||||
@@ -64,6 +66,88 @@ class PkiCliTest {
|
||||
@TempDir
|
||||
Path temporaryDirectory;
|
||||
|
||||
@Test
|
||||
void helpListsCompletedSafeAdministrationFamilies() {
|
||||
System.out.println("helpListsCompletedSafeAdministrationFamilies");
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
int code = PkiCli.execute(new String[] { "--help" }, output);
|
||||
String help = output.toString(StandardCharsets.UTF_8);
|
||||
System.out.println("...helpBytes=" + help.length());
|
||||
assertEquals(PkiExitCodes.SUCCESS, code);
|
||||
assertTrue(help.contains("profile.register"));
|
||||
assertTrue(help.contains("ca.list"));
|
||||
assertTrue(help.contains("request.inspect"));
|
||||
assertTrue(help.contains("revocation.snapshot"));
|
||||
assertTrue(help.contains("inventory.status"));
|
||||
assertTrue(help.contains("publication.list"));
|
||||
assertTrue(help.contains("ca.create"));
|
||||
assertTrue(help.contains("credential.issue"));
|
||||
assertTrue(help.contains("status.generate"));
|
||||
assertTrue(help.contains("publication.process"));
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void signingAndPublicationFamiliesUseOneTypedBatchSession() throws IOException {
|
||||
System.out.println("signingAndPublicationFamiliesUseOneTypedBatchSession");
|
||||
Path configuration = configuration("capability-workflow");
|
||||
Path request = temporaryDirectory.resolve("request.der");
|
||||
Files.write(request, new byte[] { 0x30, 0x00 });
|
||||
Path workflow = plan("capability-workflow.json", """
|
||||
{"version":1,"failurePolicy":"FAIL_FAST","operations":[
|
||||
{"id":"ca","operation":"ca.create","arguments":{"formatId":"x509","subjectRef":"CN=Root","profileId":"root-ca","keyRef":"managed:root.prv"}},
|
||||
{"id":"request","operation":"request.import","arguments":{"formatId":"x509","encoding":"DER","requestFile":"request.der"}},
|
||||
{"id":"issue","operation":"credential.issue","arguments":{"issuerCaId":"${ca.caId}","requestId":"${request.requestId}","profileId":"server"}},
|
||||
{"id":"publish","operation":"publication.register","arguments":{"publicationId":"publication-1","sourceType":"CREDENTIAL","sourceId":"${issue.credentialId}","targetType":"FILESYSTEM","targetId":"local"}},
|
||||
{"id":"process","operation":"publication.process","arguments":{"publicationId":"${publish.publicationId}"}}
|
||||
]}
|
||||
""");
|
||||
RecordingOpener opener = new RecordingOpener();
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
int code = PkiCli.execute(new String[] { "run", workflow.toString(), "--config", configuration.toString(),
|
||||
"--output", "json" }, output, opener, () -> false);
|
||||
System.out.println("...operations=" + opener.operationTypes.size());
|
||||
assertEquals(PkiExitCodes.SUCCESS, code);
|
||||
assertEquals(1, opener.sessions.get());
|
||||
assertEquals(List.of(PkiOperation.CreateAuthority.class, PkiOperation.ImportRequest.class,
|
||||
PkiOperation.IssueCredential.class, PkiOperation.RegisterPublication.class,
|
||||
PkiOperation.ProcessPublication.class), opener.operationTypes);
|
||||
assertTrue(output.toString(StandardCharsets.UTF_8).contains("\"operation\":\"publication.process\""));
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void productionProfileWorkflowUsesTypedOperationsAndDurableStore() throws IOException {
|
||||
System.out.println("productionProfileWorkflowUsesTypedOperationsAndDurableStore");
|
||||
Path configuration = configuration("profile-workflow");
|
||||
BuiltInCertificateProfileTemplate template = BuiltInCertificateProfileCatalog
|
||||
.load(getClass().getClassLoader()).get(0);
|
||||
Path profile = temporaryDirectory.resolve("profile.json");
|
||||
Files.write(profile, template.canonicalJson());
|
||||
ByteArrayOutputStream registered = new ByteArrayOutputStream();
|
||||
int registerCode = PkiCli.execute(new String[] { "profile.register", "--profile-file", profile.toString(),
|
||||
"--config", configuration.toString(), "--output", "json" }, registered);
|
||||
ByteArrayOutputStream activated = new ByteArrayOutputStream();
|
||||
int activateCode = PkiCli.execute(new String[] { "profile.activate", "--profile-id",
|
||||
template.definition().profileId(), "--profile-version",
|
||||
Long.toString(template.definition().profileVersion()), "--config", configuration.toString(),
|
||||
"--output", "json" }, activated);
|
||||
ByteArrayOutputStream inspected = new ByteArrayOutputStream();
|
||||
int inspectCode = PkiCli.execute(new String[] { "profile.inspect", "--profile-id",
|
||||
template.definition().profileId(), "--profile-version",
|
||||
Long.toString(template.definition().profileVersion()), "--config", configuration.toString(),
|
||||
"--output", "json" }, inspected);
|
||||
String rendered = registered.toString(StandardCharsets.UTF_8)
|
||||
+ activated.toString(StandardCharsets.UTF_8) + inspected.toString(StandardCharsets.UTF_8);
|
||||
System.out.println("...profileId=" + template.definition().profileId());
|
||||
assertEquals(PkiExitCodes.SUCCESS, registerCode);
|
||||
assertEquals(PkiExitCodes.SUCCESS, activateCode);
|
||||
assertEquals(PkiExitCodes.SUCCESS, inspectCode);
|
||||
assertTrue(rendered.contains("\"profileVersion\":" + template.definition().profileVersion()));
|
||||
assertFalse(rendered.contains("canonicalJson"));
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void productionSessionExecutesMachineAndHumanSingleCommands() throws IOException {
|
||||
System.out.println("productionSessionExecutesMachineAndHumanSingleCommands");
|
||||
@@ -84,6 +168,27 @@ class PkiCliTest {
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void versionTwoConfigurationKeepsCapabilitiesExplicit() throws IOException {
|
||||
System.out.println("versionTwoConfigurationKeepsCapabilitiesExplicit");
|
||||
Path configuration = temporaryDirectory.resolve("capabilities.json");
|
||||
Files.writeString(configuration, """
|
||||
{"version":2,
|
||||
"store":{"provider":"fs","properties":{"root":"store"}},
|
||||
"audit":{"provider":"memory","properties":{"size":"16"}},
|
||||
"signing":{"workflow":{"provider":"zeroecho-lib","properties":{"keyringPath":"keys.zek","operationRoot":"signing-operations"}},
|
||||
"framework":{"provider":"x509-bc","properties":{}},
|
||||
"busPath":"signing-bus","signatureAlgorithm":"SHA256withRSA","signingTtlSeconds":30,
|
||||
"unlockEnvironmentVariable":"ZEROECHO_TEST_UNLOCK"},
|
||||
"publishers":[{"provider":"filesystem","properties":{"root":"published","targetId":"local"}}]}
|
||||
""");
|
||||
PkiSessionConfiguration parsed = PkiCliConfiguration.read(configuration);
|
||||
System.out.println("...publishers=" + parsed.publishers().size());
|
||||
assertTrue(parsed.signing().isPresent());
|
||||
assertEquals(1, parsed.publishers().size());
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleAndBatchUseTheSameTypedExecutorAndSessionLifecycle() throws IOException {
|
||||
System.out.println("singleAndBatchUseTheSameTypedExecutorAndSessionLifecycle");
|
||||
@@ -128,6 +233,22 @@ class PkiCliTest {
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void directSigningDependentCommandUsesTheSharedTypedExecutor() throws IOException {
|
||||
System.out.println("directSigningDependentCommandUsesTheSharedTypedExecutor");
|
||||
Path configuration = configuration("direct-ca");
|
||||
RecordingOpener opener = new RecordingOpener();
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
int code = PkiCli.execute(new String[] { "ca.create", "--format-id", "x509", "--subject-ref",
|
||||
"CN=Root", "--profile-id", "root-ca", "--key-ref", "managed:root.prv", "--config",
|
||||
configuration.toString(), "--output", "json" }, output, opener, () -> false);
|
||||
System.out.println("...exit=" + code);
|
||||
assertEquals(PkiExitCodes.SUCCESS, code);
|
||||
assertEquals(List.of(PkiOperation.CreateAuthority.class), opener.operationTypes);
|
||||
assertTrue(output.toString(StandardCharsets.UTF_8).contains("\"caId\":\"ca-1\""));
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesEarlierFieldsAndContinuesOnlyIndependentSteps() throws IOException {
|
||||
System.out.println("resolvesEarlierFieldsAndContinuesOnlyIndependentSteps");
|
||||
@@ -302,6 +423,17 @@ class PkiCliTest {
|
||||
} else if (operation instanceof PkiOperation.RevokeCredential revoke) {
|
||||
fields.put("credentialId", new PkiOperationValue.Text(revoke.credentialId().value()));
|
||||
fields.put("state", new PkiOperationValue.Text("PERMANENTLY_REVOKED"));
|
||||
} else if (operation instanceof PkiOperation.CreateAuthority) {
|
||||
fields.put("caId", new PkiOperationValue.Text("ca-1"));
|
||||
} else if (operation instanceof PkiOperation.ImportRequest) {
|
||||
fields.put("requestId", new PkiOperationValue.Text("request-1"));
|
||||
} else if (operation instanceof PkiOperation.IssueCredential) {
|
||||
fields.put("credentialId", new PkiOperationValue.Text("credential-1"));
|
||||
} else if (operation instanceof PkiOperation.RegisterPublication register) {
|
||||
fields.put("publicationId", new PkiOperationValue.Text(register.publicationId().value()));
|
||||
} else if (operation instanceof PkiOperation.ProcessPublication process) {
|
||||
fields.put("publicationId", new PkiOperationValue.Text(process.publicationId().value()));
|
||||
fields.put("status", new PkiOperationValue.Text("SUCCEEDED"));
|
||||
} else {
|
||||
fields.put("status", new PkiOperationValue.Text("OK"));
|
||||
}
|
||||
|
||||
24
app/src/test/resources/pki-administration-config.json
Normal file
24
app/src/test/resources/pki-administration-config.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"version": 2,
|
||||
"store": {
|
||||
"provider": "fs",
|
||||
"properties": {
|
||||
"root": "build/packaged-cli-store"
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
"provider": "memory",
|
||||
"properties": {
|
||||
"size": "64"
|
||||
}
|
||||
},
|
||||
"publishers": [
|
||||
{
|
||||
"provider": "filesystem",
|
||||
"properties": {
|
||||
"root": "build/packaged-cli-publication",
|
||||
"targetId": "local-artifacts"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
18
app/src/test/resources/pki-administration-plan.json
Normal file
18
app/src/test/resources/pki-administration-plan.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": 1,
|
||||
"failurePolicy": "FAIL_FAST",
|
||||
"operations": [
|
||||
{
|
||||
"id": "validate-configuration",
|
||||
"operation": "configuration.validate",
|
||||
"arguments": {}
|
||||
},
|
||||
{
|
||||
"id": "list-publications",
|
||||
"operation": "publication.list",
|
||||
"arguments": {
|
||||
"limit": 25
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -44,34 +44,85 @@ import java.util.Optional;
|
||||
|
||||
import zeroecho.core.io.CancellationSignal;
|
||||
import zeroecho.pki.api.PkiException;
|
||||
import zeroecho.pki.api.PkiId;
|
||||
import zeroecho.pki.api.ca.CaCreateCommand;
|
||||
import zeroecho.pki.api.CaService;
|
||||
import zeroecho.pki.api.CertificationRequestService;
|
||||
import zeroecho.pki.api.IssuanceService;
|
||||
import zeroecho.pki.api.PublicationService;
|
||||
import zeroecho.pki.api.ProfileService;
|
||||
import zeroecho.pki.api.RevocationService;
|
||||
import zeroecho.pki.api.StatusObjectService;
|
||||
import zeroecho.pki.api.ca.CaRecord;
|
||||
import zeroecho.pki.api.credential.CaProfileBinding;
|
||||
import zeroecho.pki.api.credential.Credential;
|
||||
import zeroecho.pki.api.credential.CredentialBundle;
|
||||
import zeroecho.pki.api.credential.EndEntityProfileBinding;
|
||||
import zeroecho.pki.api.profile.CertificateProfileDefinition;
|
||||
import zeroecho.pki.api.profile.CertificateProfileDocumentCodec;
|
||||
import zeroecho.pki.api.profile.CertificateProfileRef;
|
||||
import zeroecho.pki.api.profile.ImportedCertificateProfileVersion;
|
||||
import zeroecho.pki.api.issuance.IssueEndEntityCommand;
|
||||
import zeroecho.pki.api.issuance.VerificationPolicy;
|
||||
import zeroecho.pki.api.publication.PublicationRequest;
|
||||
import zeroecho.pki.api.publication.PublicationResult;
|
||||
import zeroecho.pki.api.publication.PublicationStatus;
|
||||
import zeroecho.pki.api.publication.PublicationCursor;
|
||||
import zeroecho.pki.api.publication.PublicationRecord;
|
||||
import zeroecho.pki.api.request.CertificationRequest;
|
||||
import zeroecho.pki.api.request.ParsedCertificationRequest;
|
||||
import zeroecho.pki.api.request.ProofOfPossessionResult;
|
||||
import zeroecho.pki.api.request.RequestStorePolicy;
|
||||
import zeroecho.pki.api.revocation.RevocationCommand;
|
||||
import zeroecho.pki.api.revocation.RevocationRecord;
|
||||
import zeroecho.pki.api.revocation.RevocationQuery;
|
||||
import zeroecho.pki.api.revocation.RevocationTransition;
|
||||
import zeroecho.pki.api.status.StatusObject;
|
||||
import zeroecho.pki.api.status.StatusObjectGenerateCommand;
|
||||
import zeroecho.pki.impl.core.attr.SimpleAttributeSet;
|
||||
import zeroecho.pki.spi.store.PkiStore;
|
||||
import zeroecho.pki.spi.store.RevocationHistory;
|
||||
import zeroecho.pki.spi.store.RevocationSnapshot;
|
||||
|
||||
/** Explicit non-reflective executor used by one backend session. */
|
||||
@SuppressWarnings({ "PMD.CouplingBetweenObjects", "PMD.CyclomaticComplexity", "PMD.ExcessiveParameterList",
|
||||
"PMD.TooManyMethods" })
|
||||
final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
||||
|
||||
private static final String COUNT = "count";
|
||||
private static final String FORMAT_ID = "formatId";
|
||||
|
||||
private final PkiSessionConfiguration configuration;
|
||||
private final PkiStore store;
|
||||
private final ProfileService profiles;
|
||||
private final RevocationService revocations;
|
||||
private final Optional<CaService> authorities;
|
||||
private final Optional<CertificationRequestService> requests;
|
||||
private final Optional<IssuanceService> issuance;
|
||||
private final Optional<StatusObjectService> statusObjects;
|
||||
private final Optional<PublicationService> publications;
|
||||
private final Runnable openCheck;
|
||||
|
||||
/* default */ DefaultPkiOperationExecutor(PkiSessionConfiguration configuration, PkiStore store,
|
||||
RevocationService revocations, Runnable openCheck) {
|
||||
ProfileService profiles, RevocationService revocations, Runnable openCheck) {
|
||||
this(configuration, store, profiles, revocations, Optional.empty(), Optional.empty(), Optional.empty(),
|
||||
Optional.empty(), Optional.empty(), openCheck);
|
||||
}
|
||||
|
||||
/* default */ DefaultPkiOperationExecutor(PkiSessionConfiguration configuration, PkiStore store,
|
||||
ProfileService profiles, RevocationService revocations, Optional<CaService> authorities,
|
||||
Optional<CertificationRequestService> requests, Optional<IssuanceService> issuance,
|
||||
Optional<StatusObjectService> statusObjects, Optional<PublicationService> publications,
|
||||
Runnable openCheck) {
|
||||
this.configuration = Objects.requireNonNull(configuration, "configuration");
|
||||
this.store = Objects.requireNonNull(store, "store");
|
||||
this.profiles = Objects.requireNonNull(profiles, "profiles");
|
||||
this.revocations = Objects.requireNonNull(revocations, "revocations");
|
||||
this.authorities = Objects.requireNonNull(authorities, "authorities");
|
||||
this.requests = Objects.requireNonNull(requests, "requests");
|
||||
this.issuance = Objects.requireNonNull(issuance, "issuance");
|
||||
this.statusObjects = Objects.requireNonNull(statusObjects, "statusObjects");
|
||||
this.publications = Objects.requireNonNull(publications, "publications");
|
||||
this.openCheck = Objects.requireNonNull(openCheck, "openCheck");
|
||||
}
|
||||
|
||||
@@ -85,10 +136,32 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
||||
PkiOperationResult result = switch (exact) {
|
||||
case PkiOperation.ValidateConfiguration ignored -> validateConfiguration();
|
||||
case PkiOperation.ValidateProfile request -> validateProfile(request);
|
||||
case PkiOperation.RegisterProfile request -> registerProfile(request);
|
||||
case PkiOperation.InspectProfile request -> inspectProfile(request);
|
||||
case PkiOperation.ListProfileVersions request -> listProfileVersions(request);
|
||||
case PkiOperation.ActivateProfile request -> activateProfile(request);
|
||||
case PkiOperation.InspectAuthority request -> inspectAuthority(request);
|
||||
case PkiOperation.ListAuthorities request -> listAuthorities(request);
|
||||
case PkiOperation.CreateAuthority request -> createAuthority(request);
|
||||
case PkiOperation.TransitionAuthority request -> transitionAuthority(request);
|
||||
case PkiOperation.InspectRequest request -> inspectRequest(request);
|
||||
case PkiOperation.ImportRequest request -> importRequest(request);
|
||||
case PkiOperation.VerifyRequest request -> verifyRequest(request);
|
||||
case PkiOperation.InspectCredential request -> inspectCredential(request);
|
||||
case PkiOperation.IssueCredential request -> issueCredential(request);
|
||||
case PkiOperation.RevokeCredential request -> revokeCredential(request);
|
||||
case PkiOperation.InspectRevocation request -> inspectRevocation(request);
|
||||
case PkiOperation.ReadRevocationHistory request -> readHistory(request, signal);
|
||||
case PkiOperation.SnapshotRevocations request -> snapshotRevocations(request, signal);
|
||||
case PkiOperation.InspectStatus request -> inspectStatus(request);
|
||||
case PkiOperation.ListStatus request -> listStatus(request);
|
||||
case PkiOperation.GenerateStatus request -> generateStatus(request);
|
||||
case PkiOperation.InspectPublication request -> inspectPublication(request);
|
||||
case PkiOperation.ListPublications request -> listPublications(request, signal);
|
||||
case PkiOperation.RegisterPublication request -> registerPublication(request);
|
||||
case PkiOperation.ProcessPublication request -> processPublication(request);
|
||||
case PkiOperation.RetryPublication request -> retryPublication(request);
|
||||
case PkiOperation.ReconcilePublication request -> reconcilePublication(request);
|
||||
};
|
||||
return new PkiOperationOutcome.Success(result);
|
||||
} catch (InterruptedIOException failure) {
|
||||
@@ -114,7 +187,101 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
||||
fields.put("profileId", text(definition.profileId()));
|
||||
fields.put("profileVersion", integer(definition.profileVersion()));
|
||||
fields.put("certificateType", text(definition.certificateType().name()));
|
||||
fields.put("formatId", text(definition.formatId().value()));
|
||||
fields.put(FORMAT_ID, text(definition.formatId().value()));
|
||||
return result(request.name(), fields);
|
||||
}
|
||||
|
||||
private PkiOperationResult registerProfile(PkiOperation.RegisterProfile request) {
|
||||
CertificateProfileRef reference = profiles.importProfile(request.document());
|
||||
return profileReferenceResult(request.name(), reference);
|
||||
}
|
||||
|
||||
private PkiOperationResult inspectProfile(PkiOperation.InspectProfile request) {
|
||||
ImportedCertificateProfileVersion version = profiles
|
||||
.getImportedVersion(request.profileId(), request.profileVersion())
|
||||
.orElseThrow(MissingObjectException::new);
|
||||
return profileVersionResult(request.name(), version);
|
||||
}
|
||||
|
||||
private PkiOperationResult listProfileVersions(PkiOperation.ListProfileVersions request) {
|
||||
List<PkiOperationValue> versions = profiles.listImportedVersions(request.profileId()).stream()
|
||||
.map(DefaultPkiOperationExecutor::profileVersionValue).toList();
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
fields.put("profileId", text(request.profileId()));
|
||||
fields.put(COUNT, integer(versions.size()));
|
||||
profiles.getActiveReference(request.profileId())
|
||||
.ifPresent(reference -> fields.put("activeVersion", integer(reference.profileVersion())));
|
||||
fields.put("versions", new PkiOperationValue.ListValue(versions));
|
||||
return result(request.name(), fields);
|
||||
}
|
||||
|
||||
private PkiOperationResult activateProfile(PkiOperation.ActivateProfile request) {
|
||||
CertificateProfileRef reference = profiles.activateProfile(request.profileId(), request.profileVersion());
|
||||
return profileReferenceResult(request.name(), reference);
|
||||
}
|
||||
|
||||
private PkiOperationResult inspectAuthority(PkiOperation.InspectAuthority request) {
|
||||
return authorityResult(request.name(), store.getCa(request.caId()).orElseThrow(MissingObjectException::new));
|
||||
}
|
||||
|
||||
private PkiOperationResult listAuthorities(PkiOperation.ListAuthorities request) {
|
||||
List<CaRecord> records = store.listCas();
|
||||
int count = Math.min(records.size(), request.limit());
|
||||
List<PkiOperationValue> values = records.subList(0, count).stream()
|
||||
.map(DefaultPkiOperationExecutor::authorityValue).toList();
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
fields.put(COUNT, integer(count));
|
||||
fields.put("truncated", bool(records.size() > count));
|
||||
fields.put("authorities", new PkiOperationValue.ListValue(values));
|
||||
return result(request.name(), fields);
|
||||
}
|
||||
|
||||
private PkiOperationResult createAuthority(PkiOperation.CreateAuthority request) {
|
||||
CaCreateCommand command = new CaCreateCommand(request.formatId(), request.subjectRef(), request.profileId(),
|
||||
Optional.of(request.keyRef()), new SimpleAttributeSet());
|
||||
PkiId caId = requireCapability(authorities, "authority").createRoot(command);
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
fields.put("caId", text(caId.value()));
|
||||
fields.put("state", text(store.getCa(caId).orElseThrow(MissingObjectException::new).state().name()));
|
||||
return result(request.name(), fields);
|
||||
}
|
||||
|
||||
private PkiOperationResult transitionAuthority(PkiOperation.TransitionAuthority request) {
|
||||
requireCapability(authorities, "authority").setCaState(request.caId(), request.state(), request.reason());
|
||||
return authorityResult(request.name(), store.getCa(request.caId()).orElseThrow(MissingObjectException::new));
|
||||
}
|
||||
|
||||
private PkiOperationResult inspectRequest(PkiOperation.InspectRequest request) {
|
||||
ParsedCertificationRequest parsed = store.getRequest(request.requestId())
|
||||
.orElseThrow(MissingObjectException::new);
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
fields.put("requestId", text(parsed.requestId().value()));
|
||||
fields.put(FORMAT_ID, text(parsed.formatId().value()));
|
||||
fields.put("publicKeyBytes", integer(parsed.publicKeyInfo().bytes().length));
|
||||
parsed.requestedProfileId().ifPresent(value -> fields.put("requestedProfileId", text(value)));
|
||||
fields.put("subjectAlternativeNameCount", integer(parsed.subjectAlternativeNames().size()));
|
||||
return result(request.name(), fields);
|
||||
}
|
||||
|
||||
private PkiOperationResult importRequest(PkiOperation.ImportRequest request) {
|
||||
CertificationRequest encoded = new CertificationRequest(request.formatId(), request.encoded());
|
||||
CertificationRequestService service = requireCapability(requests, "certification-request");
|
||||
ParsedCertificationRequest parsed = service.parse(encoded);
|
||||
PkiId requestId = service.store(parsed, RequestStorePolicy.STORE_ALWAYS);
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
fields.put("requestId", text(requestId.value()));
|
||||
fields.put(FORMAT_ID, text(parsed.formatId().value()));
|
||||
return result(request.name(), fields);
|
||||
}
|
||||
|
||||
private PkiOperationResult verifyRequest(PkiOperation.VerifyRequest request) {
|
||||
CertificationRequestService service = requireCapability(requests, "certification-request");
|
||||
ParsedCertificationRequest parsed = service.get(request.requestId()).orElseThrow(MissingObjectException::new);
|
||||
ProofOfPossessionResult proof = service.verifyProofOfPossession(parsed,
|
||||
new VerificationPolicy(true, Optional.empty()));
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
fields.put("requestId", text(request.requestId().value()));
|
||||
fields.put("proofStatus", text(proof.status().name()));
|
||||
return result(request.name(), fields);
|
||||
}
|
||||
|
||||
@@ -124,7 +291,7 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
||||
CertificateProfileRef profile = profileReference(credential);
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
fields.put("credentialId", text(credential.credentialId().value()));
|
||||
fields.put("formatId", text(credential.formatId().value()));
|
||||
fields.put(FORMAT_ID, text(credential.formatId().value()));
|
||||
fields.put("issuerId", text(credential.issuerRef().caId().value()));
|
||||
fields.put("publicKeyId", text(credential.publicKeyId().value()));
|
||||
fields.put("profileId", text(profile.profileId()));
|
||||
@@ -135,6 +302,19 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
||||
return result(request.name(), fields);
|
||||
}
|
||||
|
||||
private PkiOperationResult issueCredential(PkiOperation.IssueCredential request) {
|
||||
ParsedCertificationRequest parsed = requireCapability(requests, "certification-request")
|
||||
.get(request.requestId()).orElseThrow(MissingObjectException::new);
|
||||
IssueEndEntityCommand command = new IssueEndEntityCommand(request.issuerCaId(), parsed, request.profileId(),
|
||||
Optional.empty());
|
||||
CredentialBundle bundle = requireCapability(issuance, "issuance").issueEndEntity(command);
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
fields.put("credentialId", text(bundle.credential().credentialId().value()));
|
||||
fields.put("issuerCaId", text(bundle.credential().issuerRef().caId().value()));
|
||||
fields.put("supportingObjectCount", integer(bundle.supportingObjects().size()));
|
||||
return result(request.name(), fields);
|
||||
}
|
||||
|
||||
private PkiOperationResult revokeCredential(PkiOperation.RevokeCredential request) {
|
||||
RevocationCommand.RevokePermanently command = new RevocationCommand.RevokePermanently(request.credentialId(),
|
||||
request.reason(), new SimpleAttributeSet());
|
||||
@@ -142,6 +322,11 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
||||
return revocationResult(request.name(), record);
|
||||
}
|
||||
|
||||
private PkiOperationResult inspectRevocation(PkiOperation.InspectRevocation request) {
|
||||
RevocationRecord record = revocations.get(request.credentialId()).orElseThrow(MissingObjectException::new);
|
||||
return revocationResult(request.name(), record);
|
||||
}
|
||||
|
||||
private PkiOperationResult readHistory(PkiOperation.ReadRevocationHistory request, CancellationSignal signal)
|
||||
throws IOException {
|
||||
List<PkiOperationValue> transitions = new ArrayList<>();
|
||||
@@ -154,7 +339,7 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
||||
}
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
fields.put("credentialId", text(request.credentialId().value()));
|
||||
fields.put("count", integer(transitions.size()));
|
||||
fields.put(COUNT, integer(transitions.size()));
|
||||
fields.put("truncated", bool(truncated));
|
||||
fields.put("transitions", new PkiOperationValue.ListValue(transitions));
|
||||
return result(request.name(), fields);
|
||||
@@ -176,6 +361,120 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
||||
return result(request.name(), fields);
|
||||
}
|
||||
|
||||
private PkiOperationResult snapshotRevocations(PkiOperation.SnapshotRevocations request,
|
||||
CancellationSignal signal) throws IOException {
|
||||
List<PkiOperationValue> records = new ArrayList<>();
|
||||
boolean truncated;
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
RevocationQuery query = new RevocationQuery(Optional.empty(), Optional.empty(), Optional.empty(),
|
||||
Optional.empty());
|
||||
try (RevocationSnapshot snapshot = revocations.search(query);
|
||||
RevocationSnapshot.Cursor cursor = snapshot.openCursor()) {
|
||||
while (records.size() < request.limit() && cursor.next(signal)) {
|
||||
records.add(revocationValue(cursor.current()));
|
||||
}
|
||||
truncated = records.size() == request.limit() && cursor.next(signal);
|
||||
fields.put("snapshotId", text(snapshot.snapshotId()));
|
||||
fields.put("revision", integer(snapshot.revision()));
|
||||
fields.put("boundary", integer(snapshot.boundary()));
|
||||
fields.put("commitment", text(snapshot.commitment()));
|
||||
}
|
||||
fields.put(COUNT, integer(records.size()));
|
||||
fields.put("truncated", bool(truncated));
|
||||
fields.put("records", new PkiOperationValue.ListValue(records));
|
||||
return result(request.name(), fields);
|
||||
}
|
||||
|
||||
private PkiOperationResult inspectStatus(PkiOperation.InspectStatus request) {
|
||||
StatusObject object = store.getStatusObject(request.statusObjectId())
|
||||
.orElseThrow(MissingObjectException::new);
|
||||
return statusResult(request.name(), object);
|
||||
}
|
||||
|
||||
private PkiOperationResult generateStatus(PkiOperation.GenerateStatus request) {
|
||||
StatusObjectGenerateCommand command = new StatusObjectGenerateCommand(request.issuerCaId(), request.type(),
|
||||
request.formatId(), new SimpleAttributeSet());
|
||||
StatusObject object = requireCapability(statusObjects, "status-object").generate(command);
|
||||
return statusResult(request.name(), object);
|
||||
}
|
||||
|
||||
private PkiOperationResult listStatus(PkiOperation.ListStatus request) {
|
||||
List<StatusObject> records = store.listStatusObjects(request.issuerCaId());
|
||||
int count = Math.min(records.size(), request.limit());
|
||||
List<PkiOperationValue> values = records.subList(0, count).stream()
|
||||
.map(DefaultPkiOperationExecutor::statusValue).toList();
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
fields.put("issuerCaId", text(request.issuerCaId().value()));
|
||||
fields.put(COUNT, integer(count));
|
||||
fields.put("truncated", bool(records.size() > count));
|
||||
fields.put("statusObjects", new PkiOperationValue.ListValue(values));
|
||||
return result(request.name(), fields);
|
||||
}
|
||||
|
||||
private PkiOperationResult listPublications(PkiOperation.ListPublications request, CancellationSignal signal)
|
||||
throws IOException {
|
||||
List<PkiOperationValue> records = new ArrayList<>();
|
||||
boolean truncated;
|
||||
try (PublicationCursor cursor = store.openPublicationRecords()) {
|
||||
Optional<PublicationRecord> next = cursor.next(signal);
|
||||
while (records.size() < request.limit() && next.isPresent()) {
|
||||
records.add(publicationValue(next.orElseThrow()));
|
||||
next = cursor.next(signal);
|
||||
}
|
||||
truncated = next.isPresent();
|
||||
}
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
fields.put(COUNT, integer(records.size()));
|
||||
fields.put("truncated", bool(truncated));
|
||||
fields.put("publications", new PkiOperationValue.ListValue(records));
|
||||
return result(request.name(), fields);
|
||||
}
|
||||
|
||||
private PkiOperationResult registerPublication(PkiOperation.RegisterPublication request) {
|
||||
PublicationRequest registration = new PublicationRequest(request.publicationId(), request.sourceType(),
|
||||
request.sourceId(), request.target());
|
||||
PublicationRecord record = requireCapability(publications, "publication").register(registration);
|
||||
return publicationRecordResult(request.name(), record);
|
||||
}
|
||||
|
||||
private PkiOperationResult processPublication(PkiOperation.ProcessPublication request) {
|
||||
return publicationResult(request.name(),
|
||||
requireCapability(publications, "publication").process(request.publicationId()));
|
||||
}
|
||||
|
||||
private PkiOperationResult retryPublication(PkiOperation.RetryPublication request) {
|
||||
return publicationResult(request.name(),
|
||||
requireCapability(publications, "publication").retry(request.publicationId()));
|
||||
}
|
||||
|
||||
private PkiOperationResult reconcilePublication(PkiOperation.ReconcilePublication request) {
|
||||
return publicationResult(request.name(),
|
||||
requireCapability(publications, "publication").reconcile(request.publicationId()));
|
||||
}
|
||||
|
||||
private static PkiOperationResult publicationResult(String operation, PublicationResult publication) {
|
||||
if (publication.status() == PublicationStatus.OUTCOME_UNKNOWN) {
|
||||
throw new ExternalOutcomeUnknownException();
|
||||
}
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
fields.put("publicationId", text(publication.publicationId().value()));
|
||||
fields.put("status", text(publication.status().name()));
|
||||
fields.put("attemptNumber", integer(publication.attemptNumber()));
|
||||
return result(operation, fields);
|
||||
}
|
||||
|
||||
private static PkiOperationResult publicationRecordResult(String operation, PublicationRecord publication) {
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
fields.put("publicationId", text(publication.publicationId().value()));
|
||||
fields.put("status", text(publication.status().name()));
|
||||
fields.put("attemptNumber", integer(publication.attemptNumber()));
|
||||
return result(operation, fields);
|
||||
}
|
||||
|
||||
private static <T> T requireCapability(Optional<T> service, String capability) {
|
||||
return service.orElseThrow(() -> new CapabilityUnavailableException(capability));
|
||||
}
|
||||
|
||||
private static PkiOperationResult revocationResult(String operation, RevocationRecord record) {
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
fields.put("credentialId", text(record.credentialId().value()));
|
||||
@@ -183,6 +482,95 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
||||
return result(operation, fields);
|
||||
}
|
||||
|
||||
private static PkiOperationResult profileReferenceResult(String operation, CertificateProfileRef reference) {
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
appendProfileReference(fields, reference);
|
||||
return result(operation, fields);
|
||||
}
|
||||
|
||||
private static PkiOperationResult profileVersionResult(String operation,
|
||||
ImportedCertificateProfileVersion version) {
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
appendProfileVersion(fields, version);
|
||||
return result(operation, fields);
|
||||
}
|
||||
|
||||
private static PkiOperationValue profileVersionValue(ImportedCertificateProfileVersion version) {
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
appendProfileVersion(fields, version);
|
||||
return new PkiOperationValue.ObjectValue(fields);
|
||||
}
|
||||
|
||||
private static void appendProfileVersion(Map<String, PkiOperationValue> fields,
|
||||
ImportedCertificateProfileVersion version) {
|
||||
appendProfileReference(fields, version.reference());
|
||||
fields.put("certificateType", text(version.definition().certificateType().name()));
|
||||
fields.put(FORMAT_ID, text(version.definition().formatId().value()));
|
||||
fields.put("importedAt", text(version.importedAt().toString()));
|
||||
}
|
||||
|
||||
private static void appendProfileReference(Map<String, PkiOperationValue> fields,
|
||||
CertificateProfileRef reference) {
|
||||
fields.put("profileId", text(reference.profileId()));
|
||||
fields.put("profileVersion", integer(reference.profileVersion()));
|
||||
fields.put("fingerprint", text(reference.shortFingerprint()));
|
||||
}
|
||||
|
||||
private static PkiOperationResult authorityResult(String operation, CaRecord record) {
|
||||
return result(operation, authorityFields(record));
|
||||
}
|
||||
|
||||
private static PkiOperationValue authorityValue(CaRecord record) {
|
||||
return new PkiOperationValue.ObjectValue(authorityFields(record));
|
||||
}
|
||||
|
||||
private static Map<String, PkiOperationValue> authorityFields(CaRecord record) {
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
fields.put("caId", text(record.caId().value()));
|
||||
fields.put("kind", text(record.kind().name()));
|
||||
fields.put("state", text(record.state().name()));
|
||||
fields.put("credentialCount", integer(record.credentialIds().size()));
|
||||
return fields;
|
||||
}
|
||||
|
||||
private static PkiOperationValue revocationValue(RevocationRecord record) {
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
fields.put("credentialId", text(record.credentialId().value()));
|
||||
appendTransition(fields, record.transition());
|
||||
return new PkiOperationValue.ObjectValue(fields);
|
||||
}
|
||||
|
||||
private static PkiOperationResult statusResult(String operation, StatusObject object) {
|
||||
return result(operation, statusFields(object));
|
||||
}
|
||||
|
||||
private static PkiOperationValue statusValue(StatusObject object) {
|
||||
return new PkiOperationValue.ObjectValue(statusFields(object));
|
||||
}
|
||||
|
||||
private static Map<String, PkiOperationValue> statusFields(StatusObject object) {
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
fields.put("statusObjectId", text(object.statusObjectId().value()));
|
||||
fields.put("issuerCaId", text(object.issuerCaId().value()));
|
||||
fields.put("type", text(object.type().name()));
|
||||
fields.put(FORMAT_ID, text(object.formatId().value()));
|
||||
fields.put("thisUpdate", text(object.thisUpdate().toString()));
|
||||
object.nextUpdate().ifPresent(value -> fields.put("nextUpdate", text(value.toString())));
|
||||
fields.put("contentLength", integer(object.content().length()));
|
||||
fields.put("contentSha256", text(object.content().sha256()));
|
||||
return fields;
|
||||
}
|
||||
|
||||
private static PkiOperationValue publicationValue(PublicationRecord record) {
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
fields.put("publicationId", text(record.publicationId().value()));
|
||||
fields.put("sourceType", text(record.sourceType().name()));
|
||||
fields.put("sourceId", text(record.sourceId().value()));
|
||||
fields.put("status", text(record.status().name()));
|
||||
fields.put("attemptNumber", integer(record.attemptNumber()));
|
||||
return new PkiOperationValue.ObjectValue(fields);
|
||||
}
|
||||
|
||||
private static PkiOperationValue transitionValue(RevocationTransition transition) {
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
appendTransition(fields, transition);
|
||||
@@ -207,6 +595,12 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
||||
}
|
||||
|
||||
private static PkiOperationOutcome classify(String operation, RuntimeException failure) {
|
||||
if (failure instanceof CapabilityUnavailableException) {
|
||||
return failure(operation, PkiOperationFailure.VALIDATION_FAILURE, "CAPABILITY_NOT_CONFIGURED");
|
||||
}
|
||||
if (failure instanceof ExternalOutcomeUnknownException) {
|
||||
return failure(operation, PkiOperationFailure.EXTERNAL_OUTCOME_UNKNOWN, "EXTERNAL_OUTCOME_UNKNOWN");
|
||||
}
|
||||
if (failure instanceof MissingObjectException) {
|
||||
return failure(operation, PkiOperationFailure.NOT_FOUND, "OBJECT_NOT_FOUND");
|
||||
}
|
||||
@@ -220,6 +614,10 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
||||
if (message.contains("CREDENTIAL_NOT_FOUND")) {
|
||||
return failure(operation, PkiOperationFailure.NOT_FOUND, "OBJECT_NOT_FOUND");
|
||||
}
|
||||
String lowerMessage = message.toLowerCase(java.util.Locale.ROOT);
|
||||
if (lowerMessage.contains("not found") || lowerMessage.contains("does not exist")) {
|
||||
return failure(operation, PkiOperationFailure.NOT_FOUND, "OBJECT_NOT_FOUND");
|
||||
}
|
||||
if (message.contains("CONFLICT") || message.contains("changed concurrently")) {
|
||||
return failure(operation, PkiOperationFailure.CONFLICT, "OPERATION_CONFLICT");
|
||||
}
|
||||
@@ -266,4 +664,18 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
||||
private static final class MissingObjectException extends RuntimeException {
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
|
||||
/** Internal marker for an operation whose explicitly configured service is unavailable. */
|
||||
private static final class CapabilityUnavailableException extends RuntimeException {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private CapabilityUnavailableException(String capability) {
|
||||
super(capability);
|
||||
}
|
||||
}
|
||||
|
||||
/** Internal marker preserving an unresolved external publication outcome. */
|
||||
private static final class ExternalOutcomeUnknownException extends RuntimeException {
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,17 +33,53 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.pki.application;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.Clock;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import zeroecho.core.alg.BootstrapAlgorithmIdentities;
|
||||
import zeroecho.core.spec.AlgorithmIdentity;
|
||||
import zeroecho.core.spec.AlgorithmSuite;
|
||||
import zeroecho.core.spi.AlgorithmExecutionCapability;
|
||||
import zeroecho.core.spi.AlgorithmExecutionCapabilityProvider;
|
||||
import zeroecho.core.spi.KeyringUnlockProvider;
|
||||
import zeroecho.core.storage.KeyringPassword;
|
||||
import zeroecho.pki.api.CaService;
|
||||
import zeroecho.pki.api.CertificationRequestService;
|
||||
import zeroecho.pki.api.IssuanceService;
|
||||
import zeroecho.pki.api.PublicationService;
|
||||
import zeroecho.pki.api.ProfileService;
|
||||
import zeroecho.pki.api.RevocationService;
|
||||
import zeroecho.pki.api.StatusObjectService;
|
||||
import zeroecho.pki.api.credential.EffectiveCredentialStatusResolver;
|
||||
import zeroecho.pki.impl.core.DefaultProfileService;
|
||||
import zeroecho.pki.impl.core.DefaultRevocationService;
|
||||
import zeroecho.pki.impl.core.DefaultCaService;
|
||||
import zeroecho.pki.impl.core.DefaultCertificationRequestService;
|
||||
import zeroecho.pki.impl.core.DefaultIssuanceService;
|
||||
import zeroecho.pki.impl.core.DefaultPublicationService;
|
||||
import zeroecho.pki.impl.core.DefaultStatusObjectService;
|
||||
import zeroecho.pki.impl.core.StoreBackedEffectiveCredentialStatusResolver;
|
||||
import zeroecho.pki.impl.core.async.PkiSigningBus;
|
||||
import zeroecho.pki.impl.framework.x509.X509AlgorithmResolver;
|
||||
import zeroecho.pki.impl.framework.x509.X509AuthoritySnapshot;
|
||||
import zeroecho.pki.impl.framework.x509.bc.BcX509CredentialFramework;
|
||||
import zeroecho.pki.impl.framework.x509.bc.BcX509CredentialIssuerBackend;
|
||||
import zeroecho.pki.impl.framework.x509.bc.BcX509ProofOfPossessionVerifier;
|
||||
import zeroecho.pki.impl.framework.x509.bc.BcX509StatusObjectGenerator;
|
||||
import zeroecho.pki.impl.framework.x509.bc.BcX509VerificationExecutor;
|
||||
import zeroecho.pki.spi.ProviderConfig;
|
||||
import zeroecho.pki.spi.audit.AuditSink;
|
||||
import zeroecho.pki.spi.bootstrap.PkiBootstrap;
|
||||
import zeroecho.pki.spi.crypto.PublicKeyInfoSource;
|
||||
import zeroecho.pki.spi.crypto.SignatureWorkflow;
|
||||
import zeroecho.pki.spi.crypto.SignatureWorkflowRuntimeDependencies;
|
||||
import zeroecho.pki.spi.publish.Publisher;
|
||||
import zeroecho.pki.spi.store.PkiStore;
|
||||
|
||||
/** Default synchronous session composition. */
|
||||
@@ -53,37 +89,76 @@ final class DefaultPkiSession implements PkiSession {
|
||||
private final AuditSink audit;
|
||||
private final ProfileService profiles;
|
||||
private final RevocationService revocations;
|
||||
private final Optional<CaService> authorities;
|
||||
private final Optional<CertificationRequestService> requests;
|
||||
private final Optional<IssuanceService> issuance;
|
||||
private final Optional<StatusObjectService> statusObjects;
|
||||
private final Optional<PublicationService> publications;
|
||||
private final Optional<PkiSigningBus> signingBus;
|
||||
private final Optional<SignatureWorkflow> signatureWorkflow;
|
||||
private final PkiOperationExecutor operations;
|
||||
private final AtomicBoolean closed = new AtomicBoolean();
|
||||
|
||||
private DefaultPkiSession(PkiSessionConfiguration configuration, PkiStore store, AuditSink audit, Clock clock) {
|
||||
private DefaultPkiSession(PkiSessionConfiguration configuration, PkiStore store, AuditSink audit,
|
||||
ProfileService profiles, RevocationService revocations, ServiceGraph graph) {
|
||||
this.store = store;
|
||||
this.audit = audit;
|
||||
this.profiles = new DefaultProfileService(store, clock, audit);
|
||||
this.revocations = new DefaultRevocationService(store, clock, audit);
|
||||
this.operations = new DefaultPkiOperationExecutor(configuration, store, revocations,
|
||||
this::requireOpen);
|
||||
this.profiles = Objects.requireNonNull(profiles, "profiles");
|
||||
this.revocations = Objects.requireNonNull(revocations, "revocations");
|
||||
this.authorities = graph.authorities();
|
||||
this.requests = graph.requests();
|
||||
this.issuance = graph.issuance();
|
||||
this.statusObjects = graph.statusObjects();
|
||||
this.publications = graph.publications();
|
||||
this.signingBus = graph.signingBus();
|
||||
this.signatureWorkflow = graph.signatureWorkflow();
|
||||
this.operations = new DefaultPkiOperationExecutor(configuration, store, profiles, revocations, authorities,
|
||||
requests, issuance, statusObjects, publications, this::requireOpen);
|
||||
}
|
||||
|
||||
/* default */ static PkiSession open(PkiSessionConfiguration configuration) {
|
||||
return open(configuration, Clock.systemUTC(), ProductionBootstrap.INSTANCE);
|
||||
return open(configuration, runtimeDependencies(configuration), Clock.systemUTC(), ProductionBootstrap.INSTANCE);
|
||||
}
|
||||
|
||||
/* default */ static PkiSession open(PkiSessionConfiguration configuration,
|
||||
PkiSessionRuntimeDependencies dependencies) {
|
||||
return open(configuration, dependencies, Clock.systemUTC(), ProductionBootstrap.INSTANCE);
|
||||
}
|
||||
|
||||
/* default */ static PkiSession open(PkiSessionConfiguration configuration, Clock clock, Bootstrap bootstrap) {
|
||||
return open(configuration, PkiSessionRuntimeDependencies.none(), clock, bootstrap);
|
||||
}
|
||||
|
||||
@SuppressWarnings("PMD.AvoidCatchingGenericException")
|
||||
/* default */ static PkiSession open(PkiSessionConfiguration configuration, Clock clock, Bootstrap bootstrap) {
|
||||
/* default */ static PkiSession open(PkiSessionConfiguration configuration,
|
||||
PkiSessionRuntimeDependencies dependencies, Clock clock, Bootstrap bootstrap) {
|
||||
PkiSessionConfiguration exact = Objects.requireNonNull(configuration, "configuration");
|
||||
Objects.requireNonNull(clock, "clock");
|
||||
Objects.requireNonNull(bootstrap, "bootstrap");
|
||||
Objects.requireNonNull(dependencies, "dependencies");
|
||||
bootstrap.validateStore(exact.store());
|
||||
bootstrap.validateAudit(exact.audit());
|
||||
exact.signing().ifPresent(signing -> {
|
||||
bootstrap.validateSignatureWorkflow(signing.workflow());
|
||||
bootstrap.validateCredentialFramework(signing.framework());
|
||||
if (!"x509-bc".equals(signing.framework().backendId())) {
|
||||
throw new IllegalArgumentException("Configured framework cannot compose the X.509 service graph");
|
||||
}
|
||||
});
|
||||
exact.publishers().forEach(bootstrap::validatePublisher);
|
||||
|
||||
PkiStore store = null;
|
||||
AuditSink audit = null;
|
||||
ServiceGraph graph = ServiceGraph.empty();
|
||||
try {
|
||||
store = Objects.requireNonNull(bootstrap.openStore(exact.store()), "opened store");
|
||||
audit = Objects.requireNonNull(bootstrap.openAudit(exact.audit()), "opened audit sink");
|
||||
return new DefaultPkiSession(exact, store, audit, clock);
|
||||
ProfileService profiles = new DefaultProfileService(store, clock, audit);
|
||||
RevocationService revocations = new DefaultRevocationService(store, clock, audit);
|
||||
graph = composeGraph(exact, dependencies, store, audit, profiles, clock, bootstrap);
|
||||
return new DefaultPkiSession(exact, store, audit, profiles, revocations, graph);
|
||||
} catch (RuntimeException | Error primary) {
|
||||
closeGraphAfterConstructionFailure(graph, primary);
|
||||
closeAfterConstructionFailure(audit, store, primary);
|
||||
throw primary;
|
||||
}
|
||||
@@ -101,6 +176,36 @@ final class DefaultPkiSession implements PkiSession {
|
||||
return revocations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<CaService> authorities() {
|
||||
requireOpen();
|
||||
return authorities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<CertificationRequestService> requests() {
|
||||
requireOpen();
|
||||
return requests;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<IssuanceService> issuance() {
|
||||
requireOpen();
|
||||
return issuance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<StatusObjectService> statusObjects() {
|
||||
requireOpen();
|
||||
return statusObjects;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<PublicationService> publications() {
|
||||
requireOpen();
|
||||
return publications;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PkiOperationExecutor operations() {
|
||||
requireOpen();
|
||||
@@ -113,6 +218,9 @@ final class DefaultPkiSession implements PkiSession {
|
||||
return;
|
||||
}
|
||||
Throwable primary = null;
|
||||
primary = closeResource(publications.orElse(null), primary);
|
||||
primary = closeResource(signingBus.orElse(null), primary);
|
||||
primary = closeResource(signatureWorkflow.orElse(null), primary);
|
||||
try {
|
||||
audit.close();
|
||||
} catch (Throwable failure) { // NOPMD - preserve Error and checked close failures.
|
||||
@@ -136,6 +244,154 @@ final class DefaultPkiSession implements PkiSession {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "PMD.AvoidCatchingGenericException", "PMD.CloseResource", "PMD.ExceptionAsFlowControl" })
|
||||
private static ServiceGraph composeGraph(PkiSessionConfiguration configuration,
|
||||
PkiSessionRuntimeDependencies dependencies, PkiStore store, AuditSink audit, ProfileService profiles,
|
||||
Clock clock, Bootstrap bootstrap) {
|
||||
Optional<SignatureWorkflow> workflow = Optional.empty();
|
||||
Optional<PkiSigningBus> bus = Optional.empty();
|
||||
Optional<CaService> authorities = Optional.empty();
|
||||
Optional<CertificationRequestService> requests = Optional.empty();
|
||||
Optional<IssuanceService> issuance = Optional.empty();
|
||||
Optional<StatusObjectService> statuses = Optional.empty();
|
||||
Optional<PublicationService> publications = Optional.empty();
|
||||
try {
|
||||
if (configuration.signing().isPresent()) {
|
||||
PkiSessionConfiguration.SigningConfiguration signing = configuration.signing().orElseThrow();
|
||||
KeyringUnlockProvider unlock = dependencies.keyringUnlockProvider().orElseThrow(
|
||||
() -> new IllegalStateException("Configured signing requires key-unlock capability"));
|
||||
SignatureWorkflow openedWorkflow = bootstrap.openSignatureWorkflow(signing.workflow(),
|
||||
SignatureWorkflowRuntimeDependencies.withKeyringUnlockProvider(unlock));
|
||||
workflow = Optional.of(openedWorkflow);
|
||||
if (!(openedWorkflow instanceof PublicKeyInfoSource publicKeys)) {
|
||||
throw new IllegalStateException("Signature workflow lacks managed public-key capability");
|
||||
}
|
||||
BcX509VerificationExecutor verification = new BcX509VerificationExecutor();
|
||||
X509AuthoritySnapshot authority = authority(openedWorkflow, verification);
|
||||
PkiSigningBus openedBus = new PkiSigningBus(store, openedWorkflow, Path.of(signing.busPath()),
|
||||
authority);
|
||||
bus = Optional.of(openedBus);
|
||||
BcX509CredentialIssuerBackend issuerBackend = new BcX509CredentialIssuerBackend(openedBus,
|
||||
signing.signatureAlgorithm(), signing.signingTtl());
|
||||
BcX509StatusObjectGenerator statusGenerator = new BcX509StatusObjectGenerator(openedBus,
|
||||
signing.signatureAlgorithm(), signing.signingTtl());
|
||||
BcX509CredentialFramework framework = new BcX509CredentialFramework(authority, verification)
|
||||
.wired(statusGenerator, new BcX509ProofOfPossessionVerifier(authority, verification));
|
||||
EffectiveCredentialStatusResolver statusResolver = new StoreBackedEffectiveCredentialStatusResolver(
|
||||
store, clock);
|
||||
requests = Optional.of(new DefaultCertificationRequestService(store, framework));
|
||||
issuance = Optional.of(new DefaultIssuanceService(store, framework, issuerBackend, audit,
|
||||
statusResolver, profiles, clock));
|
||||
statuses = Optional.of(new DefaultStatusObjectService(store, framework, audit, statusResolver,
|
||||
authority));
|
||||
authorities = Optional.of(new DefaultCaService(store, framework, issuerBackend,
|
||||
publicKeys::resolvePublicKeyInfo, openedBus, audit, statusResolver, profiles, clock,
|
||||
signing.signatureAlgorithm(), signing.signingTtl()));
|
||||
}
|
||||
List<Publisher> configuredPublishers = new ArrayList<>();
|
||||
for (ProviderConfig publisher : configuration.publishers()) {
|
||||
configuredPublishers.add(bootstrap.openPublisher(publisher));
|
||||
}
|
||||
publications = configuredPublishers.isEmpty() ? Optional.empty()
|
||||
: Optional.of(new DefaultPublicationService(store, clock, configuredPublishers));
|
||||
return new ServiceGraph(authorities, requests, issuance, statuses, publications, bus, workflow);
|
||||
} catch (RuntimeException | Error primary) {
|
||||
Throwable cleanup = closeResource(publications.orElse(null), primary);
|
||||
cleanup = closeResource(bus.orElse(null), cleanup);
|
||||
closeResource(workflow.orElse(null), cleanup);
|
||||
throw primary;
|
||||
}
|
||||
}
|
||||
|
||||
private static X509AuthoritySnapshot authority(SignatureWorkflow workflow,
|
||||
BcX509VerificationExecutor verification) {
|
||||
Set<String> supported = Set.copyOf(workflow.supportedAlgorithms());
|
||||
Set<AlgorithmIdentity> identities = BootstrapAlgorithmIdentities.catalog().identities().stream()
|
||||
.filter(identity -> identity.kind() == AlgorithmIdentity.Kind.SIGNATURE)
|
||||
.filter(identity -> supported.contains(identity.canonicalForm())
|
||||
|| BootstrapAlgorithmIdentities.compatibilityAliases().entrySet().stream()
|
||||
.anyMatch(entry -> entry.getValue().equals(identity) && supported.contains(entry.getKey())))
|
||||
.collect(java.util.stream.Collectors.toUnmodifiableSet());
|
||||
String implementationId = "workflow." + workflow.id();
|
||||
AlgorithmExecutionCapability signing = new AlgorithmExecutionCapability() {
|
||||
@Override
|
||||
public String implementationId() {
|
||||
return implementationId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String domainFingerprint() {
|
||||
return "session-signing-v1:" + workflow.id() + ":" + identities.stream()
|
||||
.map(AlgorithmIdentity::canonicalForm).sorted().reduce("", String::concat);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(AlgorithmIdentity identity, AlgorithmSuite suite, Direction direction) {
|
||||
return direction == Direction.SIGN && identities.contains(identity) && identity.equals(suite.signature());
|
||||
}
|
||||
};
|
||||
AlgorithmExecutionCapabilityProvider signingProvider = () -> List.of(signing);
|
||||
X509AlgorithmResolver.Policy policy = new X509AlgorithmResolver.Policy() {
|
||||
@Override
|
||||
public boolean permits(AlgorithmSuite suite, AlgorithmExecutionCapability.Direction direction) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String semanticFingerprint() {
|
||||
return "pki-session-bootstrap-policy-v1:configured-capabilities-only";
|
||||
}
|
||||
};
|
||||
return X509AuthoritySnapshot.compose(List.of(), List.of(signingProvider, verification),
|
||||
List.of(X509AuthoritySnapshot.bindExecutor(implementationId,
|
||||
AlgorithmExecutionCapability.Direction.SIGN, workflow),
|
||||
X509AuthoritySnapshot.bindExecutor(BcX509VerificationExecutor.IMPLEMENTATION_ID,
|
||||
AlgorithmExecutionCapability.Direction.VERIFY, verification)), policy);
|
||||
}
|
||||
|
||||
private static PkiSessionRuntimeDependencies runtimeDependencies(PkiSessionConfiguration configuration) {
|
||||
Optional<String> environment = configuration.signing()
|
||||
.flatMap(PkiSessionConfiguration.SigningConfiguration::unlockEnvironmentVariable);
|
||||
if (environment.isEmpty()) {
|
||||
return PkiSessionRuntimeDependencies.none();
|
||||
}
|
||||
KeyringUnlockProvider provider = () -> {
|
||||
String value = System.getenv(environment.orElseThrow());
|
||||
if (value == null || value.isEmpty()) {
|
||||
throw new java.io.IOException("Configured key-unlock source is unavailable");
|
||||
}
|
||||
char[] password = value.toCharArray();
|
||||
try {
|
||||
return new KeyringPassword(password);
|
||||
} finally {
|
||||
java.util.Arrays.fill(password, '\0');
|
||||
}
|
||||
};
|
||||
return PkiSessionRuntimeDependencies.withKeyringUnlockProvider(provider);
|
||||
}
|
||||
|
||||
@SuppressWarnings("PMD.AvoidCatchingGenericException")
|
||||
private static Throwable closeResource(AutoCloseable resource, Throwable primary) {
|
||||
if (resource == null) {
|
||||
return primary;
|
||||
}
|
||||
try {
|
||||
resource.close();
|
||||
} catch (Throwable failure) {
|
||||
if (primary == null) {
|
||||
return failure;
|
||||
}
|
||||
primary.addSuppressed(failure);
|
||||
}
|
||||
return primary;
|
||||
}
|
||||
|
||||
private static void closeGraphAfterConstructionFailure(ServiceGraph graph, Throwable primary) {
|
||||
Throwable ignored = closeResource(graph.publications().orElse(null), primary);
|
||||
ignored = closeResource(graph.signingBus().orElse(null), ignored);
|
||||
closeResource(graph.signatureWorkflow().orElse(null), ignored);
|
||||
}
|
||||
|
||||
private static void closeAfterConstructionFailure(AuditSink audit, PkiStore store, Throwable primary) {
|
||||
if (audit != null) {
|
||||
try {
|
||||
@@ -180,6 +436,42 @@ final class DefaultPkiSession implements PkiSession {
|
||||
|
||||
/** Opens the configured audit sink. */
|
||||
AuditSink openAudit(ProviderConfig configuration);
|
||||
|
||||
/** Validates signing-workflow configuration. */
|
||||
default void validateSignatureWorkflow(ProviderConfig configuration) {
|
||||
PkiBootstrap.validateSignatureWorkflowConfiguration(configuration);
|
||||
}
|
||||
|
||||
/** Validates credential-framework configuration. */
|
||||
default void validateCredentialFramework(ProviderConfig configuration) {
|
||||
PkiBootstrap.validateCredentialFrameworkConfiguration(configuration);
|
||||
}
|
||||
|
||||
/** Validates publisher configuration. */
|
||||
default void validatePublisher(ProviderConfig configuration) {
|
||||
PkiBootstrap.validatePublisherConfiguration(configuration);
|
||||
}
|
||||
|
||||
/** Opens the configured signature workflow. */
|
||||
default SignatureWorkflow openSignatureWorkflow(ProviderConfig configuration,
|
||||
SignatureWorkflowRuntimeDependencies dependencies) {
|
||||
return PkiBootstrap.openConfiguredSignatureWorkflow(configuration, dependencies);
|
||||
}
|
||||
|
||||
/** Opens one configured publisher. */
|
||||
default Publisher openPublisher(ProviderConfig configuration) {
|
||||
return PkiBootstrap.openPublisher(configuration);
|
||||
}
|
||||
}
|
||||
|
||||
private record ServiceGraph(Optional<CaService> authorities, Optional<CertificationRequestService> requests,
|
||||
Optional<IssuanceService> issuance, Optional<StatusObjectService> statusObjects,
|
||||
Optional<PublicationService> publications, Optional<PkiSigningBus> signingBus,
|
||||
Optional<SignatureWorkflow> signatureWorkflow) {
|
||||
private static ServiceGraph empty() {
|
||||
return new ServiceGraph(Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(),
|
||||
Optional.empty(), Optional.empty(), Optional.empty());
|
||||
}
|
||||
}
|
||||
|
||||
/** Production provider bootstrap implementation. */
|
||||
|
||||
@@ -35,8 +35,16 @@ package zeroecho.pki.application;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import zeroecho.pki.api.EncodedObject;
|
||||
import zeroecho.pki.api.FormatId;
|
||||
import zeroecho.pki.api.KeyRef;
|
||||
import zeroecho.pki.api.PkiId;
|
||||
import zeroecho.pki.api.SubjectRef;
|
||||
import zeroecho.pki.api.ca.CaState;
|
||||
import zeroecho.pki.api.publication.PublicationSourceType;
|
||||
import zeroecho.pki.api.publication.PublicationTarget;
|
||||
import zeroecho.pki.api.revocation.RevocationReason;
|
||||
import zeroecho.pki.api.status.StatusObjectType;
|
||||
|
||||
/**
|
||||
* Closed set of typed synchronous PKI operations available to transports.
|
||||
@@ -45,8 +53,18 @@ import zeroecho.pki.api.revocation.RevocationReason;
|
||||
* explicit type and never by reflection or Java class name.</p>
|
||||
*/
|
||||
public sealed interface PkiOperation permits PkiOperation.ValidateConfiguration, PkiOperation.ValidateProfile,
|
||||
PkiOperation.InspectCredential, PkiOperation.RevokeCredential, PkiOperation.ReadRevocationHistory,
|
||||
PkiOperation.InspectPublication {
|
||||
PkiOperation.RegisterProfile, PkiOperation.InspectProfile, PkiOperation.ListProfileVersions,
|
||||
PkiOperation.ActivateProfile, PkiOperation.InspectAuthority, PkiOperation.ListAuthorities,
|
||||
PkiOperation.CreateAuthority, PkiOperation.TransitionAuthority, PkiOperation.InspectRequest,
|
||||
PkiOperation.ImportRequest, PkiOperation.VerifyRequest, PkiOperation.InspectCredential,
|
||||
PkiOperation.IssueCredential, PkiOperation.RevokeCredential,
|
||||
PkiOperation.InspectRevocation, PkiOperation.ReadRevocationHistory, PkiOperation.SnapshotRevocations,
|
||||
PkiOperation.InspectStatus, PkiOperation.ListStatus, PkiOperation.GenerateStatus,
|
||||
PkiOperation.InspectPublication, PkiOperation.ListPublications, PkiOperation.RegisterPublication,
|
||||
PkiOperation.ProcessPublication, PkiOperation.RetryPublication, PkiOperation.ReconcilePublication {
|
||||
|
||||
/** Maximum finite entries returned by one presentation operation. */
|
||||
int MAXIMUM_RESULT_ENTRIES = 1_000;
|
||||
|
||||
/** @return stable semantic operation name */
|
||||
String name();
|
||||
@@ -83,6 +101,204 @@ public sealed interface PkiOperation permits PkiOperation.ValidateConfiguration,
|
||||
}
|
||||
}
|
||||
|
||||
/** Imports one bounded validated certificate-profile document. */
|
||||
record RegisterProfile(byte[] document) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
public static final String NAME = "profile.register";
|
||||
|
||||
/** Defensively snapshots the document. */
|
||||
public RegisterProfile {
|
||||
document = Objects.requireNonNull(document, "document").clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] document() {
|
||||
return document.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads one exact imported certificate-profile version. */
|
||||
record InspectProfile(String profileId, long profileVersion) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
public static final String NAME = "profile.inspect";
|
||||
|
||||
/** Validates the exact profile identity. */
|
||||
public InspectProfile {
|
||||
if (profileId == null || profileId.isBlank() || profileVersion <= 0) {
|
||||
throw new IllegalArgumentException("profile identity is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/** Lists the finite imported versions of one logical profile. */
|
||||
record ListProfileVersions(String profileId) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
public static final String NAME = "profile.list";
|
||||
|
||||
/** Validates the logical profile identity. */
|
||||
public ListProfileVersions {
|
||||
if (profileId == null || profileId.isBlank()) {
|
||||
throw new IllegalArgumentException("profileId must not be blank");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/** Activates one already imported certificate-profile version. */
|
||||
record ActivateProfile(String profileId, long profileVersion) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
public static final String NAME = "profile.activate";
|
||||
|
||||
/** Validates the exact profile identity. */
|
||||
public ActivateProfile {
|
||||
if (profileId == null || profileId.isBlank() || profileVersion <= 0) {
|
||||
throw new IllegalArgumentException("profile identity is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads safe metadata for one committed certificate authority. */
|
||||
record InspectAuthority(PkiId caId) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
public static final String NAME = "ca.inspect";
|
||||
|
||||
/** Validates the authority identity. */
|
||||
public InspectAuthority {
|
||||
Objects.requireNonNull(caId, "caId");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/** Lists committed certificate authorities with a finite presentation limit. */
|
||||
record ListAuthorities(int limit) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
public static final String NAME = "ca.list";
|
||||
|
||||
/** Validates the presentation limit. */
|
||||
public ListAuthorities {
|
||||
requireLimit(limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates one root certificate authority using an existing managed key reference. */
|
||||
record CreateAuthority(FormatId formatId, SubjectRef subjectRef, String profileId, KeyRef keyRef)
|
||||
implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
public static final String NAME = "ca.create";
|
||||
|
||||
/** Validates the closed authority-creation input. */
|
||||
public CreateAuthority {
|
||||
Objects.requireNonNull(formatId, "formatId");
|
||||
Objects.requireNonNull(subjectRef, "subjectRef");
|
||||
Objects.requireNonNull(keyRef, "keyRef");
|
||||
if (profileId == null || profileId.isBlank()) {
|
||||
throw new IllegalArgumentException("profileId must not be blank");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/** Applies one explicit lifecycle transition to an existing authority. */
|
||||
record TransitionAuthority(PkiId caId, CaState state, String reason) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
public static final String NAME = "ca.transition";
|
||||
|
||||
/** Validates the lifecycle-transition input. */
|
||||
public TransitionAuthority {
|
||||
Objects.requireNonNull(caId, "caId");
|
||||
Objects.requireNonNull(state, "state");
|
||||
if (reason == null || reason.isBlank()) {
|
||||
throw new IllegalArgumentException("reason must not be blank");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads safe metadata for one stored certification request. */
|
||||
record InspectRequest(PkiId requestId) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
public static final String NAME = "request.inspect";
|
||||
|
||||
/** Validates the request identity. */
|
||||
public InspectRequest {
|
||||
Objects.requireNonNull(requestId, "requestId");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/** Strictly parses and durably imports one certification request. */
|
||||
record ImportRequest(FormatId formatId, EncodedObject encoded) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
public static final String NAME = "request.import";
|
||||
|
||||
/** Validates the finite request input. */
|
||||
public ImportRequest {
|
||||
Objects.requireNonNull(formatId, "formatId");
|
||||
Objects.requireNonNull(encoded, "encoded");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/** Verifies proof of possession for one stored request. */
|
||||
record VerifyRequest(PkiId requestId) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
public static final String NAME = "request.verify";
|
||||
|
||||
/** Validates the request identity. */
|
||||
public VerifyRequest {
|
||||
Objects.requireNonNull(requestId, "requestId");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads safe metadata for one committed credential. */
|
||||
record InspectCredential(PkiId credentialId) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
@@ -99,6 +315,26 @@ public sealed interface PkiOperation permits PkiOperation.ValidateConfiguration,
|
||||
}
|
||||
}
|
||||
|
||||
/** Issues one end-entity credential from an already imported request. */
|
||||
record IssueCredential(PkiId issuerCaId, PkiId requestId, String profileId) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
public static final String NAME = "credential.issue";
|
||||
|
||||
/** Validates the issuance input. */
|
||||
public IssueCredential {
|
||||
Objects.requireNonNull(issuerCaId, "issuerCaId");
|
||||
Objects.requireNonNull(requestId, "requestId");
|
||||
if (profileId == null || profileId.isBlank()) {
|
||||
throw new IllegalArgumentException("profileId must not be blank");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/** Permanently revokes one committed credential. */
|
||||
record RevokeCredential(PkiId credentialId, RevocationReason reason) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
@@ -140,6 +376,89 @@ public sealed interface PkiOperation permits PkiOperation.ValidateConfiguration,
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads the validated current revocation state for one credential. */
|
||||
record InspectRevocation(PkiId credentialId) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
public static final String NAME = "revocation.inspect";
|
||||
|
||||
/** Validates the credential identity. */
|
||||
public InspectRevocation {
|
||||
Objects.requireNonNull(credentialId, "credentialId");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads a bounded page from one immutable current-revocation snapshot. */
|
||||
record SnapshotRevocations(int limit) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
public static final String NAME = "revocation.snapshot";
|
||||
|
||||
/** Validates the presentation limit. */
|
||||
public SnapshotRevocations {
|
||||
requireLimit(limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads safe metadata for one committed status object. */
|
||||
record InspectStatus(PkiId statusObjectId) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
public static final String NAME = "status.inspect";
|
||||
|
||||
/** Validates the status-object identity. */
|
||||
public InspectStatus {
|
||||
Objects.requireNonNull(statusObjectId, "statusObjectId");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/** Lists status objects for one issuer with a finite presentation limit. */
|
||||
record ListStatus(PkiId issuerCaId, int limit) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
public static final String NAME = "inventory.status";
|
||||
|
||||
/** Validates the issuer identity and presentation limit. */
|
||||
public ListStatus {
|
||||
Objects.requireNonNull(issuerCaId, "issuerCaId");
|
||||
requireLimit(limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/** Generates one signed status object from a stable revocation view. */
|
||||
record GenerateStatus(PkiId issuerCaId, StatusObjectType type, FormatId formatId) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
public static final String NAME = "status.generate";
|
||||
|
||||
/** Validates the generation input. */
|
||||
public GenerateStatus {
|
||||
Objects.requireNonNull(issuerCaId, "issuerCaId");
|
||||
Objects.requireNonNull(type, "type");
|
||||
Objects.requireNonNull(formatId, "formatId");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads safe durable state for one publication operation. */
|
||||
record InspectPublication(PkiId publicationId) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
@@ -155,4 +474,94 @@ public sealed interface PkiOperation permits PkiOperation.ValidateConfiguration,
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/** Lists publication records using a bounded-memory cursor and finite result page. */
|
||||
record ListPublications(int limit) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
public static final String NAME = "publication.list";
|
||||
|
||||
/** Validates the presentation limit. */
|
||||
public ListPublications {
|
||||
requireLimit(limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/** Registers post-commit publication of one authoritative source object. */
|
||||
record RegisterPublication(PkiId publicationId, PublicationSourceType sourceType, PkiId sourceId,
|
||||
PublicationTarget target) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
public static final String NAME = "publication.register";
|
||||
|
||||
/** Validates the immutable publication registration fields. */
|
||||
public RegisterPublication {
|
||||
Objects.requireNonNull(publicationId, "publicationId");
|
||||
Objects.requireNonNull(sourceType, "sourceType");
|
||||
Objects.requireNonNull(sourceId, "sourceId");
|
||||
Objects.requireNonNull(target, "target");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/** Processes one explicitly selected pending publication operation. */
|
||||
record ProcessPublication(PkiId publicationId) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
public static final String NAME = "publication.process";
|
||||
|
||||
/** Validates the publication identity. */
|
||||
public ProcessPublication {
|
||||
Objects.requireNonNull(publicationId, "publicationId");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/** Explicitly retries one eligible publication operation. */
|
||||
record RetryPublication(PkiId publicationId) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
public static final String NAME = "publication.retry";
|
||||
|
||||
/** Validates the publication identity. */
|
||||
public RetryPublication {
|
||||
Objects.requireNonNull(publicationId, "publicationId");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/** Reconciles one publication operation whose external outcome is unknown. */
|
||||
record ReconcilePublication(PkiId publicationId) implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
public static final String NAME = "publication.reconcile";
|
||||
|
||||
/** Validates the publication identity. */
|
||||
public ReconcilePublication {
|
||||
Objects.requireNonNull(publicationId, "publicationId");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireLimit(int limit) {
|
||||
if (limit < 1 || limit > MAXIMUM_RESULT_ENTRIES) {
|
||||
throw new IllegalArgumentException("limit must be between 1 and 1000");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,15 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.pki.application;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import zeroecho.pki.api.CaService;
|
||||
import zeroecho.pki.api.CertificationRequestService;
|
||||
import zeroecho.pki.api.IssuanceService;
|
||||
import zeroecho.pki.api.PublicationService;
|
||||
import zeroecho.pki.api.ProfileService;
|
||||
import zeroecho.pki.api.RevocationService;
|
||||
import zeroecho.pki.api.StatusObjectService;
|
||||
|
||||
/**
|
||||
* Lifecycle-owned synchronous PKI backend session.
|
||||
@@ -58,12 +65,48 @@ public interface PkiSession extends AutoCloseable {
|
||||
return DefaultPkiSession.open(configuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a production session with explicit process-local security capabilities.
|
||||
*
|
||||
* @param configuration validated immutable provider configuration
|
||||
* @param dependencies process-local key-access capabilities
|
||||
* @return opened lifecycle-owned session
|
||||
*/
|
||||
static PkiSession open(PkiSessionConfiguration configuration, PkiSessionRuntimeDependencies dependencies) {
|
||||
return DefaultPkiSession.open(configuration, dependencies);
|
||||
}
|
||||
|
||||
/** @return profile lifecycle service owned by this session */
|
||||
ProfileService profiles();
|
||||
|
||||
/** @return revocation service owned by this session */
|
||||
RevocationService revocations();
|
||||
|
||||
/** @return configured CA service, or empty for a read-only session */
|
||||
default Optional<CaService> authorities() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/** @return configured certification-request service, or empty when X.509 is unavailable */
|
||||
default Optional<CertificationRequestService> requests() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/** @return configured issuance service, or empty when signing is unavailable */
|
||||
default Optional<IssuanceService> issuance() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/** @return configured status-object service, or empty when signing is unavailable */
|
||||
default Optional<StatusObjectService> statusObjects() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/** @return configured publication service, or empty when no destination is enabled */
|
||||
default Optional<PublicationService> publications() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/** @return shared typed operation executor owned by this session */
|
||||
PkiOperationExecutor operations();
|
||||
|
||||
|
||||
@@ -33,7 +33,10 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.pki.application;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import zeroecho.pki.spi.ProviderConfig;
|
||||
|
||||
@@ -47,22 +50,75 @@ import zeroecho.pki.spi.ProviderConfig;
|
||||
* @param version configuration schema version
|
||||
* @param store store-provider configuration
|
||||
* @param audit audit-provider configuration
|
||||
* @param signing optional signing and X.509 service composition
|
||||
* @param publishers explicitly enabled publication destinations
|
||||
*/
|
||||
public record PkiSessionConfiguration(int version, ProviderConfig store, ProviderConfig audit) {
|
||||
public record PkiSessionConfiguration(int version, ProviderConfig store, ProviderConfig audit,
|
||||
Optional<SigningConfiguration> signing, List<ProviderConfig> publishers) {
|
||||
|
||||
/** Current configuration schema version. */
|
||||
public static final int CURRENT_VERSION = 1;
|
||||
public static final int CURRENT_VERSION = 2;
|
||||
|
||||
/** Creates a read-only-compatible version-one configuration. */
|
||||
public PkiSessionConfiguration(int version, ProviderConfig store, ProviderConfig audit) {
|
||||
this(version, store, audit, Optional.empty(), List.of());
|
||||
}
|
||||
|
||||
/** Validates and snapshots the configuration. */
|
||||
public PkiSessionConfiguration {
|
||||
if (version != CURRENT_VERSION) {
|
||||
if (version < 1 || version > CURRENT_VERSION) {
|
||||
throw new IllegalArgumentException("Unsupported PKI session configuration version");
|
||||
}
|
||||
store = snapshot(Objects.requireNonNull(store, "store"));
|
||||
audit = snapshot(Objects.requireNonNull(audit, "audit"));
|
||||
signing = Objects.requireNonNull(signing, "signing").map(SigningConfiguration::snapshot);
|
||||
publishers = Objects.requireNonNull(publishers, "publishers").stream()
|
||||
.map(PkiSessionConfiguration::snapshot).toList();
|
||||
if (version == 1 && (signing.isPresent() || !publishers.isEmpty())) {
|
||||
throw new IllegalArgumentException("Version-one configuration cannot enable optional capabilities");
|
||||
}
|
||||
}
|
||||
|
||||
private static ProviderConfig snapshot(ProviderConfig config) {
|
||||
return new ProviderConfig(config.backendId(), config.properties());
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicit signing and X.509 service-graph configuration.
|
||||
*
|
||||
* @param workflow signature-workflow provider configuration
|
||||
* @param framework credential-framework provider configuration
|
||||
* @param busPath durable signing-bus path
|
||||
* @param signatureAlgorithm canonical configured signing algorithm
|
||||
* @param signingTtl positive synchronous signing deadline
|
||||
* @param unlockEnvironmentVariable optional environment-variable reference used
|
||||
* only by the default process composition
|
||||
*/
|
||||
public record SigningConfiguration(ProviderConfig workflow, ProviderConfig framework, String busPath,
|
||||
String signatureAlgorithm, Duration signingTtl, Optional<String> unlockEnvironmentVariable) {
|
||||
|
||||
/** Validates and snapshots the signing configuration. */
|
||||
public SigningConfiguration {
|
||||
workflow = PkiSessionConfiguration.snapshot(Objects.requireNonNull(workflow, "workflow"));
|
||||
framework = PkiSessionConfiguration.snapshot(Objects.requireNonNull(framework, "framework"));
|
||||
if (busPath == null || busPath.isBlank() || signatureAlgorithm == null || signatureAlgorithm.isBlank()) {
|
||||
throw new IllegalArgumentException("Signing paths and algorithm must not be blank");
|
||||
}
|
||||
if (signingTtl == null || signingTtl.isZero() || signingTtl.isNegative()) {
|
||||
throw new IllegalArgumentException("signingTtl must be positive");
|
||||
}
|
||||
unlockEnvironmentVariable = Objects.requireNonNull(unlockEnvironmentVariable,
|
||||
"unlockEnvironmentVariable");
|
||||
unlockEnvironmentVariable.ifPresent(value -> {
|
||||
if (!value.matches("[A-Z][A-Z0-9_]{0,127}")) {
|
||||
throw new IllegalArgumentException("Unlock environment-variable reference is invalid");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static SigningConfiguration snapshot(SigningConfiguration source) {
|
||||
return new SigningConfiguration(source.workflow, source.framework, source.busPath,
|
||||
source.signatureAlgorithm, source.signingTtl, source.unlockEnvironmentVariable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*******************************************************************************
|
||||
* 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.core.spi.KeyringUnlockProvider;
|
||||
|
||||
/** Immutable process-local capabilities for one PKI session composition. */
|
||||
public record PkiSessionRuntimeDependencies(Optional<KeyringUnlockProvider> keyringUnlockProvider) {
|
||||
|
||||
/** Validates the optional capability container. */
|
||||
public PkiSessionRuntimeDependencies {
|
||||
keyringUnlockProvider = Objects.requireNonNull(keyringUnlockProvider, "keyringUnlockProvider");
|
||||
}
|
||||
|
||||
/** @return an immutable dependency set without key-unlock access */
|
||||
public static PkiSessionRuntimeDependencies none() {
|
||||
return new PkiSessionRuntimeDependencies(Optional.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates dependencies containing one explicit process-local unlock provider.
|
||||
*
|
||||
* @param provider secure unlock capability
|
||||
* @return immutable runtime dependencies
|
||||
*/
|
||||
public static PkiSessionRuntimeDependencies withKeyringUnlockProvider(KeyringUnlockProvider provider) {
|
||||
return new PkiSessionRuntimeDependencies(Optional.of(Objects.requireNonNull(provider, "provider")));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,36 @@
|
||||
/*******************************************************************************
|
||||
* 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.impl.core;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -60,6 +60,7 @@ import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
@@ -97,9 +98,11 @@ import zeroecho.core.storage.KeyringStore;
|
||||
import zeroecho.pki.api.EncodedObject;
|
||||
import zeroecho.pki.api.Encoding;
|
||||
import zeroecho.pki.api.KeyRef;
|
||||
import zeroecho.pki.api.PkiException;
|
||||
import zeroecho.pki.api.PkiId;
|
||||
import zeroecho.pki.api.orch.SigningSubmissionId;
|
||||
import zeroecho.pki.spi.crypto.SignatureWorkflow;
|
||||
import zeroecho.pki.spi.crypto.PublicKeyInfoSource;
|
||||
import zeroecho.sdk.ZeroEchoSession;
|
||||
|
||||
/**
|
||||
@@ -238,7 +241,7 @@ import zeroecho.sdk.ZeroEchoSession;
|
||||
*/
|
||||
// The provider deliberately centralizes operation lifecycle and cleanup in one implementation.
|
||||
@SuppressWarnings({ "PMD.CouplingBetweenObjects", "PMD.CyclomaticComplexity", "PMD.TooManyMethods" })
|
||||
public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
|
||||
public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow, PublicKeyInfoSource {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(ZeroEchoLibSignatureWorkflow.class.getName());
|
||||
|
||||
@@ -306,7 +309,7 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
|
||||
KeyringUnlockProvider keyringUnlockProvider, KeyringStore keyring) {
|
||||
this(id, keyringPath, operationRoot, clock, operationHorizon, keyRefPrefix, requireComponentSuffix,
|
||||
keyringUnlockProvider);
|
||||
this.keyringOrNull = java.util.Objects.requireNonNull(keyring, "keyring must not be null");
|
||||
this.keyringOrNull = Objects.requireNonNull(keyring, "keyring must not be null");
|
||||
}
|
||||
|
||||
/* default */ ZeroEchoLibSignatureWorkflow(String id, Path keyringPath, Path operationRoot, Clock clock,
|
||||
@@ -362,7 +365,7 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
|
||||
this.requireComponentSuffix = requireComponentSuffix;
|
||||
this.cleanupObserver = cleanupObserver;
|
||||
this.keyringUnlockProvider = keyringUnlockProvider;
|
||||
this.session = java.util.Objects.requireNonNull(session, "session must not be null");
|
||||
this.session = Objects.requireNonNull(session, "session must not be null");
|
||||
|
||||
this.statuses = new ConcurrentHashMap<>();
|
||||
this.fingerprints = new ConcurrentHashMap<>();
|
||||
@@ -1004,6 +1007,21 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
|
||||
throw new InvalidRequestException(DC_UNSUPPORTED_PUBLICKEY_FORM);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EncodedObject resolvePublicKeyInfo(KeyRef keyRef) {
|
||||
Objects.requireNonNull(keyRef, "keyRef");
|
||||
try {
|
||||
KeyRefParts parts = parseKeyRefOrThrow(keyRef, true);
|
||||
String privateAlias = parts.privateAlias;
|
||||
String logicalAlias = privateAlias.endsWith(".prv")
|
||||
? privateAlias.substring(0, privateAlias.length() - ".prv".length()) : privateAlias;
|
||||
PublicKey publicKey = requireKeyringOrThrow().getPublic(logicalAlias);
|
||||
return new EncodedObject(Encoding.DER, publicKey.getEncoded());
|
||||
} catch (InvalidRequestException | IOException | GeneralSecurityException failure) {
|
||||
throw new PkiException("Managed public key resolution failed: code=PUBLIC_KEY_RESOLUTION_FAILED", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private <S extends AlgorithmKeySpec> PublicKey importPublic(String algorithmId, S spec)
|
||||
throws GeneralSecurityException {
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -1642,8 +1660,8 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
|
||||
/** Package-local dependencies used to exercise provider failure boundaries. */
|
||||
/* default */ record SigningDependencies(KeyringStore keyring, ZeroEchoSession session) {
|
||||
SigningDependencies {
|
||||
java.util.Objects.requireNonNull(keyring, "keyring must not be null");
|
||||
java.util.Objects.requireNonNull(session, "session must not be null");
|
||||
Objects.requireNonNull(keyring, "keyring must not be null");
|
||||
Objects.requireNonNull(session, "session must not be null");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -303,7 +303,7 @@ public final class FilesystemPkiStore implements PkiStore, Closeable {
|
||||
}
|
||||
}
|
||||
|
||||
LOG.log(Level.INFO, "running in {0}", root);
|
||||
LOG.log(Level.INFO, "Filesystem PKI store opened");
|
||||
this.ownership = acquiredOwnership;
|
||||
ownershipTransferred = true;
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
/*******************************************************************************
|
||||
* 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.impl.publish;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import zeroecho.core.io.RepeatableContent;
|
||||
import zeroecho.pki.api.publication.PublicationTarget;
|
||||
import zeroecho.pki.spi.publish.PublicationAttempt;
|
||||
import zeroecho.pki.spi.publish.PublicationOutcome;
|
||||
import zeroecho.pki.spi.publish.Publisher;
|
||||
|
||||
/** Bounded-memory, idempotent publisher for one administrator-owned directory. */
|
||||
final class FilesystemPublisher implements Publisher {
|
||||
private static final int BUFFER_BYTES = 16 * 1024;
|
||||
|
||||
private final PublicationTarget target;
|
||||
private final Path root;
|
||||
|
||||
/* default */ FilesystemPublisher(PublicationTarget target, Path root) {
|
||||
this.target = Objects.requireNonNull(target, "target");
|
||||
this.root = Objects.requireNonNull(root, "root").toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicationTarget target() {
|
||||
return target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicationOutcome publish(PublicationAttempt attempt, RepeatableContent payload) {
|
||||
Objects.requireNonNull(attempt, "attempt");
|
||||
Objects.requireNonNull(payload, "payload");
|
||||
Path destination = destination(attempt);
|
||||
Path temporary = destination.resolveSibling(destination.getFileName() + ".tmp-" + attempt.attemptToken());
|
||||
try {
|
||||
Files.createDirectories(root);
|
||||
if (Files.exists(destination)) {
|
||||
return matches(destination, attempt) ? PublicationOutcome.SUCCESS : PublicationOutcome.TERMINAL_FAILURE;
|
||||
}
|
||||
writeValidated(temporary, payload, attempt);
|
||||
try {
|
||||
Files.move(temporary, destination, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException failure) {
|
||||
return PublicationOutcome.RETRYABLE_FAILURE;
|
||||
}
|
||||
forceDirectory();
|
||||
return PublicationOutcome.SUCCESS;
|
||||
} catch (IOException failure) {
|
||||
return Files.exists(destination) && matchesUnchecked(destination, attempt) ? PublicationOutcome.SUCCESS
|
||||
: PublicationOutcome.RETRYABLE_FAILURE;
|
||||
} finally {
|
||||
try {
|
||||
Files.deleteIfExists(temporary);
|
||||
} catch (IOException ignored) {
|
||||
// Definite publication outcome is not changed by temporary cleanup.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<PublicationOutcome> reconcile(PublicationAttempt attempt) {
|
||||
Objects.requireNonNull(attempt, "attempt");
|
||||
Path destination = destination(attempt);
|
||||
if (!Files.exists(destination)) {
|
||||
return Optional.of(PublicationOutcome.RETRYABLE_FAILURE);
|
||||
}
|
||||
return Optional.of(matchesUnchecked(destination, attempt) ? PublicationOutcome.SUCCESS
|
||||
: PublicationOutcome.TERMINAL_FAILURE);
|
||||
}
|
||||
|
||||
private Path destination(PublicationAttempt attempt) {
|
||||
byte[] identity = attempt.publicationId().value().getBytes(StandardCharsets.UTF_8);
|
||||
return root.resolve(HexFormat.of().formatHex(sha256().digest(identity)) + ".bin");
|
||||
}
|
||||
|
||||
private static void writeValidated(Path path, RepeatableContent payload, PublicationAttempt attempt)
|
||||
throws IOException {
|
||||
MessageDigest digest = sha256();
|
||||
long count = 0L;
|
||||
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_BYTES);
|
||||
try (InputStream input = payload.openStream(); FileChannel output = FileChannel.open(path,
|
||||
StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
||||
byte[] bytes = buffer.array();
|
||||
int read;
|
||||
while ((read = input.read(bytes)) >= 0) {
|
||||
if (read == 0) {
|
||||
continue;
|
||||
}
|
||||
count = Math.addExact(count, read);
|
||||
digest.update(bytes, 0, read);
|
||||
buffer.clear().limit(read);
|
||||
while (buffer.hasRemaining()) {
|
||||
output.write(buffer);
|
||||
}
|
||||
}
|
||||
output.force(true);
|
||||
}
|
||||
String actual = HexFormat.of().formatHex(digest.digest());
|
||||
if (count != attempt.length() || !actual.equals(attempt.sha256())) {
|
||||
throw new IOException("Publication content commitment mismatch");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean matches(Path path, PublicationAttempt attempt) throws IOException {
|
||||
if (Files.size(path) != attempt.length()) {
|
||||
return false;
|
||||
}
|
||||
MessageDigest digest = sha256();
|
||||
byte[] buffer = new byte[BUFFER_BYTES];
|
||||
try (InputStream input = Files.newInputStream(path)) {
|
||||
int read;
|
||||
while ((read = input.read(buffer)) >= 0) {
|
||||
if (read > 0) {
|
||||
digest.update(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
}
|
||||
return HexFormat.of().formatHex(digest.digest()).equals(attempt.sha256());
|
||||
}
|
||||
|
||||
private static boolean matchesUnchecked(Path path, PublicationAttempt attempt) {
|
||||
try {
|
||||
return matches(path, attempt);
|
||||
} catch (IOException failure) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void forceDirectory() throws IOException {
|
||||
try (FileChannel directory = FileChannel.open(root, StandardOpenOption.READ)) {
|
||||
directory.force(true);
|
||||
}
|
||||
}
|
||||
|
||||
private static MessageDigest sha256() {
|
||||
try {
|
||||
return MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException failure) {
|
||||
throw new IllegalStateException("SHA-256 is unavailable", failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*******************************************************************************
|
||||
* 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.impl.publish;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
import zeroecho.pki.api.publication.PublicationTarget;
|
||||
import zeroecho.pki.api.publication.PublicationTargetType;
|
||||
import zeroecho.pki.spi.ProviderConfig;
|
||||
import zeroecho.pki.spi.publish.Publisher;
|
||||
import zeroecho.pki.spi.publish.PublisherProvider;
|
||||
|
||||
/** Explicit filesystem publication-destination provider. */
|
||||
public final class FilesystemPublisherProvider implements PublisherProvider {
|
||||
private static final String ROOT = "root";
|
||||
private static final String TARGET_ID = "targetId";
|
||||
|
||||
@Override
|
||||
public String id() {
|
||||
return "filesystem";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> supportedKeys() {
|
||||
return Set.of(ROOT, TARGET_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateConfig(ProviderConfig config) {
|
||||
PublisherProvider.super.validateConfig(config);
|
||||
Path.of(config.require(ROOT));
|
||||
new PublicationTarget(PublicationTargetType.FILESYSTEM, config.require(TARGET_ID));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Publisher allocate(ProviderConfig config) {
|
||||
validateConfig(config);
|
||||
PublicationTarget target = new PublicationTarget(PublicationTargetType.FILESYSTEM,
|
||||
config.require(TARGET_ID));
|
||||
return new FilesystemPublisher(target, Path.of(config.require(ROOT)));
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,8 @@ import zeroecho.pki.spi.crypto.SignatureWorkflowProvider;
|
||||
import zeroecho.pki.spi.crypto.SignatureWorkflowRuntimeDependencies;
|
||||
import zeroecho.pki.spi.framework.CredentialFramework;
|
||||
import zeroecho.pki.spi.framework.CredentialFrameworkProvider;
|
||||
import zeroecho.pki.spi.publish.Publisher;
|
||||
import zeroecho.pki.spi.publish.PublisherProvider;
|
||||
import zeroecho.pki.spi.store.PkiStore;
|
||||
import zeroecho.pki.spi.store.PkiStoreProvider;
|
||||
import zeroecho.pki.util.async.AsyncBus;
|
||||
@@ -286,6 +288,38 @@ public final class PkiBootstrap {
|
||||
return provider.allocate(config, dependencies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates one explicit signature-workflow provider configuration without
|
||||
* allocating key access or workflow resources.
|
||||
*
|
||||
* @param config immutable workflow-provider configuration
|
||||
* @throws RuntimeException if the provider identity, property set, or
|
||||
* provider-specific values are invalid
|
||||
*/
|
||||
public static void validateSignatureWorkflowConfiguration(ProviderConfig config) {
|
||||
SignatureWorkflowProvider provider = selectSignatureWorkflowProvider(
|
||||
Objects.requireNonNull(config, "config").backendId());
|
||||
requireKnownKeys(provider, config);
|
||||
provider.validateConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens one explicitly configured signature workflow with process-local key
|
||||
* access supplied outside persistent configuration.
|
||||
*
|
||||
* @param config immutable workflow-provider configuration
|
||||
* @param dependencies process-local security capabilities
|
||||
* @return opened workflow owned by the caller
|
||||
* @throws IllegalArgumentException if configuration or capabilities are invalid
|
||||
* @throws RuntimeException if secure workflow allocation fails
|
||||
*/
|
||||
public static SignatureWorkflow openConfiguredSignatureWorkflow(ProviderConfig config,
|
||||
SignatureWorkflowRuntimeDependencies dependencies) {
|
||||
validateSignatureWorkflowConfiguration(config);
|
||||
return selectSignatureWorkflowProvider(config.backendId()).allocate(config,
|
||||
Objects.requireNonNull(dependencies, "dependencies"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an async operation bus using {@link AsyncBusProvider} discovered via
|
||||
* ServiceLoader.
|
||||
@@ -380,6 +414,44 @@ public final class PkiBootstrap {
|
||||
return provider.allocate(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates one explicit credential-framework provider configuration without
|
||||
* constructing a framework instance.
|
||||
*
|
||||
* @param config immutable framework-provider configuration
|
||||
* @throws RuntimeException if provider selection or configuration is invalid
|
||||
*/
|
||||
public static void validateCredentialFrameworkConfiguration(ProviderConfig config) {
|
||||
CredentialFrameworkProvider provider = selectCredentialFrameworkProvider(
|
||||
Objects.requireNonNull(config, "config").backendId());
|
||||
requireKnownKeys(provider, config);
|
||||
provider.validateConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates one explicitly enabled publication destination without opening it.
|
||||
*
|
||||
* @param config immutable publisher-provider configuration
|
||||
* @throws RuntimeException if provider selection or configuration is invalid
|
||||
*/
|
||||
public static void validatePublisherConfiguration(ProviderConfig config) {
|
||||
PublisherProvider provider = selectPublisherProvider(Objects.requireNonNull(config, "config").backendId());
|
||||
requireKnownKeys(provider, config);
|
||||
provider.validateConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens one explicitly configured publication destination.
|
||||
*
|
||||
* @param config immutable publisher-provider configuration
|
||||
* @return configured publisher selected by stable provider identity
|
||||
* @throws RuntimeException if provider selection, configuration, or allocation fails
|
||||
*/
|
||||
public static Publisher openPublisher(ProviderConfig config) {
|
||||
validatePublisherConfiguration(config);
|
||||
return selectPublisherProvider(config.backendId()).allocate(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs provider help information (supported keys) for diagnostics.
|
||||
*
|
||||
@@ -414,6 +486,33 @@ public final class PkiBootstrap {
|
||||
});
|
||||
}
|
||||
|
||||
private static SignatureWorkflowProvider selectSignatureWorkflowProvider(String requestedId) {
|
||||
return SpiSelector.select(SignatureWorkflowProvider.class, requestedId, new SpiSelector.ProviderId<>() {
|
||||
@Override
|
||||
public String id(SignatureWorkflowProvider provider) {
|
||||
return provider.id();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static CredentialFrameworkProvider selectCredentialFrameworkProvider(String requestedId) {
|
||||
return SpiSelector.select(CredentialFrameworkProvider.class, requestedId, new SpiSelector.ProviderId<>() {
|
||||
@Override
|
||||
public String id(CredentialFrameworkProvider provider) {
|
||||
return provider.id();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static PublisherProvider selectPublisherProvider(String requestedId) {
|
||||
return SpiSelector.select(PublisherProvider.class, requestedId, new SpiSelector.ProviderId<>() {
|
||||
@Override
|
||||
public String id(PublisherProvider provider) {
|
||||
return provider.id();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void requireKnownKeys(ConfigurableProvider<?> provider, ProviderConfig config) {
|
||||
for (String key : config.properties().keySet()) {
|
||||
if (!provider.supportedKeys().contains(key)) {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*******************************************************************************
|
||||
* 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.spi.crypto;
|
||||
|
||||
import zeroecho.pki.api.EncodedObject;
|
||||
import zeroecho.pki.api.KeyRef;
|
||||
|
||||
/**
|
||||
* Resolves exportable public-key information from the same managed-key authority
|
||||
* used by a signature workflow.
|
||||
*
|
||||
* <p>This capability exposes only canonical DER SubjectPublicKeyInfo. It must
|
||||
* never return private, secret, seed, unlock, or provider credential material.</p>
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface PublicKeyInfoSource {
|
||||
|
||||
/**
|
||||
* Resolves one managed key reference to canonical DER SubjectPublicKeyInfo.
|
||||
*
|
||||
* @param keyRef managed signing or public-key reference
|
||||
* @return immutable DER-encoded public-key information
|
||||
* @throws IllegalArgumentException if the reference is malformed or unknown
|
||||
* @throws RuntimeException if secure key-store access fails
|
||||
*/
|
||||
EncodedObject resolvePublicKeyInfo(KeyRef keyRef);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*******************************************************************************
|
||||
* 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.spi.publish;
|
||||
|
||||
import zeroecho.pki.spi.ConfigurableProvider;
|
||||
|
||||
/** Service-provider contract for explicitly configured publication destinations. */
|
||||
public interface PublisherProvider extends ConfigurableProvider<Publisher> {
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
zeroecho.pki.impl.publish.FilesystemPublisherProvider
|
||||
@@ -49,8 +49,19 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import zeroecho.core.io.CancellationSignal;
|
||||
import zeroecho.pki.api.PkiId;
|
||||
import zeroecho.pki.api.FormatId;
|
||||
import zeroecho.pki.api.KeyRef;
|
||||
import zeroecho.pki.api.ProfileService;
|
||||
import zeroecho.pki.api.PublicationService;
|
||||
import zeroecho.pki.api.RevocationService;
|
||||
import zeroecho.pki.api.SubjectRef;
|
||||
import zeroecho.pki.api.profile.BuiltInCertificateProfileCatalog;
|
||||
import zeroecho.pki.api.publication.PublicationCursor;
|
||||
import zeroecho.pki.api.publication.PublicationQuery;
|
||||
import zeroecho.pki.api.publication.PublicationRecord;
|
||||
import zeroecho.pki.api.publication.PublicationRequest;
|
||||
import zeroecho.pki.api.publication.PublicationResult;
|
||||
import zeroecho.pki.api.publication.PublicationStatus;
|
||||
import zeroecho.pki.api.revocation.RevocationCommand;
|
||||
import zeroecho.pki.api.revocation.RevocationQuery;
|
||||
import zeroecho.pki.api.revocation.RevocationReason;
|
||||
@@ -120,6 +131,36 @@ class PkiOperationExecutorTest {
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsSigningDependentOperationWhenCapabilityIsNotConfigured() {
|
||||
System.out.println("rejectsSigningDependentOperationWhenCapabilityIsNotConfigured");
|
||||
DefaultPkiOperationExecutor executor = executor(new AtomicInteger());
|
||||
PkiOperation operation = new PkiOperation.CreateAuthority(new FormatId("x509"),
|
||||
new SubjectRef("CN=Unavailable"), "root-ca", new KeyRef("managed:root.prv"));
|
||||
PkiOperationOutcome.Failure failure = (PkiOperationOutcome.Failure) executor.execute(operation,
|
||||
CancellationSignal.NONE);
|
||||
System.out.println("...classification=" + failure.classification());
|
||||
assertEquals(PkiOperationFailure.VALIDATION_FAILURE, failure.classification());
|
||||
assertEquals("CAPABILITY_NOT_CONFIGURED", failure.code());
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void preservesExplicitPublicationResultsAndUnknownOutcome() {
|
||||
System.out.println("preservesExplicitPublicationResultsAndUnknownOutcome");
|
||||
PkiId publicationId = new PkiId("publication:test");
|
||||
DefaultPkiOperationExecutor executor = publicationExecutor();
|
||||
PkiOperationOutcome processed = executor.execute(new PkiOperation.ProcessPublication(publicationId),
|
||||
CancellationSignal.NONE);
|
||||
PkiOperationOutcome reconciled = executor.execute(new PkiOperation.ReconcilePublication(publicationId),
|
||||
CancellationSignal.NONE);
|
||||
System.out.println("...process=" + processed.getClass().getSimpleName());
|
||||
assertInstanceOf(PkiOperationOutcome.Success.class, processed);
|
||||
assertEquals(PkiOperationFailure.EXTERNAL_OUTCOME_UNKNOWN,
|
||||
((PkiOperationOutcome.Failure) reconciled).classification());
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
private static DefaultPkiOperationExecutor executor(AtomicInteger mutations) {
|
||||
PkiSessionConfiguration configuration = new PkiSessionConfiguration(1,
|
||||
new ProviderConfig("fs", Map.of("root", "unused")), new ProviderConfig("memory", Map.of()));
|
||||
@@ -129,7 +170,64 @@ class PkiOperationExecutorTest {
|
||||
case "close" -> null;
|
||||
default -> throw new UnsupportedOperationException(method.getName());
|
||||
});
|
||||
return new DefaultPkiOperationExecutor(configuration, store, revocations(mutations), () -> {
|
||||
ProfileService profiles = (ProfileService) Proxy.newProxyInstance(ProfileService.class.getClassLoader(),
|
||||
new Class<?>[] { ProfileService.class }, (proxy, method, arguments) -> {
|
||||
throw new UnsupportedOperationException(method.getName());
|
||||
});
|
||||
return new DefaultPkiOperationExecutor(configuration, store, profiles, revocations(mutations), () -> {
|
||||
});
|
||||
}
|
||||
|
||||
private static DefaultPkiOperationExecutor publicationExecutor() {
|
||||
PkiSessionConfiguration configuration = new PkiSessionConfiguration(1,
|
||||
new ProviderConfig("fs", Map.of("root", "unused")), new ProviderConfig("memory", Map.of()));
|
||||
PkiStore store = (PkiStore) Proxy.newProxyInstance(PkiStore.class.getClassLoader(),
|
||||
new Class<?>[] { PkiStore.class }, (proxy, method, arguments) -> {
|
||||
throw new UnsupportedOperationException(method.getName());
|
||||
});
|
||||
ProfileService profiles = (ProfileService) Proxy.newProxyInstance(ProfileService.class.getClassLoader(),
|
||||
new Class<?>[] { ProfileService.class }, (proxy, method, arguments) -> {
|
||||
throw new UnsupportedOperationException(method.getName());
|
||||
});
|
||||
PublicationService publications = new PublicationService() {
|
||||
@Override
|
||||
public PublicationRecord register(PublicationRequest request) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicationResult process(PkiId selected) {
|
||||
return new PublicationResult(selected, PublicationStatus.SUCCEEDED, 1L);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicationResult retry(PkiId selected) {
|
||||
return new PublicationResult(selected, PublicationStatus.SUCCEEDED, 2L);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicationResult reconcile(PkiId selected) {
|
||||
return new PublicationResult(selected, PublicationStatus.OUTCOME_UNKNOWN, 1L);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<PublicationRecord> find(PkiId selected) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicationCursor openPublications(PublicationQuery query) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
};
|
||||
return new DefaultPkiOperationExecutor(configuration, store, profiles, revocations(new AtomicInteger()),
|
||||
Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(publications),
|
||||
() -> {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -36,23 +36,38 @@ package zeroecho.pki.application;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import zeroecho.core.io.CancellationSignal;
|
||||
import zeroecho.core.storage.KeyringPassword;
|
||||
import zeroecho.pki.api.KeyRef;
|
||||
import zeroecho.pki.api.FormatId;
|
||||
import zeroecho.pki.api.SubjectRef;
|
||||
import zeroecho.pki.api.profile.BuiltInCertificateProfileCatalog;
|
||||
import zeroecho.pki.api.profile.BuiltInCertificateProfileTemplate;
|
||||
import zeroecho.pki.spi.bootstrap.PkiBootstrap;
|
||||
import zeroecho.pki.spi.crypto.SignatureWorkflow;
|
||||
import zeroecho.pki.spi.crypto.SignatureWorkflowRuntimeDependencies;
|
||||
import zeroecho.pki.spi.ProviderConfig;
|
||||
import zeroecho.pki.spi.audit.AuditSink;
|
||||
import zeroecho.pki.spi.store.PkiStore;
|
||||
import zeroecho.pki.testkit.InMemorySignatureWorkflow;
|
||||
|
||||
class PkiSessionLifecycleTest {
|
||||
|
||||
@@ -116,6 +131,99 @@ class PkiSessionLifecycleTest {
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void configuredSigningRequiresExplicitUnlockAndCleansAllocatedResources() {
|
||||
System.out.println("configuredSigningRequiresExplicitUnlockAndCleansAllocatedResources");
|
||||
AtomicInteger storesOpened = new AtomicInteger();
|
||||
AtomicInteger storesClosed = new AtomicInteger();
|
||||
DefaultPkiSession.Bootstrap bootstrap = bootstrap(storesOpened, storesClosed, false, false, false, false);
|
||||
ProviderConfig placeholder = new ProviderConfig("test", Map.of());
|
||||
PkiSessionConfiguration.SigningConfiguration signing = new PkiSessionConfiguration.SigningConfiguration(
|
||||
placeholder, new ProviderConfig("x509-bc", Map.of()),
|
||||
temporaryDirectory.resolve("signing-bus").toString(), "RSA-2048-PSS-SHA256", Duration.ofSeconds(5),
|
||||
Optional.empty());
|
||||
PkiSessionConfiguration configuration = new PkiSessionConfiguration(2,
|
||||
new ProviderConfig("fs", Map.of("root", temporaryDirectory.resolve("store-signing").toString())),
|
||||
new ProviderConfig("memory", Map.of("size", "16")), Optional.of(signing), java.util.List.of());
|
||||
IllegalStateException failure = assertThrows(IllegalStateException.class,
|
||||
() -> DefaultPkiSession.open(configuration, PkiSessionRuntimeDependencies.none(), CLOCK, bootstrap));
|
||||
System.out.println("...storesOpened=" + storesOpened.get() + ", storesClosed=" + storesClosed.get());
|
||||
assertEquals("Configured signing requires key-unlock capability", failure.getMessage());
|
||||
assertEquals(1, storesOpened.get());
|
||||
assertEquals(1, storesClosed.get());
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void opensExplicitPublisherCapabilityWithoutSigning() throws Exception {
|
||||
System.out.println("opensExplicitPublisherCapabilityWithoutSigning");
|
||||
ProviderConfig publisher = new ProviderConfig("filesystem",
|
||||
Map.of("root", temporaryDirectory.resolve("published").toString(), "targetId", "local-crls"));
|
||||
PkiSessionConfiguration configuration = new PkiSessionConfiguration(2,
|
||||
new ProviderConfig("fs", Map.of("root", temporaryDirectory.resolve("store-publisher").toString())),
|
||||
new ProviderConfig("memory", Map.of("size", "16")), Optional.empty(), java.util.List.of(publisher));
|
||||
try (PkiSession session = PkiSession.open(configuration)) {
|
||||
System.out.println("...publicationConfigured=" + session.publications().isPresent());
|
||||
assertTrue(session.publications().isPresent());
|
||||
assertTrue(session.authorities().isEmpty());
|
||||
}
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnknownPublisherBeforeStoreAllocation() {
|
||||
System.out.println("rejectsUnknownPublisherBeforeStoreAllocation");
|
||||
Path storeRoot = temporaryDirectory.resolve("must-not-open");
|
||||
PkiSessionConfiguration configuration = new PkiSessionConfiguration(2,
|
||||
new ProviderConfig("fs", Map.of("root", storeRoot.toString())),
|
||||
new ProviderConfig("memory", Map.of("size", "16")), Optional.empty(),
|
||||
java.util.List.of(new ProviderConfig("unknown-publisher", Map.of())));
|
||||
assertThrows(IllegalStateException.class, () -> PkiSession.open(configuration));
|
||||
System.out.println("...storeAllocated=" + java.nio.file.Files.exists(storeRoot));
|
||||
assertTrue(java.nio.file.Files.notExists(storeRoot));
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void composesOneSharedSigningAndX509ServiceGraph() throws Exception {
|
||||
System.out.println("composesOneSharedSigningAndX509ServiceGraph");
|
||||
KeyRef keyRef = new KeyRef("managed:test-root");
|
||||
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
|
||||
generator.initialize(2048);
|
||||
KeyPair keyPair = generator.generateKeyPair();
|
||||
InMemorySignatureWorkflow workflow = new InMemorySignatureWorkflow(Map.of(keyRef.value(), keyPair));
|
||||
ProviderConfig placeholder = new ProviderConfig("test", Map.of());
|
||||
PkiSessionConfiguration.SigningConfiguration signing = new PkiSessionConfiguration.SigningConfiguration(
|
||||
placeholder, new ProviderConfig("x509-bc", Map.of()),
|
||||
temporaryDirectory.resolve("composed-signing-bus").toString(), "SHA256withRSA",
|
||||
Duration.ofSeconds(5), Optional.empty());
|
||||
PkiSessionConfiguration configuration = new PkiSessionConfiguration(2,
|
||||
new ProviderConfig("fs", Map.of("root", temporaryDirectory.resolve("composed-store").toString())),
|
||||
new ProviderConfig("memory", Map.of("size", "16")), Optional.of(signing), java.util.List.of());
|
||||
DefaultPkiSession.Bootstrap bootstrap = signingBootstrap(workflow);
|
||||
PkiSessionRuntimeDependencies dependencies = PkiSessionRuntimeDependencies.withKeyringUnlockProvider(
|
||||
() -> new KeyringPassword("unused-test-unlock".toCharArray()));
|
||||
try (PkiSession session = DefaultPkiSession.open(configuration, dependencies, CLOCK, bootstrap)) {
|
||||
System.out.println("...services=ca,request,issuance,status");
|
||||
assertTrue(session.authorities().isPresent());
|
||||
assertTrue(session.requests().isPresent());
|
||||
assertTrue(session.issuance().isPresent());
|
||||
assertTrue(session.statusObjects().isPresent());
|
||||
assertTrue(session.publications().isEmpty());
|
||||
BuiltInCertificateProfileTemplate root = BuiltInCertificateProfileCatalog
|
||||
.load(getClass().getClassLoader()).stream()
|
||||
.filter(candidate -> "root-ca".equals(candidate.definition().profileId())).findFirst()
|
||||
.orElseThrow();
|
||||
session.profiles().importProfile(root.canonicalJson());
|
||||
session.profiles().activateProfile("root-ca", root.definition().profileVersion());
|
||||
PkiOperationOutcome outcome = session.operations().execute(new PkiOperation.CreateAuthority(
|
||||
new FormatId("x509"), new SubjectRef("CN=Composed Root"), "root-ca", keyRef),
|
||||
CancellationSignal.NONE);
|
||||
assertInstanceOf(PkiOperationOutcome.Success.class, outcome);
|
||||
}
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
private static PkiSessionConfiguration configuration(Path storeRoot) {
|
||||
return new PkiSessionConfiguration(1, new ProviderConfig("fs", Map.of("root", storeRoot.toString())),
|
||||
new ProviderConfig("memory", Map.of("size", "16")));
|
||||
@@ -136,6 +244,16 @@ class PkiSessionLifecycleTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateSignatureWorkflow(ProviderConfig configuration) {
|
||||
// valid test provider
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateCredentialFramework(ProviderConfig configuration) {
|
||||
// valid test framework
|
||||
}
|
||||
|
||||
@Override
|
||||
public PkiStore openStore(ProviderConfig configuration) {
|
||||
storesOpened.incrementAndGet();
|
||||
@@ -169,6 +287,46 @@ class PkiSessionLifecycleTest {
|
||||
};
|
||||
}
|
||||
|
||||
private static DefaultPkiSession.Bootstrap signingBootstrap(SignatureWorkflow workflow) {
|
||||
return new DefaultPkiSession.Bootstrap() {
|
||||
@Override
|
||||
public void validateStore(ProviderConfig configuration) {
|
||||
PkiBootstrap.validateStoreConfiguration(configuration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateAudit(ProviderConfig configuration) {
|
||||
PkiBootstrap.validateAuditConfiguration(configuration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PkiStore openStore(ProviderConfig configuration) {
|
||||
return PkiBootstrap.openStore(configuration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuditSink openAudit(ProviderConfig configuration) {
|
||||
return PkiBootstrap.openAudit(configuration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateSignatureWorkflow(ProviderConfig configuration) {
|
||||
// Explicit deterministic test provider.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateCredentialFramework(ProviderConfig configuration) {
|
||||
PkiBootstrap.validateCredentialFrameworkConfiguration(configuration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignatureWorkflow openSignatureWorkflow(ProviderConfig configuration,
|
||||
SignatureWorkflowRuntimeDependencies dependencies) {
|
||||
return workflow;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static PkiStore proxyStore(CloseAction close) {
|
||||
return (PkiStore) Proxy.newProxyInstance(PkiStore.class.getClassLoader(), new Class<?>[] { PkiStore.class },
|
||||
(proxy, method, arguments) -> {
|
||||
|
||||
@@ -48,12 +48,13 @@ import zeroecho.pki.api.EncodedObject;
|
||||
import zeroecho.pki.api.Encoding;
|
||||
import zeroecho.pki.api.KeyRef;
|
||||
import zeroecho.pki.api.PkiId;
|
||||
import zeroecho.pki.spi.crypto.PublicKeyInfoSource;
|
||||
import zeroecho.pki.spi.crypto.SignatureWorkflow;
|
||||
|
||||
/**
|
||||
* Test-only in-memory signature workflow.
|
||||
*/
|
||||
public final class InMemorySignatureWorkflow implements SignatureWorkflow {
|
||||
public final class InMemorySignatureWorkflow implements SignatureWorkflow, PublicKeyInfoSource {
|
||||
|
||||
private final Map<String, KeyPair> keys;
|
||||
private final Map<PkiId, OperationStatus> status;
|
||||
@@ -245,6 +246,15 @@ public final class InMemorySignatureWorkflow implements SignatureWorkflow {
|
||||
return Set.of("SHA256withRSA");
|
||||
}
|
||||
|
||||
@Override
|
||||
public EncodedObject resolvePublicKeyInfo(KeyRef keyRef) {
|
||||
KeyPair keyPair = keys.get(Objects.requireNonNull(keyRef, "keyRef").value());
|
||||
if (keyPair == null) {
|
||||
throw new IllegalArgumentException("Managed test key is unavailable");
|
||||
}
|
||||
return new EncodedObject(Encoding.DER, keyPair.getPublic().getEncoded());
|
||||
}
|
||||
|
||||
public int submittedSignCount() {
|
||||
return submittedSignCount.get();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user