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:
2026-08-04 03:11:14 +02:00
parent 3de6cd7a34
commit 5c66a2b25b
25 changed files with 2561 additions and 46 deletions

View File

@@ -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();
}

View File

@@ -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,14 +61,42 @@ 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");
}
return new PkiSessionConfiguration(version, store, audit);
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) {
@@ -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();

View File

@@ -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();

View File

@@ -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"));
}

View 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"
}
}
]
}

View 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
}
}
]
}