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

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