feat(pki): add extensible X.509 algorithm bindings
Add an immutable X.509 binding registry for sealed standard mappings, versioned ZeroEcho private OIDs and explicitly enabled deployer bindings. Integrate binding commitments with profiles, issuance, verification, CRLs, PKI sessions and typed CLI operations.
This commit is contained in:
26
README.md
26
README.md
@@ -60,7 +60,8 @@ lifecycle transitions; strict CSR import and proof verification; credential
|
|||||||
issuance and inspection; revocation inspection and bounded history/snapshot
|
issuance and inspection; revocation inspection and bounded history/snapshot
|
||||||
presentation; signed status/CRL generation and inspection; and explicit
|
presentation; signed status/CRL generation and inspection; and explicit
|
||||||
publication registration, processing, retry, reconciliation, inspection, and
|
publication registration, processing, retry, reconciliation, inspection, and
|
||||||
listing. Presentation limits bound terminal output only; backend revocation and
|
listing. It also exposes immutable X.509 algorithm-binding listing, inspection,
|
||||||
|
and commitment validation. Presentation limits bound terminal output only; backend revocation and
|
||||||
publication cursors remain streaming and do not acquire an aggregate population
|
publication cursors remain streaming and do not acquire an aggregate population
|
||||||
limit.
|
limit.
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ only `FAIL_FAST` and `CONTINUE_INDEPENDENT`; they have no scripts, expressions,
|
|||||||
loops, implicit retries, or rollback across already committed operations. Binary
|
loops, implicit retries, or rollback across already committed operations. Binary
|
||||||
artifacts and secrets are never emitted in terminal JSON.
|
artifacts and secrets are never emitted in terminal JSON.
|
||||||
|
|
||||||
Version-two session configuration enables capabilities explicitly. A signing
|
Version-three session configuration enables capabilities explicitly. A signing
|
||||||
section selects one signature-workflow provider, one X.509 framework, a durable
|
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
|
signing-bus location, an algorithm, a finite signing deadline, and the name of an
|
||||||
environment variable supplying the process-local keyring unlock capability.
|
environment variable supplying the process-local keyring unlock capability.
|
||||||
@@ -80,13 +81,26 @@ private keys, and publisher credentials are never operation arguments or output.
|
|||||||
Read-only version-one configurations remain valid and allocate neither signing
|
Read-only version-one configurations remain valid and allocate neither signing
|
||||||
nor publisher capabilities.
|
nor publisher capabilities.
|
||||||
|
|
||||||
|
X.509 algorithm identity is independent of its role-specific OID representation.
|
||||||
|
Published standard bindings remain sealed. ZeroEcho private assignments are
|
||||||
|
frozen below the Egothor PEN branch `1.3.6.1.4.1.31424.1.1`; explicitly enabled
|
||||||
|
deployer providers may contribute immutable bindings only below configuration-
|
||||||
|
authorized roots, including deployment-local arcs below
|
||||||
|
`1.3.6.1.4.1.31424.1.2`. Profiles default to standard-only mode and pin both a
|
||||||
|
binding ID and its semantic commitment when private mode is deliberately chosen.
|
||||||
|
Private-OID certificates and CRLs require matching binding and cryptographic
|
||||||
|
support at every relying party; generic PKI software may reject them.
|
||||||
|
|
||||||
|
```text
|
||||||
|
zeroecho pki algorithm.binding.list --limit 100 --config pki-config.json
|
||||||
|
zeroecho pki algorithm.binding.inspect --binding-id zeroecho.private.sphincs-plus-default.certificate-signature.v1 --config pki-config.json
|
||||||
|
zeroecho pki algorithm.binding.validate --config pki-config.json
|
||||||
|
```
|
||||||
|
|
||||||
The CLI and a future server share the same transport-neutral `PkiSession` and
|
The CLI and a future server share the same transport-neutral `PkiSession` and
|
||||||
typed operations; batch execution opens that service graph once. Backup and
|
typed operations; batch execution opens that service graph once. Backup and
|
||||||
import/export commands remain unavailable because the current backend has no
|
import/export commands remain unavailable because the current backend has no
|
||||||
production service implementation for them. Final backend release readiness is
|
production service implementation for them.
|
||||||
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
|
## Development Status
|
||||||
|
|||||||
@@ -67,7 +67,10 @@ public final class PkiCli {
|
|||||||
Map.entry("--type", "type"), Map.entry("--publication-id", "publicationId"),
|
Map.entry("--type", "type"), Map.entry("--publication-id", "publicationId"),
|
||||||
Map.entry("--source-type", "sourceType"), Map.entry("--source-id", "sourceId"),
|
Map.entry("--source-type", "sourceType"), Map.entry("--source-id", "sourceId"),
|
||||||
Map.entry("--target-type", "targetType"), Map.entry("--target-id", "targetId"),
|
Map.entry("--target-type", "targetType"), Map.entry("--target-id", "targetId"),
|
||||||
Map.entry("--reason", "reason"), Map.entry("--limit", "limit"));
|
Map.entry("--reason", "reason"), Map.entry("--limit", "limit"),
|
||||||
|
Map.entry("--binding-id", "bindingId"),
|
||||||
|
Map.entry("--binding-commitment", "bindingCommitment"),
|
||||||
|
Map.entry("--origin", "origin"), Map.entry("--role", "role"));
|
||||||
private static final int RUN_TOKEN_COUNT = 2;
|
private static final int RUN_TOKEN_COUNT = 2;
|
||||||
|
|
||||||
private PkiCli() {
|
private PkiCli() {
|
||||||
@@ -263,6 +266,9 @@ public final class PkiCli {
|
|||||||
writer.println("Usage: zeroecho pki <operation> --config <file> [--output human|json] [arguments]");
|
writer.println("Usage: zeroecho pki <operation> --config <file> [--output human|json] [arguments]");
|
||||||
writer.println(" zeroecho pki run <plan-file> --config <file> [--output human|json]");
|
writer.println(" zeroecho pki run <plan-file> --config <file> [--output human|json]");
|
||||||
writer.println("Operations:");
|
writer.println("Operations:");
|
||||||
|
writer.println(" algorithm.binding.list --limit <1..1000> [--origin <origin>] [--role <role>]");
|
||||||
|
writer.println(" algorithm.binding.inspect --binding-id <id>");
|
||||||
|
writer.println(" algorithm.binding.validate [--binding-id <id> --binding-commitment <commitment>]");
|
||||||
writer.println(" configuration.validate");
|
writer.println(" configuration.validate");
|
||||||
writer.println(" profile.validate --profile-file <file>");
|
writer.println(" profile.validate --profile-file <file>");
|
||||||
writer.println(" profile.register --profile-file <file>");
|
writer.println(" profile.register --profile-file <file>");
|
||||||
|
|||||||
@@ -48,10 +48,17 @@ import zeroecho.pki.spi.ProviderConfig;
|
|||||||
final class PkiCliConfiguration {
|
final class PkiCliConfiguration {
|
||||||
|
|
||||||
private static final int VERSION_ONE = 1;
|
private static final int VERSION_ONE = 1;
|
||||||
|
private static final int VERSION_TWO = 2;
|
||||||
|
private static final int VERSION_THREE = 3;
|
||||||
private static final Set<String> ROOT_FIELDS_V1 = Set.of("version", "store", "audit");
|
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> ROOT_FIELDS_V2 = Set.of("version", "store", "audit", "signing", "publishers");
|
||||||
|
private static final Set<String> ROOT_FIELDS_V3 = Set.of("version", "store", "audit", "signing", "publishers",
|
||||||
|
"bindingProviders");
|
||||||
private static final Set<String> PROVIDER_FIELDS = Set.of("provider", "properties");
|
private static final Set<String> PROVIDER_FIELDS = Set.of("provider", "properties");
|
||||||
private static final Set<String> SIGNING_FIELDS = Set.of("workflow", "framework", "busPath",
|
private static final Set<String> SIGNING_FIELDS = Set.of("workflow", "framework", "busPath",
|
||||||
|
"signatureAlgorithm", "signingTtlSeconds", "unlockEnvironmentVariable",
|
||||||
|
"certificateSignatureBinding", "crlSignatureBinding", "subjectPublicKeyBinding");
|
||||||
|
private static final Set<String> SIGNING_REQUIRED_FIELDS = Set.of("workflow", "framework", "busPath",
|
||||||
"signatureAlgorithm", "signingTtlSeconds", "unlockEnvironmentVariable");
|
"signatureAlgorithm", "signingTtlSeconds", "unlockEnvironmentVariable");
|
||||||
private static final String STDOUT_PROVIDER = "stdout";
|
private static final String STDOUT_PROVIDER = "stdout";
|
||||||
|
|
||||||
@@ -64,8 +71,12 @@ final class PkiCliConfiguration {
|
|||||||
int version = Math.toIntExact(integer(required(root, "version")));
|
int version = Math.toIntExact(integer(required(root, "version")));
|
||||||
if (version == VERSION_ONE) {
|
if (version == VERSION_ONE) {
|
||||||
requireExactFields(root.fields(), ROOT_FIELDS_V1);
|
requireExactFields(root.fields(), ROOT_FIELDS_V1);
|
||||||
|
} else if (version == VERSION_TWO) {
|
||||||
|
requireFields(root.fields(), ROOT_FIELDS_V2);
|
||||||
|
} else if (version == VERSION_THREE) {
|
||||||
|
requireFields(root.fields(), ROOT_FIELDS_V3);
|
||||||
} else {
|
} else {
|
||||||
requireVersionTwoFields(root.fields());
|
throw new IllegalArgumentException("Unsupported CLI configuration version");
|
||||||
}
|
}
|
||||||
ProviderConfig store = provider(required(root, "store"));
|
ProviderConfig store = provider(required(root, "store"));
|
||||||
ProviderConfig audit = provider(required(root, "audit"));
|
ProviderConfig audit = provider(required(root, "audit"));
|
||||||
@@ -80,23 +91,46 @@ final class PkiCliConfiguration {
|
|||||||
PkiOperationValue publisherValue = root.fields().get("publishers");
|
PkiOperationValue publisherValue = root.fields().get("publishers");
|
||||||
List<ProviderConfig> publishers = publisherValue == null ? List.of()
|
List<ProviderConfig> publishers = publisherValue == null ? List.of()
|
||||||
: list(publisherValue).values().stream().map(PkiCliConfiguration::provider).toList();
|
: list(publisherValue).values().stream().map(PkiCliConfiguration::provider).toList();
|
||||||
return new PkiSessionConfiguration(version, store, audit, signing, publishers);
|
PkiOperationValue bindingProviderValue = root.fields().get("bindingProviders");
|
||||||
|
List<PkiSessionConfiguration.BindingProviderConfiguration> bindingProviders = bindingProviderValue == null
|
||||||
|
? List.of() : list(bindingProviderValue).values().stream()
|
||||||
|
.map(PkiCliConfiguration::bindingProvider).toList();
|
||||||
|
return new PkiSessionConfiguration(version, store, audit, signing, publishers, bindingProviders);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void requireVersionTwoFields(Map<String, PkiOperationValue> actual) {
|
private static void requireFields(Map<String, PkiOperationValue> actual, Set<String> allowed) {
|
||||||
if (!actual.keySet().containsAll(ROOT_FIELDS_V1) || !ROOT_FIELDS_V2.containsAll(actual.keySet())) {
|
if (!actual.keySet().containsAll(ROOT_FIELDS_V1) || !allowed.containsAll(actual.keySet())) {
|
||||||
throw new IllegalArgumentException("CLI document fields are invalid");
|
throw new IllegalArgumentException("CLI document fields are invalid");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static PkiSessionConfiguration.BindingProviderConfiguration bindingProvider(PkiOperationValue value) {
|
||||||
|
PkiOperationValue.ObjectValue object = object(value);
|
||||||
|
Set<String> fields = object.fields().keySet();
|
||||||
|
if (!fields.containsAll(Set.of("providerId", "authorizedOidRoots"))
|
||||||
|
|| !Set.of("providerId", "authorizedOidRoots", "expectedBindingSetVersion").containsAll(fields)) {
|
||||||
|
throw new IllegalArgumentException("Binding-provider configuration fields are invalid");
|
||||||
|
}
|
||||||
|
List<String> roots = list(required(object, "authorizedOidRoots")).values().stream()
|
||||||
|
.map(PkiCliConfiguration::text).toList();
|
||||||
|
PkiOperationValue expected = object.fields().get("expectedBindingSetVersion");
|
||||||
|
return new PkiSessionConfiguration.BindingProviderConfiguration(text(required(object, "providerId")), roots,
|
||||||
|
expected == null ? java.util.Optional.empty() : java.util.Optional.of(text(expected)));
|
||||||
|
}
|
||||||
|
|
||||||
private static PkiSessionConfiguration.SigningConfiguration signing(PkiOperationValue value) {
|
private static PkiSessionConfiguration.SigningConfiguration signing(PkiOperationValue value) {
|
||||||
PkiOperationValue.ObjectValue object = object(value);
|
PkiOperationValue.ObjectValue object = object(value);
|
||||||
requireExactFields(object.fields(), SIGNING_FIELDS);
|
if (!object.fields().keySet().containsAll(SIGNING_REQUIRED_FIELDS)
|
||||||
|
|| !SIGNING_FIELDS.containsAll(object.fields().keySet())) {
|
||||||
|
throw new IllegalArgumentException("Signing configuration fields are invalid");
|
||||||
|
}
|
||||||
return new PkiSessionConfiguration.SigningConfiguration(provider(required(object, "workflow")),
|
return new PkiSessionConfiguration.SigningConfiguration(provider(required(object, "workflow")),
|
||||||
provider(required(object, "framework")), text(required(object, "busPath")),
|
provider(required(object, "framework")), text(required(object, "busPath")),
|
||||||
text(required(object, "signatureAlgorithm")),
|
text(required(object, "signatureAlgorithm")),
|
||||||
Duration.ofSeconds(integer(required(object, "signingTtlSeconds"))),
|
Duration.ofSeconds(integer(required(object, "signingTtlSeconds"))),
|
||||||
java.util.Optional.of(text(required(object, "unlockEnvironmentVariable"))));
|
java.util.Optional.of(text(required(object, "unlockEnvironmentVariable"))),
|
||||||
|
optionalText(object, "certificateSignatureBinding"), optionalText(object, "crlSignatureBinding"),
|
||||||
|
optionalText(object, "subjectPublicKeyBinding"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ProviderConfig provider(PkiOperationValue value) {
|
private static ProviderConfig provider(PkiOperationValue value) {
|
||||||
@@ -140,6 +174,11 @@ final class PkiCliConfiguration {
|
|||||||
throw new IllegalArgumentException("CLI field has the wrong type");
|
throw new IllegalArgumentException("CLI field has the wrong type");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static java.util.Optional<String> optionalText(PkiOperationValue.ObjectValue object, String field) {
|
||||||
|
PkiOperationValue value = object.fields().get(field);
|
||||||
|
return value == null ? java.util.Optional.empty() : java.util.Optional.of(text(value));
|
||||||
|
}
|
||||||
|
|
||||||
/* default */ static long integer(PkiOperationValue value) {
|
/* default */ static long integer(PkiOperationValue value) {
|
||||||
if (value instanceof PkiOperationValue.IntegerValue integer) {
|
if (value instanceof PkiOperationValue.IntegerValue integer) {
|
||||||
return integer.value();
|
return integer.value();
|
||||||
|
|||||||
@@ -68,12 +68,16 @@ final class PkiOperationRegistry {
|
|||||||
private static final String PROFILE_ID = "profileId";
|
private static final String PROFILE_ID = "profileId";
|
||||||
private static final String PROFILE_VERSION = "profileVersion";
|
private static final String PROFILE_VERSION = "profileVersion";
|
||||||
private static final String REQUEST_ID = "requestId";
|
private static final String REQUEST_ID = "requestId";
|
||||||
|
private static final String BINDING_ID = "bindingId";
|
||||||
private static final int MAXIMUM_REQUEST_BYTES = 1024 * 1024;
|
private static final int MAXIMUM_REQUEST_BYTES = 1024 * 1024;
|
||||||
|
|
||||||
private final Map<String, Binding> bindings;
|
private final Map<String, Binding> bindings;
|
||||||
|
|
||||||
/* default */ PkiOperationRegistry() {
|
/* default */ PkiOperationRegistry() {
|
||||||
Map<String, Binding> configured = new LinkedHashMap<>();
|
Map<String, Binding> configured = new LinkedHashMap<>();
|
||||||
|
add(configured, PkiOperation.ListAlgorithmBindings.NAME, this::listAlgorithmBindings);
|
||||||
|
add(configured, PkiOperation.InspectAlgorithmBinding.NAME, this::inspectAlgorithmBinding);
|
||||||
|
add(configured, PkiOperation.ValidateAlgorithmBindings.NAME, this::validateAlgorithmBindings);
|
||||||
add(configured, PkiOperation.ValidateConfiguration.NAME, this::configuration);
|
add(configured, PkiOperation.ValidateConfiguration.NAME, this::configuration);
|
||||||
add(configured, PkiOperation.ValidateProfile.NAME, this::profile);
|
add(configured, PkiOperation.ValidateProfile.NAME, this::profile);
|
||||||
add(configured, PkiOperation.RegisterProfile.NAME, this::registerProfile);
|
add(configured, PkiOperation.RegisterProfile.NAME, this::registerProfile);
|
||||||
@@ -123,6 +127,29 @@ final class PkiOperationRegistry {
|
|||||||
return new PkiOperation.ValidateConfiguration();
|
return new PkiOperation.ValidateConfiguration();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PkiOperation listAlgorithmBindings(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||||
|
Set<String> supplied = arguments.fields().keySet();
|
||||||
|
if (!supplied.contains(LIMIT) || !Set.of(LIMIT, "origin", "role").containsAll(supplied)) {
|
||||||
|
throw new IllegalArgumentException("PKI operation arguments are invalid");
|
||||||
|
}
|
||||||
|
return new PkiOperation.ListAlgorithmBindings(optionalText(arguments, "origin"),
|
||||||
|
optionalText(arguments, "role"), limit(arguments));
|
||||||
|
}
|
||||||
|
|
||||||
|
private PkiOperation inspectAlgorithmBinding(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||||
|
requireFields(arguments, Set.of(BINDING_ID));
|
||||||
|
return new PkiOperation.InspectAlgorithmBinding(text(arguments, BINDING_ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
private PkiOperation validateAlgorithmBindings(PkiOperationValue.ObjectValue arguments, Path baseDirectory) {
|
||||||
|
Set<String> supplied = arguments.fields().keySet();
|
||||||
|
if (!(supplied.isEmpty() || supplied.equals(Set.of(BINDING_ID, "bindingCommitment")))) {
|
||||||
|
throw new IllegalArgumentException("PKI operation arguments are invalid");
|
||||||
|
}
|
||||||
|
return new PkiOperation.ValidateAlgorithmBindings(optionalText(arguments, BINDING_ID),
|
||||||
|
optionalText(arguments, "bindingCommitment"));
|
||||||
|
}
|
||||||
|
|
||||||
private PkiOperation profile(PkiOperationValue.ObjectValue arguments, Path baseDirectory) throws IOException {
|
private PkiOperation profile(PkiOperationValue.ObjectValue arguments, Path baseDirectory) throws IOException {
|
||||||
requireFields(arguments, Set.of(PROFILE_FILE));
|
requireFields(arguments, Set.of(PROFILE_FILE));
|
||||||
Path path = resolve(baseDirectory, text(arguments, PROFILE_FILE));
|
Path path = resolve(baseDirectory, text(arguments, PROFILE_FILE));
|
||||||
@@ -296,6 +323,11 @@ final class PkiOperationRegistry {
|
|||||||
return PkiCliConfiguration.text(PkiCliConfiguration.required(arguments, name));
|
return PkiCliConfiguration.text(PkiCliConfiguration.required(arguments, name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static java.util.Optional<String> optionalText(PkiOperationValue.ObjectValue arguments, String name) {
|
||||||
|
PkiOperationValue value = arguments.fields().get(name);
|
||||||
|
return value == null ? java.util.Optional.empty() : java.util.Optional.of(PkiCliConfiguration.text(value));
|
||||||
|
}
|
||||||
|
|
||||||
private static long integer(PkiOperationValue.ObjectValue arguments, String name) {
|
private static long integer(PkiOperationValue.ObjectValue arguments, String name) {
|
||||||
return PkiCliConfiguration.integer(PkiCliConfiguration.required(arguments, name));
|
return PkiCliConfiguration.integer(PkiCliConfiguration.required(arguments, name));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,6 +168,33 @@ class PkiCliTest {
|
|||||||
System.out.println("...ok");
|
System.out.println("...ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bindingAdministrationUsesDirectAndBatchTypedExecution() throws IOException {
|
||||||
|
System.out.println("bindingAdministrationUsesDirectAndBatchTypedExecution");
|
||||||
|
Path configuration = configuration("bindings");
|
||||||
|
ByteArrayOutputStream listed = new ByteArrayOutputStream();
|
||||||
|
int listCode = PkiCli.execute(new String[] { "algorithm.binding.list", "--limit", "64", "--config",
|
||||||
|
configuration.toString(), "--output", "json" }, listed);
|
||||||
|
String bindingId = "zeroecho.private.sphincs-plus-default.certificate-signature.v1";
|
||||||
|
Path workflow = plan("binding-workflow.json", """
|
||||||
|
{"version":1,"failurePolicy":"FAIL_FAST","operations":[
|
||||||
|
{"id":"binding","operation":"algorithm.binding.inspect","arguments":{"bindingId":"%s"}},
|
||||||
|
{"id":"validate","operation":"algorithm.binding.validate","arguments":{"bindingId":"${binding.bindingId}","bindingCommitment":"${binding.bindingCommitment}"}}
|
||||||
|
]}
|
||||||
|
""".formatted(bindingId));
|
||||||
|
ByteArrayOutputStream batch = new ByteArrayOutputStream();
|
||||||
|
int batchCode = PkiCli.execute(new String[] { "run", workflow.toString(), "--config",
|
||||||
|
configuration.toString(), "--output", "json" }, batch);
|
||||||
|
String output = listed.toString(StandardCharsets.UTF_8) + batch.toString(StandardCharsets.UTF_8);
|
||||||
|
System.out.println("...outputBytes=" + output.length());
|
||||||
|
assertEquals(PkiExitCodes.SUCCESS, listCode);
|
||||||
|
assertEquals(PkiExitCodes.SUCCESS, batchCode);
|
||||||
|
assertTrue(output.contains("1.3.6.1.4.1.31424.1.1.3"));
|
||||||
|
assertTrue(output.contains("ZEROECHO_PRIVATE"));
|
||||||
|
assertFalse(output.contains("codecClass"));
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void versionTwoConfigurationKeepsCapabilitiesExplicit() throws IOException {
|
void versionTwoConfigurationKeepsCapabilitiesExplicit() throws IOException {
|
||||||
System.out.println("versionTwoConfigurationKeepsCapabilitiesExplicit");
|
System.out.println("versionTwoConfigurationKeepsCapabilitiesExplicit");
|
||||||
@@ -393,6 +420,40 @@ class PkiCliTest {
|
|||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public zeroecho.pki.api.algorithm.X509AlgorithmBindingRegistry algorithmBindings() {
|
||||||
|
return new zeroecho.pki.api.algorithm.X509AlgorithmBindingRegistry() {
|
||||||
|
@Override
|
||||||
|
public java.util.List<zeroecho.pki.api.algorithm.X509AlgorithmBinding> bindings() {
|
||||||
|
return java.util.List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public java.util.Optional<zeroecho.pki.api.algorithm.X509AlgorithmBinding> find(
|
||||||
|
String bindingId) {
|
||||||
|
return java.util.Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public java.util.Optional<zeroecho.pki.api.algorithm.X509AlgorithmBinding> standard(
|
||||||
|
zeroecho.core.spec.AlgorithmIdentity identity,
|
||||||
|
zeroecho.pki.api.algorithm.X509AlgorithmBinding.Role role) {
|
||||||
|
return java.util.Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String commitment() {
|
||||||
|
return "test-registry";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public zeroecho.pki.api.algorithm.X509AlgorithmBinding require(String bindingId,
|
||||||
|
String expectedCommitment) {
|
||||||
|
throw new IllegalArgumentException("inactive test binding");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public zeroecho.pki.application.PkiOperationExecutor operations() {
|
public zeroecho.pki.application.PkiOperationExecutor operations() {
|
||||||
return (operation, cancellation) -> outcome(operation);
|
return (operation, cancellation) -> outcome(operation);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 3,
|
||||||
"store": {
|
"store": {
|
||||||
"provider": "fs",
|
"provider": "fs",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -7,6 +7,21 @@
|
|||||||
"operation": "configuration.validate",
|
"operation": "configuration.validate",
|
||||||
"arguments": {}
|
"arguments": {}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "inspect-binding",
|
||||||
|
"operation": "algorithm.binding.inspect",
|
||||||
|
"arguments": {
|
||||||
|
"bindingId": "zeroecho.private.sphincs-plus-default.certificate-signature.v1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "validate-binding",
|
||||||
|
"operation": "algorithm.binding.validate",
|
||||||
|
"arguments": {
|
||||||
|
"bindingId": "${inspect-binding.bindingId}",
|
||||||
|
"bindingCommitment": "${inspect-binding.bindingCommitment}"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "list-publications",
|
"id": "list-publications",
|
||||||
"operation": "publication.list",
|
"operation": "publication.list",
|
||||||
|
|||||||
1
app/src/test/resources/pki-private-sphincs-profile.json
Normal file
1
app/src/test/resources/pki-private-sphincs-profile.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"schemaVersion":3,"certificateType":"END_ENTITY","profileId":"server-tls-sphincs","profileVersion":1,"formatId":"x509","displayName":"Server TLS SPHINCS+ private binding","maxValidity":"PT8760H","algorithmBindings":{"mode":"EXPLICIT","subjectPublicKey":{"bindingId":"zeroecho.private.sphincs-plus-default.spki.v1","semanticCommitment":"zeroecho.private.sphincs-plus-default.spki.v1|zealg:2:10:public_key8:zeroecho12:sphincs-plus16:zeroecho.builtin6:bm9uZQ|SUBJECT_PUBLIC_KEY|1.3.6.1.4.1.31424.1.1.1|1|ABSENT|NESTED_SPKI_DER|NOT_APPLICABLE|ZEROECHO_ECOSYSTEM"},"csrSignature":{"bindingId":"zeroecho.private.sphincs-plus-default.csr-signature.v1","semanticCommitment":"zeroecho.private.sphincs-plus-default.csr-signature.v1|zealg:2:9:signature8:zeroecho12:sphincs-plus16:zeroecho.builtin6:bm9uZQ|CSR_SIGNATURE|1.3.6.1.4.1.31424.1.1.2|1|ABSENT|NOT_APPLICABLE|OPAQUE|ZEROECHO_ECOSYSTEM"},"certificateSignature":{"bindingId":"zeroecho.private.sphincs-plus-default.certificate-signature.v1","semanticCommitment":"zeroecho.private.sphincs-plus-default.certificate-signature.v1|zealg:2:9:signature8:zeroecho12:sphincs-plus16:zeroecho.builtin6:bm9uZQ|CERTIFICATE_SIGNATURE|1.3.6.1.4.1.31424.1.1.3|1|ABSENT|NOT_APPLICABLE|OPAQUE|ZEROECHO_ECOSYSTEM"}},"subject":{"allowEmpty":true,"rules":[{"oid":"2.5.4.3","source":"REQUESTER","minimumOccurrences":0,"maximumOccurrences":1,"maximumUtf8Bytes":253}]},"subjectAlternativeNames":{"minimumTotal":1,"maximumTotal":64,"serviceIdentityRequired":true,"emailIdentityRequired":false,"criticalWhenSubjectNonEmpty":false,"rules":[{"type":"DNS_NAME","minimumOccurrences":0,"maximumOccurrences":64,"wildcardAllowed":false},{"type":"IP_ADDRESS","minimumOccurrences":0,"maximumOccurrences":16,"ipv4Allowed":true,"ipv6Allowed":true}]},"leafCertificate":{"basicConstraintsCritical":true,"keyUsageCritical":true,"keyUsage":["DIGITAL_SIGNATURE"],"extendedKeyUsageCritical":false,"extendedKeyUsage":["1.3.6.1.5.5.7.3.1"],"allowedKeyAlgorithms":["SPHINCS+"]}}
|
||||||
@@ -63,6 +63,7 @@ public final class BootstrapAlgorithmIdentities {
|
|||||||
private static final AlgorithmIdentity.Family ECDSA = family("ecdsa");
|
private static final AlgorithmIdentity.Family ECDSA = family("ecdsa");
|
||||||
private static final AlgorithmIdentity.Family ED25519_FAMILY = family("ed25519");
|
private static final AlgorithmIdentity.Family ED25519_FAMILY = family("ed25519");
|
||||||
private static final AlgorithmIdentity.Family ED448_FAMILY = family("ed448");
|
private static final AlgorithmIdentity.Family ED448_FAMILY = family("ed448");
|
||||||
|
private static final AlgorithmIdentity.Family SPHINCS_PLUS_FAMILY = family("sphincs-plus");
|
||||||
private static final AlgorithmIdentity.Family RSA_KEY = family("rsa");
|
private static final AlgorithmIdentity.Family RSA_KEY = family("rsa");
|
||||||
private static final AlgorithmIdentity.Family EC_KEY = family("ec");
|
private static final AlgorithmIdentity.Family EC_KEY = family("ec");
|
||||||
|
|
||||||
@@ -99,6 +100,9 @@ public final class BootstrapAlgorithmIdentities {
|
|||||||
/** Ed448 signature identity. */
|
/** Ed448 signature identity. */
|
||||||
public static final AlgorithmIdentity ED448_SIGNATURE = identity(AlgorithmIdentity.Kind.SIGNATURE, ED448_FAMILY,
|
public static final AlgorithmIdentity ED448_SIGNATURE = identity(AlgorithmIdentity.Kind.SIGNATURE, ED448_FAMILY,
|
||||||
AlgorithmIdentity.NoParameters.INSTANCE);
|
AlgorithmIdentity.NoParameters.INSTANCE);
|
||||||
|
/** ZeroEcho SPHINCS+ default signature identity. */
|
||||||
|
public static final AlgorithmIdentity SPHINCS_PLUS_SIGNATURE = identity(AlgorithmIdentity.Kind.SIGNATURE,
|
||||||
|
SPHINCS_PLUS_FAMILY, AlgorithmIdentity.NoParameters.INSTANCE);
|
||||||
|
|
||||||
/** RSA public-key identity. */
|
/** RSA public-key identity. */
|
||||||
public static final AlgorithmIdentity RSA_PUBLIC_KEY = identity(AlgorithmIdentity.Kind.PUBLIC_KEY, RSA_KEY,
|
public static final AlgorithmIdentity RSA_PUBLIC_KEY = identity(AlgorithmIdentity.Kind.PUBLIC_KEY, RSA_KEY,
|
||||||
@@ -115,6 +119,9 @@ public final class BootstrapAlgorithmIdentities {
|
|||||||
/** Ed448 public-key identity. */
|
/** Ed448 public-key identity. */
|
||||||
public static final AlgorithmIdentity ED448_PUBLIC_KEY = identity(AlgorithmIdentity.Kind.PUBLIC_KEY, ED448_FAMILY,
|
public static final AlgorithmIdentity ED448_PUBLIC_KEY = identity(AlgorithmIdentity.Kind.PUBLIC_KEY, ED448_FAMILY,
|
||||||
AlgorithmIdentity.NoParameters.INSTANCE);
|
AlgorithmIdentity.NoParameters.INSTANCE);
|
||||||
|
/** ZeroEcho SPHINCS+ default public-key identity. */
|
||||||
|
public static final AlgorithmIdentity SPHINCS_PLUS_PUBLIC_KEY = identity(AlgorithmIdentity.Kind.PUBLIC_KEY,
|
||||||
|
SPHINCS_PLUS_FAMILY, AlgorithmIdentity.NoParameters.INSTANCE);
|
||||||
|
|
||||||
/** Current ECDSA P-256 signing suite. */
|
/** Current ECDSA P-256 signing suite. */
|
||||||
public static final AlgorithmSuite ECDSA_SHA256_P256 = new AlgorithmSuite(ECDSA_SHA256, EC_P256_PUBLIC_KEY);
|
public static final AlgorithmSuite ECDSA_SHA256_P256 = new AlgorithmSuite(ECDSA_SHA256, EC_P256_PUBLIC_KEY);
|
||||||
@@ -127,8 +134,9 @@ public final class BootstrapAlgorithmIdentities {
|
|||||||
|
|
||||||
private static final List<AlgorithmIdentity> IDENTITIES = List.of(SHA256, SHA384, SHA512, MGF1, RSA_PKCS1_SHA256,
|
private static final List<AlgorithmIdentity> IDENTITIES = List.of(SHA256, SHA384, SHA512, MGF1, RSA_PKCS1_SHA256,
|
||||||
RSA_PKCS1_SHA384, RSA_PKCS1_SHA512, RSA_PSS_SHA256, ECDSA_SHA256, ECDSA_SHA384, ECDSA_SHA512,
|
RSA_PKCS1_SHA384, RSA_PKCS1_SHA512, RSA_PSS_SHA256, ECDSA_SHA256, ECDSA_SHA384, ECDSA_SHA512,
|
||||||
ED25519_SIGNATURE, ED448_SIGNATURE, RSA_PUBLIC_KEY, EC_P256_PUBLIC_KEY, EC_P384_PUBLIC_KEY,
|
ED25519_SIGNATURE, ED448_SIGNATURE, SPHINCS_PLUS_SIGNATURE, RSA_PUBLIC_KEY, EC_P256_PUBLIC_KEY,
|
||||||
EC_P521_PUBLIC_KEY, ED25519_PUBLIC_KEY, ED448_PUBLIC_KEY);
|
EC_P384_PUBLIC_KEY, EC_P521_PUBLIC_KEY, ED25519_PUBLIC_KEY, ED448_PUBLIC_KEY,
|
||||||
|
SPHINCS_PLUS_PUBLIC_KEY);
|
||||||
|
|
||||||
private static final AlgorithmIdentityCatalog CATALOG = AlgorithmIdentityCatalog.builtIn(IDENTITIES);
|
private static final AlgorithmIdentityCatalog CATALOG = AlgorithmIdentityCatalog.builtIn(IDENTITIES);
|
||||||
|
|
||||||
@@ -141,7 +149,8 @@ public final class BootstrapAlgorithmIdentities {
|
|||||||
Map.entry("SHA384withECDSA", ECDSA_SHA384),
|
Map.entry("SHA384withECDSA", ECDSA_SHA384),
|
||||||
Map.entry("SHA512withECDSA", ECDSA_SHA512),
|
Map.entry("SHA512withECDSA", ECDSA_SHA512),
|
||||||
Map.entry("Ed25519", ED25519_SIGNATURE),
|
Map.entry("Ed25519", ED25519_SIGNATURE),
|
||||||
Map.entry("Ed448", ED448_SIGNATURE));
|
Map.entry("Ed448", ED448_SIGNATURE),
|
||||||
|
Map.entry("SPHINCS+", SPHINCS_PLUS_SIGNATURE));
|
||||||
|
|
||||||
private BootstrapAlgorithmIdentities() {
|
private BootstrapAlgorithmIdentities() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,9 @@ public final class SignatureInteropProfiles {
|
|||||||
new SignatureInteropProfile("Ed25519", "Ed25519", "Ed25519", VoidSpec.INSTANCE,
|
new SignatureInteropProfile("Ed25519", "Ed25519", "Ed25519", VoidSpec.INSTANCE,
|
||||||
SignatureInteropProfile.SignatureRepresentation.IDENTITY, 0)),
|
SignatureInteropProfile.SignatureRepresentation.IDENTITY, 0)),
|
||||||
Map.entry("Ed448", new SignatureInteropProfile("Ed448", "Ed448", "Ed448", VoidSpec.INSTANCE,
|
Map.entry("Ed448", new SignatureInteropProfile("Ed448", "Ed448", "Ed448", VoidSpec.INSTANCE,
|
||||||
SignatureInteropProfile.SignatureRepresentation.IDENTITY, 0)));
|
SignatureInteropProfile.SignatureRepresentation.IDENTITY, 0)),
|
||||||
|
Map.entry("SPHINCS+", new SignatureInteropProfile("SPHINCS+", "SPHINCS+", "SPHINCS+",
|
||||||
|
VoidSpec.INSTANCE, SignatureInteropProfile.SignatureRepresentation.IDENTITY, 0)));
|
||||||
|
|
||||||
private static final Map<String, SignatureInteropProfile> CANONICAL_PROFILES = Map.ofEntries(
|
private static final Map<String, SignatureInteropProfile> CANONICAL_PROFILES = Map.ofEntries(
|
||||||
canonical(BootstrapAlgorithmIdentities.RSA_PKCS1_SHA256, "SHA256withRSA"),
|
canonical(BootstrapAlgorithmIdentities.RSA_PKCS1_SHA256, "SHA256withRSA"),
|
||||||
@@ -129,7 +131,8 @@ public final class SignatureInteropProfiles {
|
|||||||
canonical(BootstrapAlgorithmIdentities.ECDSA_SHA384, "SHA384withECDSA"),
|
canonical(BootstrapAlgorithmIdentities.ECDSA_SHA384, "SHA384withECDSA"),
|
||||||
canonical(BootstrapAlgorithmIdentities.ECDSA_SHA512, "SHA512withECDSA"),
|
canonical(BootstrapAlgorithmIdentities.ECDSA_SHA512, "SHA512withECDSA"),
|
||||||
canonical(BootstrapAlgorithmIdentities.ED25519_SIGNATURE, "Ed25519"),
|
canonical(BootstrapAlgorithmIdentities.ED25519_SIGNATURE, "Ed25519"),
|
||||||
canonical(BootstrapAlgorithmIdentities.ED448_SIGNATURE, "Ed448"));
|
canonical(BootstrapAlgorithmIdentities.ED448_SIGNATURE, "Ed448"),
|
||||||
|
canonical(BootstrapAlgorithmIdentities.SPHINCS_PLUS_SIGNATURE, "SPHINCS+"));
|
||||||
|
|
||||||
private SignatureInteropProfiles() {
|
private SignatureInteropProfiles() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* 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.api.algorithm;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import zeroecho.core.spec.AlgorithmIdentity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immutable safe descriptor of one role-specific X.509 algorithm binding.
|
||||||
|
*
|
||||||
|
* <p>The descriptor separates cryptographic identity, ASN.1 identity, namespace
|
||||||
|
* ownership, encoding rules, and provider implementation. It contains no codec
|
||||||
|
* class, key material, provider secret, or caller-supplied DER.</p>
|
||||||
|
*
|
||||||
|
* @param bindingId stable semantic binding identifier
|
||||||
|
* @param algorithmIdentity exact provider-independent cryptographic identity
|
||||||
|
* @param role exact X.509 use role
|
||||||
|
* @param oid complete dotted-decimal object identifier
|
||||||
|
* @param origin namespace ownership
|
||||||
|
* @param encodingVersion positive immutable encoding-contract version
|
||||||
|
* @param parameterRule canonical AlgorithmIdentifier parameter rule
|
||||||
|
* @param publicKeyEncoding public-key BIT STRING contract
|
||||||
|
* @param signatureEncoding signature BIT STRING contract
|
||||||
|
* @param interoperability finite interoperability classification
|
||||||
|
* @param providerId contributing provider for deployer bindings
|
||||||
|
* @param semanticCommitment deterministic commitment to the complete contract
|
||||||
|
*/
|
||||||
|
public record X509AlgorithmBinding(String bindingId, AlgorithmIdentity algorithmIdentity, Role role, String oid,
|
||||||
|
Origin origin, int encodingVersion, ParameterRule parameterRule, PublicKeyEncoding publicKeyEncoding,
|
||||||
|
SignatureEncoding signatureEncoding, Interoperability interoperability, Optional<String> providerId,
|
||||||
|
String semanticCommitment) {
|
||||||
|
|
||||||
|
/** X.509 locations whose representations are independently authorized. */
|
||||||
|
public enum Role {
|
||||||
|
/** SubjectPublicKeyInfo algorithm and public-key BIT STRING. */
|
||||||
|
SUBJECT_PUBLIC_KEY,
|
||||||
|
/** PKCS#10 certification-request signature. */
|
||||||
|
CSR_SIGNATURE,
|
||||||
|
/** Certificate signature. */
|
||||||
|
CERTIFICATE_SIGNATURE,
|
||||||
|
/** Certificate-revocation-list signature. */
|
||||||
|
CRL_SIGNATURE
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Authority that owns the OID assignment. */
|
||||||
|
public enum Origin {
|
||||||
|
/** Published standards assignment. */
|
||||||
|
STANDARD,
|
||||||
|
/** Frozen ZeroEcho assignment under the Egothor PEN. */
|
||||||
|
ZEROECHO_PRIVATE,
|
||||||
|
/** Explicitly enabled deployment-owned assignment. */
|
||||||
|
DEPLOYER_PRIVATE
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Canonical AlgorithmIdentifier parameter representation. */
|
||||||
|
public enum ParameterRule {
|
||||||
|
/** Parameters must be absent. */
|
||||||
|
ABSENT,
|
||||||
|
/** Parameters must be canonical DER NULL. */
|
||||||
|
DER_NULL,
|
||||||
|
/** Parameters are a binding-owned canonical DER structure. */
|
||||||
|
STRUCTURED_DER
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Subject-public-key BIT STRING representation. */
|
||||||
|
public enum PublicKeyEncoding {
|
||||||
|
/** This binding is not an SPKI binding. */
|
||||||
|
NOT_APPLICABLE,
|
||||||
|
/** Standard RSA PKCS#1 public-key DER. */
|
||||||
|
RSA_PKCS1_DER,
|
||||||
|
/** SEC1 elliptic-curve point. */
|
||||||
|
EC_POINT,
|
||||||
|
/** Algorithm-defined raw public-key bytes. */
|
||||||
|
RAW,
|
||||||
|
/** Canonical native SPKI DER nested as the BIT STRING payload. */
|
||||||
|
NESTED_SPKI_DER
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Signature BIT STRING representation. */
|
||||||
|
public enum SignatureEncoding {
|
||||||
|
/** This binding is not a signature binding. */
|
||||||
|
NOT_APPLICABLE,
|
||||||
|
/** Algorithm-defined opaque signature bytes. */
|
||||||
|
OPAQUE,
|
||||||
|
/** Canonical DER sequence of positive ECDSA integers. */
|
||||||
|
ECDSA_DER
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Relying-party interoperability expectation. */
|
||||||
|
public enum Interoperability {
|
||||||
|
/** Published standards ecosystems. */
|
||||||
|
STANDARD_INTEROPERABLE,
|
||||||
|
/** Relying parties with matching ZeroEcho bindings. */
|
||||||
|
ZEROECHO_ECOSYSTEM,
|
||||||
|
/** One explicitly configured deployment ecosystem. */
|
||||||
|
DEPLOYER_ECOSYSTEM,
|
||||||
|
/** Experimental representation without general interoperability. */
|
||||||
|
EXPERIMENTAL
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Validates and snapshots one descriptor. */
|
||||||
|
public X509AlgorithmBinding {
|
||||||
|
bindingId = requireText(bindingId, "bindingId");
|
||||||
|
Objects.requireNonNull(algorithmIdentity, "algorithmIdentity");
|
||||||
|
Objects.requireNonNull(role, "role");
|
||||||
|
oid = requireText(oid, "oid");
|
||||||
|
Objects.requireNonNull(origin, "origin");
|
||||||
|
if (encodingVersion <= 0) {
|
||||||
|
throw new IllegalArgumentException("encodingVersion must be positive");
|
||||||
|
}
|
||||||
|
Objects.requireNonNull(parameterRule, "parameterRule");
|
||||||
|
Objects.requireNonNull(publicKeyEncoding, "publicKeyEncoding");
|
||||||
|
Objects.requireNonNull(signatureEncoding, "signatureEncoding");
|
||||||
|
Objects.requireNonNull(interoperability, "interoperability");
|
||||||
|
providerId = Objects.requireNonNull(providerId, "providerId").map(value -> requireText(value, "providerId"));
|
||||||
|
semanticCommitment = requireText(semanticCommitment, "semanticCommitment");
|
||||||
|
if (origin == Origin.DEPLOYER_PRIVATE != providerId.isPresent()) {
|
||||||
|
throw new IllegalArgumentException("Only deployer bindings declare a providerId");
|
||||||
|
}
|
||||||
|
if ((role == Role.SUBJECT_PUBLIC_KEY) != (signatureEncoding == SignatureEncoding.NOT_APPLICABLE)) {
|
||||||
|
throw new IllegalArgumentException("Binding role and signature encoding disagree");
|
||||||
|
}
|
||||||
|
if ((role != Role.SUBJECT_PUBLIC_KEY) != (publicKeyEncoding == PublicKeyEncoding.NOT_APPLICABLE)) {
|
||||||
|
throw new IllegalArgumentException("Binding role and public-key encoding disagree");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String requireText(String value, String name) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
throw new IllegalArgumentException(name + " must not be blank");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* 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.api.algorithm;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import zeroecho.core.spec.AlgorithmIdentity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immutable active X.509 algorithm-binding registry shared by one PKI session.
|
||||||
|
*
|
||||||
|
* <p>Implementations provide deterministic constant-time identifier lookup and
|
||||||
|
* linear-time ordered listing. Returned collections are immutable snapshots.</p>
|
||||||
|
*/
|
||||||
|
public interface X509AlgorithmBindingRegistry {
|
||||||
|
|
||||||
|
/** @return deterministic immutable binding order */
|
||||||
|
List<X509AlgorithmBinding> bindings();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a stable binding identifier.
|
||||||
|
*
|
||||||
|
* @param bindingId stable identifier
|
||||||
|
* @return exact descriptor, or empty when inactive
|
||||||
|
*/
|
||||||
|
Optional<X509AlgorithmBinding> find(String bindingId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the standard default for one identity and role.
|
||||||
|
*
|
||||||
|
* @param identity exact cryptographic identity
|
||||||
|
* @param role X.509 role
|
||||||
|
* @return official binding, or empty when no official binding exists
|
||||||
|
*/
|
||||||
|
Optional<X509AlgorithmBinding> standard(AlgorithmIdentity identity, X509AlgorithmBinding.Role role);
|
||||||
|
|
||||||
|
/** @return deterministic SHA-256 commitment to the complete active set */
|
||||||
|
String commitment();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requires that one explicit binding remains active with an exact commitment.
|
||||||
|
*
|
||||||
|
* @param bindingId stable binding identifier
|
||||||
|
* @param expectedCommitment persisted binding-contract commitment
|
||||||
|
* @return validated binding
|
||||||
|
* @throws IllegalArgumentException if the binding is absent or changed
|
||||||
|
*/
|
||||||
|
X509AlgorithmBinding require(String bindingId, String expectedCommitment);
|
||||||
|
}
|
||||||
@@ -60,7 +60,8 @@ public record CaCertificatePolicy(boolean basicConstraintsCritical, int pathLeng
|
|||||||
/** Maximum supported path-length constraint. */
|
/** Maximum supported path-length constraint. */
|
||||||
public static final int MAXIMUM_PATH_LENGTH = 32;
|
public static final int MAXIMUM_PATH_LENGTH = 32;
|
||||||
|
|
||||||
private static final Set<String> SUPPORTED_SUBJECT_KEY_ALGORITHMS = Set.of("RSA", "ECDSA", "Ed25519", "Ed448");
|
private static final Set<String> SUPPORTED_SUBJECT_KEY_ALGORITHMS = Set.of("RSA", "ECDSA", "Ed25519", "Ed448",
|
||||||
|
"SPHINCS+");
|
||||||
|
|
||||||
/** Validates and constructs the CA policy. */
|
/** Validates and constructs the CA policy. */
|
||||||
public CaCertificatePolicy {
|
public CaCertificatePolicy {
|
||||||
|
|||||||
@@ -56,9 +56,18 @@ import zeroecho.pki.api.FormatId;
|
|||||||
* @param maximumValidity positive maximum validity
|
* @param maximumValidity positive maximum validity
|
||||||
* @param subjectPolicy complete end-entity subject policy
|
* @param subjectPolicy complete end-entity subject policy
|
||||||
* @param leafPolicy complete end-entity extension policy
|
* @param leafPolicy complete end-entity extension policy
|
||||||
|
* @param algorithmBindingPolicy explicit role-specific X.509 representation policy
|
||||||
*/
|
*/
|
||||||
public record CertificateProfile(String profileId, FormatId formatId, String displayName, Duration maximumValidity,
|
public record CertificateProfile(String profileId, FormatId formatId, String displayName, Duration maximumValidity,
|
||||||
SubjectPolicy subjectPolicy, LeafCertificatePolicy leafPolicy) {
|
SubjectPolicy subjectPolicy, LeafCertificatePolicy leafPolicy,
|
||||||
|
X509AlgorithmBindingPolicy algorithmBindingPolicy) {
|
||||||
|
|
||||||
|
/** Creates a standard-only runtime profile. */
|
||||||
|
public CertificateProfile(String profileId, FormatId formatId, String displayName, Duration maximumValidity,
|
||||||
|
SubjectPolicy subjectPolicy, LeafCertificatePolicy leafPolicy) {
|
||||||
|
this(profileId, formatId, displayName, maximumValidity, subjectPolicy, leafPolicy,
|
||||||
|
X509AlgorithmBindingPolicy.standardOnly());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a certificate profile.
|
* Creates a certificate profile.
|
||||||
@@ -77,7 +86,7 @@ public record CertificateProfile(String profileId, FormatId formatId, String dis
|
|||||||
throw new IllegalArgumentException("displayName must not be null/blank");
|
throw new IllegalArgumentException("displayName must not be null/blank");
|
||||||
}
|
}
|
||||||
if (maximumValidity == null || maximumValidity.isZero() || maximumValidity.isNegative() || subjectPolicy == null
|
if (maximumValidity == null || maximumValidity.isZero() || maximumValidity.isNegative() || subjectPolicy == null
|
||||||
|| leafPolicy == null) {
|
|| leafPolicy == null || algorithmBindingPolicy == null) {
|
||||||
throw new IllegalArgumentException("profile policies and maximum validity must be valid");
|
throw new IllegalArgumentException("profile policies and maximum validity must be valid");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,6 +105,7 @@ public record CertificateProfile(String profileId, FormatId formatId, String dis
|
|||||||
throw new IllegalArgumentException("Only end-entity definitions have a runtime issuance projection");
|
throw new IllegalArgumentException("Only end-entity definitions have a runtime issuance projection");
|
||||||
}
|
}
|
||||||
return new CertificateProfile(definition.profileId(), definition.formatId(), definition.displayName(),
|
return new CertificateProfile(definition.profileId(), definition.formatId(), definition.displayName(),
|
||||||
definition.maximumValidity(), definition.subjectPolicy(), definition.leafPolicy());
|
definition.maximumValidity(), definition.subjectPolicy(), definition.leafPolicy(),
|
||||||
|
definition.algorithmBindingPolicy());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,8 +41,9 @@ import zeroecho.pki.api.FormatId;
|
|||||||
* Immutable, versioned certificate-profile configuration.
|
* Immutable, versioned certificate-profile configuration.
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
* Schema version 2 replaces the pre-release version 1 shape. The certificate
|
* Schema version 3 adds explicit immutable X.509 algorithm-binding selection to
|
||||||
* kind and its closed policy variant are immutable across the definition.
|
* the closed pre-release profile shape. The certificate kind and its closed
|
||||||
|
* policy variant are immutable across the definition.
|
||||||
* Runtime activation state is deliberately excluded.
|
* Runtime activation state is deliberately excluded.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
@@ -54,13 +55,23 @@ import zeroecho.pki.api.FormatId;
|
|||||||
* @param maximumValidity positive maximum validity
|
* @param maximumValidity positive maximum validity
|
||||||
* @param subjectPolicy complete subject policy
|
* @param subjectPolicy complete subject policy
|
||||||
* @param certificatePolicy closed certificate-specific policy variant
|
* @param certificatePolicy closed certificate-specific policy variant
|
||||||
|
* @param algorithmBindingPolicy explicit X.509 representation policy
|
||||||
*/
|
*/
|
||||||
public record CertificateProfileDefinition(CertificateProfileKind certificateType, String profileId,
|
public record CertificateProfileDefinition(CertificateProfileKind certificateType, String profileId,
|
||||||
long profileVersion, FormatId formatId, String displayName, Duration maximumValidity,
|
long profileVersion, FormatId formatId, String displayName, Duration maximumValidity,
|
||||||
SubjectPolicy subjectPolicy, CertificatePolicy certificatePolicy) {
|
SubjectPolicy subjectPolicy, CertificatePolicy certificatePolicy,
|
||||||
|
X509AlgorithmBindingPolicy algorithmBindingPolicy) {
|
||||||
|
|
||||||
/** Current certificate-profile document schema version. */
|
/** Current certificate-profile document schema version. */
|
||||||
public static final int SCHEMA_VERSION = 2;
|
public static final int SCHEMA_VERSION = 3;
|
||||||
|
|
||||||
|
/** Creates a standard-only profile definition. */
|
||||||
|
public CertificateProfileDefinition(CertificateProfileKind certificateType, String profileId,
|
||||||
|
long profileVersion, FormatId formatId, String displayName, Duration maximumValidity,
|
||||||
|
SubjectPolicy subjectPolicy, CertificatePolicy certificatePolicy) {
|
||||||
|
this(certificateType, profileId, profileVersion, formatId, displayName, maximumValidity, subjectPolicy,
|
||||||
|
certificatePolicy, X509AlgorithmBindingPolicy.standardOnly());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a certificate-profile definition.
|
* Creates a certificate-profile definition.
|
||||||
@@ -87,7 +98,7 @@ public record CertificateProfileDefinition(CertificateProfileKind certificateTyp
|
|||||||
if (maximumValidity == null || maximumValidity.isZero() || maximumValidity.isNegative()) {
|
if (maximumValidity == null || maximumValidity.isZero() || maximumValidity.isNegative()) {
|
||||||
throw new IllegalArgumentException("maximumValidity must be positive");
|
throw new IllegalArgumentException("maximumValidity must be positive");
|
||||||
}
|
}
|
||||||
if (subjectPolicy == null || certificatePolicy == null) {
|
if (subjectPolicy == null || certificatePolicy == null || algorithmBindingPolicy == null) {
|
||||||
throw new IllegalArgumentException("profile policies must not be null");
|
throw new IllegalArgumentException("profile policies must not be null");
|
||||||
}
|
}
|
||||||
boolean leaf = certificatePolicy instanceof LeafCertificatePolicy;
|
boolean leaf = certificatePolicy instanceof LeafCertificatePolicy;
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ public final class CertificateProfileDocumentCodec {
|
|||||||
requireValue(parser, "$." + field);
|
requireValue(parser, "$." + field);
|
||||||
readDocumentField(parser, field, document);
|
readDocumentField(parser, field, document);
|
||||||
}
|
}
|
||||||
requireAll(document.seen, 8, "$");
|
requireAll(document.seen, 9, "$");
|
||||||
if (document.schemaVersion != CertificateProfileDefinition.SCHEMA_VERSION) {
|
if (document.schemaVersion != CertificateProfileDefinition.SCHEMA_VERSION) {
|
||||||
throw failure("SCHEMA_VERSION_UNSUPPORTED", "$.schemaVersion");
|
throw failure("SCHEMA_VERSION_UNSUPPORTED", "$.schemaVersion");
|
||||||
}
|
}
|
||||||
@@ -286,6 +286,10 @@ public final class CertificateProfileDocumentCodec {
|
|||||||
document.seen = mark(document.seen, 7, "$.subject");
|
document.seen = mark(document.seen, 7, "$.subject");
|
||||||
document.subject = readSubject(parser, "$.subject");
|
document.subject = readSubject(parser, "$.subject");
|
||||||
}
|
}
|
||||||
|
case "algorithmBindings" -> {
|
||||||
|
document.seen = mark(document.seen, 8, "$.algorithmBindings");
|
||||||
|
document.algorithmBindings = readAlgorithmBindings(parser, "$.algorithmBindings");
|
||||||
|
}
|
||||||
case "subjectAlternativeNames" -> readDocumentSan(parser, document);
|
case "subjectAlternativeNames" -> readDocumentSan(parser, document);
|
||||||
case "leafCertificate" -> readDocumentLeaf(parser, document);
|
case "leafCertificate" -> readDocumentLeaf(parser, document);
|
||||||
case "caCertificate" -> readDocumentCa(parser, document);
|
case "caCertificate" -> readDocumentCa(parser, document);
|
||||||
@@ -297,7 +301,7 @@ public final class CertificateProfileDocumentCodec {
|
|||||||
if (document.certificateType != null && document.certificateType != CertificateProfileKind.END_ENTITY) {
|
if (document.certificateType != null && document.certificateType != CertificateProfileKind.END_ENTITY) {
|
||||||
throw failure("FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE", "$.subjectAlternativeNames");
|
throw failure("FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE", "$.subjectAlternativeNames");
|
||||||
}
|
}
|
||||||
document.seen = mark(document.seen, 8, "$.subjectAlternativeNames");
|
document.seen = mark(document.seen, 9, "$.subjectAlternativeNames");
|
||||||
document.san = readSan(parser, "$.subjectAlternativeNames");
|
document.san = readSan(parser, "$.subjectAlternativeNames");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +309,7 @@ public final class CertificateProfileDocumentCodec {
|
|||||||
if (document.certificateType != null && document.certificateType != CertificateProfileKind.END_ENTITY) {
|
if (document.certificateType != null && document.certificateType != CertificateProfileKind.END_ENTITY) {
|
||||||
throw failure("FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE", "$.leafCertificate");
|
throw failure("FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE", "$.leafCertificate");
|
||||||
}
|
}
|
||||||
document.seen = mark(document.seen, 9, "$.leafCertificate");
|
document.seen = mark(document.seen, 10, "$.leafCertificate");
|
||||||
document.leaf = readLeaf(parser, "$.leafCertificate");
|
document.leaf = readLeaf(parser, "$.leafCertificate");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,14 +317,14 @@ public final class CertificateProfileDocumentCodec {
|
|||||||
if (document.certificateType == CertificateProfileKind.END_ENTITY) {
|
if (document.certificateType == CertificateProfileKind.END_ENTITY) {
|
||||||
throw failure("FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE", "$.caCertificate");
|
throw failure("FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE", "$.caCertificate");
|
||||||
}
|
}
|
||||||
document.seen = mark(document.seen, 10, "$.caCertificate");
|
document.seen = mark(document.seen, 11, "$.caCertificate");
|
||||||
document.ca = readCa(parser, "$.caCertificate");
|
document.ca = readCa(parser, "$.caCertificate");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void requirePolicyFields(CertificateProfileKind certificateType, long seen) {
|
private static void requirePolicyFields(CertificateProfileKind certificateType, long seen) {
|
||||||
boolean hasSan = isSeen(seen, 8);
|
boolean hasSan = isSeen(seen, 9);
|
||||||
boolean hasLeaf = isSeen(seen, 9);
|
boolean hasLeaf = isSeen(seen, 10);
|
||||||
boolean hasCa = isSeen(seen, 10);
|
boolean hasCa = isSeen(seen, 11);
|
||||||
if (certificateType == CertificateProfileKind.END_ENTITY) {
|
if (certificateType == CertificateProfileKind.END_ENTITY) {
|
||||||
if (hasCa) {
|
if (hasCa) {
|
||||||
throw failure("FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE", "$.caCertificate");
|
throw failure("FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE", "$.caCertificate");
|
||||||
@@ -366,7 +370,7 @@ public final class CertificateProfileDocumentCodec {
|
|||||||
}
|
}
|
||||||
return new CertificateProfileDefinition(document.certificateType, document.profileId,
|
return new CertificateProfileDefinition(document.certificateType, document.profileId,
|
||||||
document.profileVersion, new FormatId(document.formatId), document.displayName,
|
document.profileVersion, new FormatId(document.formatId), document.displayName,
|
||||||
document.maximumValidity, subjectPolicy, policy);
|
document.maximumValidity, subjectPolicy, policy, document.algorithmBindings);
|
||||||
} catch (IllegalArgumentException | ArithmeticException ex) {
|
} catch (IllegalArgumentException | ArithmeticException ex) {
|
||||||
throw failure("SEMANTIC_INVALID", "$");
|
throw failure("SEMANTIC_INVALID", "$");
|
||||||
}
|
}
|
||||||
@@ -397,6 +401,89 @@ public final class CertificateProfileDocumentCodec {
|
|||||||
return new SubjectSection(allowEmpty, rules);
|
return new SubjectSection(allowEmpty, rules);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static X509AlgorithmBindingPolicy readAlgorithmBindings(JsonParser parser, String path)
|
||||||
|
throws JacksonException {
|
||||||
|
requireToken(parser.currentToken(), JsonToken.START_OBJECT, path);
|
||||||
|
long seen = 0;
|
||||||
|
X509AlgorithmBindingPolicy.Mode mode = null;
|
||||||
|
Optional<X509AlgorithmBindingPolicy.BindingReference> subject = Optional.empty();
|
||||||
|
Optional<X509AlgorithmBindingPolicy.BindingReference> csr = Optional.empty();
|
||||||
|
Optional<X509AlgorithmBindingPolicy.BindingReference> certificate = Optional.empty();
|
||||||
|
Optional<X509AlgorithmBindingPolicy.BindingReference> crl = Optional.empty();
|
||||||
|
while (parser.nextToken() != JsonToken.END_OBJECT) {
|
||||||
|
requireToken(parser.currentToken(), JsonToken.PROPERTY_NAME, path);
|
||||||
|
String field = parser.currentName();
|
||||||
|
requireValue(parser, path + "." + field);
|
||||||
|
switch (field) {
|
||||||
|
case "mode" -> {
|
||||||
|
seen = mark(seen, 0, path + ".mode");
|
||||||
|
mode = readEnum(parser, X509AlgorithmBindingPolicy.Mode.class, path + ".mode");
|
||||||
|
}
|
||||||
|
case "subjectPublicKey" -> {
|
||||||
|
seen = mark(seen, 1, path + ".subjectPublicKey");
|
||||||
|
subject = Optional.of(readBindingReference(parser, path + ".subjectPublicKey"));
|
||||||
|
}
|
||||||
|
case "csrSignature" -> {
|
||||||
|
seen = mark(seen, 2, path + ".csrSignature");
|
||||||
|
csr = Optional.of(readBindingReference(parser, path + ".csrSignature"));
|
||||||
|
}
|
||||||
|
case "certificateSignature" -> {
|
||||||
|
seen = mark(seen, 3, path + ".certificateSignature");
|
||||||
|
certificate = Optional.of(readBindingReference(parser, path + ".certificateSignature"));
|
||||||
|
}
|
||||||
|
case "crlSignature" -> {
|
||||||
|
seen = mark(seen, 4, path + ".crlSignature");
|
||||||
|
crl = Optional.of(readBindingReference(parser, path + ".crlSignature"));
|
||||||
|
}
|
||||||
|
default -> throw failure("UNKNOWN_FIELD", path + ".?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isSeen(seen, 0)) {
|
||||||
|
throw failure("MISSING_FIELD", path + ".mode");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new X509AlgorithmBindingPolicy(mode, subject, csr, certificate, crl);
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
throw failure("SEMANTIC_INVALID", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static X509AlgorithmBindingPolicy.BindingReference readBindingReference(JsonParser parser, String path)
|
||||||
|
throws JacksonException {
|
||||||
|
requireToken(parser.currentToken(), JsonToken.START_OBJECT, path);
|
||||||
|
long seen = 0;
|
||||||
|
String bindingId = null;
|
||||||
|
String commitment = null;
|
||||||
|
while (parser.nextToken() != JsonToken.END_OBJECT) {
|
||||||
|
requireToken(parser.currentToken(), JsonToken.PROPERTY_NAME, path);
|
||||||
|
String field = parser.currentName();
|
||||||
|
requireValue(parser, path + "." + field);
|
||||||
|
switch (field) {
|
||||||
|
case "bindingId" -> {
|
||||||
|
seen = mark(seen, 0, path + ".bindingId");
|
||||||
|
bindingId = readBoundedString(parser, path + ".bindingId", 512);
|
||||||
|
}
|
||||||
|
case "semanticCommitment" -> {
|
||||||
|
seen = mark(seen, 1, path + ".semanticCommitment");
|
||||||
|
commitment = readBoundedString(parser, path + ".semanticCommitment", 1024);
|
||||||
|
}
|
||||||
|
default -> throw failure("UNKNOWN_FIELD", path + ".?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requireAll(seen, 2, path);
|
||||||
|
return new X509AlgorithmBindingPolicy.BindingReference(bindingId, commitment);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <E extends Enum<E>> E readEnum(JsonParser parser, Class<E> type, String path)
|
||||||
|
throws JacksonException {
|
||||||
|
String value = readString(parser, path);
|
||||||
|
try {
|
||||||
|
return Enum.valueOf(type, value);
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
throw failure("TOKEN_INVALID", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static List<SubjectRdnRule> readSubjectRules(JsonParser parser, String path) throws JacksonException {
|
private static List<SubjectRdnRule> readSubjectRules(JsonParser parser, String path) throws JacksonException {
|
||||||
requireToken(parser.currentToken(), JsonToken.START_ARRAY, path);
|
requireToken(parser.currentToken(), JsonToken.START_ARRAY, path);
|
||||||
List<SubjectRdnRule> rules = new ArrayList<>();
|
List<SubjectRdnRule> rules = new ArrayList<>();
|
||||||
@@ -785,6 +872,7 @@ public final class CertificateProfileDocumentCodec {
|
|||||||
generator.writeStringProperty("formatId", definition.formatId().value());
|
generator.writeStringProperty("formatId", definition.formatId().value());
|
||||||
generator.writeStringProperty("displayName", definition.displayName());
|
generator.writeStringProperty("displayName", definition.displayName());
|
||||||
generator.writeStringProperty("maxValidity", definition.maximumValidity().toString());
|
generator.writeStringProperty("maxValidity", definition.maximumValidity().toString());
|
||||||
|
writeAlgorithmBindings(generator, definition.algorithmBindingPolicy());
|
||||||
writeSubject(generator, definition.subjectPolicy());
|
writeSubject(generator, definition.subjectPolicy());
|
||||||
if (definition.certificateType() == CertificateProfileKind.END_ENTITY) {
|
if (definition.certificateType() == CertificateProfileKind.END_ENTITY) {
|
||||||
LeafCertificatePolicy leaf = definition.leafPolicy();
|
LeafCertificatePolicy leaf = definition.leafPolicy();
|
||||||
@@ -816,6 +904,29 @@ public final class CertificateProfileDocumentCodec {
|
|||||||
generator.writeEndObject();
|
generator.writeEndObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void writeAlgorithmBindings(JsonGenerator generator, X509AlgorithmBindingPolicy policy)
|
||||||
|
throws JacksonException {
|
||||||
|
generator.writeObjectPropertyStart("algorithmBindings");
|
||||||
|
generator.writeStringProperty("mode", policy.mode().name());
|
||||||
|
writeBindingReference(generator, "subjectPublicKey", policy.subjectPublicKey());
|
||||||
|
writeBindingReference(generator, "csrSignature", policy.csrSignature());
|
||||||
|
writeBindingReference(generator, "certificateSignature", policy.certificateSignature());
|
||||||
|
writeBindingReference(generator, "crlSignature", policy.crlSignature());
|
||||||
|
generator.writeEndObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeBindingReference(JsonGenerator generator, String field,
|
||||||
|
Optional<X509AlgorithmBindingPolicy.BindingReference> reference) throws JacksonException {
|
||||||
|
if (reference.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
X509AlgorithmBindingPolicy.BindingReference value = reference.orElseThrow();
|
||||||
|
generator.writeObjectPropertyStart(field);
|
||||||
|
generator.writeStringProperty("bindingId", value.bindingId());
|
||||||
|
generator.writeStringProperty("semanticCommitment", value.semanticCommitment());
|
||||||
|
generator.writeEndObject();
|
||||||
|
}
|
||||||
|
|
||||||
private static void writeSan(JsonGenerator generator, SubjectAlternativeNamePolicy policy) throws JacksonException {
|
private static void writeSan(JsonGenerator generator, SubjectAlternativeNamePolicy policy) throws JacksonException {
|
||||||
generator.writeObjectPropertyStart("subjectAlternativeNames");
|
generator.writeObjectPropertyStart("subjectAlternativeNames");
|
||||||
generator.writeNumberProperty("minimumTotal", policy.minimumTotal());
|
generator.writeNumberProperty("minimumTotal", policy.minimumTotal());
|
||||||
@@ -1098,7 +1209,8 @@ public final class CertificateProfileDocumentCodec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isAllowedAlgorithm(String value) {
|
private static boolean isAllowedAlgorithm(String value) {
|
||||||
return "RSA".equals(value) || "ECDSA".equals(value) || "Ed25519".equals(value) || "Ed448".equals(value);
|
return "RSA".equals(value) || "ECDSA".equals(value) || "Ed25519".equals(value) || "Ed448".equals(value)
|
||||||
|
|| "SPHINCS+".equals(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean hasUtf8Bom(byte[] value) {
|
private static boolean hasUtf8Bom(byte[] value) {
|
||||||
@@ -1124,6 +1236,7 @@ public final class CertificateProfileDocumentCodec {
|
|||||||
private String formatId;
|
private String formatId;
|
||||||
private String displayName;
|
private String displayName;
|
||||||
private Duration maximumValidity;
|
private Duration maximumValidity;
|
||||||
|
private X509AlgorithmBindingPolicy algorithmBindings;
|
||||||
private SubjectSection subject;
|
private SubjectSection subject;
|
||||||
private SanSection san;
|
private SanSection san;
|
||||||
private LeafSection leaf;
|
private LeafSection leaf;
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ public record LeafCertificatePolicy(SubjectAlternativeNamePolicy subjectAlternat
|
|||||||
boolean extendedKeyUsageCritical, boolean basicConstraintsCritical, Set<String> allowedSubjectKeyAlgorithmIds)
|
boolean extendedKeyUsageCritical, boolean basicConstraintsCritical, Set<String> allowedSubjectKeyAlgorithmIds)
|
||||||
implements CertificatePolicy {
|
implements CertificatePolicy {
|
||||||
|
|
||||||
private static final Set<String> SUPPORTED_SUBJECT_KEY_ALGORITHMS = Set.of("RSA", "ECDSA", "Ed25519", "Ed448");
|
private static final Set<String> SUPPORTED_SUBJECT_KEY_ALGORITHMS = Set.of("RSA", "ECDSA", "Ed25519", "Ed448",
|
||||||
|
"SPHINCS+");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates and constructs the policy.
|
* Validates and constructs the policy.
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* 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.api.profile;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immutable explicit X.509 representation policy persisted with a certificate
|
||||||
|
* profile.
|
||||||
|
*
|
||||||
|
* <p>Standard mode never falls back to a private OID. Explicit mode pins stable
|
||||||
|
* binding identifiers and their immutable semantic commitments. Raw OIDs and DER
|
||||||
|
* parameters are deliberately absent from this model.</p>
|
||||||
|
*
|
||||||
|
* @param mode standard-only or explicit binding selection
|
||||||
|
* @param subjectPublicKey optional SPKI binding
|
||||||
|
* @param csrSignature optional CSR-signature binding
|
||||||
|
* @param certificateSignature optional certificate-signature binding
|
||||||
|
* @param crlSignature optional CRL-signature binding
|
||||||
|
*/
|
||||||
|
public record X509AlgorithmBindingPolicy(Mode mode, Optional<BindingReference> subjectPublicKey,
|
||||||
|
Optional<BindingReference> csrSignature, Optional<BindingReference> certificateSignature,
|
||||||
|
Optional<BindingReference> crlSignature) {
|
||||||
|
|
||||||
|
/** Selection mode. */
|
||||||
|
public enum Mode {
|
||||||
|
/** Only sealed official bindings may be selected. */
|
||||||
|
STANDARD_ONLY,
|
||||||
|
/** Every custom representation is selected by stable binding identity. */
|
||||||
|
EXPLICIT
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stable reference to one immutable binding contract.
|
||||||
|
*
|
||||||
|
* @param bindingId stable binding identity
|
||||||
|
* @param semanticCommitment exact contract commitment
|
||||||
|
*/
|
||||||
|
public record BindingReference(String bindingId, String semanticCommitment) {
|
||||||
|
/** Validates one binding reference. */
|
||||||
|
public BindingReference {
|
||||||
|
if (bindingId == null || bindingId.isBlank() || semanticCommitment == null
|
||||||
|
|| semanticCommitment.isBlank() || semanticCommitment.length() > 1024) {
|
||||||
|
throw new IllegalArgumentException("X.509 binding reference is invalid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Validates and snapshots the policy. */
|
||||||
|
public X509AlgorithmBindingPolicy {
|
||||||
|
Objects.requireNonNull(mode, "mode");
|
||||||
|
subjectPublicKey = Objects.requireNonNull(subjectPublicKey, "subjectPublicKey");
|
||||||
|
csrSignature = Objects.requireNonNull(csrSignature, "csrSignature");
|
||||||
|
certificateSignature = Objects.requireNonNull(certificateSignature, "certificateSignature");
|
||||||
|
crlSignature = Objects.requireNonNull(crlSignature, "crlSignature");
|
||||||
|
boolean selected = subjectPublicKey.isPresent() || csrSignature.isPresent()
|
||||||
|
|| certificateSignature.isPresent() || crlSignature.isPresent();
|
||||||
|
if (mode == Mode.STANDARD_ONLY == selected) {
|
||||||
|
throw new IllegalArgumentException("X.509 binding mode and selections disagree");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return standard-only policy with no private fallback */
|
||||||
|
public static X509AlgorithmBindingPolicy standardOnly() {
|
||||||
|
return new X509AlgorithmBindingPolicy(Mode.STANDARD_ONLY, Optional.empty(), Optional.empty(),
|
||||||
|
Optional.empty(), Optional.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,6 +53,8 @@ import zeroecho.pki.api.PublicationService;
|
|||||||
import zeroecho.pki.api.ProfileService;
|
import zeroecho.pki.api.ProfileService;
|
||||||
import zeroecho.pki.api.RevocationService;
|
import zeroecho.pki.api.RevocationService;
|
||||||
import zeroecho.pki.api.StatusObjectService;
|
import zeroecho.pki.api.StatusObjectService;
|
||||||
|
import zeroecho.pki.api.algorithm.X509AlgorithmBinding;
|
||||||
|
import zeroecho.pki.api.algorithm.X509AlgorithmBindingRegistry;
|
||||||
import zeroecho.pki.api.ca.CaRecord;
|
import zeroecho.pki.api.ca.CaRecord;
|
||||||
import zeroecho.pki.api.credential.CaProfileBinding;
|
import zeroecho.pki.api.credential.CaProfileBinding;
|
||||||
import zeroecho.pki.api.credential.Credential;
|
import zeroecho.pki.api.credential.Credential;
|
||||||
@@ -62,6 +64,7 @@ import zeroecho.pki.api.profile.CertificateProfileDefinition;
|
|||||||
import zeroecho.pki.api.profile.CertificateProfileDocumentCodec;
|
import zeroecho.pki.api.profile.CertificateProfileDocumentCodec;
|
||||||
import zeroecho.pki.api.profile.CertificateProfileRef;
|
import zeroecho.pki.api.profile.CertificateProfileRef;
|
||||||
import zeroecho.pki.api.profile.ImportedCertificateProfileVersion;
|
import zeroecho.pki.api.profile.ImportedCertificateProfileVersion;
|
||||||
|
import zeroecho.pki.api.profile.X509AlgorithmBindingPolicy;
|
||||||
import zeroecho.pki.api.issuance.IssueEndEntityCommand;
|
import zeroecho.pki.api.issuance.IssueEndEntityCommand;
|
||||||
import zeroecho.pki.api.issuance.VerificationPolicy;
|
import zeroecho.pki.api.issuance.VerificationPolicy;
|
||||||
import zeroecho.pki.api.publication.PublicationRequest;
|
import zeroecho.pki.api.publication.PublicationRequest;
|
||||||
@@ -101,12 +104,13 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
|||||||
private final Optional<IssuanceService> issuance;
|
private final Optional<IssuanceService> issuance;
|
||||||
private final Optional<StatusObjectService> statusObjects;
|
private final Optional<StatusObjectService> statusObjects;
|
||||||
private final Optional<PublicationService> publications;
|
private final Optional<PublicationService> publications;
|
||||||
|
private final X509AlgorithmBindingRegistry algorithmBindings;
|
||||||
private final Runnable openCheck;
|
private final Runnable openCheck;
|
||||||
|
|
||||||
/* default */ DefaultPkiOperationExecutor(PkiSessionConfiguration configuration, PkiStore store,
|
/* default */ DefaultPkiOperationExecutor(PkiSessionConfiguration configuration, PkiStore store,
|
||||||
ProfileService profiles, RevocationService revocations, Runnable openCheck) {
|
ProfileService profiles, RevocationService revocations, Runnable openCheck) {
|
||||||
this(configuration, store, profiles, revocations, Optional.empty(), Optional.empty(), Optional.empty(),
|
this(configuration, store, profiles, revocations, Optional.empty(), Optional.empty(), Optional.empty(),
|
||||||
Optional.empty(), Optional.empty(), openCheck);
|
Optional.empty(), Optional.empty(), defaultBindings(), openCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* default */ DefaultPkiOperationExecutor(PkiSessionConfiguration configuration, PkiStore store,
|
/* default */ DefaultPkiOperationExecutor(PkiSessionConfiguration configuration, PkiStore store,
|
||||||
@@ -114,6 +118,16 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
|||||||
Optional<CertificationRequestService> requests, Optional<IssuanceService> issuance,
|
Optional<CertificationRequestService> requests, Optional<IssuanceService> issuance,
|
||||||
Optional<StatusObjectService> statusObjects, Optional<PublicationService> publications,
|
Optional<StatusObjectService> statusObjects, Optional<PublicationService> publications,
|
||||||
Runnable openCheck) {
|
Runnable openCheck) {
|
||||||
|
this(configuration, store, profiles, revocations, authorities, requests, issuance, statusObjects,
|
||||||
|
publications, defaultBindings(), 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,
|
||||||
|
X509AlgorithmBindingRegistry algorithmBindings,
|
||||||
|
Runnable openCheck) {
|
||||||
this.configuration = Objects.requireNonNull(configuration, "configuration");
|
this.configuration = Objects.requireNonNull(configuration, "configuration");
|
||||||
this.store = Objects.requireNonNull(store, "store");
|
this.store = Objects.requireNonNull(store, "store");
|
||||||
this.profiles = Objects.requireNonNull(profiles, "profiles");
|
this.profiles = Objects.requireNonNull(profiles, "profiles");
|
||||||
@@ -123,6 +137,7 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
|||||||
this.issuance = Objects.requireNonNull(issuance, "issuance");
|
this.issuance = Objects.requireNonNull(issuance, "issuance");
|
||||||
this.statusObjects = Objects.requireNonNull(statusObjects, "statusObjects");
|
this.statusObjects = Objects.requireNonNull(statusObjects, "statusObjects");
|
||||||
this.publications = Objects.requireNonNull(publications, "publications");
|
this.publications = Objects.requireNonNull(publications, "publications");
|
||||||
|
this.algorithmBindings = Objects.requireNonNull(algorithmBindings, "algorithmBindings");
|
||||||
this.openCheck = Objects.requireNonNull(openCheck, "openCheck");
|
this.openCheck = Objects.requireNonNull(openCheck, "openCheck");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,6 +149,9 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
|||||||
try {
|
try {
|
||||||
signal.throwIfCancelled();
|
signal.throwIfCancelled();
|
||||||
PkiOperationResult result = switch (exact) {
|
PkiOperationResult result = switch (exact) {
|
||||||
|
case PkiOperation.ListAlgorithmBindings request -> listAlgorithmBindings(request);
|
||||||
|
case PkiOperation.InspectAlgorithmBinding request -> inspectAlgorithmBinding(request);
|
||||||
|
case PkiOperation.ValidateAlgorithmBindings request -> validateAlgorithmBindings(request);
|
||||||
case PkiOperation.ValidateConfiguration ignored -> validateConfiguration();
|
case PkiOperation.ValidateConfiguration ignored -> validateConfiguration();
|
||||||
case PkiOperation.ValidateProfile request -> validateProfile(request);
|
case PkiOperation.ValidateProfile request -> validateProfile(request);
|
||||||
case PkiOperation.RegisterProfile request -> registerProfile(request);
|
case PkiOperation.RegisterProfile request -> registerProfile(request);
|
||||||
@@ -162,7 +180,7 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
|||||||
case PkiOperation.ProcessPublication request -> processPublication(request);
|
case PkiOperation.ProcessPublication request -> processPublication(request);
|
||||||
case PkiOperation.RetryPublication request -> retryPublication(request);
|
case PkiOperation.RetryPublication request -> retryPublication(request);
|
||||||
case PkiOperation.ReconcilePublication request -> reconcilePublication(request);
|
case PkiOperation.ReconcilePublication request -> reconcilePublication(request);
|
||||||
};
|
};
|
||||||
return new PkiOperationOutcome.Success(result);
|
return new PkiOperationOutcome.Success(result);
|
||||||
} catch (InterruptedIOException failure) {
|
} catch (InterruptedIOException failure) {
|
||||||
return failure(exact.name(), PkiOperationFailure.CANCELLED, "OPERATION_CANCELLED");
|
return failure(exact.name(), PkiOperationFailure.CANCELLED, "OPERATION_CANCELLED");
|
||||||
@@ -173,6 +191,61 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PkiOperationResult listAlgorithmBindings(PkiOperation.ListAlgorithmBindings request) {
|
||||||
|
List<PkiOperationValue> values = algorithmBindings.bindings().stream()
|
||||||
|
.filter(binding -> request.origin().isEmpty() || binding.origin().name().equals(request.origin().get()))
|
||||||
|
.filter(binding -> request.role().isEmpty() || binding.role().name().equals(request.role().get()))
|
||||||
|
.limit(request.limit()).map(binding -> (PkiOperationValue) bindingValue(binding)).toList();
|
||||||
|
Map<String, PkiOperationValue> fields = fields();
|
||||||
|
fields.put(COUNT, integer(values.size()));
|
||||||
|
fields.put("registryCommitment", text(algorithmBindings.commitment()));
|
||||||
|
fields.put("bindings", new PkiOperationValue.ListValue(values));
|
||||||
|
return result(request.name(), fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PkiOperationResult inspectAlgorithmBinding(PkiOperation.InspectAlgorithmBinding request) {
|
||||||
|
X509AlgorithmBinding binding = algorithmBindings.find(request.bindingId())
|
||||||
|
.orElseThrow(MissingObjectException::new);
|
||||||
|
Map<String, PkiOperationValue> fields = fields();
|
||||||
|
fields.putAll(bindingValue(binding).fields());
|
||||||
|
fields.put("registryCommitment", text(algorithmBindings.commitment()));
|
||||||
|
return result(request.name(), fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PkiOperationResult validateAlgorithmBindings(PkiOperation.ValidateAlgorithmBindings request) {
|
||||||
|
request.bindingId().ifPresent(bindingId -> algorithmBindings.require(bindingId,
|
||||||
|
request.expectedCommitment().orElseThrow()));
|
||||||
|
Map<String, PkiOperationValue> fields = fields();
|
||||||
|
fields.put("valid", bool(true));
|
||||||
|
fields.put(COUNT, integer(algorithmBindings.bindings().size()));
|
||||||
|
fields.put("registryCommitment", text(algorithmBindings.commitment()));
|
||||||
|
return result(request.name(), fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PkiOperationValue.ObjectValue bindingValue(X509AlgorithmBinding binding) {
|
||||||
|
Map<String, PkiOperationValue> fields = fields();
|
||||||
|
fields.put("bindingId", text(binding.bindingId()));
|
||||||
|
fields.put("algorithmId", text(binding.algorithmIdentity().canonicalForm()));
|
||||||
|
fields.put("role", text(binding.role().name()));
|
||||||
|
fields.put("oid", text(binding.oid()));
|
||||||
|
fields.put("origin", text(binding.origin().name()));
|
||||||
|
fields.put("encodingVersion", integer(binding.encodingVersion()));
|
||||||
|
fields.put("parameterRule", text(binding.parameterRule().name()));
|
||||||
|
fields.put("publicKeyEncoding", text(binding.publicKeyEncoding().name()));
|
||||||
|
fields.put("signatureEncoding", text(binding.signatureEncoding().name()));
|
||||||
|
fields.put("interoperability", text(binding.interoperability().name()));
|
||||||
|
fields.put("bindingCommitment", text(binding.semanticCommitment()));
|
||||||
|
binding.providerId().ifPresent(value -> fields.put("providerId", text(value)));
|
||||||
|
return new PkiOperationValue.ObjectValue(fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static X509AlgorithmBindingRegistry defaultBindings() {
|
||||||
|
List<X509AlgorithmBinding> descriptors = new ArrayList<>(
|
||||||
|
zeroecho.pki.impl.framework.x509.StandardX509Bindings.descriptors());
|
||||||
|
descriptors.addAll(zeroecho.pki.impl.framework.x509.ZeroEchoPrivateX509Bindings.descriptors());
|
||||||
|
return zeroecho.pki.impl.framework.x509.ImmutableX509AlgorithmBindingRegistry.create(descriptors);
|
||||||
|
}
|
||||||
|
|
||||||
private PkiOperationResult validateConfiguration() {
|
private PkiOperationResult validateConfiguration() {
|
||||||
Map<String, PkiOperationValue> fields = fields();
|
Map<String, PkiOperationValue> fields = fields();
|
||||||
fields.put("version", integer(configuration.version()));
|
fields.put("version", integer(configuration.version()));
|
||||||
@@ -181,8 +254,9 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
|||||||
return result(PkiOperation.ValidateConfiguration.NAME, fields);
|
return result(PkiOperation.ValidateConfiguration.NAME, fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PkiOperationResult validateProfile(PkiOperation.ValidateProfile request) {
|
private PkiOperationResult validateProfile(PkiOperation.ValidateProfile request) {
|
||||||
CertificateProfileDefinition definition = CertificateProfileDocumentCodec.parse(request.document());
|
CertificateProfileDefinition definition = CertificateProfileDocumentCodec.parse(request.document());
|
||||||
|
validateProfileBindings(definition.algorithmBindingPolicy());
|
||||||
Map<String, PkiOperationValue> fields = fields();
|
Map<String, PkiOperationValue> fields = fields();
|
||||||
fields.put("profileId", text(definition.profileId()));
|
fields.put("profileId", text(definition.profileId()));
|
||||||
fields.put("profileVersion", integer(definition.profileVersion()));
|
fields.put("profileVersion", integer(definition.profileVersion()));
|
||||||
@@ -191,6 +265,26 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
|||||||
return result(request.name(), fields);
|
return result(request.name(), fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateProfileBindings(X509AlgorithmBindingPolicy policy) {
|
||||||
|
if (policy.mode() == X509AlgorithmBindingPolicy.Mode.STANDARD_ONLY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
validateProfileBinding(policy.subjectPublicKey(), X509AlgorithmBinding.Role.SUBJECT_PUBLIC_KEY);
|
||||||
|
validateProfileBinding(policy.csrSignature(), X509AlgorithmBinding.Role.CSR_SIGNATURE);
|
||||||
|
validateProfileBinding(policy.certificateSignature(), X509AlgorithmBinding.Role.CERTIFICATE_SIGNATURE);
|
||||||
|
validateProfileBinding(policy.crlSignature(), X509AlgorithmBinding.Role.CRL_SIGNATURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateProfileBinding(Optional<X509AlgorithmBindingPolicy.BindingReference> reference,
|
||||||
|
X509AlgorithmBinding.Role role) {
|
||||||
|
reference.ifPresent(value -> {
|
||||||
|
X509AlgorithmBinding binding = algorithmBindings.require(value.bindingId(), value.semanticCommitment());
|
||||||
|
if (binding.role() != role || binding.origin() == X509AlgorithmBinding.Origin.STANDARD) {
|
||||||
|
throw new IllegalArgumentException("Explicit profile binding is invalid for its role");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private PkiOperationResult registerProfile(PkiOperation.RegisterProfile request) {
|
private PkiOperationResult registerProfile(PkiOperation.RegisterProfile request) {
|
||||||
CertificateProfileRef reference = profiles.importProfile(request.document());
|
CertificateProfileRef reference = profiles.importProfile(request.document());
|
||||||
return profileReferenceResult(request.name(), reference);
|
return profileReferenceResult(request.name(), reference);
|
||||||
|
|||||||
@@ -36,9 +36,13 @@ package zeroecho.pki.application;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.ServiceLoader;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
@@ -51,11 +55,16 @@ import zeroecho.core.spi.KeyringUnlockProvider;
|
|||||||
import zeroecho.core.storage.KeyringPassword;
|
import zeroecho.core.storage.KeyringPassword;
|
||||||
import zeroecho.pki.api.CaService;
|
import zeroecho.pki.api.CaService;
|
||||||
import zeroecho.pki.api.CertificationRequestService;
|
import zeroecho.pki.api.CertificationRequestService;
|
||||||
|
import zeroecho.pki.api.EncodedObject;
|
||||||
|
import zeroecho.pki.api.Encoding;
|
||||||
|
import zeroecho.pki.api.PkiException;
|
||||||
import zeroecho.pki.api.IssuanceService;
|
import zeroecho.pki.api.IssuanceService;
|
||||||
import zeroecho.pki.api.PublicationService;
|
import zeroecho.pki.api.PublicationService;
|
||||||
import zeroecho.pki.api.ProfileService;
|
import zeroecho.pki.api.ProfileService;
|
||||||
import zeroecho.pki.api.RevocationService;
|
import zeroecho.pki.api.RevocationService;
|
||||||
import zeroecho.pki.api.StatusObjectService;
|
import zeroecho.pki.api.StatusObjectService;
|
||||||
|
import zeroecho.pki.api.algorithm.X509AlgorithmBinding;
|
||||||
|
import zeroecho.pki.api.algorithm.X509AlgorithmBindingRegistry;
|
||||||
import zeroecho.pki.api.credential.EffectiveCredentialStatusResolver;
|
import zeroecho.pki.api.credential.EffectiveCredentialStatusResolver;
|
||||||
import zeroecho.pki.impl.core.DefaultProfileService;
|
import zeroecho.pki.impl.core.DefaultProfileService;
|
||||||
import zeroecho.pki.impl.core.DefaultRevocationService;
|
import zeroecho.pki.impl.core.DefaultRevocationService;
|
||||||
@@ -65,15 +74,22 @@ import zeroecho.pki.impl.core.DefaultIssuanceService;
|
|||||||
import zeroecho.pki.impl.core.DefaultPublicationService;
|
import zeroecho.pki.impl.core.DefaultPublicationService;
|
||||||
import zeroecho.pki.impl.core.DefaultStatusObjectService;
|
import zeroecho.pki.impl.core.DefaultStatusObjectService;
|
||||||
import zeroecho.pki.impl.core.StoreBackedEffectiveCredentialStatusResolver;
|
import zeroecho.pki.impl.core.StoreBackedEffectiveCredentialStatusResolver;
|
||||||
|
import zeroecho.pki.impl.core.PublicKeyInfoResolver;
|
||||||
import zeroecho.pki.impl.core.async.PkiSigningBus;
|
import zeroecho.pki.impl.core.async.PkiSigningBus;
|
||||||
import zeroecho.pki.impl.framework.x509.X509AlgorithmResolver;
|
import zeroecho.pki.impl.framework.x509.X509AlgorithmResolver;
|
||||||
import zeroecho.pki.impl.framework.x509.X509AuthoritySnapshot;
|
import zeroecho.pki.impl.framework.x509.X509AuthoritySnapshot;
|
||||||
|
import zeroecho.pki.impl.framework.x509.ImmutableX509AlgorithmBindingRegistry;
|
||||||
|
import zeroecho.pki.impl.framework.x509.StandardX509Bindings;
|
||||||
|
import zeroecho.pki.impl.framework.x509.ZeroEchoPrivateX509Bindings;
|
||||||
|
import zeroecho.pki.impl.framework.x509.X509BindingRuleProvider;
|
||||||
import zeroecho.pki.impl.framework.x509.bc.BcX509CredentialFramework;
|
import zeroecho.pki.impl.framework.x509.bc.BcX509CredentialFramework;
|
||||||
import zeroecho.pki.impl.framework.x509.bc.BcX509CredentialIssuerBackend;
|
import zeroecho.pki.impl.framework.x509.bc.BcX509CredentialIssuerBackend;
|
||||||
import zeroecho.pki.impl.framework.x509.bc.BcX509ProofOfPossessionVerifier;
|
import zeroecho.pki.impl.framework.x509.bc.BcX509ProofOfPossessionVerifier;
|
||||||
|
import zeroecho.pki.impl.framework.x509.bc.BcX509PublicKeyAdapter;
|
||||||
import zeroecho.pki.impl.framework.x509.bc.BcX509StatusObjectGenerator;
|
import zeroecho.pki.impl.framework.x509.bc.BcX509StatusObjectGenerator;
|
||||||
import zeroecho.pki.impl.framework.x509.bc.BcX509VerificationExecutor;
|
import zeroecho.pki.impl.framework.x509.bc.BcX509VerificationExecutor;
|
||||||
import zeroecho.pki.spi.ProviderConfig;
|
import zeroecho.pki.spi.ProviderConfig;
|
||||||
|
import zeroecho.pki.spi.algorithm.X509AlgorithmBindingProvider;
|
||||||
import zeroecho.pki.spi.audit.AuditSink;
|
import zeroecho.pki.spi.audit.AuditSink;
|
||||||
import zeroecho.pki.spi.bootstrap.PkiBootstrap;
|
import zeroecho.pki.spi.bootstrap.PkiBootstrap;
|
||||||
import zeroecho.pki.spi.crypto.PublicKeyInfoSource;
|
import zeroecho.pki.spi.crypto.PublicKeyInfoSource;
|
||||||
@@ -83,6 +99,7 @@ import zeroecho.pki.spi.publish.Publisher;
|
|||||||
import zeroecho.pki.spi.store.PkiStore;
|
import zeroecho.pki.spi.store.PkiStore;
|
||||||
|
|
||||||
/** Default synchronous session composition. */
|
/** Default synchronous session composition. */
|
||||||
|
@SuppressWarnings({ "PMD.CouplingBetweenObjects", "PMD.CyclomaticComplexity" })
|
||||||
final class DefaultPkiSession implements PkiSession {
|
final class DefaultPkiSession implements PkiSession {
|
||||||
|
|
||||||
private final PkiStore store;
|
private final PkiStore store;
|
||||||
@@ -96,11 +113,13 @@ final class DefaultPkiSession implements PkiSession {
|
|||||||
private final Optional<PublicationService> publications;
|
private final Optional<PublicationService> publications;
|
||||||
private final Optional<PkiSigningBus> signingBus;
|
private final Optional<PkiSigningBus> signingBus;
|
||||||
private final Optional<SignatureWorkflow> signatureWorkflow;
|
private final Optional<SignatureWorkflow> signatureWorkflow;
|
||||||
|
private final X509AlgorithmBindingRegistry algorithmBindings;
|
||||||
private final PkiOperationExecutor operations;
|
private final PkiOperationExecutor operations;
|
||||||
private final AtomicBoolean closed = new AtomicBoolean();
|
private final AtomicBoolean closed = new AtomicBoolean();
|
||||||
|
|
||||||
private DefaultPkiSession(PkiSessionConfiguration configuration, PkiStore store, AuditSink audit,
|
private DefaultPkiSession(PkiSessionConfiguration configuration, PkiStore store, AuditSink audit,
|
||||||
ProfileService profiles, RevocationService revocations, ServiceGraph graph) {
|
ProfileService profiles, RevocationService revocations, X509AlgorithmBindingRegistry algorithmBindings,
|
||||||
|
ServiceGraph graph) {
|
||||||
this.store = store;
|
this.store = store;
|
||||||
this.audit = audit;
|
this.audit = audit;
|
||||||
this.profiles = Objects.requireNonNull(profiles, "profiles");
|
this.profiles = Objects.requireNonNull(profiles, "profiles");
|
||||||
@@ -112,8 +131,9 @@ final class DefaultPkiSession implements PkiSession {
|
|||||||
this.publications = graph.publications();
|
this.publications = graph.publications();
|
||||||
this.signingBus = graph.signingBus();
|
this.signingBus = graph.signingBus();
|
||||||
this.signatureWorkflow = graph.signatureWorkflow();
|
this.signatureWorkflow = graph.signatureWorkflow();
|
||||||
|
this.algorithmBindings = Objects.requireNonNull(algorithmBindings, "algorithmBindings");
|
||||||
this.operations = new DefaultPkiOperationExecutor(configuration, store, profiles, revocations, authorities,
|
this.operations = new DefaultPkiOperationExecutor(configuration, store, profiles, revocations, authorities,
|
||||||
requests, issuance, statusObjects, publications, this::requireOpen);
|
requests, issuance, statusObjects, publications, algorithmBindings, this::requireOpen);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* default */ static PkiSession open(PkiSessionConfiguration configuration) {
|
/* default */ static PkiSession open(PkiSessionConfiguration configuration) {
|
||||||
@@ -146,6 +166,9 @@ final class DefaultPkiSession implements PkiSession {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
exact.publishers().forEach(bootstrap::validatePublisher);
|
exact.publishers().forEach(bootstrap::validatePublisher);
|
||||||
|
BindingComposition bindingComposition = composeBindings(exact);
|
||||||
|
X509AlgorithmBindingRegistry algorithmBindings = bindingComposition.registry();
|
||||||
|
exact.signing().ifPresent(signing -> validateConfiguredBindings(signing, algorithmBindings));
|
||||||
|
|
||||||
PkiStore store = null;
|
PkiStore store = null;
|
||||||
AuditSink audit = null;
|
AuditSink audit = null;
|
||||||
@@ -153,10 +176,15 @@ final class DefaultPkiSession implements PkiSession {
|
|||||||
try {
|
try {
|
||||||
store = Objects.requireNonNull(bootstrap.openStore(exact.store()), "opened store");
|
store = Objects.requireNonNull(bootstrap.openStore(exact.store()), "opened store");
|
||||||
audit = Objects.requireNonNull(bootstrap.openAudit(exact.audit()), "opened audit sink");
|
audit = Objects.requireNonNull(bootstrap.openAudit(exact.audit()), "opened audit sink");
|
||||||
ProfileService profiles = new DefaultProfileService(store, clock, audit);
|
ProfileService profiles = new DefaultProfileService(store, clock, audit, algorithmBindings,
|
||||||
|
exact.signing().isPresent(),
|
||||||
|
exact.signing().flatMap(PkiSessionConfiguration.SigningConfiguration::certificateSignatureBinding),
|
||||||
|
exact.signing().flatMap(PkiSessionConfiguration.SigningConfiguration::crlSignatureBinding),
|
||||||
|
exact.signing().flatMap(PkiSessionConfiguration.SigningConfiguration::subjectPublicKeyBinding));
|
||||||
RevocationService revocations = new DefaultRevocationService(store, clock, audit);
|
RevocationService revocations = new DefaultRevocationService(store, clock, audit);
|
||||||
graph = composeGraph(exact, dependencies, store, audit, profiles, clock, bootstrap);
|
graph = composeGraph(exact, dependencies, store, audit, profiles, clock, bootstrap,
|
||||||
return new DefaultPkiSession(exact, store, audit, profiles, revocations, graph);
|
bindingComposition.providers());
|
||||||
|
return new DefaultPkiSession(exact, store, audit, profiles, revocations, algorithmBindings, graph);
|
||||||
} catch (RuntimeException | Error primary) {
|
} catch (RuntimeException | Error primary) {
|
||||||
closeGraphAfterConstructionFailure(graph, primary);
|
closeGraphAfterConstructionFailure(graph, primary);
|
||||||
closeAfterConstructionFailure(audit, store, primary);
|
closeAfterConstructionFailure(audit, store, primary);
|
||||||
@@ -206,6 +234,12 @@ final class DefaultPkiSession implements PkiSession {
|
|||||||
return publications;
|
return publications;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public X509AlgorithmBindingRegistry algorithmBindings() {
|
||||||
|
requireOpen();
|
||||||
|
return algorithmBindings;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PkiOperationExecutor operations() {
|
public PkiOperationExecutor operations() {
|
||||||
requireOpen();
|
requireOpen();
|
||||||
@@ -247,7 +281,7 @@ final class DefaultPkiSession implements PkiSession {
|
|||||||
@SuppressWarnings({ "PMD.AvoidCatchingGenericException", "PMD.CloseResource", "PMD.ExceptionAsFlowControl" })
|
@SuppressWarnings({ "PMD.AvoidCatchingGenericException", "PMD.CloseResource", "PMD.ExceptionAsFlowControl" })
|
||||||
private static ServiceGraph composeGraph(PkiSessionConfiguration configuration,
|
private static ServiceGraph composeGraph(PkiSessionConfiguration configuration,
|
||||||
PkiSessionRuntimeDependencies dependencies, PkiStore store, AuditSink audit, ProfileService profiles,
|
PkiSessionRuntimeDependencies dependencies, PkiStore store, AuditSink audit, ProfileService profiles,
|
||||||
Clock clock, Bootstrap bootstrap) {
|
Clock clock, Bootstrap bootstrap, List<X509BindingRuleProvider> bindingProviders) {
|
||||||
Optional<SignatureWorkflow> workflow = Optional.empty();
|
Optional<SignatureWorkflow> workflow = Optional.empty();
|
||||||
Optional<PkiSigningBus> bus = Optional.empty();
|
Optional<PkiSigningBus> bus = Optional.empty();
|
||||||
Optional<CaService> authorities = Optional.empty();
|
Optional<CaService> authorities = Optional.empty();
|
||||||
@@ -267,14 +301,15 @@ final class DefaultPkiSession implements PkiSession {
|
|||||||
throw new IllegalStateException("Signature workflow lacks managed public-key capability");
|
throw new IllegalStateException("Signature workflow lacks managed public-key capability");
|
||||||
}
|
}
|
||||||
BcX509VerificationExecutor verification = new BcX509VerificationExecutor();
|
BcX509VerificationExecutor verification = new BcX509VerificationExecutor();
|
||||||
X509AuthoritySnapshot authority = authority(openedWorkflow, verification);
|
X509AuthoritySnapshot authority = authority(openedWorkflow, verification, bindingProviders);
|
||||||
PkiSigningBus openedBus = new PkiSigningBus(store, openedWorkflow, Path.of(signing.busPath()),
|
PkiSigningBus openedBus = new PkiSigningBus(store, openedWorkflow, Path.of(signing.busPath()),
|
||||||
authority);
|
authority);
|
||||||
bus = Optional.of(openedBus);
|
bus = Optional.of(openedBus);
|
||||||
|
AlgorithmIdentity signatureIdentity = authority.resolveIdentity(signing.signatureAlgorithm());
|
||||||
BcX509CredentialIssuerBackend issuerBackend = new BcX509CredentialIssuerBackend(openedBus,
|
BcX509CredentialIssuerBackend issuerBackend = new BcX509CredentialIssuerBackend(openedBus,
|
||||||
signing.signatureAlgorithm(), signing.signingTtl());
|
signatureIdentity, signing.certificateSignatureBinding(), signing.signingTtl());
|
||||||
BcX509StatusObjectGenerator statusGenerator = new BcX509StatusObjectGenerator(openedBus,
|
BcX509StatusObjectGenerator statusGenerator = new BcX509StatusObjectGenerator(openedBus,
|
||||||
signing.signatureAlgorithm(), signing.signingTtl());
|
signatureIdentity, signing.crlSignatureBinding(), signing.signingTtl());
|
||||||
BcX509CredentialFramework framework = new BcX509CredentialFramework(authority, verification)
|
BcX509CredentialFramework framework = new BcX509CredentialFramework(authority, verification)
|
||||||
.wired(statusGenerator, new BcX509ProofOfPossessionVerifier(authority, verification));
|
.wired(statusGenerator, new BcX509ProofOfPossessionVerifier(authority, verification));
|
||||||
EffectiveCredentialStatusResolver statusResolver = new StoreBackedEffectiveCredentialStatusResolver(
|
EffectiveCredentialStatusResolver statusResolver = new StoreBackedEffectiveCredentialStatusResolver(
|
||||||
@@ -285,8 +320,8 @@ final class DefaultPkiSession implements PkiSession {
|
|||||||
statuses = Optional.of(new DefaultStatusObjectService(store, framework, audit, statusResolver,
|
statuses = Optional.of(new DefaultStatusObjectService(store, framework, audit, statusResolver,
|
||||||
authority));
|
authority));
|
||||||
authorities = Optional.of(new DefaultCaService(store, framework, issuerBackend,
|
authorities = Optional.of(new DefaultCaService(store, framework, issuerBackend,
|
||||||
publicKeys::resolvePublicKeyInfo, openedBus, audit, statusResolver, profiles, clock,
|
publicKeySource(publicKeys, authority, signing), openedBus, audit, statusResolver, profiles, clock,
|
||||||
signing.signatureAlgorithm(), signing.signingTtl()));
|
signing.signatureAlgorithm(), signing.certificateSignatureBinding(), signing.signingTtl()));
|
||||||
}
|
}
|
||||||
List<Publisher> configuredPublishers = new ArrayList<>();
|
List<Publisher> configuredPublishers = new ArrayList<>();
|
||||||
for (ProviderConfig publisher : configuration.publishers()) {
|
for (ProviderConfig publisher : configuration.publishers()) {
|
||||||
@@ -304,7 +339,7 @@ final class DefaultPkiSession implements PkiSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static X509AuthoritySnapshot authority(SignatureWorkflow workflow,
|
private static X509AuthoritySnapshot authority(SignatureWorkflow workflow,
|
||||||
BcX509VerificationExecutor verification) {
|
BcX509VerificationExecutor verification, List<X509BindingRuleProvider> bindingProviders) {
|
||||||
Set<String> supported = Set.copyOf(workflow.supportedAlgorithms());
|
Set<String> supported = Set.copyOf(workflow.supportedAlgorithms());
|
||||||
Set<AlgorithmIdentity> identities = BootstrapAlgorithmIdentities.catalog().identities().stream()
|
Set<AlgorithmIdentity> identities = BootstrapAlgorithmIdentities.catalog().identities().stream()
|
||||||
.filter(identity -> identity.kind() == AlgorithmIdentity.Kind.SIGNATURE)
|
.filter(identity -> identity.kind() == AlgorithmIdentity.Kind.SIGNATURE)
|
||||||
@@ -342,13 +377,153 @@ final class DefaultPkiSession implements PkiSession {
|
|||||||
return "pki-session-bootstrap-policy-v1:configured-capabilities-only";
|
return "pki-session-bootstrap-policy-v1:configured-capabilities-only";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return X509AuthoritySnapshot.compose(List.of(), List.of(signingProvider, verification),
|
return X509AuthoritySnapshot.compose(bindingProviders, List.of(signingProvider, verification),
|
||||||
List.of(X509AuthoritySnapshot.bindExecutor(implementationId,
|
List.of(X509AuthoritySnapshot.bindExecutor(implementationId,
|
||||||
AlgorithmExecutionCapability.Direction.SIGN, workflow),
|
AlgorithmExecutionCapability.Direction.SIGN, workflow),
|
||||||
X509AuthoritySnapshot.bindExecutor(BcX509VerificationExecutor.IMPLEMENTATION_ID,
|
X509AuthoritySnapshot.bindExecutor(BcX509VerificationExecutor.IMPLEMENTATION_ID,
|
||||||
AlgorithmExecutionCapability.Direction.VERIFY, verification)), policy);
|
AlgorithmExecutionCapability.Direction.VERIFY, verification)), policy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static BindingComposition composeBindings(PkiSessionConfiguration configuration) {
|
||||||
|
List<X509AlgorithmBindingProvider> discovered = ServiceLoader.load(X509AlgorithmBindingProvider.class)
|
||||||
|
.stream().map(ServiceLoader.Provider::get)
|
||||||
|
.sorted(Comparator.comparing(X509AlgorithmBindingProvider::providerId)).toList();
|
||||||
|
Map<String, X509AlgorithmBindingProvider> byId = new LinkedHashMap<>();
|
||||||
|
for (X509AlgorithmBindingProvider provider : discovered) {
|
||||||
|
if (provider.providerId() == null || provider.providerId().isBlank()
|
||||||
|
|| byId.putIfAbsent(provider.providerId(), provider) != null) {
|
||||||
|
throw new IllegalArgumentException("Duplicate or invalid X.509 binding-provider identity");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<X509AlgorithmBinding> descriptors = new ArrayList<>(StandardX509Bindings.descriptors());
|
||||||
|
descriptors.addAll(ZeroEchoPrivateX509Bindings.descriptors());
|
||||||
|
List<X509BindingRuleProvider> ruleProviders = new ArrayList<>();
|
||||||
|
for (PkiSessionConfiguration.BindingProviderConfiguration activation : configuration.bindingProviders()) {
|
||||||
|
X509AlgorithmBindingProvider provider = byId.get(activation.providerId());
|
||||||
|
if (provider == null) {
|
||||||
|
throw new IllegalArgumentException("Configured X.509 binding provider is unavailable");
|
||||||
|
}
|
||||||
|
activation.expectedBindingSetVersion().ifPresent(expected -> {
|
||||||
|
if (!expected.equals(provider.bindingSetVersion())) {
|
||||||
|
throw new IllegalArgumentException("X.509 binding-provider version changed");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
List<X509AlgorithmBinding> additions = List.copyOf(provider.bindings());
|
||||||
|
for (X509AlgorithmBinding binding : additions) {
|
||||||
|
if (binding.origin() != X509AlgorithmBinding.Origin.DEPLOYER_PRIVATE
|
||||||
|
|| !binding.providerId().orElse("").equals(provider.providerId())
|
||||||
|
|| BootstrapAlgorithmIdentities.catalog()
|
||||||
|
.resolve(binding.algorithmIdentity().canonicalForm()).isEmpty()
|
||||||
|
|| activation.authorizedOidRoots().stream()
|
||||||
|
.noneMatch(root -> authorizedRoot(root, binding.oid()))) {
|
||||||
|
throw new IllegalArgumentException("Unauthorized deployer X.509 binding");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
descriptors.addAll(additions);
|
||||||
|
List<zeroecho.pki.impl.framework.x509.X509BindingRule> rules = List.copyOf(provider.rules());
|
||||||
|
Set<String> descriptorIds = additions.stream().map(X509AlgorithmBinding::bindingId)
|
||||||
|
.collect(java.util.stream.Collectors.toUnmodifiableSet());
|
||||||
|
Set<String> ruleIds = rules.stream().flatMap(rule -> rule.bindings().stream())
|
||||||
|
.map(X509AlgorithmBinding::bindingId).collect(java.util.stream.Collectors.toUnmodifiableSet());
|
||||||
|
if (rules.isEmpty() || !ruleIds.equals(descriptorIds)) {
|
||||||
|
throw new IllegalArgumentException("Deployer binding provider has inconsistent codecs");
|
||||||
|
}
|
||||||
|
ruleProviders.add(new X509BindingRuleProvider() {
|
||||||
|
@Override
|
||||||
|
public List<zeroecho.pki.impl.framework.x509.X509BindingRule> rules() {
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new BindingComposition(ImmutableX509AlgorithmBindingRegistry.create(descriptors),
|
||||||
|
List.copyOf(ruleProviders));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateConfiguredBindings(PkiSessionConfiguration.SigningConfiguration signing,
|
||||||
|
X509AlgorithmBindingRegistry registry) {
|
||||||
|
if (signing.certificateSignatureBinding().isEmpty() && signing.crlSignatureBinding().isEmpty()
|
||||||
|
&& signing.subjectPublicKeyBinding().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AlgorithmIdentity identity = BootstrapAlgorithmIdentities.resolve(signing.signatureAlgorithm())
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Configured signature identity is unknown"));
|
||||||
|
validateConfiguredBinding(registry, signing.certificateSignatureBinding(), identity,
|
||||||
|
X509AlgorithmBinding.Role.CERTIFICATE_SIGNATURE);
|
||||||
|
validateConfiguredBinding(registry, signing.crlSignatureBinding(), identity,
|
||||||
|
X509AlgorithmBinding.Role.CRL_SIGNATURE);
|
||||||
|
validateConfiguredBinding(registry, signing.subjectPublicKeyBinding(),
|
||||||
|
BootstrapAlgorithmIdentities.SPHINCS_PLUS_SIGNATURE.equals(identity)
|
||||||
|
? BootstrapAlgorithmIdentities.SPHINCS_PLUS_PUBLIC_KEY
|
||||||
|
: bootstrapPublicKey(identity), X509AlgorithmBinding.Role.SUBJECT_PUBLIC_KEY);
|
||||||
|
boolean privateAlgorithm = identity.equals(BootstrapAlgorithmIdentities.SPHINCS_PLUS_SIGNATURE);
|
||||||
|
if (privateAlgorithm && (signing.certificateSignatureBinding().isEmpty()
|
||||||
|
|| signing.crlSignatureBinding().isEmpty() || signing.subjectPublicKeyBinding().isEmpty())) {
|
||||||
|
throw new IllegalArgumentException("Private X.509 signature identity requires explicit bindings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AlgorithmIdentity bootstrapPublicKey(AlgorithmIdentity signature) {
|
||||||
|
if (signature.equals(BootstrapAlgorithmIdentities.ECDSA_SHA256)) {
|
||||||
|
return BootstrapAlgorithmIdentities.EC_P256_PUBLIC_KEY;
|
||||||
|
}
|
||||||
|
if (signature.equals(BootstrapAlgorithmIdentities.ECDSA_SHA384)) {
|
||||||
|
return BootstrapAlgorithmIdentities.EC_P384_PUBLIC_KEY;
|
||||||
|
}
|
||||||
|
if (signature.equals(BootstrapAlgorithmIdentities.ECDSA_SHA512)) {
|
||||||
|
return BootstrapAlgorithmIdentities.EC_P521_PUBLIC_KEY;
|
||||||
|
}
|
||||||
|
if (signature.equals(BootstrapAlgorithmIdentities.ED25519_SIGNATURE)) {
|
||||||
|
return BootstrapAlgorithmIdentities.ED25519_PUBLIC_KEY;
|
||||||
|
}
|
||||||
|
if (signature.equals(BootstrapAlgorithmIdentities.ED448_SIGNATURE)) {
|
||||||
|
return BootstrapAlgorithmIdentities.ED448_PUBLIC_KEY;
|
||||||
|
}
|
||||||
|
return BootstrapAlgorithmIdentities.RSA_PUBLIC_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("PMD.PreserveStackTrace")
|
||||||
|
private static PublicKeyInfoResolver publicKeySource(PublicKeyInfoSource source, X509AuthoritySnapshot authority,
|
||||||
|
PkiSessionConfiguration.SigningConfiguration signing) {
|
||||||
|
if (signing.subjectPublicKeyBinding().isEmpty()) {
|
||||||
|
return source::resolvePublicKeyInfo;
|
||||||
|
}
|
||||||
|
return keyRef -> {
|
||||||
|
EncodedObject nativeKey = source.resolvePublicKeyInfo(keyRef);
|
||||||
|
if (nativeKey.encoding() != Encoding.DER) {
|
||||||
|
throw new PkiException("Managed public key is not canonical DER: code=PUBLIC_KEY_ENCODING_INVALID");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
byte[] wrapped = new BcX509PublicKeyAdapter(authority.bindings())
|
||||||
|
.wrap(nativeKey.bytes(), BootstrapAlgorithmIdentities.SPHINCS_PLUS_PUBLIC_KEY,
|
||||||
|
signing.subjectPublicKeyBinding().orElseThrow())
|
||||||
|
.getEncoded(org.bouncycastle.asn1.ASN1Encoding.DER);
|
||||||
|
return new EncodedObject(Encoding.DER, wrapped);
|
||||||
|
} catch (java.io.IOException | IllegalArgumentException failure) {
|
||||||
|
throw new PkiException("Managed public key binding failed: code=PUBLIC_KEY_BINDING_FAILED");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateConfiguredBinding(X509AlgorithmBindingRegistry registry,
|
||||||
|
Optional<String> bindingId, AlgorithmIdentity identity, X509AlgorithmBinding.Role role) {
|
||||||
|
bindingId.ifPresent(value -> {
|
||||||
|
X509AlgorithmBinding binding = registry.find(value)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Configured X.509 binding is inactive"));
|
||||||
|
if (binding.role() != role || !binding.algorithmIdentity().equals(identity)) {
|
||||||
|
throw new IllegalArgumentException("Configured X.509 binding does not match the signing role");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean authorizedRoot(String root, String oid) {
|
||||||
|
boolean egothorDeployment = ImmutableX509AlgorithmBindingRegistry.isWithin(root,
|
||||||
|
ImmutableX509AlgorithmBindingRegistry.ZEROECHO_DEPLOYMENT_ROOT)
|
||||||
|
&& !ImmutableX509AlgorithmBindingRegistry.ZEROECHO_DEPLOYMENT_ROOT.equals(root);
|
||||||
|
boolean externalPen = root.matches("1\\.3\\.6\\.1\\.4\\.1\\.(?!31424(?:\\.|$))[0-9]+(?:\\.[0-9]+)*");
|
||||||
|
return (egothorDeployment || externalPen)
|
||||||
|
&& ImmutableX509AlgorithmBindingRegistry.isWithin(oid, root);
|
||||||
|
}
|
||||||
|
|
||||||
private static PkiSessionRuntimeDependencies runtimeDependencies(PkiSessionConfiguration configuration) {
|
private static PkiSessionRuntimeDependencies runtimeDependencies(PkiSessionConfiguration configuration) {
|
||||||
Optional<String> environment = configuration.signing()
|
Optional<String> environment = configuration.signing()
|
||||||
.flatMap(PkiSessionConfiguration.SigningConfiguration::unlockEnvironmentVariable);
|
.flatMap(PkiSessionConfiguration.SigningConfiguration::unlockEnvironmentVariable);
|
||||||
@@ -474,6 +649,14 @@ final class DefaultPkiSession implements PkiSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record BindingComposition(X509AlgorithmBindingRegistry registry,
|
||||||
|
List<X509BindingRuleProvider> providers) {
|
||||||
|
private BindingComposition {
|
||||||
|
Objects.requireNonNull(registry, "registry");
|
||||||
|
providers = List.copyOf(providers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Production provider bootstrap implementation. */
|
/** Production provider bootstrap implementation. */
|
||||||
private enum ProductionBootstrap implements Bootstrap {
|
private enum ProductionBootstrap implements Bootstrap {
|
||||||
INSTANCE;
|
INSTANCE;
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ import zeroecho.pki.api.status.StatusObjectType;
|
|||||||
* explicit type and never by reflection or Java class name.</p>
|
* explicit type and never by reflection or Java class name.</p>
|
||||||
*/
|
*/
|
||||||
public sealed interface PkiOperation permits PkiOperation.ValidateConfiguration, PkiOperation.ValidateProfile,
|
public sealed interface PkiOperation permits PkiOperation.ValidateConfiguration, PkiOperation.ValidateProfile,
|
||||||
|
PkiOperation.ListAlgorithmBindings, PkiOperation.InspectAlgorithmBinding,
|
||||||
|
PkiOperation.ValidateAlgorithmBindings,
|
||||||
PkiOperation.RegisterProfile, PkiOperation.InspectProfile, PkiOperation.ListProfileVersions,
|
PkiOperation.RegisterProfile, PkiOperation.InspectProfile, PkiOperation.ListProfileVersions,
|
||||||
PkiOperation.ActivateProfile, PkiOperation.InspectAuthority, PkiOperation.ListAuthorities,
|
PkiOperation.ActivateProfile, PkiOperation.InspectAuthority, PkiOperation.ListAuthorities,
|
||||||
PkiOperation.CreateAuthority, PkiOperation.TransitionAuthority, PkiOperation.InspectRequest,
|
PkiOperation.CreateAuthority, PkiOperation.TransitionAuthority, PkiOperation.InspectRequest,
|
||||||
@@ -69,6 +71,81 @@ public sealed interface PkiOperation permits PkiOperation.ValidateConfiguration,
|
|||||||
/** @return stable semantic operation name */
|
/** @return stable semantic operation name */
|
||||||
String name();
|
String name();
|
||||||
|
|
||||||
|
/** Lists active X.509 algorithm bindings with optional finite filters. */
|
||||||
|
record ListAlgorithmBindings(java.util.Optional<String> origin, java.util.Optional<String> role, int limit)
|
||||||
|
implements PkiOperation {
|
||||||
|
/** Stable operation name. */
|
||||||
|
public static final String NAME = "algorithm.binding.list";
|
||||||
|
|
||||||
|
/** Validates immutable filters and the presentation limit. */
|
||||||
|
public ListAlgorithmBindings {
|
||||||
|
origin = Objects.requireNonNull(origin, "origin").map(ListAlgorithmBindings::requireText);
|
||||||
|
role = Objects.requireNonNull(role, "role").map(ListAlgorithmBindings::requireText);
|
||||||
|
requireLimit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String requireText(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("Binding filter must not be blank");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Inspects one active X.509 algorithm binding by stable identifier. */
|
||||||
|
record InspectAlgorithmBinding(String bindingId) implements PkiOperation {
|
||||||
|
/** Stable operation name. */
|
||||||
|
public static final String NAME = "algorithm.binding.inspect";
|
||||||
|
|
||||||
|
/** Validates the stable identifier. */
|
||||||
|
public InspectAlgorithmBinding {
|
||||||
|
if (bindingId == null || bindingId.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("bindingId must not be blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return NAME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Validates the immutable active registry and optionally one exact binding. */
|
||||||
|
record ValidateAlgorithmBindings(java.util.Optional<String> bindingId,
|
||||||
|
java.util.Optional<String> expectedCommitment) implements PkiOperation {
|
||||||
|
/** Stable operation name. */
|
||||||
|
public static final String NAME = "algorithm.binding.validate";
|
||||||
|
|
||||||
|
/** Validates paired optional fields. */
|
||||||
|
public ValidateAlgorithmBindings {
|
||||||
|
bindingId = Objects.requireNonNull(bindingId, "bindingId");
|
||||||
|
expectedCommitment = Objects.requireNonNull(expectedCommitment, "expectedCommitment");
|
||||||
|
if (bindingId.isPresent() != expectedCommitment.isPresent()) {
|
||||||
|
throw new IllegalArgumentException("Binding validation fields must be supplied together");
|
||||||
|
}
|
||||||
|
bindingId.ifPresent(value -> {
|
||||||
|
if (value.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("bindingId must not be blank");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expectedCommitment.ifPresent(value -> {
|
||||||
|
if (!value.matches("[0-9a-f]{64}|[^\\s]{1,512}")) {
|
||||||
|
throw new IllegalArgumentException("Binding commitment is invalid");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return NAME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Validates the already-open session configuration. */
|
/** Validates the already-open session configuration. */
|
||||||
record ValidateConfiguration() implements PkiOperation {
|
record ValidateConfiguration() implements PkiOperation {
|
||||||
/** Stable operation name. */
|
/** Stable operation name. */
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import zeroecho.pki.api.PublicationService;
|
|||||||
import zeroecho.pki.api.ProfileService;
|
import zeroecho.pki.api.ProfileService;
|
||||||
import zeroecho.pki.api.RevocationService;
|
import zeroecho.pki.api.RevocationService;
|
||||||
import zeroecho.pki.api.StatusObjectService;
|
import zeroecho.pki.api.StatusObjectService;
|
||||||
|
import zeroecho.pki.api.algorithm.X509AlgorithmBindingRegistry;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lifecycle-owned synchronous PKI backend session.
|
* Lifecycle-owned synchronous PKI backend session.
|
||||||
@@ -107,6 +108,13 @@ public interface PkiSession extends AutoCloseable {
|
|||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the immutable active X.509 algorithm-binding registry.
|
||||||
|
*
|
||||||
|
* @return session-owned immutable registry, available to read-only sessions
|
||||||
|
*/
|
||||||
|
X509AlgorithmBindingRegistry algorithmBindings();
|
||||||
|
|
||||||
/** @return shared typed operation executor owned by this session */
|
/** @return shared typed operation executor owned by this session */
|
||||||
PkiOperationExecutor operations();
|
PkiOperationExecutor operations();
|
||||||
|
|
||||||
|
|||||||
@@ -54,14 +54,21 @@ import zeroecho.pki.spi.ProviderConfig;
|
|||||||
* @param publishers explicitly enabled publication destinations
|
* @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) {
|
Optional<SigningConfiguration> signing, List<ProviderConfig> publishers,
|
||||||
|
List<BindingProviderConfiguration> bindingProviders) {
|
||||||
|
|
||||||
/** Current configuration schema version. */
|
/** Current configuration schema version. */
|
||||||
public static final int CURRENT_VERSION = 2;
|
public static final int CURRENT_VERSION = 3;
|
||||||
|
|
||||||
/** Creates a read-only-compatible version-one configuration. */
|
/** Creates a read-only-compatible version-one configuration. */
|
||||||
public PkiSessionConfiguration(int version, ProviderConfig store, ProviderConfig audit) {
|
public PkiSessionConfiguration(int version, ProviderConfig store, ProviderConfig audit) {
|
||||||
this(version, store, audit, Optional.empty(), List.of());
|
this(version, store, audit, Optional.empty(), List.of(), List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates a version-one or version-two configuration without custom bindings. */
|
||||||
|
public PkiSessionConfiguration(int version, ProviderConfig store, ProviderConfig audit,
|
||||||
|
Optional<SigningConfiguration> signing, List<ProviderConfig> publishers) {
|
||||||
|
this(version, store, audit, signing, publishers, List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Validates and snapshots the configuration. */
|
/** Validates and snapshots the configuration. */
|
||||||
@@ -74,9 +81,17 @@ public record PkiSessionConfiguration(int version, ProviderConfig store, Provide
|
|||||||
signing = Objects.requireNonNull(signing, "signing").map(SigningConfiguration::snapshot);
|
signing = Objects.requireNonNull(signing, "signing").map(SigningConfiguration::snapshot);
|
||||||
publishers = Objects.requireNonNull(publishers, "publishers").stream()
|
publishers = Objects.requireNonNull(publishers, "publishers").stream()
|
||||||
.map(PkiSessionConfiguration::snapshot).toList();
|
.map(PkiSessionConfiguration::snapshot).toList();
|
||||||
|
bindingProviders = List.copyOf(Objects.requireNonNull(bindingProviders, "bindingProviders"));
|
||||||
if (version == 1 && (signing.isPresent() || !publishers.isEmpty())) {
|
if (version == 1 && (signing.isPresent() || !publishers.isEmpty())) {
|
||||||
throw new IllegalArgumentException("Version-one configuration cannot enable optional capabilities");
|
throw new IllegalArgumentException("Version-one configuration cannot enable optional capabilities");
|
||||||
}
|
}
|
||||||
|
if (version < 3 && !bindingProviders.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Custom X.509 bindings require configuration version three");
|
||||||
|
}
|
||||||
|
if (bindingProviders.stream().map(BindingProviderConfiguration::providerId).distinct().count()
|
||||||
|
!= bindingProviders.size()) {
|
||||||
|
throw new IllegalArgumentException("Duplicate X.509 binding-provider identity");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ProviderConfig snapshot(ProviderConfig config) {
|
private static ProviderConfig snapshot(ProviderConfig config) {
|
||||||
@@ -93,9 +108,21 @@ public record PkiSessionConfiguration(int version, ProviderConfig store, Provide
|
|||||||
* @param signingTtl positive synchronous signing deadline
|
* @param signingTtl positive synchronous signing deadline
|
||||||
* @param unlockEnvironmentVariable optional environment-variable reference used
|
* @param unlockEnvironmentVariable optional environment-variable reference used
|
||||||
* only by the default process composition
|
* only by the default process composition
|
||||||
|
* @param certificateSignatureBinding optional explicit certificate signature binding
|
||||||
|
* @param crlSignatureBinding optional explicit CRL signature binding
|
||||||
|
* @param subjectPublicKeyBinding optional explicit SPKI binding
|
||||||
*/
|
*/
|
||||||
public record SigningConfiguration(ProviderConfig workflow, ProviderConfig framework, String busPath,
|
public record SigningConfiguration(ProviderConfig workflow, ProviderConfig framework, String busPath,
|
||||||
String signatureAlgorithm, Duration signingTtl, Optional<String> unlockEnvironmentVariable) {
|
String signatureAlgorithm, Duration signingTtl, Optional<String> unlockEnvironmentVariable,
|
||||||
|
Optional<String> certificateSignatureBinding, Optional<String> crlSignatureBinding,
|
||||||
|
Optional<String> subjectPublicKeyBinding) {
|
||||||
|
|
||||||
|
/** Creates standard-mode signing configuration. */
|
||||||
|
public SigningConfiguration(ProviderConfig workflow, ProviderConfig framework, String busPath,
|
||||||
|
String signatureAlgorithm, Duration signingTtl, Optional<String> unlockEnvironmentVariable) {
|
||||||
|
this(workflow, framework, busPath, signatureAlgorithm, signingTtl, unlockEnvironmentVariable,
|
||||||
|
Optional.empty(), Optional.empty(), Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
/** Validates and snapshots the signing configuration. */
|
/** Validates and snapshots the signing configuration. */
|
||||||
public SigningConfiguration {
|
public SigningConfiguration {
|
||||||
@@ -109,6 +136,9 @@ public record PkiSessionConfiguration(int version, ProviderConfig store, Provide
|
|||||||
}
|
}
|
||||||
unlockEnvironmentVariable = Objects.requireNonNull(unlockEnvironmentVariable,
|
unlockEnvironmentVariable = Objects.requireNonNull(unlockEnvironmentVariable,
|
||||||
"unlockEnvironmentVariable");
|
"unlockEnvironmentVariable");
|
||||||
|
certificateSignatureBinding = binding(certificateSignatureBinding, "certificateSignatureBinding");
|
||||||
|
crlSignatureBinding = binding(crlSignatureBinding, "crlSignatureBinding");
|
||||||
|
subjectPublicKeyBinding = binding(subjectPublicKeyBinding, "subjectPublicKeyBinding");
|
||||||
unlockEnvironmentVariable.ifPresent(value -> {
|
unlockEnvironmentVariable.ifPresent(value -> {
|
||||||
if (!value.matches("[A-Z][A-Z0-9_]{0,127}")) {
|
if (!value.matches("[A-Z][A-Z0-9_]{0,127}")) {
|
||||||
throw new IllegalArgumentException("Unlock environment-variable reference is invalid");
|
throw new IllegalArgumentException("Unlock environment-variable reference is invalid");
|
||||||
@@ -118,7 +148,51 @@ public record PkiSessionConfiguration(int version, ProviderConfig store, Provide
|
|||||||
|
|
||||||
private static SigningConfiguration snapshot(SigningConfiguration source) {
|
private static SigningConfiguration snapshot(SigningConfiguration source) {
|
||||||
return new SigningConfiguration(source.workflow, source.framework, source.busPath,
|
return new SigningConfiguration(source.workflow, source.framework, source.busPath,
|
||||||
source.signatureAlgorithm, source.signingTtl, source.unlockEnvironmentVariable);
|
source.signatureAlgorithm, source.signingTtl, source.unlockEnvironmentVariable,
|
||||||
|
source.certificateSignatureBinding, source.crlSignatureBinding, source.subjectPublicKeyBinding);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Optional<String> binding(Optional<String> source, String name) {
|
||||||
|
return Objects.requireNonNull(source, name).map(value -> {
|
||||||
|
if (value.isBlank() || value.length() > 512) {
|
||||||
|
throw new IllegalArgumentException(name + " is invalid");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Explicit activation and namespace authorization for one installed binding
|
||||||
|
* provider.
|
||||||
|
*
|
||||||
|
* @param providerId stable installed provider identity
|
||||||
|
* @param authorizedOidRoots finite roots owned by the deployment
|
||||||
|
* @param expectedBindingSetVersion optional pinned provider binding-set version
|
||||||
|
*/
|
||||||
|
public record BindingProviderConfiguration(String providerId, List<String> authorizedOidRoots,
|
||||||
|
Optional<String> expectedBindingSetVersion) {
|
||||||
|
|
||||||
|
/** Validates and snapshots the activation. */
|
||||||
|
public BindingProviderConfiguration {
|
||||||
|
if (providerId == null || providerId.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("binding providerId must not be blank");
|
||||||
|
}
|
||||||
|
authorizedOidRoots = List.copyOf(Objects.requireNonNull(authorizedOidRoots, "authorizedOidRoots"));
|
||||||
|
if (authorizedOidRoots.isEmpty() || authorizedOidRoots.stream().anyMatch(root -> root == null
|
||||||
|
|| !root.matches("[0-2](?:\\.[0-9]+)+"))) {
|
||||||
|
throw new IllegalArgumentException("Authorized OID roots are invalid");
|
||||||
|
}
|
||||||
|
if (authorizedOidRoots.stream().distinct().count() != authorizedOidRoots.size()) {
|
||||||
|
throw new IllegalArgumentException("Authorized OID roots are duplicated");
|
||||||
|
}
|
||||||
|
expectedBindingSetVersion = Objects.requireNonNull(expectedBindingSetVersion,
|
||||||
|
"expectedBindingSetVersion").map(value -> {
|
||||||
|
if (value.isBlank() || value.length() > 512) {
|
||||||
|
throw new IllegalArgumentException("Expected binding-set version is invalid");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ final class CaCertificateProfileValidator {
|
|||||||
SubjectRef canonicalSubject = new SubjectRef(BcX509ProfileSupport.subject(subjectSnapshot).toString());
|
SubjectRef canonicalSubject = new SubjectRef(BcX509ProfileSupport.subject(subjectSnapshot).toString());
|
||||||
return new ValidatedCaCertificateRequest(operation, formatId, issuerCaId, subjectCaId,
|
return new ValidatedCaCertificateRequest(operation, formatId, issuerCaId, subjectCaId,
|
||||||
activeProfile.reference(), expectedKind, canonicalSubject, subjectSnapshot, exactPublicKey, validity,
|
activeProfile.reference(), expectedKind, canonicalSubject, subjectSnapshot, exactPublicKey, validity,
|
||||||
serial, definition.caPolicy());
|
serial, definition.caPolicy(), definition.algorithmBindingPolicy());
|
||||||
}
|
}
|
||||||
|
|
||||||
/* package */ static void requireProfileShape(ActiveCertificateProfile activeProfile,
|
/* package */ static void requireProfileShape(ActiveCertificateProfile activeProfile,
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ import zeroecho.pki.impl.framework.x509.X509ExecutionPlan;
|
|||||||
import zeroecho.pki.impl.framework.x509.StreamingDerReader;
|
import zeroecho.pki.impl.framework.x509.StreamingDerReader;
|
||||||
import zeroecho.pki.impl.framework.x509.bc.BcX509VerificationExecutor;
|
import zeroecho.pki.impl.framework.x509.bc.BcX509VerificationExecutor;
|
||||||
import zeroecho.pki.impl.framework.x509.bc.BcX509AlgorithmAdapter;
|
import zeroecho.pki.impl.framework.x509.bc.BcX509AlgorithmAdapter;
|
||||||
|
import zeroecho.pki.impl.framework.x509.bc.PkiBusContentSigner;
|
||||||
import zeroecho.pki.spi.audit.AuditSink;
|
import zeroecho.pki.spi.audit.AuditSink;
|
||||||
import zeroecho.pki.util.async.AsyncState;
|
import zeroecho.pki.util.async.AsyncState;
|
||||||
import zeroecho.pki.util.async.AsyncStatus;
|
import zeroecho.pki.util.async.AsyncStatus;
|
||||||
@@ -93,26 +94,46 @@ final class CaProofGate {
|
|||||||
private final PkiSigningBus signingBus;
|
private final PkiSigningBus signingBus;
|
||||||
private final AuditSink auditSink;
|
private final AuditSink auditSink;
|
||||||
private final AlgorithmIdentity signatureIdentity;
|
private final AlgorithmIdentity signatureIdentity;
|
||||||
|
private final Optional<String> signatureBindingId;
|
||||||
private final Duration signingTtl;
|
private final Duration signingTtl;
|
||||||
|
|
||||||
/* default */ CaProofGate(PublicKeyInfoResolver publicKeyResolver, PkiSigningBus signingBus, AuditSink auditSink,
|
/* default */ CaProofGate(PublicKeyInfoResolver publicKeyResolver, PkiSigningBus signingBus, AuditSink auditSink,
|
||||||
String signatureAlgorithmId, Duration signingTtl) {
|
String signatureAlgorithmId, Duration signingTtl) {
|
||||||
this(publicKeyResolver, signingBus, auditSink,
|
this(publicKeyResolver, signingBus, auditSink,
|
||||||
signingBus.authority().resolveIdentity(Objects.requireNonNull(signatureAlgorithmId,
|
signingBus.authority().resolveIdentity(Objects.requireNonNull(signatureAlgorithmId,
|
||||||
"signatureAlgorithmId")), signingTtl);
|
"signatureAlgorithmId")), Optional.empty(), signingTtl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* default */ CaProofGate(PublicKeyInfoResolver publicKeyResolver, PkiSigningBus signingBus, AuditSink auditSink,
|
/* default */ CaProofGate(PublicKeyInfoResolver publicKeyResolver, PkiSigningBus signingBus, AuditSink auditSink,
|
||||||
AlgorithmIdentity signatureIdentity, Duration signingTtl) {
|
AlgorithmIdentity signatureIdentity, Duration signingTtl) {
|
||||||
|
this(publicKeyResolver, signingBus, auditSink, signatureIdentity, Optional.empty(), signingTtl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* default */ CaProofGate(PublicKeyInfoResolver publicKeyResolver, PkiSigningBus signingBus, AuditSink auditSink,
|
||||||
|
AlgorithmIdentity signatureIdentity, Optional<String> signatureBindingId, Duration signingTtl) {
|
||||||
this.publicKeyResolver = Objects.requireNonNull(publicKeyResolver, "publicKeyResolver");
|
this.publicKeyResolver = Objects.requireNonNull(publicKeyResolver, "publicKeyResolver");
|
||||||
this.signingBus = Objects.requireNonNull(signingBus, "signingBus");
|
this.signingBus = Objects.requireNonNull(signingBus, "signingBus");
|
||||||
this.auditSink = Objects.requireNonNull(auditSink, "auditSink");
|
this.auditSink = Objects.requireNonNull(auditSink, "auditSink");
|
||||||
this.signatureIdentity = Objects.requireNonNull(signatureIdentity, "signatureIdentity");
|
this.signatureIdentity = Objects.requireNonNull(signatureIdentity, "signatureIdentity");
|
||||||
|
this.signatureBindingId = Objects.requireNonNull(signatureBindingId, "signatureBindingId");
|
||||||
this.signingTtl = Objects.requireNonNull(signingTtl, "signingTtl");
|
this.signingTtl = Objects.requireNonNull(signingTtl, "signingTtl");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* default */ ContentSigner signer(ManagedKeyProof proof) {
|
/* default */ ContentSigner signer(ManagedKeyProof proof) {
|
||||||
return new BusBackedContentSigner(signingBus, proof.keyRef(), signatureIdentity, signingTtl);
|
return signatureBindingId.isPresent()
|
||||||
|
? new PkiBusContentSigner(signingBus, proof.keyRef(), signatureIdentity,
|
||||||
|
signatureBindingId.orElseThrow(), signingTtl)
|
||||||
|
: new BusBackedContentSigner(signingBus, proof.keyRef(), signatureIdentity, signingTtl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* default */ ContentSigner signer(ManagedKeyProof proof,
|
||||||
|
zeroecho.pki.api.profile.X509AlgorithmBindingPolicy bindingPolicy) {
|
||||||
|
Optional<String> bindingId = bindingPolicy.certificateSignature()
|
||||||
|
.map(zeroecho.pki.api.profile.X509AlgorithmBindingPolicy.BindingReference::bindingId);
|
||||||
|
return bindingId.isPresent()
|
||||||
|
? new PkiBusContentSigner(signingBus, proof.keyRef(), signatureIdentity, bindingId.orElseThrow(),
|
||||||
|
signingTtl)
|
||||||
|
: new BusBackedContentSigner(signingBus, proof.keyRef(), signatureIdentity, signingTtl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* default */ SubjectPublicKeyInfo parseRootSpki(EncodedObject spki, FormatId formatId) {
|
/* default */ SubjectPublicKeyInfo parseRootSpki(EncodedObject spki, FormatId formatId) {
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ final class CertificateProfileValidator {
|
|||||||
return new ValidatedCertificateRequest(candidate.issuerCaId(), profileReference, approvedSubjectRef,
|
return new ValidatedCertificateRequest(candidate.issuerCaId(), profileReference, approvedSubjectRef,
|
||||||
approvedSubject, approvedSans, sanCritical, candidate.exactPublicKey(), validity, policy.keyUsages(),
|
approvedSubject, approvedSans, sanCritical, candidate.exactPublicKey(), validity, policy.keyUsages(),
|
||||||
policy.extendedKeyUsages(), policy.keyUsageCritical(), policy.extendedKeyUsageCritical(),
|
policy.extendedKeyUsages(), policy.keyUsageCritical(), policy.extendedKeyUsageCritical(),
|
||||||
policy.basicConstraintsCritical());
|
policy.basicConstraintsCritical(), profile.algorithmBindingPolicy());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void requireCanonicalRequestAttributes(ParsedCertificationRequest request) {
|
private static void requireCanonicalRequestAttributes(ParsedCertificationRequest request) {
|
||||||
@@ -252,14 +252,21 @@ final class CertificateProfileValidator {
|
|||||||
if (!allowedAlgorithms.contains(algorithm.profileId())) {
|
if (!allowedAlgorithms.contains(algorithm.profileId())) {
|
||||||
throw reject("SUBJECT_KEY_ALGORITHM_FORBIDDEN");
|
throw reject("SUBJECT_KEY_ALGORITHM_FORBIDDEN");
|
||||||
}
|
}
|
||||||
PublicKey reconstructed = KeyFactory.getInstance(algorithm.jcaName())
|
SubjectPublicKeyInfo nativeSpki = new zeroecho.pki.impl.framework.x509.bc.BcX509PublicKeyAdapter(
|
||||||
.generatePublic(new X509EncodedKeySpec(encoded));
|
authority.bindings()).unwrap(spki);
|
||||||
|
byte[] nativeEncoded = nativeSpki.getEncoded();
|
||||||
|
KeyFactory keyFactory = BootstrapAlgorithmIdentities.SPHINCS_PLUS_PUBLIC_KEY.equals(identity)
|
||||||
|
? KeyFactory.getInstance(algorithm.jcaName(),
|
||||||
|
new org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider())
|
||||||
|
: KeyFactory.getInstance(algorithm.jcaName());
|
||||||
|
PublicKey reconstructed = keyFactory.generatePublic(new X509EncodedKeySpec(nativeEncoded));
|
||||||
byte[] canonical = reconstructed.getEncoded();
|
byte[] canonical = reconstructed.getEncoded();
|
||||||
try {
|
try {
|
||||||
if (canonical == null || !MessageDigest.isEqual(encoded, canonical)) {
|
if (canonical == null || !MessageDigest.isEqual(nativeEncoded, canonical)) {
|
||||||
throw reject("SUBJECT_KEY_NOT_CANONICAL");
|
throw reject("SUBJECT_KEY_NOT_CANONICAL");
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
java.util.Arrays.fill(nativeEncoded, (byte) 0);
|
||||||
if (canonical != null) {
|
if (canonical != null) {
|
||||||
java.util.Arrays.fill(canonical, (byte) 0);
|
java.util.Arrays.fill(canonical, (byte) 0);
|
||||||
}
|
}
|
||||||
@@ -286,6 +293,9 @@ final class CertificateProfileValidator {
|
|||||||
if (identity.equals(BootstrapAlgorithmIdentities.ED448_PUBLIC_KEY)) {
|
if (identity.equals(BootstrapAlgorithmIdentities.ED448_PUBLIC_KEY)) {
|
||||||
return new SubjectKeyAlgorithm("Ed448", "Ed448");
|
return new SubjectKeyAlgorithm("Ed448", "Ed448");
|
||||||
}
|
}
|
||||||
|
if (identity.equals(BootstrapAlgorithmIdentities.SPHINCS_PLUS_PUBLIC_KEY)) {
|
||||||
|
return new SubjectKeyAlgorithm("SPHINCS+", "SPHINCS+");
|
||||||
|
}
|
||||||
throw reject("SUBJECT_KEY_ALGORITHM_UNKNOWN");
|
throw reject("SUBJECT_KEY_ALGORITHM_UNKNOWN");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -232,6 +232,30 @@ public final class DefaultCaService implements CaService {
|
|||||||
PublicKeyInfoResolver publicKeyResolver, PkiSigningBus signingBus, AuditSink auditSink,
|
PublicKeyInfoResolver publicKeyResolver, PkiSigningBus signingBus, AuditSink auditSink,
|
||||||
EffectiveCredentialStatusResolver statusResolver, ProfileService profileService, Clock clock,
|
EffectiveCredentialStatusResolver statusResolver, ProfileService profileService, Clock clock,
|
||||||
String signatureAlgorithmId, Duration signingTtl) {
|
String signatureAlgorithmId, Duration signingTtl) {
|
||||||
|
this(store, framework, issuerBackend, publicKeyResolver, signingBus, auditSink, statusResolver, profileService,
|
||||||
|
clock, signatureAlgorithmId, Optional.empty(), signingTtl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a CA service with an optional explicit X.509 signature binding.
|
||||||
|
*
|
||||||
|
* @param store persistent PKI store
|
||||||
|
* @param framework credential framework
|
||||||
|
* @param issuerBackend issuer backend
|
||||||
|
* @param publicKeyResolver managed public-key resolver
|
||||||
|
* @param signingBus signing bus
|
||||||
|
* @param auditSink audit sink
|
||||||
|
* @param statusResolver effective status resolver
|
||||||
|
* @param profileService profile service
|
||||||
|
* @param clock injected clock
|
||||||
|
* @param signatureAlgorithmId cryptographic signature identity
|
||||||
|
* @param signatureBindingId explicit binding identity, when private mode is selected
|
||||||
|
* @param signingTtl signing operation time-to-live
|
||||||
|
*/
|
||||||
|
public DefaultCaService(PkiStore store, CredentialFramework framework, CredentialIssuerBackend issuerBackend,
|
||||||
|
PublicKeyInfoResolver publicKeyResolver, PkiSigningBus signingBus, AuditSink auditSink,
|
||||||
|
EffectiveCredentialStatusResolver statusResolver, ProfileService profileService, Clock clock,
|
||||||
|
String signatureAlgorithmId, Optional<String> signatureBindingId, Duration signingTtl) {
|
||||||
|
|
||||||
this.store = Objects.requireNonNull(store, "store");
|
this.store = Objects.requireNonNull(store, "store");
|
||||||
this.framework = Objects.requireNonNull(framework, "framework");
|
this.framework = Objects.requireNonNull(framework, "framework");
|
||||||
@@ -250,11 +274,14 @@ public final class DefaultCaService implements CaService {
|
|||||||
throw new IllegalArgumentException("signingTtl must be positive");
|
throw new IllegalArgumentException("signingTtl must be positive");
|
||||||
}
|
}
|
||||||
AlgorithmIdentity signatureIdentity = signingBus.authority().resolveIdentity(signatureAlgorithmId);
|
AlgorithmIdentity signatureIdentity = signingBus.authority().resolveIdentity(signatureAlgorithmId);
|
||||||
X509ExecutionPlan<SignatureWorkflow> plan = signingBus.authority()
|
X509ExecutionPlan<SignatureWorkflow> plan = signatureBindingId.isPresent()
|
||||||
.planSigning(signatureIdentity.canonicalForm(), SignatureWorkflow.class);
|
? signingBus.authority().planSigningWithBinding(signatureIdentity.canonicalForm(),
|
||||||
|
signatureBindingId.orElseThrow(), SignatureWorkflow.class)
|
||||||
|
: signingBus.authority().planSigning(signatureIdentity.canonicalForm(), SignatureWorkflow.class);
|
||||||
signingBus.authority().authorize(plan, plan.executor(), AlgorithmExecutionCapability.Direction.SIGN);
|
signingBus.authority().authorize(plan, plan.executor(), AlgorithmExecutionCapability.Direction.SIGN);
|
||||||
this.signatureIdentity = signatureIdentity;
|
this.signatureIdentity = signatureIdentity;
|
||||||
this.proofGate = new CaProofGate(publicKeyResolver, signingBus, auditSink, signatureIdentity, signingTtl);
|
this.proofGate = new CaProofGate(publicKeyResolver, signingBus, auditSink, signatureIdentity,
|
||||||
|
signatureBindingId, signingTtl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -326,7 +353,7 @@ public final class DefaultCaService implements CaService {
|
|||||||
throw new PkiException("Root extension construction failed: code=ROOT_EXTENSION_BUILD_FAILED");
|
throw new PkiException("Root extension construction failed: code=ROOT_EXTENSION_BUILD_FAILED");
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentSigner signer = proofGate.signer(proof);
|
ContentSigner signer = proofGate.signer(proof, request.algorithmBindingPolicy());
|
||||||
X509CertificateHolder cert;
|
X509CertificateHolder cert;
|
||||||
try {
|
try {
|
||||||
cert = b.build(signer);
|
cert = b.build(signer);
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ import zeroecho.pki.api.profile.CertificateProfileDefinition;
|
|||||||
import zeroecho.pki.api.profile.CertificateProfileDocumentCodec;
|
import zeroecho.pki.api.profile.CertificateProfileDocumentCodec;
|
||||||
import zeroecho.pki.api.profile.CertificateProfileRef;
|
import zeroecho.pki.api.profile.CertificateProfileRef;
|
||||||
import zeroecho.pki.api.profile.ImportedCertificateProfileVersion;
|
import zeroecho.pki.api.profile.ImportedCertificateProfileVersion;
|
||||||
|
import zeroecho.pki.api.profile.X509AlgorithmBindingPolicy;
|
||||||
|
import zeroecho.pki.api.algorithm.X509AlgorithmBinding;
|
||||||
|
import zeroecho.pki.api.algorithm.X509AlgorithmBindingRegistry;
|
||||||
import zeroecho.pki.impl.ProfileLifecycleFailure;
|
import zeroecho.pki.impl.ProfileLifecycleFailure;
|
||||||
import zeroecho.pki.impl.ProfileLifecycleFailure.Code;
|
import zeroecho.pki.impl.ProfileLifecycleFailure.Code;
|
||||||
import zeroecho.pki.spi.audit.AuditSink;
|
import zeroecho.pki.spi.audit.AuditSink;
|
||||||
@@ -70,12 +73,42 @@ public final class DefaultProfileService implements ProfileService {
|
|||||||
private final PkiStore store;
|
private final PkiStore store;
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
private final AuditSink auditSink;
|
private final AuditSink auditSink;
|
||||||
|
private final X509AlgorithmBindingRegistry algorithmBindings;
|
||||||
|
private final boolean signingConfigured;
|
||||||
|
private final Optional<String> certificateBinding;
|
||||||
|
private final Optional<String> crlBinding;
|
||||||
|
private final Optional<String> publicKeyBinding;
|
||||||
|
|
||||||
/** Creates a profile service using one authoritative clock. */
|
/** Creates a profile service using one authoritative clock. */
|
||||||
public DefaultProfileService(PkiStore store, Clock clock, AuditSink auditSink) {
|
public DefaultProfileService(PkiStore store, Clock clock, AuditSink auditSink) {
|
||||||
|
this(store, clock, auditSink, defaultBindings());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a profile service bound to one immutable algorithm registry.
|
||||||
|
*
|
||||||
|
* @param store authoritative PKI store
|
||||||
|
* @param clock authoritative clock
|
||||||
|
* @param auditSink audit sink
|
||||||
|
* @param algorithmBindings active immutable binding registry
|
||||||
|
*/
|
||||||
|
public DefaultProfileService(PkiStore store, Clock clock, AuditSink auditSink,
|
||||||
|
X509AlgorithmBindingRegistry algorithmBindings) {
|
||||||
|
this(store, clock, auditSink, algorithmBindings, false, Optional.empty(), Optional.empty(), Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates a profile service with the session's explicit signing bindings. */
|
||||||
|
public DefaultProfileService(PkiStore store, Clock clock, AuditSink auditSink,
|
||||||
|
X509AlgorithmBindingRegistry algorithmBindings, boolean signingConfigured,
|
||||||
|
Optional<String> certificateBinding, Optional<String> crlBinding, Optional<String> publicKeyBinding) {
|
||||||
this.store = Objects.requireNonNull(store, "store");
|
this.store = Objects.requireNonNull(store, "store");
|
||||||
this.clock = Objects.requireNonNull(clock, "clock");
|
this.clock = Objects.requireNonNull(clock, "clock");
|
||||||
this.auditSink = Objects.requireNonNull(auditSink, "auditSink");
|
this.auditSink = Objects.requireNonNull(auditSink, "auditSink");
|
||||||
|
this.algorithmBindings = Objects.requireNonNull(algorithmBindings, "algorithmBindings");
|
||||||
|
this.signingConfigured = signingConfigured;
|
||||||
|
this.certificateBinding = Objects.requireNonNull(certificateBinding, "certificateBinding");
|
||||||
|
this.crlBinding = Objects.requireNonNull(crlBinding, "crlBinding");
|
||||||
|
this.publicKeyBinding = Objects.requireNonNull(publicKeyBinding, "publicKeyBinding");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -83,6 +116,7 @@ public final class DefaultProfileService implements ProfileService {
|
|||||||
Objects.requireNonNull(jsonDocument, "jsonDocument");
|
Objects.requireNonNull(jsonDocument, "jsonDocument");
|
||||||
return executeSanitized(() -> {
|
return executeSanitized(() -> {
|
||||||
CertificateProfileDefinition definition = CertificateProfileDocumentCodec.parse(jsonDocument);
|
CertificateProfileDefinition definition = CertificateProfileDocumentCodec.parse(jsonDocument);
|
||||||
|
validateBindings(definition);
|
||||||
byte[] canonical = CertificateProfileDocumentCodec.writeCanonical(definition);
|
byte[] canonical = CertificateProfileDocumentCodec.writeCanonical(definition);
|
||||||
return importCanonical(definition, canonical, clock.instant());
|
return importCanonical(definition, canonical, clock.instant());
|
||||||
}, Code.PROFILE_IMPORT_VALIDATION_FAILED);
|
}, Code.PROFILE_IMPORT_VALIDATION_FAILED);
|
||||||
@@ -93,6 +127,7 @@ public final class DefaultProfileService implements ProfileService {
|
|||||||
Objects.requireNonNull(jsonDocument, "jsonDocument");
|
Objects.requireNonNull(jsonDocument, "jsonDocument");
|
||||||
return executeSanitized(() -> {
|
return executeSanitized(() -> {
|
||||||
CertificateProfileDefinition definition = CertificateProfileDocumentCodec.parse(jsonDocument);
|
CertificateProfileDefinition definition = CertificateProfileDocumentCodec.parse(jsonDocument);
|
||||||
|
validateBindings(definition);
|
||||||
byte[] canonical = CertificateProfileDocumentCodec.writeCanonical(definition);
|
byte[] canonical = CertificateProfileDocumentCodec.writeCanonical(definition);
|
||||||
return importCanonical(definition, canonical, clock.instant());
|
return importCanonical(definition, canonical, clock.instant());
|
||||||
}, Code.PROFILE_IMPORT_VALIDATION_FAILED);
|
}, Code.PROFILE_IMPORT_VALIDATION_FAILED);
|
||||||
@@ -122,6 +157,10 @@ public final class DefaultProfileService implements ProfileService {
|
|||||||
throw new IllegalArgumentException("profileVersion must be positive");
|
throw new IllegalArgumentException("profileVersion must be positive");
|
||||||
}
|
}
|
||||||
return executeAudited("PROFILE_ACTIVATE", profileId, profileVersion, Code.PROFILE_ACTIVATION_FAILED, () -> {
|
return executeAudited("PROFILE_ACTIVATE", profileId, profileVersion, Code.PROFILE_ACTIVATION_FAILED, () -> {
|
||||||
|
ImportedCertificateProfileVersion version = store.getProfileVersion(profileId, profileVersion)
|
||||||
|
.orElseThrow(() -> new ProfileLifecycleFailure(Code.PROFILE_VERSION_NOT_FOUND));
|
||||||
|
validateBindings(version.definition());
|
||||||
|
validateRuntimeBindings(version.definition());
|
||||||
CertificateProfileRef result = store.activateProfile(profileId, profileVersion);
|
CertificateProfileRef result = store.activateProfile(profileId, profileVersion);
|
||||||
audit("PROFILE_ACTIVATE", result, "SUCCESS");
|
audit("PROFILE_ACTIVATE", result, "SUCCESS");
|
||||||
return result;
|
return result;
|
||||||
@@ -131,8 +170,12 @@ public final class DefaultProfileService implements ProfileService {
|
|||||||
@Override
|
@Override
|
||||||
public ActiveCertificateProfile requireActiveProfile(String profileId) {
|
public ActiveCertificateProfile requireActiveProfile(String profileId) {
|
||||||
requireProfileId(profileId);
|
requireProfileId(profileId);
|
||||||
return executeAudited("PROFILE_RESOLVE", profileId, 0L, Code.PROFILE_STORE_FAILURE,
|
return executeAudited("PROFILE_RESOLVE", profileId, 0L, Code.PROFILE_STORE_FAILURE, () -> {
|
||||||
() -> store.requireActiveProfile(profileId));
|
ActiveCertificateProfile active = store.requireActiveProfile(profileId);
|
||||||
|
validateBindings(active.definition());
|
||||||
|
validateRuntimeBindings(active.definition());
|
||||||
|
return active;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -217,6 +260,60 @@ public final class DefaultProfileService implements ProfileService {
|
|||||||
return new ProfileLifecycleFailure(fallback);
|
return new ProfileLifecycleFailure(fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateBindings(CertificateProfileDefinition definition) {
|
||||||
|
X509AlgorithmBindingPolicy policy = definition.algorithmBindingPolicy();
|
||||||
|
if (policy.mode() == X509AlgorithmBindingPolicy.Mode.STANDARD_ONLY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
validateBinding(policy.subjectPublicKey(), X509AlgorithmBinding.Role.SUBJECT_PUBLIC_KEY);
|
||||||
|
validateBinding(policy.csrSignature(), X509AlgorithmBinding.Role.CSR_SIGNATURE);
|
||||||
|
validateBinding(policy.certificateSignature(), X509AlgorithmBinding.Role.CERTIFICATE_SIGNATURE);
|
||||||
|
validateBinding(policy.crlSignature(), X509AlgorithmBinding.Role.CRL_SIGNATURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateBinding(Optional<X509AlgorithmBindingPolicy.BindingReference> reference,
|
||||||
|
X509AlgorithmBinding.Role role) {
|
||||||
|
reference.ifPresent(value -> {
|
||||||
|
X509AlgorithmBinding binding = algorithmBindings.require(value.bindingId(), value.semanticCommitment());
|
||||||
|
if (binding.role() != role || binding.origin() == X509AlgorithmBinding.Origin.STANDARD) {
|
||||||
|
throw new IllegalArgumentException("Explicit profile binding is invalid for its role");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateRuntimeBindings(CertificateProfileDefinition definition) {
|
||||||
|
if (!signingConfigured) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
X509AlgorithmBindingPolicy policy = definition.algorithmBindingPolicy();
|
||||||
|
if (policy.mode() == X509AlgorithmBindingPolicy.Mode.STANDARD_ONLY) {
|
||||||
|
if (certificateBinding.isPresent() || crlBinding.isPresent() || publicKeyBinding.isPresent()) {
|
||||||
|
throw new IllegalArgumentException("Standard profile cannot use private runtime bindings");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
requireRuntimeBinding(policy.certificateSignature(), certificateBinding);
|
||||||
|
requireRuntimeBinding(policy.subjectPublicKey(), publicKeyBinding);
|
||||||
|
if (policy.crlSignature().isPresent()) {
|
||||||
|
requireRuntimeBinding(policy.crlSignature(), crlBinding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void requireRuntimeBinding(Optional<X509AlgorithmBindingPolicy.BindingReference> selected,
|
||||||
|
Optional<String> configured) {
|
||||||
|
if (selected.isEmpty() || configured.isEmpty()
|
||||||
|
|| !selected.orElseThrow().bindingId().equals(configured.orElseThrow())) {
|
||||||
|
throw new IllegalArgumentException("Profile binding does not match the signing runtime");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static X509AlgorithmBindingRegistry defaultBindings() {
|
||||||
|
List<X509AlgorithmBinding> descriptors = new java.util.ArrayList<>(
|
||||||
|
zeroecho.pki.impl.framework.x509.StandardX509Bindings.descriptors());
|
||||||
|
descriptors.addAll(zeroecho.pki.impl.framework.x509.ZeroEchoPrivateX509Bindings.descriptors());
|
||||||
|
return zeroecho.pki.impl.framework.x509.ImmutableX509AlgorithmBindingRegistry.create(descriptors);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Attacker-controlled parser and store causes are intentionally replaced by
|
* Attacker-controlled parser and store causes are intentionally replaced by
|
||||||
* stable, cause-free lifecycle failures.
|
* stable, cause-free lifecycle failures.
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import zeroecho.pki.api.Validity;
|
|||||||
import zeroecho.pki.api.profile.CaCertificatePolicy;
|
import zeroecho.pki.api.profile.CaCertificatePolicy;
|
||||||
import zeroecho.pki.api.profile.CertificateProfileKind;
|
import zeroecho.pki.api.profile.CertificateProfileKind;
|
||||||
import zeroecho.pki.api.profile.CertificateProfileRef;
|
import zeroecho.pki.api.profile.CertificateProfileRef;
|
||||||
|
import zeroecho.pki.api.profile.X509AlgorithmBindingPolicy;
|
||||||
import zeroecho.pki.api.request.SubjectRdn;
|
import zeroecho.pki.api.request.SubjectRdn;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,6 +84,7 @@ public final class ValidatedCaCertificateRequest {
|
|||||||
private final Validity validity;
|
private final Validity validity;
|
||||||
private final BigInteger serial;
|
private final BigInteger serial;
|
||||||
private final CaCertificatePolicy policy;
|
private final CaCertificatePolicy policy;
|
||||||
|
private final X509AlgorithmBindingPolicy algorithmBindingPolicy;
|
||||||
|
|
||||||
// The constructor is the single cohesive gate output boundary.
|
// The constructor is the single cohesive gate output boundary.
|
||||||
@SuppressWarnings("PMD.ExcessiveParameterList")
|
@SuppressWarnings("PMD.ExcessiveParameterList")
|
||||||
@@ -90,6 +92,15 @@ public final class ValidatedCaCertificateRequest {
|
|||||||
PkiId subjectCaId, CertificateProfileRef profileReference, CertificateProfileKind certificateType,
|
PkiId subjectCaId, CertificateProfileRef profileReference, CertificateProfileKind certificateType,
|
||||||
SubjectRef subjectRef, List<SubjectRdn> subjectRdns, EncodedObject exactPublicKey, Validity validity,
|
SubjectRef subjectRef, List<SubjectRdn> subjectRdns, EncodedObject exactPublicKey, Validity validity,
|
||||||
BigInteger serial, CaCertificatePolicy policy) {
|
BigInteger serial, CaCertificatePolicy policy) {
|
||||||
|
this(operation, formatId, issuerCaId, subjectCaId, profileReference, certificateType, subjectRef, subjectRdns,
|
||||||
|
exactPublicKey, validity, serial, policy, X509AlgorithmBindingPolicy.standardOnly());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("PMD.ExcessiveParameterList")
|
||||||
|
/* package */ ValidatedCaCertificateRequest(Operation operation, FormatId formatId, PkiId issuerCaId,
|
||||||
|
PkiId subjectCaId, CertificateProfileRef profileReference, CertificateProfileKind certificateType,
|
||||||
|
SubjectRef subjectRef, List<SubjectRdn> subjectRdns, EncodedObject exactPublicKey, Validity validity,
|
||||||
|
BigInteger serial, CaCertificatePolicy policy, X509AlgorithmBindingPolicy algorithmBindingPolicy) {
|
||||||
this.operation = Objects.requireNonNull(operation, "operation");
|
this.operation = Objects.requireNonNull(operation, "operation");
|
||||||
this.formatId = Objects.requireNonNull(formatId, "formatId");
|
this.formatId = Objects.requireNonNull(formatId, "formatId");
|
||||||
this.issuerCaId = Objects.requireNonNull(issuerCaId, "issuerCaId");
|
this.issuerCaId = Objects.requireNonNull(issuerCaId, "issuerCaId");
|
||||||
@@ -103,6 +114,7 @@ public final class ValidatedCaCertificateRequest {
|
|||||||
this.validity = Objects.requireNonNull(validity, "validity");
|
this.validity = Objects.requireNonNull(validity, "validity");
|
||||||
this.serial = Objects.requireNonNull(serial, "serial");
|
this.serial = Objects.requireNonNull(serial, "serial");
|
||||||
this.policy = Objects.requireNonNull(policy, "policy");
|
this.policy = Objects.requireNonNull(policy, "policy");
|
||||||
|
this.algorithmBindingPolicy = Objects.requireNonNull(algorithmBindingPolicy, "algorithmBindingPolicy");
|
||||||
if (serial.signum() <= 0 || serial.toByteArray().length > 20) {
|
if (serial.signum() <= 0 || serial.toByteArray().length > 20) {
|
||||||
throw new IllegalArgumentException("serial must be positive and at most 20 bytes");
|
throw new IllegalArgumentException("serial must be positive and at most 20 bytes");
|
||||||
}
|
}
|
||||||
@@ -167,4 +179,9 @@ public final class ValidatedCaCertificateRequest {
|
|||||||
public CaCertificatePolicy policy() {
|
public CaCertificatePolicy policy() {
|
||||||
return policy;
|
return policy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return exact immutable X.509 representation selection */
|
||||||
|
public X509AlgorithmBindingPolicy algorithmBindingPolicy() {
|
||||||
|
return algorithmBindingPolicy;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import zeroecho.pki.api.Validity;
|
|||||||
import zeroecho.pki.api.profile.CertificateProfileRef;
|
import zeroecho.pki.api.profile.CertificateProfileRef;
|
||||||
import zeroecho.pki.api.profile.ExtendedKeyUsageId;
|
import zeroecho.pki.api.profile.ExtendedKeyUsageId;
|
||||||
import zeroecho.pki.api.profile.LeafKeyUsage;
|
import zeroecho.pki.api.profile.LeafKeyUsage;
|
||||||
|
import zeroecho.pki.api.profile.X509AlgorithmBindingPolicy;
|
||||||
import zeroecho.pki.api.request.SubjectAlternativeName;
|
import zeroecho.pki.api.request.SubjectAlternativeName;
|
||||||
import zeroecho.pki.api.request.SubjectRdn;
|
import zeroecho.pki.api.request.SubjectRdn;
|
||||||
|
|
||||||
@@ -71,12 +72,25 @@ public final class ValidatedCertificateRequest {
|
|||||||
private final boolean keyUsageCritical;
|
private final boolean keyUsageCritical;
|
||||||
private final boolean extendedKeyUsageCritical;
|
private final boolean extendedKeyUsageCritical;
|
||||||
private final boolean basicConstraintsCritical;
|
private final boolean basicConstraintsCritical;
|
||||||
|
private final X509AlgorithmBindingPolicy algorithmBindingPolicy;
|
||||||
|
|
||||||
/* default */ ValidatedCertificateRequest(PkiId issuerCaId, CertificateProfileRef profileReference,
|
/* default */ ValidatedCertificateRequest(PkiId issuerCaId, CertificateProfileRef profileReference,
|
||||||
SubjectRef subjectRef, List<SubjectRdn> subjectRdns, List<SubjectAlternativeName> subjectAlternativeNames,
|
SubjectRef subjectRef, List<SubjectRdn> subjectRdns, List<SubjectAlternativeName> subjectAlternativeNames,
|
||||||
boolean subjectAlternativeNameCritical, EncodedObject exactPublicKey, Validity validity,
|
boolean subjectAlternativeNameCritical, EncodedObject exactPublicKey, Validity validity,
|
||||||
Set<LeafKeyUsage> keyUsages, Set<ExtendedKeyUsageId> extendedKeyUsages, boolean keyUsageCritical,
|
Set<LeafKeyUsage> keyUsages, Set<ExtendedKeyUsageId> extendedKeyUsages, boolean keyUsageCritical,
|
||||||
boolean extendedKeyUsageCritical, boolean basicConstraintsCritical) {
|
boolean extendedKeyUsageCritical, boolean basicConstraintsCritical) {
|
||||||
|
this(issuerCaId, profileReference, subjectRef, subjectRdns, subjectAlternativeNames,
|
||||||
|
subjectAlternativeNameCritical, exactPublicKey, validity, keyUsages, extendedKeyUsages,
|
||||||
|
keyUsageCritical, extendedKeyUsageCritical, basicConstraintsCritical,
|
||||||
|
X509AlgorithmBindingPolicy.standardOnly());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* default */ ValidatedCertificateRequest(PkiId issuerCaId, CertificateProfileRef profileReference,
|
||||||
|
SubjectRef subjectRef, List<SubjectRdn> subjectRdns, List<SubjectAlternativeName> subjectAlternativeNames,
|
||||||
|
boolean subjectAlternativeNameCritical, EncodedObject exactPublicKey, Validity validity,
|
||||||
|
Set<LeafKeyUsage> keyUsages, Set<ExtendedKeyUsageId> extendedKeyUsages, boolean keyUsageCritical,
|
||||||
|
boolean extendedKeyUsageCritical, boolean basicConstraintsCritical,
|
||||||
|
X509AlgorithmBindingPolicy algorithmBindingPolicy) {
|
||||||
this.issuerCaId = Objects.requireNonNull(issuerCaId, "issuerCaId");
|
this.issuerCaId = Objects.requireNonNull(issuerCaId, "issuerCaId");
|
||||||
this.profileReference = Objects.requireNonNull(profileReference, "profileReference");
|
this.profileReference = Objects.requireNonNull(profileReference, "profileReference");
|
||||||
this.subjectRef = Objects.requireNonNull(subjectRef, "subjectRef");
|
this.subjectRef = Objects.requireNonNull(subjectRef, "subjectRef");
|
||||||
@@ -90,6 +104,7 @@ public final class ValidatedCertificateRequest {
|
|||||||
this.keyUsageCritical = keyUsageCritical;
|
this.keyUsageCritical = keyUsageCritical;
|
||||||
this.extendedKeyUsageCritical = extendedKeyUsageCritical;
|
this.extendedKeyUsageCritical = extendedKeyUsageCritical;
|
||||||
this.basicConstraintsCritical = basicConstraintsCritical;
|
this.basicConstraintsCritical = basicConstraintsCritical;
|
||||||
|
this.algorithmBindingPolicy = Objects.requireNonNull(algorithmBindingPolicy, "algorithmBindingPolicy");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return authoritative issuer CA identifier */
|
/** @return authoritative issuer CA identifier */
|
||||||
@@ -161,4 +176,9 @@ public final class ValidatedCertificateRequest {
|
|||||||
public boolean basicConstraintsCritical() {
|
public boolean basicConstraintsCritical() {
|
||||||
return basicConstraintsCritical;
|
return basicConstraintsCritical;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return exact immutable X.509 representation selection */
|
||||||
|
public X509AlgorithmBindingPolicy algorithmBindingPolicy() {
|
||||||
|
return algorithmBindingPolicy;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,7 +150,8 @@ public final class ZeroEchoLibSignatureWorkflowProvider
|
|||||||
BootstrapAlgorithmIdentities.RSA_PKCS1_SHA384, BootstrapAlgorithmIdentities.RSA_PKCS1_SHA512,
|
BootstrapAlgorithmIdentities.RSA_PKCS1_SHA384, BootstrapAlgorithmIdentities.RSA_PKCS1_SHA512,
|
||||||
BootstrapAlgorithmIdentities.RSA_PSS_SHA256, BootstrapAlgorithmIdentities.ECDSA_SHA256,
|
BootstrapAlgorithmIdentities.RSA_PSS_SHA256, BootstrapAlgorithmIdentities.ECDSA_SHA256,
|
||||||
BootstrapAlgorithmIdentities.ECDSA_SHA384, BootstrapAlgorithmIdentities.ECDSA_SHA512,
|
BootstrapAlgorithmIdentities.ECDSA_SHA384, BootstrapAlgorithmIdentities.ECDSA_SHA512,
|
||||||
BootstrapAlgorithmIdentities.ED25519_SIGNATURE, BootstrapAlgorithmIdentities.ED448_SIGNATURE);
|
BootstrapAlgorithmIdentities.ED25519_SIGNATURE, BootstrapAlgorithmIdentities.ED448_SIGNATURE,
|
||||||
|
BootstrapAlgorithmIdentities.SPHINCS_PLUS_SIGNATURE);
|
||||||
return java.util.List.of(new AlgorithmExecutionCapability() {
|
return java.util.List.of(new AlgorithmExecutionCapability() {
|
||||||
@Override
|
@Override
|
||||||
public String implementationId() {
|
public String implementationId() {
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* 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.framework.x509;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import zeroecho.core.spec.AlgorithmIdentity;
|
||||||
|
import zeroecho.pki.api.algorithm.X509AlgorithmBinding;
|
||||||
|
import zeroecho.pki.api.algorithm.X509AlgorithmBindingRegistry;
|
||||||
|
|
||||||
|
/** Package-owned immutable registry implementation used by session bootstrap. */
|
||||||
|
public final class ImmutableX509AlgorithmBindingRegistry implements X509AlgorithmBindingRegistry {
|
||||||
|
|
||||||
|
/** Egothor Private Enterprise Number root. */
|
||||||
|
public static final String EGOTHOR_PEN = "1.3.6.1.4.1.31424";
|
||||||
|
/** Frozen ZeroEcho private assignment branch. */
|
||||||
|
public static final String ZEROECHO_ASSIGNMENT_ROOT = EGOTHOR_PEN + ".1.1";
|
||||||
|
/** Deployment-local experimental branch. */
|
||||||
|
public static final String ZEROECHO_DEPLOYMENT_ROOT = EGOTHOR_PEN + ".1.2";
|
||||||
|
|
||||||
|
private final List<X509AlgorithmBinding> bindings;
|
||||||
|
private final Map<String, X509AlgorithmBinding> byId;
|
||||||
|
private final Map<String, X509AlgorithmBinding> standards;
|
||||||
|
private final String commitment;
|
||||||
|
|
||||||
|
private ImmutableX509AlgorithmBindingRegistry(List<X509AlgorithmBinding> source) {
|
||||||
|
this.bindings = source;
|
||||||
|
Map<String, X509AlgorithmBinding> identifiers = new LinkedHashMap<>();
|
||||||
|
Map<String, X509AlgorithmBinding> official = new LinkedHashMap<>();
|
||||||
|
Map<String, X509AlgorithmBinding> reverse = new LinkedHashMap<>();
|
||||||
|
for (X509AlgorithmBinding binding : source) {
|
||||||
|
if (identifiers.putIfAbsent(binding.bindingId(), binding) != null) {
|
||||||
|
throw new IllegalArgumentException("Duplicate X.509 binding identifier");
|
||||||
|
}
|
||||||
|
String reverseKey = binding.role() + "|" + binding.oid() + "|"
|
||||||
|
+ (binding.parameterRule() == X509AlgorithmBinding.ParameterRule.STRUCTURED_DER
|
||||||
|
? binding.algorithmIdentity().canonicalForm() : "fixed");
|
||||||
|
if (reverse.putIfAbsent(reverseKey, binding) != null) {
|
||||||
|
throw new IllegalArgumentException("Ambiguous X.509 role and OID");
|
||||||
|
}
|
||||||
|
if (binding.origin() == X509AlgorithmBinding.Origin.STANDARD
|
||||||
|
&& official.putIfAbsent(key(binding.algorithmIdentity(), binding.role()), binding) != null) {
|
||||||
|
throw new IllegalArgumentException("Ambiguous standard X.509 binding");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.byId = Map.copyOf(identifiers);
|
||||||
|
this.standards = Map.copyOf(official);
|
||||||
|
this.commitment = digest(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs and validates one deterministic registry snapshot.
|
||||||
|
*
|
||||||
|
* @param bindings complete explicitly selected binding set
|
||||||
|
* @return immutable registry
|
||||||
|
*/
|
||||||
|
public static ImmutableX509AlgorithmBindingRegistry create(List<X509AlgorithmBinding> bindings) {
|
||||||
|
Objects.requireNonNull(bindings, "bindings");
|
||||||
|
List<X509AlgorithmBinding> ordered = new ArrayList<>(bindings);
|
||||||
|
ordered.sort(Comparator.comparing(X509AlgorithmBinding::bindingId));
|
||||||
|
for (X509AlgorithmBinding binding : ordered) {
|
||||||
|
Objects.requireNonNull(binding, "binding");
|
||||||
|
if (binding.origin() == X509AlgorithmBinding.Origin.ZEROECHO_PRIVATE
|
||||||
|
&& !isWithin(binding.oid(), ZEROECHO_ASSIGNMENT_ROOT)) {
|
||||||
|
throw new IllegalArgumentException("ZeroEcho binding is outside the frozen assignment branch");
|
||||||
|
}
|
||||||
|
if (binding.origin() == X509AlgorithmBinding.Origin.STANDARD
|
||||||
|
&& isWithin(binding.oid(), EGOTHOR_PEN)) {
|
||||||
|
throw new IllegalArgumentException("Standard binding cannot use the Egothor private namespace");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ImmutableX509AlgorithmBindingRegistry(List.copyOf(ordered));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tests whether one OID is strictly below or exactly at an authorized root. */
|
||||||
|
public static boolean isWithin(String oid, String root) {
|
||||||
|
Objects.requireNonNull(oid, "oid");
|
||||||
|
Objects.requireNonNull(root, "root");
|
||||||
|
return oid.equals(root) || oid.startsWith(root + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<X509AlgorithmBinding> bindings() {
|
||||||
|
return bindings;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<X509AlgorithmBinding> find(String bindingId) {
|
||||||
|
return Optional.ofNullable(byId.get(Objects.requireNonNull(bindingId, "bindingId")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<X509AlgorithmBinding> standard(AlgorithmIdentity identity, X509AlgorithmBinding.Role role) {
|
||||||
|
return Optional.ofNullable(standards.get(key(Objects.requireNonNull(identity, "identity"),
|
||||||
|
Objects.requireNonNull(role, "role"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String commitment() {
|
||||||
|
return commitment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public X509AlgorithmBinding require(String bindingId, String expectedCommitment) {
|
||||||
|
X509AlgorithmBinding binding = find(bindingId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("X.509 binding is not active"));
|
||||||
|
if (!binding.semanticCommitment().equals(Objects.requireNonNull(expectedCommitment, "expectedCommitment"))) {
|
||||||
|
throw new IllegalArgumentException("X.509 binding contract has changed");
|
||||||
|
}
|
||||||
|
return binding;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String key(AlgorithmIdentity identity, X509AlgorithmBinding.Role role) {
|
||||||
|
return identity.canonicalForm() + "|" + role;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String digest(List<X509AlgorithmBinding> bindings) {
|
||||||
|
StringBuilder semantic = new StringBuilder();
|
||||||
|
for (X509AlgorithmBinding binding : bindings) {
|
||||||
|
semantic.append(binding.bindingId()).append('|').append(binding.algorithmIdentity().canonicalForm())
|
||||||
|
.append('|').append(binding.role()).append('|').append(binding.oid()).append('|')
|
||||||
|
.append(binding.origin()).append('|').append(binding.encodingVersion()).append('|')
|
||||||
|
.append(binding.parameterRule()).append('|').append(binding.publicKeyEncoding()).append('|')
|
||||||
|
.append(binding.signatureEncoding()).append('|').append(binding.interoperability()).append('|')
|
||||||
|
.append(binding.providerId().orElse("")).append('|').append(binding.semanticCommitment())
|
||||||
|
.append('\n');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||||
|
.digest(semantic.toString().getBytes(StandardCharsets.UTF_8)));
|
||||||
|
} catch (NoSuchAlgorithmException impossible) {
|
||||||
|
throw new IllegalStateException("SHA-256 unavailable", impossible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ import java.util.Optional;
|
|||||||
|
|
||||||
import zeroecho.core.alg.BootstrapAlgorithmIdentities;
|
import zeroecho.core.alg.BootstrapAlgorithmIdentities;
|
||||||
import zeroecho.core.spec.AlgorithmIdentity;
|
import zeroecho.core.spec.AlgorithmIdentity;
|
||||||
|
import zeroecho.pki.api.algorithm.X509AlgorithmBinding;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Immutable authoritative standard X.509 bootstrap bindings.
|
* Immutable authoritative standard X.509 bootstrap bindings.
|
||||||
@@ -150,6 +151,16 @@ public final class StandardX509Bindings {
|
|||||||
return createCatalog(components);
|
return createCatalog(components);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns deterministic safe descriptors for every sealed standard binding.
|
||||||
|
*
|
||||||
|
* @return immutable standard binding descriptors
|
||||||
|
*/
|
||||||
|
public static List<X509AlgorithmBinding> descriptors() {
|
||||||
|
return CATALOG.rules().stream().flatMap(rule -> rule.bindings().stream())
|
||||||
|
.sorted(java.util.Comparator.comparing(X509AlgorithmBinding::bindingId)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
private static X509BindingRule fixed(String id, X509AlgorithmRole role, AlgorithmIdentity identity,
|
private static X509BindingRule fixed(String id, X509AlgorithmRole role, AlgorithmIdentity identity,
|
||||||
X509AlgorithmIdentifier identifier, X509BindingRule.SignatureEncoding signatureEncoding,
|
X509AlgorithmIdentifier identifier, X509BindingRule.SignatureEncoding signatureEncoding,
|
||||||
X509BindingRule.PublicKeyEncoding publicKeyEncoding) {
|
X509BindingRule.PublicKeyEncoding publicKeyEncoding) {
|
||||||
@@ -180,6 +191,12 @@ public final class StandardX509Bindings {
|
|||||||
+ signatureEncoding + "|" + publicKeyEncoding;
|
+ signatureEncoding + "|" + publicKeyEncoding;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<X509AlgorithmBinding> bindings() {
|
||||||
|
return descriptors(id, role, identity, identifier, signatureEncoding, publicKeyEncoding,
|
||||||
|
semanticFingerprint());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<X509AlgorithmIdentifier> encode(AlgorithmIdentity candidate) {
|
public Optional<X509AlgorithmIdentifier> encode(AlgorithmIdentity candidate) {
|
||||||
return identity.equals(candidate) ? Optional.of(identifier) : Optional.empty();
|
return identity.equals(candidate) ? Optional.of(identifier) : Optional.empty();
|
||||||
@@ -236,6 +253,13 @@ public final class StandardX509Bindings {
|
|||||||
return PublicKeyEncoding.NOT_APPLICABLE;
|
return PublicKeyEncoding.NOT_APPLICABLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<X509AlgorithmBinding> bindings() {
|
||||||
|
X509AlgorithmIdentifier identifier = encode(BootstrapAlgorithmIdentities.RSA_PSS_SHA256).orElseThrow();
|
||||||
|
return descriptors(id(), role(), BootstrapAlgorithmIdentities.RSA_PSS_SHA256, identifier,
|
||||||
|
signatureEncoding(), publicKeyEncoding(), semanticFingerprint());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<X509AlgorithmIdentifier> encode(AlgorithmIdentity identity) {
|
public Optional<X509AlgorithmIdentifier> encode(AlgorithmIdentity identity) {
|
||||||
if (identity.kind() != AlgorithmIdentity.Kind.SIGNATURE
|
if (identity.kind() != AlgorithmIdentity.Kind.SIGNATURE
|
||||||
@@ -302,6 +326,16 @@ public final class StandardX509Bindings {
|
|||||||
return PublicKeyEncoding.EC_POINT;
|
return PublicKeyEncoding.EC_POINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<X509AlgorithmBinding> bindings() {
|
||||||
|
return List.of(BootstrapAlgorithmIdentities.EC_P256_PUBLIC_KEY,
|
||||||
|
BootstrapAlgorithmIdentities.EC_P384_PUBLIC_KEY,
|
||||||
|
BootstrapAlgorithmIdentities.EC_P521_PUBLIC_KEY).stream()
|
||||||
|
.flatMap(identity -> descriptors(id() + "." + identity.parameters().canonicalForm(), role(),
|
||||||
|
identity, encode(identity).orElseThrow(), signatureEncoding(), publicKeyEncoding(),
|
||||||
|
semanticFingerprint()).stream()).toList();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<X509AlgorithmIdentifier> encode(AlgorithmIdentity identity) {
|
public Optional<X509AlgorithmIdentifier> encode(AlgorithmIdentity identity) {
|
||||||
if (identity.kind() != AlgorithmIdentity.Kind.PUBLIC_KEY
|
if (identity.kind() != AlgorithmIdentity.Kind.PUBLIC_KEY
|
||||||
@@ -341,6 +375,42 @@ public final class StandardX509Bindings {
|
|||||||
StrictDer.explicit(2, StrictDer.integer(parameters.saltLength())));
|
StrictDer.explicit(2, StrictDer.integer(parameters.saltLength())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<X509AlgorithmBinding> descriptors(String id, X509AlgorithmRole role,
|
||||||
|
AlgorithmIdentity identity, X509AlgorithmIdentifier identifier, X509BindingRule.SignatureEncoding signature,
|
||||||
|
X509BindingRule.PublicKeyEncoding publicKey, String commitment) {
|
||||||
|
X509AlgorithmBinding.ParameterRule parameters = switch (identifier.parameterForm()) {
|
||||||
|
case ABSENT -> X509AlgorithmBinding.ParameterRule.ABSENT;
|
||||||
|
case DER_NULL -> X509AlgorithmBinding.ParameterRule.DER_NULL;
|
||||||
|
case EXACT_DER -> X509AlgorithmBinding.ParameterRule.STRUCTURED_DER;
|
||||||
|
};
|
||||||
|
X509AlgorithmBinding.PublicKeyEncoding keyEncoding = switch (publicKey) {
|
||||||
|
case RSA_PKCS1_DER -> X509AlgorithmBinding.PublicKeyEncoding.RSA_PKCS1_DER;
|
||||||
|
case EC_POINT -> X509AlgorithmBinding.PublicKeyEncoding.EC_POINT;
|
||||||
|
case RAW -> X509AlgorithmBinding.PublicKeyEncoding.RAW;
|
||||||
|
case NESTED_SPKI_DER -> X509AlgorithmBinding.PublicKeyEncoding.NESTED_SPKI_DER;
|
||||||
|
case NOT_APPLICABLE -> X509AlgorithmBinding.PublicKeyEncoding.NOT_APPLICABLE;
|
||||||
|
};
|
||||||
|
X509AlgorithmBinding.SignatureEncoding signatureEncoding = switch (signature) {
|
||||||
|
case OPAQUE -> X509AlgorithmBinding.SignatureEncoding.OPAQUE;
|
||||||
|
case ECDSA_DER -> X509AlgorithmBinding.SignatureEncoding.ECDSA_DER;
|
||||||
|
case NOT_APPLICABLE -> X509AlgorithmBinding.SignatureEncoding.NOT_APPLICABLE;
|
||||||
|
};
|
||||||
|
if (role == X509AlgorithmRole.SUBJECT_PUBLIC_KEY_ALGORITHM) {
|
||||||
|
return List.of(new X509AlgorithmBinding(id, identity, X509AlgorithmBinding.Role.SUBJECT_PUBLIC_KEY,
|
||||||
|
identifier.oid(), X509AlgorithmBinding.Origin.STANDARD, 1, parameters, keyEncoding,
|
||||||
|
signatureEncoding, X509AlgorithmBinding.Interoperability.STANDARD_INTEROPERABLE,
|
||||||
|
Optional.empty(), commitment));
|
||||||
|
}
|
||||||
|
return List.of(X509AlgorithmBinding.Role.CSR_SIGNATURE, X509AlgorithmBinding.Role.CERTIFICATE_SIGNATURE,
|
||||||
|
X509AlgorithmBinding.Role.CRL_SIGNATURE).stream()
|
||||||
|
.map(bindingRole -> new X509AlgorithmBinding(id + "."
|
||||||
|
+ bindingRole.name().toLowerCase(java.util.Locale.ROOT), identity,
|
||||||
|
bindingRole, identifier.oid(), X509AlgorithmBinding.Origin.STANDARD, 1, parameters, keyEncoding,
|
||||||
|
signatureEncoding, X509AlgorithmBinding.Interoperability.STANDARD_INTEROPERABLE,
|
||||||
|
Optional.empty(), commitment))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
private static AlgorithmIdentity.RsaPssParameters decodePss(byte[] encoded, X509ComponentCatalog components) {
|
private static AlgorithmIdentity.RsaPssParameters decodePss(byte[] encoded, X509ComponentCatalog components) {
|
||||||
StrictDer.Reader sequence = StrictDer.reader(encoded).readConstructed(0x30);
|
StrictDer.Reader sequence = StrictDer.reader(encoded).readConstructed(0x30);
|
||||||
byte[] hashAlgorithm = sequence.readConstructed(0xa0).readOnlyValue(0x30);
|
byte[] hashAlgorithm = sequence.readConstructed(0xa0).readOnlyValue(0x30);
|
||||||
|
|||||||
@@ -194,6 +194,14 @@ public final class X509AlgorithmResolver {
|
|||||||
public Selection resolve(AlgorithmIdentity requested, AlgorithmIdentity key,
|
public Selection resolve(AlgorithmIdentity requested, AlgorithmIdentity key,
|
||||||
AlgorithmExecutionCapability.Direction direction, Optional<String> implementation, String provenance,
|
AlgorithmExecutionCapability.Direction direction, Optional<String> implementation, String provenance,
|
||||||
String authorityFingerprint) {
|
String authorityFingerprint) {
|
||||||
|
return resolve(requested, key, direction, implementation, provenance, authorityFingerprint,
|
||||||
|
Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolves an operation with an optional explicit X.509 binding identity. */
|
||||||
|
public Selection resolve(AlgorithmIdentity requested, AlgorithmIdentity key,
|
||||||
|
AlgorithmExecutionCapability.Direction direction, Optional<String> implementation, String provenance,
|
||||||
|
String authorityFingerprint, Optional<String> bindingId) {
|
||||||
Objects.requireNonNull(requested, "requested");
|
Objects.requireNonNull(requested, "requested");
|
||||||
Objects.requireNonNull(key, "key");
|
Objects.requireNonNull(key, "key");
|
||||||
Objects.requireNonNull(direction, "direction");
|
Objects.requireNonNull(direction, "direction");
|
||||||
@@ -213,7 +221,9 @@ public final class X509AlgorithmResolver {
|
|||||||
}
|
}
|
||||||
X509AlgorithmIdentifier binding;
|
X509AlgorithmIdentifier binding;
|
||||||
try {
|
try {
|
||||||
binding = bindings.resolve(requested, X509AlgorithmRole.SIGNATURE_ALGORITHM);
|
binding = bindingId.isPresent()
|
||||||
|
? bindings.resolve(bindingId.orElseThrow(), requested, X509AlgorithmRole.SIGNATURE_ALGORITHM)
|
||||||
|
: bindings.resolve(requested, X509AlgorithmRole.SIGNATURE_ALGORITHM);
|
||||||
} catch (IllegalArgumentException missing) {
|
} catch (IllegalArgumentException missing) {
|
||||||
throw new ResolutionException(Failure.NO_BINDING, missing);
|
throw new ResolutionException(Failure.NO_BINDING, missing);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -188,7 +188,8 @@ public final class X509AuthoritySnapshot {
|
|||||||
mergeDefaults(defaults, provider.defaults());
|
mergeDefaults(defaults, provider.defaults());
|
||||||
}
|
}
|
||||||
X509ComponentCatalog components = X509ComponentCatalog.builtIn().merge(componentExtensions);
|
X509ComponentCatalog components = X509ComponentCatalog.builtIn().merge(componentExtensions);
|
||||||
X509BindingCatalog builtInBindings = StandardX509Bindings.catalog(components);
|
X509BindingCatalog builtInBindings = StandardX509Bindings.catalog(components)
|
||||||
|
.merge(List.of(ZeroEchoPrivateX509Bindings.catalog()));
|
||||||
for (X509BindingRuleProvider provider : ordered) {
|
for (X509BindingRuleProvider provider : ordered) {
|
||||||
List<X509BindingRule> rules = List.copyOf(provider.rules());
|
List<X509BindingRule> rules = List.copyOf(provider.rules());
|
||||||
if (!rules.isEmpty()) {
|
if (!rules.isEmpty()) {
|
||||||
@@ -248,6 +249,14 @@ public final class X509AuthoritySnapshot {
|
|||||||
return resolver.resolve(signature, key, direction, implementation, provenance, semanticFingerprint);
|
return resolver.resolve(signature, key, direction, implementation, provenance, semanticFingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Resolves one operation through an explicitly selected role-specific binding. */
|
||||||
|
public X509AlgorithmResolver.Selection resolve(AlgorithmIdentity signature, AlgorithmIdentity key,
|
||||||
|
AlgorithmExecutionCapability.Direction direction, Optional<String> implementation, String provenance,
|
||||||
|
String bindingId) {
|
||||||
|
return resolver.resolve(signature, key, direction, implementation, provenance, semanticFingerprint,
|
||||||
|
Optional.of(Objects.requireNonNull(bindingId, "bindingId")));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves and authorizes one process-local execution plan.
|
* Resolves and authorizes one process-local execution plan.
|
||||||
*
|
*
|
||||||
@@ -272,6 +281,20 @@ public final class X509AuthoritySnapshot {
|
|||||||
return new X509ExecutionPlan<>(selection, executorType.cast(executor), provenanceToken);
|
return new X509ExecutionPlan<>(selection, executorType.cast(executor), provenanceToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Resolves and authorizes one plan through an explicit binding identity. */
|
||||||
|
public <E> X509ExecutionPlan<E> plan(AlgorithmIdentity signature, AlgorithmIdentity key,
|
||||||
|
AlgorithmExecutionCapability.Direction direction, Optional<String> implementation, String provenance,
|
||||||
|
String bindingId, Class<E> executorType) {
|
||||||
|
X509AlgorithmResolver.Selection selection = resolve(signature, key, direction, implementation, provenance,
|
||||||
|
bindingId);
|
||||||
|
ExecutorKey executorKey = new ExecutorKey(selection.implementation().implementationId(), direction);
|
||||||
|
Object executor = executors.get(executorKey);
|
||||||
|
if (executor == null || !executorType.isInstance(executor)) {
|
||||||
|
throw new X509AlgorithmResolver.ResolutionException(X509AlgorithmResolver.Failure.NO_EXECUTOR);
|
||||||
|
}
|
||||||
|
return new X509ExecutionPlan<>(selection, executorType.cast(executor), provenanceToken);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves a signing plan for a legacy upper API that carries only the
|
* Resolves a signing plan for a legacy upper API that carries only the
|
||||||
* signature identity.
|
* signature identity.
|
||||||
@@ -304,6 +327,14 @@ public final class X509AuthoritySnapshot {
|
|||||||
executorType);
|
executorType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Resolves a signing plan through one explicit binding. */
|
||||||
|
public <E> X509ExecutionPlan<E> planSigningWithBinding(String value, String bindingId, Class<E> executorType) {
|
||||||
|
AlgorithmIdentity signature = resolveIdentity(value);
|
||||||
|
AlgorithmIdentity key = bootstrapKeyFor(signature);
|
||||||
|
return plan(signature, key, AlgorithmExecutionCapability.Direction.SIGN, Optional.empty(), "explicit",
|
||||||
|
bindingId, executorType);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates an execution plan immediately before invoking its executor.
|
* Validates an execution plan immediately before invoking its executor.
|
||||||
*
|
*
|
||||||
@@ -562,6 +593,9 @@ public final class X509AuthoritySnapshot {
|
|||||||
if (signature.equals(BootstrapAlgorithmIdentities.ED448_SIGNATURE)) {
|
if (signature.equals(BootstrapAlgorithmIdentities.ED448_SIGNATURE)) {
|
||||||
return BootstrapAlgorithmIdentities.ED448_PUBLIC_KEY;
|
return BootstrapAlgorithmIdentities.ED448_PUBLIC_KEY;
|
||||||
}
|
}
|
||||||
|
if (signature.equals(BootstrapAlgorithmIdentities.SPHINCS_PLUS_SIGNATURE)) {
|
||||||
|
return BootstrapAlgorithmIdentities.SPHINCS_PLUS_PUBLIC_KEY;
|
||||||
|
}
|
||||||
if ("rsa-pkcs1-v1_5".equals(signature.family().name())
|
if ("rsa-pkcs1-v1_5".equals(signature.family().name())
|
||||||
|| "rsa-pss".equals(signature.family().name())) {
|
|| "rsa-pss".equals(signature.family().name())) {
|
||||||
return BootstrapAlgorithmIdentities.RSA_PUBLIC_KEY;
|
return BootstrapAlgorithmIdentities.RSA_PUBLIC_KEY;
|
||||||
|
|||||||
@@ -130,6 +130,11 @@ public final class X509BindingCatalog {
|
|||||||
if (rule.role() != role) {
|
if (rule.role() != role) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (!rule.bindings().isEmpty() && rule.bindings().stream()
|
||||||
|
.noneMatch(binding -> binding.origin()
|
||||||
|
== zeroecho.pki.api.algorithm.X509AlgorithmBinding.Origin.STANDARD)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
Optional<X509AlgorithmIdentifier> candidate = rule.encode(identity);
|
Optional<X509AlgorithmIdentifier> candidate = rule.encode(identity);
|
||||||
if (candidate.isPresent()) {
|
if (candidate.isPresent()) {
|
||||||
if (resolved != null) {
|
if (resolved != null) {
|
||||||
@@ -144,6 +149,30 @@ public final class X509BindingCatalog {
|
|||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves one exact identity through an explicitly selected binding.
|
||||||
|
*
|
||||||
|
* @param bindingId stable binding identifier
|
||||||
|
* @param identity exact cryptographic identity
|
||||||
|
* @param role internal structural role
|
||||||
|
* @return canonical identifier
|
||||||
|
* @throws IllegalArgumentException if the binding is absent, has the wrong
|
||||||
|
* role, or cannot encode the identity
|
||||||
|
*/
|
||||||
|
public X509AlgorithmIdentifier resolve(String bindingId, AlgorithmIdentity identity, X509AlgorithmRole role) {
|
||||||
|
Objects.requireNonNull(bindingId, "bindingId");
|
||||||
|
Objects.requireNonNull(identity, "identity");
|
||||||
|
Objects.requireNonNull(role, "role");
|
||||||
|
X509BindingRule rule = rules.stream()
|
||||||
|
.filter(candidate -> candidate.id().equals(bindingId)
|
||||||
|
|| candidate.bindings().stream().anyMatch(binding -> binding.bindingId().equals(bindingId)))
|
||||||
|
.findFirst().orElseThrow(() -> new IllegalArgumentException("Unknown X.509 binding identifier"));
|
||||||
|
if (rule.role() != role) {
|
||||||
|
throw new IllegalArgumentException("X.509 binding role mismatch");
|
||||||
|
}
|
||||||
|
return rule.encode(identity).orElseThrow(() -> new IllegalArgumentException("X.509 binding identity mismatch"));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reverse-resolves an exact role-specific representation.
|
* Reverse-resolves an exact role-specific representation.
|
||||||
*
|
*
|
||||||
@@ -167,6 +196,23 @@ public final class X509BindingCatalog {
|
|||||||
return identity;
|
return identity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the stable binding identifier owning an exact representation.
|
||||||
|
*
|
||||||
|
* @param identifier canonical representation
|
||||||
|
* @param role structural role
|
||||||
|
* @return stable binding identifier
|
||||||
|
*/
|
||||||
|
public String bindingId(X509AlgorithmIdentifier identifier, X509AlgorithmRole role) {
|
||||||
|
Objects.requireNonNull(identifier, "identifier");
|
||||||
|
Objects.requireNonNull(role, "role");
|
||||||
|
X509BindingRule rule = byRoleAndOid.get(key(role, identifier.oid()));
|
||||||
|
if (rule == null || rule.decode(identifier).isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Unknown X.509 algorithm identifier");
|
||||||
|
}
|
||||||
|
return rule.id();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns deterministic immutable binding rules.
|
* Returns deterministic immutable binding rules.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -34,8 +34,10 @@
|
|||||||
package zeroecho.pki.impl.framework.x509;
|
package zeroecho.pki.impl.framework.x509;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import zeroecho.core.spec.AlgorithmIdentity;
|
import zeroecho.core.spec.AlgorithmIdentity;
|
||||||
|
import zeroecho.pki.api.algorithm.X509AlgorithmBinding;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Immutable trusted-code rule between exact ZeroEcho identities and X.509
|
* Immutable trusted-code rule between exact ZeroEcho identities and X.509
|
||||||
@@ -49,6 +51,19 @@ import zeroecho.core.spec.AlgorithmIdentity;
|
|||||||
*/
|
*/
|
||||||
public interface X509BindingRule {
|
public interface X509BindingRule {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the finite safe descriptors owned by this encoding rule.
|
||||||
|
*
|
||||||
|
* <p>A parameterized rule may expose one descriptor for every currently
|
||||||
|
* registered exact identity. Descriptors do not expose implementation
|
||||||
|
* classes or mutable codec state.</p>
|
||||||
|
*
|
||||||
|
* @return immutable descriptors
|
||||||
|
*/
|
||||||
|
default List<X509AlgorithmBinding> bindings() {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signature byte representation owned by a signature rule.
|
* Signature byte representation owned by a signature rule.
|
||||||
*/
|
*/
|
||||||
@@ -71,6 +86,8 @@ public interface X509BindingRule {
|
|||||||
EC_POINT,
|
EC_POINT,
|
||||||
/** Algorithm-defined raw public-key bytes. */
|
/** Algorithm-defined raw public-key bytes. */
|
||||||
RAW,
|
RAW,
|
||||||
|
/** Canonical native SubjectPublicKeyInfo DER nested in the BIT STRING. */
|
||||||
|
NESTED_SPKI_DER,
|
||||||
/** Not applicable to a signature-only rule. */
|
/** Not applicable to a signature-only rule. */
|
||||||
NOT_APPLICABLE
|
NOT_APPLICABLE
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ public final class X509SuiteCompatibility {
|
|||||||
case "ecdsa" -> "ec".equals(keyFamily);
|
case "ecdsa" -> "ec".equals(keyFamily);
|
||||||
case "ed25519" -> "ed25519".equals(keyFamily);
|
case "ed25519" -> "ed25519".equals(keyFamily);
|
||||||
case "ed448" -> "ed448".equals(keyFamily);
|
case "ed448" -> "ed448".equals(keyFamily);
|
||||||
|
case "sphincs-plus" -> "sphincs-plus".equals(keyFamily);
|
||||||
default -> false;
|
default -> false;
|
||||||
};
|
};
|
||||||
if (!compatible) {
|
if (!compatible) {
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* 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.framework.x509;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import tools.jackson.core.JsonParser;
|
||||||
|
import tools.jackson.core.JsonToken;
|
||||||
|
import tools.jackson.core.ObjectReadContext;
|
||||||
|
import tools.jackson.core.StreamReadConstraints;
|
||||||
|
import tools.jackson.core.json.JsonFactory;
|
||||||
|
import tools.jackson.core.json.JsonFactoryBuilder;
|
||||||
|
import zeroecho.core.alg.BootstrapAlgorithmIdentities;
|
||||||
|
import zeroecho.core.spec.AlgorithmIdentity;
|
||||||
|
import zeroecho.pki.api.algorithm.X509AlgorithmBinding;
|
||||||
|
|
||||||
|
/** Loads and locks the version-one ZeroEcho-owned private X.509 assignments. */
|
||||||
|
public final class ZeroEchoPrivateX509Bindings {
|
||||||
|
|
||||||
|
/** Versioned assignment resource. */
|
||||||
|
public static final String RESOURCE = "/zeroecho/pki/x509/zeroecho-private-bindings-v1.json";
|
||||||
|
private static final int MAXIMUM_RESOURCE_BYTES = 64 * 1024;
|
||||||
|
private static final Set<String> ROOT_FIELDS = Set.of("schemaVersion", "bindings");
|
||||||
|
private static final Set<String> BINDING_FIELDS = Set.of("bindingId", "algorithmIdentity", "role", "oid",
|
||||||
|
"encodingVersion", "parameterRule", "publicKeyEncoding", "signatureEncoding", "interoperability");
|
||||||
|
private static final JsonFactory JSON = new JsonFactoryBuilder()
|
||||||
|
.streamReadConstraints(StreamReadConstraints.builder().maxDocumentLength(MAXIMUM_RESOURCE_BYTES)
|
||||||
|
.maxNestingDepth(8).maxStringLength(512).build())
|
||||||
|
.build();
|
||||||
|
private static final AssignmentSet ASSIGNMENTS = loadResource();
|
||||||
|
|
||||||
|
private ZeroEchoPrivateX509Bindings() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return frozen safe descriptors in resource order */
|
||||||
|
public static List<X509AlgorithmBinding> descriptors() {
|
||||||
|
return ASSIGNMENTS.descriptors();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return trusted immutable encoding-rule catalog */
|
||||||
|
public static X509BindingCatalog catalog() {
|
||||||
|
return ASSIGNMENTS.catalog();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({ "PMD.AvoidCatchingGenericException", "PMD.ExceptionAsFlowControl",
|
||||||
|
"PMD.AvoidLiteralsInIfCondition" })
|
||||||
|
private static AssignmentSet loadResource() {
|
||||||
|
try (InputStream input = ZeroEchoPrivateX509Bindings.class.getResourceAsStream(RESOURCE)) {
|
||||||
|
if (input == null) {
|
||||||
|
throw new IllegalStateException("ZeroEcho X.509 assignment resource is missing");
|
||||||
|
}
|
||||||
|
try (JsonParser parser = JSON.createParser(ObjectReadContext.empty(), input)) {
|
||||||
|
require(parser.nextToken(), JsonToken.START_OBJECT);
|
||||||
|
Set<String> seen = new HashSet<>();
|
||||||
|
int schema = 0;
|
||||||
|
List<X509AlgorithmBinding> bindings = null;
|
||||||
|
while (parser.nextToken() != JsonToken.END_OBJECT) {
|
||||||
|
String field = parser.currentName();
|
||||||
|
if (!ROOT_FIELDS.contains(field) || !seen.add(field)) {
|
||||||
|
throw invalid();
|
||||||
|
}
|
||||||
|
parser.nextToken();
|
||||||
|
if ("schemaVersion".equals(field)) {
|
||||||
|
schema = parser.getIntValue();
|
||||||
|
} else {
|
||||||
|
bindings = readBindings(parser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parser.nextToken() != null || schema != 1 || bindings == null || seen.size() != ROOT_FIELDS.size()) {
|
||||||
|
throw invalid();
|
||||||
|
}
|
||||||
|
return validate(bindings);
|
||||||
|
}
|
||||||
|
} catch (IOException | RuntimeException failure) {
|
||||||
|
throw new IllegalStateException("ZeroEcho X.509 assignment resource is invalid", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({ "PMD.CyclomaticComplexity", "PMD.AvoidInstantiatingObjectsInLoops",
|
||||||
|
"PMD.AvoidLiteralsInIfCondition" })
|
||||||
|
private static List<X509AlgorithmBinding> readBindings(JsonParser parser) throws IOException {
|
||||||
|
require(parser.currentToken(), JsonToken.START_ARRAY);
|
||||||
|
List<X509AlgorithmBinding> bindings = new ArrayList<>();
|
||||||
|
while (parser.nextToken() != JsonToken.END_ARRAY) {
|
||||||
|
require(parser.currentToken(), JsonToken.START_OBJECT);
|
||||||
|
Set<String> seen = new HashSet<>();
|
||||||
|
String id = null;
|
||||||
|
String identity = null;
|
||||||
|
String role = null;
|
||||||
|
String oid = null;
|
||||||
|
int version = 0;
|
||||||
|
String parameters = null;
|
||||||
|
String publicKey = null;
|
||||||
|
String signature = null;
|
||||||
|
String interoperability = null;
|
||||||
|
while (parser.nextToken() != JsonToken.END_OBJECT) {
|
||||||
|
String field = parser.currentName();
|
||||||
|
if (!BINDING_FIELDS.contains(field) || !seen.add(field)) {
|
||||||
|
throw invalid();
|
||||||
|
}
|
||||||
|
parser.nextToken();
|
||||||
|
if ("encodingVersion".equals(field)) {
|
||||||
|
version = parser.getIntValue();
|
||||||
|
} else {
|
||||||
|
String value = parser.getValueAsString();
|
||||||
|
switch (field) {
|
||||||
|
case "bindingId" -> id = value;
|
||||||
|
case "algorithmIdentity" -> identity = value;
|
||||||
|
case "role" -> role = value;
|
||||||
|
case "oid" -> oid = value;
|
||||||
|
case "parameterRule" -> parameters = value;
|
||||||
|
case "publicKeyEncoding" -> publicKey = value;
|
||||||
|
case "signatureEncoding" -> signature = value;
|
||||||
|
case "interoperability" -> interoperability = value;
|
||||||
|
default -> throw invalid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (seen.size() != BINDING_FIELDS.size()) {
|
||||||
|
throw invalid();
|
||||||
|
}
|
||||||
|
AlgorithmIdentity exactIdentity = identity(identity);
|
||||||
|
String commitment = String.join("|", id, exactIdentity.canonicalForm(), role, oid,
|
||||||
|
Integer.toString(version), parameters, publicKey, signature, interoperability);
|
||||||
|
bindings.add(new X509AlgorithmBinding(id, exactIdentity, X509AlgorithmBinding.Role.valueOf(role), oid,
|
||||||
|
X509AlgorithmBinding.Origin.ZEROECHO_PRIVATE, version,
|
||||||
|
X509AlgorithmBinding.ParameterRule.valueOf(parameters),
|
||||||
|
X509AlgorithmBinding.PublicKeyEncoding.valueOf(publicKey),
|
||||||
|
X509AlgorithmBinding.SignatureEncoding.valueOf(signature),
|
||||||
|
X509AlgorithmBinding.Interoperability.valueOf(interoperability), Optional.empty(), commitment));
|
||||||
|
}
|
||||||
|
return List.copyOf(bindings);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
|
||||||
|
private static AssignmentSet validate(List<X509AlgorithmBinding> descriptors) {
|
||||||
|
if (descriptors.size() != 4) {
|
||||||
|
throw invalid();
|
||||||
|
}
|
||||||
|
ImmutableX509AlgorithmBindingRegistry.create(descriptors);
|
||||||
|
List<X509BindingRule> rules = descriptors.stream().map(PrivateRule::new).map(X509BindingRule.class::cast)
|
||||||
|
.toList();
|
||||||
|
return new AssignmentSet(descriptors, X509BindingCatalog.builtIn(rules));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AlgorithmIdentity identity(String value) {
|
||||||
|
return switch (Objects.requireNonNull(value, "algorithmIdentity")) {
|
||||||
|
case "SPHINCS_PLUS_SIGNATURE" -> BootstrapAlgorithmIdentities.SPHINCS_PLUS_SIGNATURE;
|
||||||
|
case "SPHINCS_PLUS_PUBLIC_KEY" -> BootstrapAlgorithmIdentities.SPHINCS_PLUS_PUBLIC_KEY;
|
||||||
|
default -> throw invalid();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void require(JsonToken actual, JsonToken expected) {
|
||||||
|
if (actual != expected) {
|
||||||
|
throw invalid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IllegalArgumentException invalid() {
|
||||||
|
return new IllegalArgumentException("Invalid ZeroEcho X.509 assignment schema");
|
||||||
|
}
|
||||||
|
|
||||||
|
private record AssignmentSet(List<X509AlgorithmBinding> descriptors, X509BindingCatalog catalog) {
|
||||||
|
private AssignmentSet {
|
||||||
|
descriptors = List.copyOf(descriptors);
|
||||||
|
Objects.requireNonNull(catalog, "catalog");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record PrivateRule(X509AlgorithmBinding binding) implements X509BindingRule {
|
||||||
|
private PrivateRule {
|
||||||
|
Objects.requireNonNull(binding, "binding");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String id() {
|
||||||
|
return binding.bindingId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public X509AlgorithmRole role() {
|
||||||
|
return binding.role() == X509AlgorithmBinding.Role.SUBJECT_PUBLIC_KEY
|
||||||
|
? X509AlgorithmRole.SUBJECT_PUBLIC_KEY_ALGORITHM : X509AlgorithmRole.SIGNATURE_ALGORITHM;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String oid() {
|
||||||
|
return binding.oid();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String semanticFingerprint() {
|
||||||
|
return binding.semanticCommitment();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SignatureEncoding signatureEncoding() {
|
||||||
|
return switch (binding.signatureEncoding()) {
|
||||||
|
case OPAQUE -> SignatureEncoding.OPAQUE;
|
||||||
|
case ECDSA_DER -> SignatureEncoding.ECDSA_DER;
|
||||||
|
case NOT_APPLICABLE -> SignatureEncoding.NOT_APPLICABLE;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PublicKeyEncoding publicKeyEncoding() {
|
||||||
|
return switch (binding.publicKeyEncoding()) {
|
||||||
|
case NESTED_SPKI_DER -> PublicKeyEncoding.NESTED_SPKI_DER;
|
||||||
|
case RAW -> PublicKeyEncoding.RAW;
|
||||||
|
case RSA_PKCS1_DER -> PublicKeyEncoding.RSA_PKCS1_DER;
|
||||||
|
case EC_POINT -> PublicKeyEncoding.EC_POINT;
|
||||||
|
case NOT_APPLICABLE -> PublicKeyEncoding.NOT_APPLICABLE;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<X509AlgorithmBinding> bindings() {
|
||||||
|
return List.of(binding);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<X509AlgorithmIdentifier> encode(AlgorithmIdentity identity) {
|
||||||
|
return binding.algorithmIdentity().equals(identity)
|
||||||
|
? Optional.of(X509AlgorithmIdentifier.absent(binding.oid())) : Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<AlgorithmIdentity> decode(X509AlgorithmIdentifier identifier) {
|
||||||
|
if (!binding.oid().equals(identifier.oid())) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
if (identifier.parameterForm() != X509AlgorithmIdentifier.ParameterForm.ABSENT) {
|
||||||
|
throw new IllegalArgumentException("Private X.509 binding parameters must be absent");
|
||||||
|
}
|
||||||
|
return Optional.of(binding.algorithmIdentity());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -165,6 +165,12 @@ public final class BcX509CredentialIssuerBackend implements CredentialIssuerBack
|
|||||||
*/
|
*/
|
||||||
public BcX509CredentialIssuerBackend(PkiSigningBus signingBus, AlgorithmIdentity signatureIdentity,
|
public BcX509CredentialIssuerBackend(PkiSigningBus signingBus, AlgorithmIdentity signatureIdentity,
|
||||||
Duration signingTtl) {
|
Duration signingTtl) {
|
||||||
|
this(signingBus, signatureIdentity, Optional.empty(), signingTtl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates an issuance backend with an optional explicit certificate binding. */
|
||||||
|
public BcX509CredentialIssuerBackend(PkiSigningBus signingBus, AlgorithmIdentity signatureIdentity,
|
||||||
|
Optional<String> signatureBindingId, Duration signingTtl) {
|
||||||
if (signingBus == null) {
|
if (signingBus == null) {
|
||||||
throw new IllegalArgumentException("signingBus must not be null");
|
throw new IllegalArgumentException("signingBus must not be null");
|
||||||
}
|
}
|
||||||
@@ -176,6 +182,7 @@ public final class BcX509CredentialIssuerBackend implements CredentialIssuerBack
|
|||||||
}
|
}
|
||||||
this.signingBus = signingBus;
|
this.signingBus = signingBus;
|
||||||
this.signatureIdentity = signatureIdentity;
|
this.signatureIdentity = signatureIdentity;
|
||||||
|
java.util.Objects.requireNonNull(signatureBindingId, "signatureBindingId");
|
||||||
this.signingTtl = signingTtl;
|
this.signingTtl = signingTtl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +229,7 @@ public final class BcX509CredentialIssuerBackend implements CredentialIssuerBack
|
|||||||
Date.from(request.validity().notBefore()), Date.from(request.validity().notAfter()), subjectDn, spki);
|
Date.from(request.validity().notBefore()), Date.from(request.validity().notAfter()), subjectDn, spki);
|
||||||
addLeafExtensions(builder, request);
|
addLeafExtensions(builder, request);
|
||||||
|
|
||||||
PkiBusContentSigner signer = new PkiBusContentSigner(signingBus, issuerKeyRef, signatureIdentity, signingTtl);
|
PkiBusContentSigner signer = signer(issuerKeyRef, request.algorithmBindingPolicy());
|
||||||
X509CertificateHolder leaf;
|
X509CertificateHolder leaf;
|
||||||
try {
|
try {
|
||||||
leaf = builder.build(signer);
|
leaf = builder.build(signer);
|
||||||
@@ -279,6 +286,16 @@ public final class BcX509CredentialIssuerBackend implements CredentialIssuerBack
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PkiBusContentSigner signer(KeyRef keyRef,
|
||||||
|
zeroecho.pki.api.profile.X509AlgorithmBindingPolicy bindingPolicy) {
|
||||||
|
Optional<String> bindingId = bindingPolicy.certificateSignature()
|
||||||
|
.map(zeroecho.pki.api.profile.X509AlgorithmBindingPolicy.BindingReference::bindingId);
|
||||||
|
return bindingId.isPresent()
|
||||||
|
? new PkiBusContentSigner(signingBus, keyRef, signatureIdentity,
|
||||||
|
bindingId.orElseThrow(), signingTtl)
|
||||||
|
: new PkiBusContentSigner(signingBus, keyRef, signatureIdentity, signingTtl);
|
||||||
|
}
|
||||||
|
|
||||||
private static int toKeyUsageBits(java.util.Set<LeafKeyUsage> usages) {
|
private static int toKeyUsageBits(java.util.Set<LeafKeyUsage> usages) {
|
||||||
int bits = 0;
|
int bits = 0;
|
||||||
for (LeafKeyUsage usage : usages) {
|
for (LeafKeyUsage usage : usages) {
|
||||||
@@ -372,7 +389,7 @@ public final class BcX509CredentialIssuerBackend implements CredentialIssuerBack
|
|||||||
throw new PkiException("X.509 extension construction failed: code=EXTENSION_BUILD_FAILED");
|
throw new PkiException("X.509 extension construction failed: code=EXTENSION_BUILD_FAILED");
|
||||||
}
|
}
|
||||||
|
|
||||||
PkiBusContentSigner signer = new PkiBusContentSigner(signingBus, issuerKeyRef, signatureIdentity, signingTtl);
|
PkiBusContentSigner signer = signer(issuerKeyRef, request.algorithmBindingPolicy());
|
||||||
X509CertificateHolder certificate;
|
X509CertificateHolder certificate;
|
||||||
try {
|
try {
|
||||||
certificate = builder.build(signer);
|
certificate = builder.build(signer);
|
||||||
|
|||||||
@@ -215,6 +215,8 @@ public final class BcX509ProofOfPossessionVerifier implements ProofOfPossessionV
|
|||||||
X509ExecutionPlan<BcX509VerificationExecutor> plan = authority.plan(signatureIdentity, keyIdentity,
|
X509ExecutionPlan<BcX509VerificationExecutor> plan = authority.plan(signatureIdentity, keyIdentity,
|
||||||
AlgorithmExecutionCapability.Direction.VERIFY,
|
AlgorithmExecutionCapability.Direction.VERIFY,
|
||||||
Optional.of(BcX509VerificationExecutor.IMPLEMENTATION_ID), "csr-proof",
|
Optional.of(BcX509VerificationExecutor.IMPLEMENTATION_ID), "csr-proof",
|
||||||
|
authority.bindings().bindingId(BcX509AlgorithmAdapter.fromBc(csr.getSignatureAlgorithm()),
|
||||||
|
X509AlgorithmRole.SIGNATURE_ALGORITHM),
|
||||||
BcX509VerificationExecutor.class);
|
BcX509VerificationExecutor.class);
|
||||||
byte[] signedBytes = csr.toASN1Structure().getCertificationRequestInfo().getEncoded();
|
byte[] signedBytes = csr.toASN1Structure().getCertificationRequestInfo().getEncoded();
|
||||||
boolean valid = executor.verify(authority, plan, csr.getSubjectPublicKeyInfo(),
|
boolean valid = executor.verify(authority, plan, csr.getSubjectPublicKeyInfo(),
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* 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.framework.x509.bc;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import org.bouncycastle.asn1.ASN1Encoding;
|
||||||
|
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||||
|
|
||||||
|
import zeroecho.core.spec.AlgorithmIdentity;
|
||||||
|
import zeroecho.pki.impl.framework.x509.StreamingDerReader;
|
||||||
|
import zeroecho.pki.impl.framework.x509.X509AlgorithmRole;
|
||||||
|
import zeroecho.pki.impl.framework.x509.X509BindingCatalog;
|
||||||
|
import zeroecho.pki.impl.framework.x509.X509BindingRule;
|
||||||
|
|
||||||
|
/** Bouncy Castle adapter for binding-owned SubjectPublicKeyInfo encodings. */
|
||||||
|
public final class BcX509PublicKeyAdapter {
|
||||||
|
|
||||||
|
private static final int MAXIMUM_SPKI_BYTES = 1024 * 1024;
|
||||||
|
private final X509BindingCatalog catalog;
|
||||||
|
|
||||||
|
/** Creates an adapter for one immutable binding catalog. */
|
||||||
|
public BcX509PublicKeyAdapter(X509BindingCatalog catalog) {
|
||||||
|
this.catalog = Objects.requireNonNull(catalog, "catalog");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps canonical native SPKI DER under one explicit binding.
|
||||||
|
*
|
||||||
|
* @param nativeSpki canonical provider-native SPKI
|
||||||
|
* @param identity exact public-key identity
|
||||||
|
* @param bindingId explicit SPKI binding
|
||||||
|
* @return canonical bound SPKI
|
||||||
|
* @throws IOException if either SPKI cannot be represented canonically
|
||||||
|
*/
|
||||||
|
public SubjectPublicKeyInfo wrap(byte[] nativeSpki, AlgorithmIdentity identity, String bindingId)
|
||||||
|
throws IOException {
|
||||||
|
requireCanonical(nativeSpki);
|
||||||
|
org.bouncycastle.asn1.x509.AlgorithmIdentifier algorithm = BcX509AlgorithmAdapter.toBc(
|
||||||
|
catalog.resolve(bindingId, identity, X509AlgorithmRole.SUBJECT_PUBLIC_KEY_ALGORITHM));
|
||||||
|
SubjectPublicKeyInfo wrapped = new SubjectPublicKeyInfo(algorithm, nativeSpki);
|
||||||
|
requireCanonical(wrapped.getEncoded(ASN1Encoding.DER));
|
||||||
|
return wrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns provider-native SPKI for verification or key import.
|
||||||
|
*
|
||||||
|
* @param input exact active SPKI
|
||||||
|
* @return canonical native SPKI; the input itself for standard encodings
|
||||||
|
* @throws IOException if nested SPKI bytes are malformed or non-canonical
|
||||||
|
*/
|
||||||
|
public SubjectPublicKeyInfo unwrap(SubjectPublicKeyInfo input) throws IOException {
|
||||||
|
Objects.requireNonNull(input, "input");
|
||||||
|
String bindingId = catalog.bindingId(BcX509AlgorithmAdapter.fromBc(input.getAlgorithm()),
|
||||||
|
X509AlgorithmRole.SUBJECT_PUBLIC_KEY_ALGORITHM);
|
||||||
|
X509BindingRule rule = catalog.rules().stream().filter(candidate -> candidate.id().equals(bindingId))
|
||||||
|
.findFirst().orElseThrow(() -> new IllegalArgumentException("SPKI binding rule is unavailable"));
|
||||||
|
if (rule.publicKeyEncoding() != X509BindingRule.PublicKeyEncoding.NESTED_SPKI_DER) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
byte[] nested = input.getPublicKeyData().getBytes();
|
||||||
|
requireCanonical(nested);
|
||||||
|
SubjectPublicKeyInfo nativeSpki = SubjectPublicKeyInfo.getInstance(nested);
|
||||||
|
if (!Arrays.equals(nested, nativeSpki.getEncoded(ASN1Encoding.DER))) {
|
||||||
|
throw new IOException("Nested SubjectPublicKeyInfo is not canonical DER");
|
||||||
|
}
|
||||||
|
return nativeSpki;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void requireCanonical(byte[] encoded) throws IOException {
|
||||||
|
new StreamingDerReader().validateSubjectPublicKeyInfo(encoded, MAXIMUM_SPKI_BYTES);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -209,6 +209,8 @@ public final class BcX509SignedObjectValidator {
|
|||||||
X509ExecutionPlan<BcX509VerificationExecutor> verification = authority.plan(signatureIdentity, issuerKey,
|
X509ExecutionPlan<BcX509VerificationExecutor> verification = authority.plan(signatureIdentity, issuerKey,
|
||||||
AlgorithmExecutionCapability.Direction.VERIFY,
|
AlgorithmExecutionCapability.Direction.VERIFY,
|
||||||
Optional.of(BcX509VerificationExecutor.IMPLEMENTATION_ID), "status-postcondition",
|
Optional.of(BcX509VerificationExecutor.IMPLEMENTATION_ID), "status-postcondition",
|
||||||
|
authority.bindings().bindingId(BcX509AlgorithmAdapter.fromBc(identifier),
|
||||||
|
X509AlgorithmRole.SIGNATURE_ALGORITHM),
|
||||||
BcX509VerificationExecutor.class);
|
BcX509VerificationExecutor.class);
|
||||||
try (RepeatableContent tbs = new ContentSlice(content, layout.tbsOffset(), layout.tbsLength())) {
|
try (RepeatableContent tbs = new ContentSlice(content, layout.tbsOffset(), layout.tbsLength())) {
|
||||||
if (!verification.executor().verify(authority, verification, issuerPublicKey, identifier, tbs,
|
if (!verification.executor().verify(authority, verification, issuerPublicKey, identifier, tbs,
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ public final class BcX509StatusObjectGenerator implements StatusObjectGenerator
|
|||||||
private final PkiSigningBus signingBus;
|
private final PkiSigningBus signingBus;
|
||||||
private final AlgorithmIdentity signatureIdentity;
|
private final AlgorithmIdentity signatureIdentity;
|
||||||
private final Duration signingTtl;
|
private final Duration signingTtl;
|
||||||
|
private final Optional<String> signatureBindingId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the X.509 status object generator.
|
* Creates the X.509 status object generator.
|
||||||
@@ -196,6 +197,12 @@ public final class BcX509StatusObjectGenerator implements StatusObjectGenerator
|
|||||||
*/
|
*/
|
||||||
public BcX509StatusObjectGenerator(PkiSigningBus signingBus, AlgorithmIdentity signatureIdentity,
|
public BcX509StatusObjectGenerator(PkiSigningBus signingBus, AlgorithmIdentity signatureIdentity,
|
||||||
Duration signingTtl) {
|
Duration signingTtl) {
|
||||||
|
this(signingBus, signatureIdentity, Optional.empty(), signingTtl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates a generator with an optional explicit CRL-signature binding. */
|
||||||
|
public BcX509StatusObjectGenerator(PkiSigningBus signingBus, AlgorithmIdentity signatureIdentity,
|
||||||
|
Optional<String> signatureBindingId, Duration signingTtl) {
|
||||||
if (signingBus == null) {
|
if (signingBus == null) {
|
||||||
throw new IllegalArgumentException("signingBus must not be null");
|
throw new IllegalArgumentException("signingBus must not be null");
|
||||||
}
|
}
|
||||||
@@ -207,6 +214,7 @@ public final class BcX509StatusObjectGenerator implements StatusObjectGenerator
|
|||||||
}
|
}
|
||||||
this.signingBus = signingBus;
|
this.signingBus = signingBus;
|
||||||
this.signatureIdentity = signatureIdentity;
|
this.signatureIdentity = signatureIdentity;
|
||||||
|
this.signatureBindingId = java.util.Objects.requireNonNull(signatureBindingId, "signatureBindingId");
|
||||||
this.signingTtl = signingTtl;
|
this.signingTtl = signingTtl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,8 +270,10 @@ public final class BcX509StatusObjectGenerator implements StatusObjectGenerator
|
|||||||
boolean accepted = false;
|
boolean accepted = false;
|
||||||
try {
|
try {
|
||||||
entries = encodeEntries(crlEntries);
|
entries = encodeEntries(crlEntries);
|
||||||
PkiBusContentSigner signer = new PkiBusContentSigner(signingBus, issuerMaterial.keyRef(),
|
PkiBusContentSigner signer = signatureBindingId.isPresent()
|
||||||
signatureIdentity, signingTtl);
|
? new PkiBusContentSigner(signingBus, issuerMaterial.keyRef(), signatureIdentity,
|
||||||
|
signatureBindingId.orElseThrow(), signingTtl)
|
||||||
|
: new PkiBusContentSigner(signingBus, issuerMaterial.keyRef(), signatureIdentity, signingTtl);
|
||||||
byte[] algorithm = signer.getAlgorithmIdentifier().getEncoded();
|
byte[] algorithm = signer.getAlgorithmIdentifier().getEncoded();
|
||||||
tbs = encodeTbs(issuerMaterial.issuerHolder(), thisUpdate, nextUpdate, entries, algorithm);
|
tbs = encodeTbs(issuerMaterial.issuerHolder(), thisUpdate, nextUpdate, entries, algorithm);
|
||||||
copyToSigner(tbs, signer);
|
copyToSigner(tbs, signer);
|
||||||
@@ -321,6 +331,8 @@ public final class BcX509StatusObjectGenerator implements StatusObjectGenerator
|
|||||||
X509ExecutionPlan<BcX509VerificationExecutor> plan = signingBus.authority().plan(actualSignature,
|
X509ExecutionPlan<BcX509VerificationExecutor> plan = signingBus.authority().plan(actualSignature,
|
||||||
issuerKey, AlgorithmExecutionCapability.Direction.VERIFY,
|
issuerKey, AlgorithmExecutionCapability.Direction.VERIFY,
|
||||||
Optional.of(BcX509VerificationExecutor.IMPLEMENTATION_ID), "crl-postcondition",
|
Optional.of(BcX509VerificationExecutor.IMPLEMENTATION_ID), "crl-postcondition",
|
||||||
|
signingBus.authority().bindings().bindingId(BcX509AlgorithmAdapter.fromBc(algorithm),
|
||||||
|
X509AlgorithmRole.SIGNATURE_ALGORITHM),
|
||||||
BcX509VerificationExecutor.class);
|
BcX509VerificationExecutor.class);
|
||||||
try (RepeatableContent signedContent = new ContentSlice(complete, layout.tbsOffset(),
|
try (RepeatableContent signedContent = new ContentSlice(complete, layout.tbsOffset(),
|
||||||
layout.tbsLength())) {
|
layout.tbsLength())) {
|
||||||
|
|||||||
@@ -37,12 +37,17 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
|
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
|
||||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
|
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
|
||||||
import org.bouncycastle.operator.ContentVerifier;
|
import org.bouncycastle.operator.ContentVerifier;
|
||||||
import org.bouncycastle.operator.OperatorCreationException;
|
import org.bouncycastle.operator.OperatorCreationException;
|
||||||
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
|
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
|
||||||
@@ -78,7 +83,8 @@ public final class BcX509VerificationExecutor implements AlgorithmExecutionCapab
|
|||||||
BootstrapAlgorithmIdentities.RSA_PKCS1_SHA512, BootstrapAlgorithmIdentities.RSA_PSS_SHA256,
|
BootstrapAlgorithmIdentities.RSA_PKCS1_SHA512, BootstrapAlgorithmIdentities.RSA_PSS_SHA256,
|
||||||
BootstrapAlgorithmIdentities.ECDSA_SHA256, BootstrapAlgorithmIdentities.ECDSA_SHA384,
|
BootstrapAlgorithmIdentities.ECDSA_SHA256, BootstrapAlgorithmIdentities.ECDSA_SHA384,
|
||||||
BootstrapAlgorithmIdentities.ECDSA_SHA512, BootstrapAlgorithmIdentities.ED25519_SIGNATURE,
|
BootstrapAlgorithmIdentities.ECDSA_SHA512, BootstrapAlgorithmIdentities.ED25519_SIGNATURE,
|
||||||
BootstrapAlgorithmIdentities.ED448_SIGNATURE);
|
BootstrapAlgorithmIdentities.ED448_SIGNATURE, BootstrapAlgorithmIdentities.SPHINCS_PLUS_SIGNATURE);
|
||||||
|
private static final BouncyCastlePQCProvider BC_PQC_PROVIDER = new BouncyCastlePQCProvider();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<AlgorithmExecutionCapability> capabilities() {
|
public List<AlgorithmExecutionCapability> capabilities() {
|
||||||
@@ -134,8 +140,16 @@ public final class BcX509VerificationExecutor implements AlgorithmExecutionCapab
|
|||||||
if (!plan.selection().suite().publicKey().equals(publicKeyIdentity)) {
|
if (!plan.selection().suite().publicKey().equals(publicKeyIdentity)) {
|
||||||
throw new IllegalArgumentException("SubjectPublicKeyInfo does not match execution plan");
|
throw new IllegalArgumentException("SubjectPublicKeyInfo does not match execution plan");
|
||||||
}
|
}
|
||||||
|
SubjectPublicKeyInfo nativePublicKey = new BcX509PublicKeyAdapter(authority.bindings()).unwrap(publicKeyInfo);
|
||||||
|
if (BootstrapAlgorithmIdentities.SPHINCS_PLUS_SIGNATURE.equals(signatureIdentity)) {
|
||||||
|
return verifySphincs(nativePublicKey, signedContent, signature);
|
||||||
|
}
|
||||||
|
if (!authority.bindings().resolve(signatureIdentity, X509AlgorithmRole.SIGNATURE_ALGORITHM)
|
||||||
|
.equals(BcX509AlgorithmAdapter.fromBc(algorithmIdentifier))) {
|
||||||
|
return verifyCustomClassic(signatureIdentity, nativePublicKey, signedContent, signature);
|
||||||
|
}
|
||||||
ContentVerifier verifier = new JcaContentVerifierProviderBuilder().setProvider(BC_PROVIDER)
|
ContentVerifier verifier = new JcaContentVerifierProviderBuilder().setProvider(BC_PROVIDER)
|
||||||
.build(publicKeyInfo).get(algorithmIdentifier);
|
.build(nativePublicKey).get(algorithmIdentifier);
|
||||||
try (InputStream input = signedContent.openStream(); OutputStream output = verifier.getOutputStream()) {
|
try (InputStream input = signedContent.openStream(); OutputStream output = verifier.getOutputStream()) {
|
||||||
byte[] buffer = new byte[16 * 1024];
|
byte[] buffer = new byte[16 * 1024];
|
||||||
int read;
|
int read;
|
||||||
@@ -148,4 +162,50 @@ public final class BcX509VerificationExecutor implements AlgorithmExecutionCapab
|
|||||||
}
|
}
|
||||||
return verifier.verify(signature);
|
return verifier.verify(signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean verifySphincs(SubjectPublicKeyInfo publicKeyInfo, RepeatableContent signedContent,
|
||||||
|
byte[] signatureBytes) throws GeneralSecurityException, IOException {
|
||||||
|
PublicKey publicKey = KeyFactory.getInstance("SPHINCS+", BC_PQC_PROVIDER)
|
||||||
|
.generatePublic(new X509EncodedKeySpec(publicKeyInfo.getEncoded()));
|
||||||
|
Signature verifier = Signature.getInstance("SPHINCS+", BC_PQC_PROVIDER);
|
||||||
|
verifier.initVerify(publicKey);
|
||||||
|
try (InputStream input = signedContent.openStream()) {
|
||||||
|
byte[] buffer = new byte[16 * 1024];
|
||||||
|
int read;
|
||||||
|
while ((read = input.read(buffer)) >= 0) {
|
||||||
|
CancellationSignal.NONE.throwIfCancelled();
|
||||||
|
if (read != 0) {
|
||||||
|
verifier.update(buffer, 0, read);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return verifier.verify(signatureBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean verifyCustomClassic(AlgorithmIdentity identity, SubjectPublicKeyInfo publicKeyInfo,
|
||||||
|
RepeatableContent signedContent, byte[] signatureBytes) throws GeneralSecurityException, IOException {
|
||||||
|
String algorithm;
|
||||||
|
if (BootstrapAlgorithmIdentities.ED25519_SIGNATURE.equals(identity)) {
|
||||||
|
algorithm = "Ed25519";
|
||||||
|
} else if (BootstrapAlgorithmIdentities.ED448_SIGNATURE.equals(identity)) {
|
||||||
|
algorithm = "Ed448";
|
||||||
|
} else {
|
||||||
|
throw new GeneralSecurityException("Custom classic binding lacks an exact JCA verification contract");
|
||||||
|
}
|
||||||
|
PublicKey publicKey = KeyFactory.getInstance(algorithm, BC_PROVIDER)
|
||||||
|
.generatePublic(new X509EncodedKeySpec(publicKeyInfo.getEncoded()));
|
||||||
|
Signature verifier = Signature.getInstance(algorithm, BC_PROVIDER);
|
||||||
|
verifier.initVerify(publicKey);
|
||||||
|
try (InputStream input = signedContent.openStream()) {
|
||||||
|
byte[] buffer = new byte[16 * 1024];
|
||||||
|
int read;
|
||||||
|
while ((read = input.read(buffer)) >= 0) {
|
||||||
|
CancellationSignal.NONE.throwIfCancelled();
|
||||||
|
if (read != 0) {
|
||||||
|
verifier.update(buffer, 0, read);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return verifier.verify(signatureBytes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ import zeroecho.pki.api.audit.Principal;
|
|||||||
import zeroecho.pki.api.audit.Purpose;
|
import zeroecho.pki.api.audit.Purpose;
|
||||||
import zeroecho.pki.impl.core.async.PkiSigningBus;
|
import zeroecho.pki.impl.core.async.PkiSigningBus;
|
||||||
import zeroecho.pki.impl.core.async.PkiSigningBus.SignContinuation;
|
import zeroecho.pki.impl.core.async.PkiSigningBus.SignContinuation;
|
||||||
import zeroecho.pki.impl.framework.x509.X509AlgorithmRole;
|
|
||||||
import zeroecho.pki.impl.framework.x509.X509ExecutionPlan;
|
import zeroecho.pki.impl.framework.x509.X509ExecutionPlan;
|
||||||
import zeroecho.pki.spi.crypto.SignatureWorkflow;
|
import zeroecho.pki.spi.crypto.SignatureWorkflow;
|
||||||
import zeroecho.pki.spi.store.ContentSink;
|
import zeroecho.pki.spi.store.ContentSink;
|
||||||
@@ -156,6 +155,25 @@ public final class PkiBusContentSigner implements ContentSigner {
|
|||||||
* authoritative signature binding exists
|
* authoritative signature binding exists
|
||||||
*/
|
*/
|
||||||
public PkiBusContentSigner(PkiSigningBus bus, KeyRef keyRef, AlgorithmIdentity algorithmIdentity, Duration ttl) {
|
public PkiBusContentSigner(PkiSigningBus bus, KeyRef keyRef, AlgorithmIdentity algorithmIdentity, Duration ttl) {
|
||||||
|
this(bus, keyRef, algorithmIdentity, Optional.empty(), ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a signer using one explicitly selected X.509 binding.
|
||||||
|
*
|
||||||
|
* @param bus signing bus
|
||||||
|
* @param keyRef managed signing-key reference
|
||||||
|
* @param algorithmIdentity exact cryptographic identity
|
||||||
|
* @param bindingId stable active binding identity
|
||||||
|
* @param ttl positive workflow time-to-live
|
||||||
|
*/
|
||||||
|
public PkiBusContentSigner(PkiSigningBus bus, KeyRef keyRef, AlgorithmIdentity algorithmIdentity,
|
||||||
|
String bindingId, Duration ttl) {
|
||||||
|
this(bus, keyRef, algorithmIdentity, Optional.of(bindingId), ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PkiBusContentSigner(PkiSigningBus bus, KeyRef keyRef, AlgorithmIdentity algorithmIdentity,
|
||||||
|
Optional<String> bindingId, Duration ttl) {
|
||||||
if (bus == null) {
|
if (bus == null) {
|
||||||
throw new IllegalArgumentException("bus must not be null");
|
throw new IllegalArgumentException("bus must not be null");
|
||||||
}
|
}
|
||||||
@@ -170,8 +188,10 @@ public final class PkiBusContentSigner implements ContentSigner {
|
|||||||
}
|
}
|
||||||
this.bus = bus;
|
this.bus = bus;
|
||||||
this.keyRef = keyRef;
|
this.keyRef = keyRef;
|
||||||
X509ExecutionPlan<SignatureWorkflow> plan = bus.authority()
|
X509ExecutionPlan<SignatureWorkflow> plan = bindingId.isPresent()
|
||||||
.planSigning(algorithmIdentity.canonicalForm(), SignatureWorkflow.class);
|
? bus.authority().planSigningWithBinding(algorithmIdentity.canonicalForm(), bindingId.orElseThrow(),
|
||||||
|
SignatureWorkflow.class)
|
||||||
|
: bus.authority().planSigning(algorithmIdentity.canonicalForm(), SignatureWorkflow.class);
|
||||||
bus.authority().authorize(plan, plan.executor(),
|
bus.authority().authorize(plan, plan.executor(),
|
||||||
zeroecho.core.spi.AlgorithmExecutionCapability.Direction.SIGN);
|
zeroecho.core.spi.AlgorithmExecutionCapability.Direction.SIGN);
|
||||||
this.executionPlan = plan;
|
this.executionPlan = plan;
|
||||||
@@ -205,8 +225,7 @@ public final class PkiBusContentSigner implements ContentSigner {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public AlgorithmIdentifier getAlgorithmIdentifier() {
|
public AlgorithmIdentifier getAlgorithmIdentifier() {
|
||||||
return new BcX509AlgorithmAdapter(bus.authority().bindings()).encode(algorithmIdentity,
|
return BcX509AlgorithmAdapter.toBc(executionPlan.selection().binding());
|
||||||
X509AlgorithmRole.SIGNATURE_ALGORITHM);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -225,7 +225,10 @@ public final class WorkflowProofOfPossessionVerifier implements ProofOfPossessio
|
|||||||
X509AlgorithmRole.SUBJECT_PUBLIC_KEY_ALGORITHM);
|
X509AlgorithmRole.SUBJECT_PUBLIC_KEY_ALGORITHM);
|
||||||
executionPlan = authority.plan(signatureIdentity, keyIdentity,
|
executionPlan = authority.plan(signatureIdentity, keyIdentity,
|
||||||
zeroecho.core.spi.AlgorithmExecutionCapability.Direction.VERIFY,
|
zeroecho.core.spi.AlgorithmExecutionCapability.Direction.VERIFY,
|
||||||
Optional.empty(), "csr-proof", SignatureWorkflow.class);
|
Optional.empty(), "csr-proof",
|
||||||
|
authority.bindings().bindingId(BcX509AlgorithmAdapter.fromBc(
|
||||||
|
csrAsn1.getSignatureAlgorithm()), X509AlgorithmRole.SIGNATURE_ALGORITHM),
|
||||||
|
SignatureWorkflow.class);
|
||||||
} catch (IllegalArgumentException unsupported) {
|
} catch (IllegalArgumentException unsupported) {
|
||||||
return new ProofOfPossessionResult(ProofOfPossessionStatus.NOT_SUPPORTED,
|
return new ProofOfPossessionResult(ProofOfPossessionStatus.NOT_SUPPORTED,
|
||||||
Optional.of("Unsupported CSR algorithm"));
|
Optional.of("Unsupported CSR algorithm"));
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* 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.algorithm;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import zeroecho.pki.api.algorithm.X509AlgorithmBinding;
|
||||||
|
import zeroecho.pki.impl.framework.x509.X509BindingRule;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installed-code provider of deployer-owned immutable X.509 bindings.
|
||||||
|
*
|
||||||
|
* <p>Discovery does not activate a provider. A session must explicitly enable
|
||||||
|
* {@link #providerId()} and authorize every contributed OID root. Provider
|
||||||
|
* implementations must be stateless and must not contain secrets.</p>
|
||||||
|
*/
|
||||||
|
public interface X509AlgorithmBindingProvider {
|
||||||
|
|
||||||
|
/** @return stable provider identity used by explicit configuration */
|
||||||
|
String providerId();
|
||||||
|
|
||||||
|
/** @return stable version or commitment of the complete contributed set */
|
||||||
|
String bindingSetVersion();
|
||||||
|
|
||||||
|
/** @return finite immutable safe descriptors */
|
||||||
|
List<X509AlgorithmBinding> bindings();
|
||||||
|
|
||||||
|
/** @return finite immutable role-specific encoding rules */
|
||||||
|
List<X509BindingRule> rules();
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
{"schemaVersion":2,"certificateType":"END_ENTITY","profileId":"email-signing","profileVersion":1,"formatId":"x509","displayName":"Email Signing","maxValidity":"PT8760H","subject":{"allowEmpty":true,"rules":[{"oid":"2.5.4.3","source":"REQUESTER","minimumOccurrences":0,"maximumOccurrences":1,"maximumUtf8Bytes":253}]},"subjectAlternativeNames":{"minimumTotal":1,"maximumTotal":16,"serviceIdentityRequired":false,"emailIdentityRequired":true,"criticalWhenSubjectNonEmpty":false,"rules":[{"type":"RFC822_NAME","minimumOccurrences":1,"maximumOccurrences":16}]},"leafCertificate":{"basicConstraintsCritical":true,"keyUsageCritical":true,"keyUsage":["DIGITAL_SIGNATURE"],"extendedKeyUsageCritical":false,"extendedKeyUsage":["1.3.6.1.5.5.7.3.4"],"allowedKeyAlgorithms":["ECDSA","Ed25519","RSA"]}}
|
{"schemaVersion":3,"certificateType":"END_ENTITY","profileId":"email-signing","profileVersion":1,"formatId":"x509","displayName":"Email Signing","maxValidity":"PT8760H","algorithmBindings":{"mode":"STANDARD_ONLY"},"subject":{"allowEmpty":true,"rules":[{"oid":"2.5.4.3","source":"REQUESTER","minimumOccurrences":0,"maximumOccurrences":1,"maximumUtf8Bytes":253}]},"subjectAlternativeNames":{"minimumTotal":1,"maximumTotal":16,"serviceIdentityRequired":false,"emailIdentityRequired":true,"criticalWhenSubjectNonEmpty":false,"rules":[{"type":"RFC822_NAME","minimumOccurrences":1,"maximumOccurrences":16}]},"leafCertificate":{"basicConstraintsCritical":true,"keyUsageCritical":true,"keyUsage":["DIGITAL_SIGNATURE"],"extendedKeyUsageCritical":false,"extendedKeyUsage":["1.3.6.1.5.5.7.3.4"],"allowedKeyAlgorithms":["ECDSA","Ed25519","RSA"]}}
|
||||||
@@ -1 +1 @@
|
|||||||
{"schemaVersion":2,"certificateType":"INTERMEDIATE_CA","profileId":"intermediate-ca","profileVersion":1,"formatId":"x509","displayName":"Intermediate CA","maxValidity":"PT43800H","subject":{"allowEmpty":false,"rules":[{"oid":"2.5.4.3","source":"REQUESTER","minimumOccurrences":1,"maximumOccurrences":1,"maximumUtf8Bytes":253}]},"caCertificate":{"basicConstraintsCritical":true,"pathLengthConstraint":0,"keyUsageCritical":true,"keyUsages":["CRL_SIGN","KEY_CERT_SIGN"],"allowedSubjectKeyAlgorithms":["ECDSA","Ed25519","RSA"]}}
|
{"schemaVersion":3,"certificateType":"INTERMEDIATE_CA","profileId":"intermediate-ca","profileVersion":1,"formatId":"x509","displayName":"Intermediate CA","maxValidity":"PT43800H","algorithmBindings":{"mode":"STANDARD_ONLY"},"subject":{"allowEmpty":false,"rules":[{"oid":"2.5.4.3","source":"REQUESTER","minimumOccurrences":1,"maximumOccurrences":1,"maximumUtf8Bytes":253}]},"caCertificate":{"basicConstraintsCritical":true,"pathLengthConstraint":0,"keyUsageCritical":true,"keyUsages":["CRL_SIGN","KEY_CERT_SIGN"],"allowedSubjectKeyAlgorithms":["ECDSA","Ed25519","RSA"]}}
|
||||||
@@ -1 +1 @@
|
|||||||
{"schemaVersion":2,"certificateType":"ROOT_CA","profileId":"root-ca","profileVersion":1,"formatId":"x509","displayName":"Root CA","maxValidity":"PT87600H","subject":{"allowEmpty":false,"rules":[{"oid":"2.5.4.3","source":"REQUESTER","minimumOccurrences":1,"maximumOccurrences":1,"maximumUtf8Bytes":253}]},"caCertificate":{"basicConstraintsCritical":true,"pathLengthConstraint":1,"keyUsageCritical":true,"keyUsages":["CRL_SIGN","KEY_CERT_SIGN"],"allowedSubjectKeyAlgorithms":["ECDSA","Ed25519","RSA"]}}
|
{"schemaVersion":3,"certificateType":"ROOT_CA","profileId":"root-ca","profileVersion":1,"formatId":"x509","displayName":"Root CA","maxValidity":"PT87600H","algorithmBindings":{"mode":"STANDARD_ONLY"},"subject":{"allowEmpty":false,"rules":[{"oid":"2.5.4.3","source":"REQUESTER","minimumOccurrences":1,"maximumOccurrences":1,"maximumUtf8Bytes":253}]},"caCertificate":{"basicConstraintsCritical":true,"pathLengthConstraint":1,"keyUsageCritical":true,"keyUsages":["CRL_SIGN","KEY_CERT_SIGN"],"allowedSubjectKeyAlgorithms":["ECDSA","Ed25519","RSA"]}}
|
||||||
@@ -1 +1 @@
|
|||||||
{"schemaVersion":2,"certificateType":"END_ENTITY","profileId":"server-tls","profileVersion":1,"formatId":"x509","displayName":"Server TLS","maxValidity":"PT8760H","subject":{"allowEmpty":true,"rules":[{"oid":"2.5.4.3","source":"REQUESTER","minimumOccurrences":0,"maximumOccurrences":1,"maximumUtf8Bytes":253}]},"subjectAlternativeNames":{"minimumTotal":1,"maximumTotal":64,"serviceIdentityRequired":true,"emailIdentityRequired":false,"criticalWhenSubjectNonEmpty":false,"rules":[{"type":"DNS_NAME","minimumOccurrences":0,"maximumOccurrences":64,"wildcardAllowed":false},{"type":"IP_ADDRESS","minimumOccurrences":0,"maximumOccurrences":16,"ipv4Allowed":true,"ipv6Allowed":true}]},"leafCertificate":{"basicConstraintsCritical":true,"keyUsageCritical":true,"keyUsage":["DIGITAL_SIGNATURE"],"extendedKeyUsageCritical":false,"extendedKeyUsage":["1.3.6.1.5.5.7.3.1"],"allowedKeyAlgorithms":["ECDSA","Ed25519","RSA"]}}
|
{"schemaVersion":3,"certificateType":"END_ENTITY","profileId":"server-tls","profileVersion":1,"formatId":"x509","displayName":"Server TLS","maxValidity":"PT8760H","algorithmBindings":{"mode":"STANDARD_ONLY"},"subject":{"allowEmpty":true,"rules":[{"oid":"2.5.4.3","source":"REQUESTER","minimumOccurrences":0,"maximumOccurrences":1,"maximumUtf8Bytes":253}]},"subjectAlternativeNames":{"minimumTotal":1,"maximumTotal":64,"serviceIdentityRequired":true,"emailIdentityRequired":false,"criticalWhenSubjectNonEmpty":false,"rules":[{"type":"DNS_NAME","minimumOccurrences":0,"maximumOccurrences":64,"wildcardAllowed":false},{"type":"IP_ADDRESS","minimumOccurrences":0,"maximumOccurrences":16,"ipv4Allowed":true,"ipv6Allowed":true}]},"leafCertificate":{"basicConstraintsCritical":true,"keyUsageCritical":true,"keyUsage":["DIGITAL_SIGNATURE"],"extendedKeyUsageCritical":false,"extendedKeyUsage":["1.3.6.1.5.5.7.3.1"],"allowedKeyAlgorithms":["ECDSA","Ed25519","RSA"]}}
|
||||||
@@ -1 +1 @@
|
|||||||
{"schemaVersion":2,"certificateType":"END_ENTITY","profileId":"vpn-client","profileVersion":1,"formatId":"x509","displayName":"VPN Client","maxValidity":"PT8760H","subject":{"allowEmpty":true,"rules":[{"oid":"2.5.4.3","source":"REQUESTER","minimumOccurrences":0,"maximumOccurrences":1,"maximumUtf8Bytes":253}]},"subjectAlternativeNames":{"minimumTotal":1,"maximumTotal":16,"serviceIdentityRequired":false,"emailIdentityRequired":false,"criticalWhenSubjectNonEmpty":false,"rules":[{"type":"RFC822_NAME","minimumOccurrences":0,"maximumOccurrences":16},{"type":"URI","minimumOccurrences":0,"maximumOccurrences":16,"allowedSchemes":["spiffe"]}]},"leafCertificate":{"basicConstraintsCritical":true,"keyUsageCritical":true,"keyUsage":["DIGITAL_SIGNATURE"],"extendedKeyUsageCritical":false,"extendedKeyUsage":["1.3.6.1.5.5.7.3.2"],"allowedKeyAlgorithms":["ECDSA","Ed25519","RSA"]}}
|
{"schemaVersion":3,"certificateType":"END_ENTITY","profileId":"vpn-client","profileVersion":1,"formatId":"x509","displayName":"VPN Client","maxValidity":"PT8760H","algorithmBindings":{"mode":"STANDARD_ONLY"},"subject":{"allowEmpty":true,"rules":[{"oid":"2.5.4.3","source":"REQUESTER","minimumOccurrences":0,"maximumOccurrences":1,"maximumUtf8Bytes":253}]},"subjectAlternativeNames":{"minimumTotal":1,"maximumTotal":16,"serviceIdentityRequired":false,"emailIdentityRequired":false,"criticalWhenSubjectNonEmpty":false,"rules":[{"type":"RFC822_NAME","minimumOccurrences":0,"maximumOccurrences":16},{"type":"URI","minimumOccurrences":0,"maximumOccurrences":16,"allowedSchemes":["spiffe"]}]},"leafCertificate":{"basicConstraintsCritical":true,"keyUsageCritical":true,"keyUsage":["DIGITAL_SIGNATURE"],"extendedKeyUsageCritical":false,"extendedKeyUsage":["1.3.6.1.5.5.7.3.2"],"allowedKeyAlgorithms":["ECDSA","Ed25519","RSA"]}}
|
||||||
@@ -1 +1 @@
|
|||||||
{"schemaVersion":2,"certificateType":"END_ENTITY","profileId":"vpn-server","profileVersion":1,"formatId":"x509","displayName":"VPN Server","maxValidity":"PT8760H","subject":{"allowEmpty":true,"rules":[{"oid":"2.5.4.3","source":"REQUESTER","minimumOccurrences":0,"maximumOccurrences":1,"maximumUtf8Bytes":253}]},"subjectAlternativeNames":{"minimumTotal":1,"maximumTotal":64,"serviceIdentityRequired":true,"emailIdentityRequired":false,"criticalWhenSubjectNonEmpty":false,"rules":[{"type":"DNS_NAME","minimumOccurrences":0,"maximumOccurrences":64,"wildcardAllowed":false},{"type":"IP_ADDRESS","minimumOccurrences":0,"maximumOccurrences":16,"ipv4Allowed":true,"ipv6Allowed":true}]},"leafCertificate":{"basicConstraintsCritical":true,"keyUsageCritical":true,"keyUsage":["DIGITAL_SIGNATURE"],"extendedKeyUsageCritical":false,"extendedKeyUsage":["1.3.6.1.5.5.7.3.1"],"allowedKeyAlgorithms":["ECDSA","Ed25519","RSA"]}}
|
{"schemaVersion":3,"certificateType":"END_ENTITY","profileId":"vpn-server","profileVersion":1,"formatId":"x509","displayName":"VPN Server","maxValidity":"PT8760H","algorithmBindings":{"mode":"STANDARD_ONLY"},"subject":{"allowEmpty":true,"rules":[{"oid":"2.5.4.3","source":"REQUESTER","minimumOccurrences":0,"maximumOccurrences":1,"maximumUtf8Bytes":253}]},"subjectAlternativeNames":{"minimumTotal":1,"maximumTotal":64,"serviceIdentityRequired":true,"emailIdentityRequired":false,"criticalWhenSubjectNonEmpty":false,"rules":[{"type":"DNS_NAME","minimumOccurrences":0,"maximumOccurrences":64,"wildcardAllowed":false},{"type":"IP_ADDRESS","minimumOccurrences":0,"maximumOccurrences":16,"ipv4Allowed":true,"ipv6Allowed":true}]},"leafCertificate":{"basicConstraintsCritical":true,"keyUsageCritical":true,"keyUsage":["DIGITAL_SIGNATURE"],"extendedKeyUsageCritical":false,"extendedKeyUsage":["1.3.6.1.5.5.7.3.1"],"allowedKeyAlgorithms":["ECDSA","Ed25519","RSA"]}}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"bindings": [
|
||||||
|
{
|
||||||
|
"bindingId": "zeroecho.private.sphincs-plus-default.spki.v1",
|
||||||
|
"algorithmIdentity": "SPHINCS_PLUS_PUBLIC_KEY",
|
||||||
|
"role": "SUBJECT_PUBLIC_KEY",
|
||||||
|
"oid": "1.3.6.1.4.1.31424.1.1.1",
|
||||||
|
"encodingVersion": 1,
|
||||||
|
"parameterRule": "ABSENT",
|
||||||
|
"publicKeyEncoding": "NESTED_SPKI_DER",
|
||||||
|
"signatureEncoding": "NOT_APPLICABLE",
|
||||||
|
"interoperability": "ZEROECHO_ECOSYSTEM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bindingId": "zeroecho.private.sphincs-plus-default.csr-signature.v1",
|
||||||
|
"algorithmIdentity": "SPHINCS_PLUS_SIGNATURE",
|
||||||
|
"role": "CSR_SIGNATURE",
|
||||||
|
"oid": "1.3.6.1.4.1.31424.1.1.2",
|
||||||
|
"encodingVersion": 1,
|
||||||
|
"parameterRule": "ABSENT",
|
||||||
|
"publicKeyEncoding": "NOT_APPLICABLE",
|
||||||
|
"signatureEncoding": "OPAQUE",
|
||||||
|
"interoperability": "ZEROECHO_ECOSYSTEM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bindingId": "zeroecho.private.sphincs-plus-default.certificate-signature.v1",
|
||||||
|
"algorithmIdentity": "SPHINCS_PLUS_SIGNATURE",
|
||||||
|
"role": "CERTIFICATE_SIGNATURE",
|
||||||
|
"oid": "1.3.6.1.4.1.31424.1.1.3",
|
||||||
|
"encodingVersion": 1,
|
||||||
|
"parameterRule": "ABSENT",
|
||||||
|
"publicKeyEncoding": "NOT_APPLICABLE",
|
||||||
|
"signatureEncoding": "OPAQUE",
|
||||||
|
"interoperability": "ZEROECHO_ECOSYSTEM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bindingId": "zeroecho.private.sphincs-plus-default.crl-signature.v1",
|
||||||
|
"algorithmIdentity": "SPHINCS_PLUS_SIGNATURE",
|
||||||
|
"role": "CRL_SIGNATURE",
|
||||||
|
"oid": "1.3.6.1.4.1.31424.1.1.4",
|
||||||
|
"encodingVersion": 1,
|
||||||
|
"parameterRule": "ABSENT",
|
||||||
|
"publicKeyEncoding": "NOT_APPLICABLE",
|
||||||
|
"signatureEncoding": "OPAQUE",
|
||||||
|
"interoperability": "ZEROECHO_ECOSYSTEM"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -245,10 +245,10 @@ final class BuiltInCertificateProfileCatalogTest {
|
|||||||
@Test
|
@Test
|
||||||
void profileDocumentsFailClosedForInvalidSchemaFieldsEncodingAndClassMetadata() {
|
void profileDocumentsFailClosedForInvalidSchemaFieldsEncodingAndClassMetadata() {
|
||||||
List<byte[]> invalidDocuments = List.of(
|
List<byte[]> invalidDocuments = List.of(
|
||||||
replace(mainResource(SERVER), "\"schemaVersion\":2", "\"schemaVersion\":1"),
|
replace(mainResource(SERVER), "\"schemaVersion\":3", "\"schemaVersion\":1"),
|
||||||
replace(mainResource(SERVER), "\"schemaVersion\":2,", "\"schemaVersion\":2,\"active\":true,"),
|
replace(mainResource(SERVER), "\"schemaVersion\":3,", "\"schemaVersion\":3,\"active\":true,"),
|
||||||
replace(mainResource(SERVER), "\"schemaVersion\":2,",
|
replace(mainResource(SERVER), "\"schemaVersion\":3,",
|
||||||
"\"schemaVersion\":2,\"@class\":\"" + InitializationSentinel.CLASS_NAME + "\","),
|
"\"schemaVersion\":3,\"@class\":\"" + InitializationSentinel.CLASS_NAME + "\","),
|
||||||
malformedUtf8(mainResource(SERVER)));
|
malformedUtf8(mainResource(SERVER)));
|
||||||
for (byte[] invalid : invalidDocuments) {
|
for (byte[] invalid : invalidDocuments) {
|
||||||
Map<String, List<byte[]>> resources = baseResources();
|
Map<String, List<byte[]>> resources = baseResources();
|
||||||
|
|||||||
@@ -63,13 +63,14 @@ final class CertificateProfileDocumentCodecTest {
|
|||||||
|
|
||||||
private static final String VALID_DOCUMENT = """
|
private static final String VALID_DOCUMENT = """
|
||||||
{
|
{
|
||||||
"schemaVersion": 2,
|
"schemaVersion": 3,
|
||||||
"certificateType": "END_ENTITY",
|
"certificateType": "END_ENTITY",
|
||||||
"profileId": "tls-service",
|
"profileId": "tls-service",
|
||||||
"profileVersion": 7,
|
"profileVersion": 7,
|
||||||
"formatId": "x509",
|
"formatId": "x509",
|
||||||
"displayName": "TLS service",
|
"displayName": "TLS service",
|
||||||
"maxValidity": "PT24H",
|
"maxValidity": "PT24H",
|
||||||
|
"algorithmBindings": {"mode":"STANDARD_ONLY"},
|
||||||
"subject": {
|
"subject": {
|
||||||
"allowEmpty": false,
|
"allowEmpty": false,
|
||||||
"rules": [
|
"rules": [
|
||||||
@@ -138,7 +139,7 @@ final class CertificateProfileDocumentCodecTest {
|
|||||||
void parsesEverySupportedRuleShapeIntoAuthoritativeTypedPolicies() {
|
void parsesEverySupportedRuleShapeIntoAuthoritativeTypedPolicies() {
|
||||||
CertificateProfileDefinition definition = parse(VALID_DOCUMENT);
|
CertificateProfileDefinition definition = parse(VALID_DOCUMENT);
|
||||||
|
|
||||||
assertEquals(2, CertificateProfileDefinition.SCHEMA_VERSION);
|
assertEquals(3, CertificateProfileDefinition.SCHEMA_VERSION);
|
||||||
assertEquals(CertificateProfileKind.END_ENTITY, definition.certificateType());
|
assertEquals(CertificateProfileKind.END_ENTITY, definition.certificateType());
|
||||||
assertEquals("tls-service", definition.profileId());
|
assertEquals("tls-service", definition.profileId());
|
||||||
assertEquals(7, definition.profileVersion());
|
assertEquals(7, definition.profileVersion());
|
||||||
@@ -311,10 +312,10 @@ final class CertificateProfileDocumentCodecTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void rejectsDuplicateUnknownMissingNullWrongAndNonintegralFields() {
|
void rejectsDuplicateUnknownMissingNullWrongAndNonintegralFields() {
|
||||||
assertCode(VALID_DOCUMENT.replace("\"schemaVersion\": 2,", "\"schemaVersion\": 2,\"schemaVersion\": 2,"),
|
assertCode(VALID_DOCUMENT.replace("\"schemaVersion\": 3,", "\"schemaVersion\": 3,\"schemaVersion\": 3,"),
|
||||||
"DUPLICATE_FIELD");
|
"DUPLICATE_FIELD");
|
||||||
for (String document : List.of(
|
for (String document : List.of(
|
||||||
VALID_DOCUMENT.replace("\"schemaVersion\": 2,", "\"secret-field\": true,\"schemaVersion\": 2,"),
|
VALID_DOCUMENT.replace("\"schemaVersion\": 3,", "\"secret-field\": true,\"schemaVersion\": 3,"),
|
||||||
VALID_DOCUMENT.replace("\"allowEmpty\": false,", "\"unknown\": true,\"allowEmpty\": false,"),
|
VALID_DOCUMENT.replace("\"allowEmpty\": false,", "\"unknown\": true,\"allowEmpty\": false,"),
|
||||||
VALID_DOCUMENT.replace("\"oid\": \"2.5.4.3\",", "\"unknown\": true,\"oid\": \"2.5.4.3\","),
|
VALID_DOCUMENT.replace("\"oid\": \"2.5.4.3\",", "\"unknown\": true,\"oid\": \"2.5.4.3\","),
|
||||||
VALID_DOCUMENT.replace("\"minimumTotal\": 1,", "\"unknown\": true,\"minimumTotal\": 1,"),
|
VALID_DOCUMENT.replace("\"minimumTotal\": 1,", "\"unknown\": true,\"minimumTotal\": 1,"),
|
||||||
@@ -330,7 +331,7 @@ final class CertificateProfileDocumentCodecTest {
|
|||||||
"\"keyUsageCritical\": true,\"keyUsageCritical\": true,"))) {
|
"\"keyUsageCritical\": true,\"keyUsageCritical\": true,"))) {
|
||||||
assertCode(document, "DUPLICATE_FIELD");
|
assertCode(document, "DUPLICATE_FIELD");
|
||||||
}
|
}
|
||||||
for (String document : List.of(VALID_DOCUMENT.replace("\"schemaVersion\": 2,\n", ""),
|
for (String document : List.of(VALID_DOCUMENT.replace("\"schemaVersion\": 3,\n", ""),
|
||||||
VALID_DOCUMENT.replace("\"allowEmpty\": false,\n", ""),
|
VALID_DOCUMENT.replace("\"allowEmpty\": false,\n", ""),
|
||||||
VALID_DOCUMENT.replace("\"oid\": \"2.5.4.3\",\n", ""),
|
VALID_DOCUMENT.replace("\"oid\": \"2.5.4.3\",\n", ""),
|
||||||
VALID_DOCUMENT.replace("\"minimumTotal\": 1,\n", ""),
|
VALID_DOCUMENT.replace("\"minimumTotal\": 1,\n", ""),
|
||||||
@@ -355,7 +356,7 @@ final class CertificateProfileDocumentCodecTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void rejectsUnsupportedVersionsTokensCaseWhitespaceAndNoncanonicalDuration() {
|
void rejectsUnsupportedVersionsTokensCaseWhitespaceAndNoncanonicalDuration() {
|
||||||
assertCode(VALID_DOCUMENT.replace("\"schemaVersion\": 2", "\"schemaVersion\": 1"),
|
assertCode(VALID_DOCUMENT.replace("\"schemaVersion\": 3", "\"schemaVersion\": 1"),
|
||||||
"SCHEMA_VERSION_UNSUPPORTED");
|
"SCHEMA_VERSION_UNSUPPORTED");
|
||||||
assertCode(VALID_DOCUMENT.replace("\"certificateType\": \"END_ENTITY\"", "\"certificateType\": \"end_entity\""),
|
assertCode(VALID_DOCUMENT.replace("\"certificateType\": \"END_ENTITY\"", "\"certificateType\": \"end_entity\""),
|
||||||
"CERTIFICATE_TYPE_UNSUPPORTED");
|
"CERTIFICATE_TYPE_UNSUPPORTED");
|
||||||
@@ -473,8 +474,8 @@ final class CertificateProfileDocumentCodecTest {
|
|||||||
@Test
|
@Test
|
||||||
void redactsHostileInputAndParserDetailsFromFailures() {
|
void redactsHostileInputAndParserDetailsFromFailures() {
|
||||||
String secret = "do-not-disclose-credential";
|
String secret = "do-not-disclose-credential";
|
||||||
String hostile = VALID_DOCUMENT.replace("\"schemaVersion\": 2,",
|
String hostile = VALID_DOCUMENT.replace("\"schemaVersion\": 3,",
|
||||||
"\"" + secret + "\": true,\"schemaVersion\": 2,");
|
"\"" + secret + "\": true,\"schemaVersion\": 3,");
|
||||||
|
|
||||||
PkiException exception = assertThrows(PkiException.class, () -> parse(hostile));
|
PkiException exception = assertThrows(PkiException.class, () -> parse(hostile));
|
||||||
|
|
||||||
@@ -508,8 +509,8 @@ final class CertificateProfileDocumentCodecTest {
|
|||||||
logger.addHandler(handler);
|
logger.addHandler(handler);
|
||||||
try {
|
try {
|
||||||
for (String field : hostileFields) {
|
for (String field : hostileFields) {
|
||||||
String document = VALID_DOCUMENT.replace("\"schemaVersion\": 2,",
|
String document = VALID_DOCUMENT.replace("\"schemaVersion\": 3,",
|
||||||
"\"" + field + "\":\"" + probeName + "\",\"schemaVersion\": 2,");
|
"\"" + field + "\":\"" + probeName + "\",\"schemaVersion\": 3,");
|
||||||
PkiException exception = assertThrows(PkiException.class, () -> parse(document));
|
PkiException exception = assertThrows(PkiException.class, () -> parse(document));
|
||||||
assertTrue(exception.getMessage().contains("code=UNKNOWN_FIELD "));
|
assertTrue(exception.getMessage().contains("code=UNKNOWN_FIELD "));
|
||||||
assertFalse(exception.getMessage().contains(field));
|
assertFalse(exception.getMessage().contains(field));
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Proxy;
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.KeyPairGenerator;
|
import java.security.KeyPairGenerator;
|
||||||
@@ -48,6 +49,7 @@ import java.time.Duration;
|
|||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
@@ -68,6 +70,7 @@ import zeroecho.pki.spi.ProviderConfig;
|
|||||||
import zeroecho.pki.spi.audit.AuditSink;
|
import zeroecho.pki.spi.audit.AuditSink;
|
||||||
import zeroecho.pki.spi.store.PkiStore;
|
import zeroecho.pki.spi.store.PkiStore;
|
||||||
import zeroecho.pki.testkit.InMemorySignatureWorkflow;
|
import zeroecho.pki.testkit.InMemorySignatureWorkflow;
|
||||||
|
import zeroecho.pki.testkit.TestX509AlgorithmBindingProvider;
|
||||||
|
|
||||||
class PkiSessionLifecycleTest {
|
class PkiSessionLifecycleTest {
|
||||||
|
|
||||||
@@ -96,6 +99,60 @@ class PkiSessionLifecycleTest {
|
|||||||
System.out.println("...ok");
|
System.out.println("...ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void explicitlyActivatesAuthorizedDeployerBindingProvider() throws Exception {
|
||||||
|
System.out.println("explicitlyActivatesAuthorizedDeployerBindingProvider");
|
||||||
|
PkiSessionConfiguration.BindingProviderConfiguration bindings =
|
||||||
|
new PkiSessionConfiguration.BindingProviderConfiguration(TestX509AlgorithmBindingProvider.ID,
|
||||||
|
List.of(TestX509AlgorithmBindingProvider.PEN_ROOT,
|
||||||
|
TestX509AlgorithmBindingProvider.EGOTHOR_ROOT),
|
||||||
|
Optional.of("test-bindings-v1"));
|
||||||
|
PkiSessionConfiguration configuration = new PkiSessionConfiguration(3,
|
||||||
|
new ProviderConfig("fs", Map.of("root", temporaryDirectory.resolve("binding-store").toString())),
|
||||||
|
new ProviderConfig("memory", Map.of("size", "16")), Optional.empty(), List.of(), List.of(bindings));
|
||||||
|
try (PkiSession session = PkiSession.open(configuration)) {
|
||||||
|
assertTrue(session.algorithmBindings().find("test.ed25519.certificate.v1").isPresent());
|
||||||
|
assertTrue(session.algorithmBindings().find("test.ed25519.crl.v1").isPresent());
|
||||||
|
System.out.println("...bindings=" + session.algorithmBindings().bindings().size());
|
||||||
|
}
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void profileValidationRejectsBindingOutsideActiveRegistry() throws Exception {
|
||||||
|
System.out.println("profileValidationRejectsBindingOutsideActiveRegistry");
|
||||||
|
PkiSessionConfiguration configuration = configuration(temporaryDirectory.resolve("profile-binding-store"));
|
||||||
|
BuiltInCertificateProfileTemplate template = BuiltInCertificateProfileCatalog
|
||||||
|
.load(getClass().getClassLoader()).stream().findFirst().orElseThrow();
|
||||||
|
String document = new String(template.canonicalJson(), StandardCharsets.UTF_8).replace(
|
||||||
|
"\"algorithmBindings\":{\"mode\":\"STANDARD_ONLY\"}",
|
||||||
|
"\"algorithmBindings\":{\"mode\":\"EXPLICIT\",\"certificateSignature\":"
|
||||||
|
+ "{\"bindingId\":\"deployer.inactive.certificate.v1\","
|
||||||
|
+ "\"commitment\":\"inactive-binding-commitment\"}}");
|
||||||
|
try (PkiSession session = PkiSession.open(configuration)) {
|
||||||
|
PkiOperationOutcome outcome = session.operations().execute(
|
||||||
|
new PkiOperation.ValidateProfile(document.getBytes(StandardCharsets.UTF_8)),
|
||||||
|
CancellationSignal.NONE);
|
||||||
|
PkiOperationOutcome.Failure failure = assertInstanceOf(PkiOperationOutcome.Failure.class, outcome);
|
||||||
|
System.out.println("...classification=" + failure.classification());
|
||||||
|
assertEquals(PkiOperationFailure.POLICY_REJECTION, failure.classification());
|
||||||
|
}
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsUnauthorizedDeployerBindingRootBeforeStoreAllocation() {
|
||||||
|
System.out.println("rejectsUnauthorizedDeployerBindingRootBeforeStoreAllocation");
|
||||||
|
PkiSessionConfiguration.BindingProviderConfiguration bindings =
|
||||||
|
new PkiSessionConfiguration.BindingProviderConfiguration(TestX509AlgorithmBindingProvider.ID,
|
||||||
|
List.of(TestX509AlgorithmBindingProvider.PEN_ROOT), Optional.of("test-bindings-v1"));
|
||||||
|
PkiSessionConfiguration configuration = new PkiSessionConfiguration(3,
|
||||||
|
new ProviderConfig("fs", Map.of("root", temporaryDirectory.resolve("unused-binding").toString())),
|
||||||
|
new ProviderConfig("memory", Map.of("size", "16")), Optional.empty(), List.of(), List.of(bindings));
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> PkiSession.open(configuration));
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void validatesBeforeAllocationAndCleansPartialConstruction() {
|
void validatesBeforeAllocationAndCleansPartialConstruction() {
|
||||||
System.out.println("validatesBeforeAllocationAndCleansPartialConstruction");
|
System.out.println("validatesBeforeAllocationAndCleansPartialConstruction");
|
||||||
@@ -142,7 +199,7 @@ class PkiSessionLifecycleTest {
|
|||||||
placeholder, new ProviderConfig("x509-bc", Map.of()),
|
placeholder, new ProviderConfig("x509-bc", Map.of()),
|
||||||
temporaryDirectory.resolve("signing-bus").toString(), "RSA-2048-PSS-SHA256", Duration.ofSeconds(5),
|
temporaryDirectory.resolve("signing-bus").toString(), "RSA-2048-PSS-SHA256", Duration.ofSeconds(5),
|
||||||
Optional.empty());
|
Optional.empty());
|
||||||
PkiSessionConfiguration configuration = new PkiSessionConfiguration(2,
|
PkiSessionConfiguration configuration = new PkiSessionConfiguration(3,
|
||||||
new ProviderConfig("fs", Map.of("root", temporaryDirectory.resolve("store-signing").toString())),
|
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());
|
new ProviderConfig("memory", Map.of("size", "16")), Optional.of(signing), java.util.List.of());
|
||||||
IllegalStateException failure = assertThrows(IllegalStateException.class,
|
IllegalStateException failure = assertThrows(IllegalStateException.class,
|
||||||
@@ -159,7 +216,7 @@ class PkiSessionLifecycleTest {
|
|||||||
System.out.println("opensExplicitPublisherCapabilityWithoutSigning");
|
System.out.println("opensExplicitPublisherCapabilityWithoutSigning");
|
||||||
ProviderConfig publisher = new ProviderConfig("filesystem",
|
ProviderConfig publisher = new ProviderConfig("filesystem",
|
||||||
Map.of("root", temporaryDirectory.resolve("published").toString(), "targetId", "local-crls"));
|
Map.of("root", temporaryDirectory.resolve("published").toString(), "targetId", "local-crls"));
|
||||||
PkiSessionConfiguration configuration = new PkiSessionConfiguration(2,
|
PkiSessionConfiguration configuration = new PkiSessionConfiguration(3,
|
||||||
new ProviderConfig("fs", Map.of("root", temporaryDirectory.resolve("store-publisher").toString())),
|
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));
|
new ProviderConfig("memory", Map.of("size", "16")), Optional.empty(), java.util.List.of(publisher));
|
||||||
try (PkiSession session = PkiSession.open(configuration)) {
|
try (PkiSession session = PkiSession.open(configuration)) {
|
||||||
@@ -174,7 +231,7 @@ class PkiSessionLifecycleTest {
|
|||||||
void rejectsUnknownPublisherBeforeStoreAllocation() {
|
void rejectsUnknownPublisherBeforeStoreAllocation() {
|
||||||
System.out.println("rejectsUnknownPublisherBeforeStoreAllocation");
|
System.out.println("rejectsUnknownPublisherBeforeStoreAllocation");
|
||||||
Path storeRoot = temporaryDirectory.resolve("must-not-open");
|
Path storeRoot = temporaryDirectory.resolve("must-not-open");
|
||||||
PkiSessionConfiguration configuration = new PkiSessionConfiguration(2,
|
PkiSessionConfiguration configuration = new PkiSessionConfiguration(3,
|
||||||
new ProviderConfig("fs", Map.of("root", storeRoot.toString())),
|
new ProviderConfig("fs", Map.of("root", storeRoot.toString())),
|
||||||
new ProviderConfig("memory", Map.of("size", "16")), Optional.empty(),
|
new ProviderConfig("memory", Map.of("size", "16")), Optional.empty(),
|
||||||
java.util.List.of(new ProviderConfig("unknown-publisher", Map.of())));
|
java.util.List.of(new ProviderConfig("unknown-publisher", Map.of())));
|
||||||
@@ -197,7 +254,7 @@ class PkiSessionLifecycleTest {
|
|||||||
placeholder, new ProviderConfig("x509-bc", Map.of()),
|
placeholder, new ProviderConfig("x509-bc", Map.of()),
|
||||||
temporaryDirectory.resolve("composed-signing-bus").toString(), "SHA256withRSA",
|
temporaryDirectory.resolve("composed-signing-bus").toString(), "SHA256withRSA",
|
||||||
Duration.ofSeconds(5), Optional.empty());
|
Duration.ofSeconds(5), Optional.empty());
|
||||||
PkiSessionConfiguration configuration = new PkiSessionConfiguration(2,
|
PkiSessionConfiguration configuration = new PkiSessionConfiguration(3,
|
||||||
new ProviderConfig("fs", Map.of("root", temporaryDirectory.resolve("composed-store").toString())),
|
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());
|
new ProviderConfig("memory", Map.of("size", "16")), Optional.of(signing), java.util.List.of());
|
||||||
DefaultPkiSession.Bootstrap bootstrap = signingBootstrap(workflow);
|
DefaultPkiSession.Bootstrap bootstrap = signingBootstrap(workflow);
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import java.time.Clock;
|
|||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
import java.util.concurrent.CyclicBarrier;
|
import java.util.concurrent.CyclicBarrier;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
@@ -60,13 +61,22 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.junit.jupiter.api.io.TempDir;
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
import zeroecho.pki.api.PkiException;
|
import zeroecho.pki.api.PkiException;
|
||||||
|
import zeroecho.pki.api.algorithm.X509AlgorithmBinding;
|
||||||
|
import zeroecho.pki.api.algorithm.X509AlgorithmBindingRegistry;
|
||||||
import zeroecho.pki.api.profile.BuiltInCertificateProfileCatalog;
|
import zeroecho.pki.api.profile.BuiltInCertificateProfileCatalog;
|
||||||
import zeroecho.pki.api.profile.BuiltInCertificateProfileTemplate;
|
import zeroecho.pki.api.profile.BuiltInCertificateProfileTemplate;
|
||||||
import zeroecho.pki.api.profile.CertificateProfileRef;
|
import zeroecho.pki.api.profile.CertificateProfileRef;
|
||||||
|
import zeroecho.pki.api.profile.CertificateProfileDefinition;
|
||||||
|
import zeroecho.pki.api.profile.CertificateProfileDocumentCodec;
|
||||||
|
import zeroecho.pki.api.profile.X509AlgorithmBindingPolicy;
|
||||||
|
import zeroecho.pki.impl.framework.x509.ZeroEchoPrivateX509Bindings;
|
||||||
|
import zeroecho.pki.impl.framework.x509.ImmutableX509AlgorithmBindingRegistry;
|
||||||
|
import zeroecho.pki.impl.framework.x509.StandardX509Bindings;
|
||||||
import zeroecho.pki.impl.audit.InMemoryAuditSink;
|
import zeroecho.pki.impl.audit.InMemoryAuditSink;
|
||||||
import zeroecho.pki.impl.fs.FilesystemPkiStore;
|
import zeroecho.pki.impl.fs.FilesystemPkiStore;
|
||||||
import zeroecho.pki.impl.fs.FsPkiStoreOptions;
|
import zeroecho.pki.impl.fs.FsPkiStoreOptions;
|
||||||
import zeroecho.pki.spi.store.PkiStore;
|
import zeroecho.pki.spi.store.PkiStore;
|
||||||
|
import zeroecho.pki.testkit.TestX509AlgorithmBindingProvider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Focused persisted profile lifecycle tests.
|
* Focused persisted profile lifecycle tests.
|
||||||
@@ -217,6 +227,68 @@ final class DefaultProfileServiceTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void explicitPrivateBindingIsPinnedAndChangedCommitmentFailsClosed(@TempDir Path directory) throws Exception {
|
||||||
|
System.out.println("explicitPrivateBindingIsPinnedAndChangedCommitmentFailsClosed");
|
||||||
|
CertificateProfileDefinition base = builtIn("server-tls").definition();
|
||||||
|
zeroecho.pki.api.algorithm.X509AlgorithmBinding binding = ZeroEchoPrivateX509Bindings.descriptors().stream()
|
||||||
|
.filter(candidate -> candidate.role()
|
||||||
|
== zeroecho.pki.api.algorithm.X509AlgorithmBinding.Role.CERTIFICATE_SIGNATURE)
|
||||||
|
.findFirst().orElseThrow();
|
||||||
|
X509AlgorithmBindingPolicy.BindingReference reference = new X509AlgorithmBindingPolicy.BindingReference(
|
||||||
|
binding.bindingId(), binding.semanticCommitment());
|
||||||
|
X509AlgorithmBindingPolicy policy = new X509AlgorithmBindingPolicy(X509AlgorithmBindingPolicy.Mode.EXPLICIT,
|
||||||
|
Optional.empty(), Optional.empty(), Optional.of(reference), Optional.empty());
|
||||||
|
CertificateProfileDefinition explicit = new CertificateProfileDefinition(base.certificateType(),
|
||||||
|
"server-tls-private", base.profileVersion(), base.formatId(), "Private server TLS",
|
||||||
|
base.maximumValidity(), base.subjectPolicy(), base.certificatePolicy(), policy);
|
||||||
|
try (FilesystemPkiStore store = new FilesystemPkiStore(directory.resolve("store"), FsPkiStoreOptions.defaults(),
|
||||||
|
CLOCK)) {
|
||||||
|
DefaultProfileService service = service(store);
|
||||||
|
service.importProfile(CertificateProfileDocumentCodec.writeCanonical(explicit));
|
||||||
|
X509AlgorithmBindingPolicy changed = new X509AlgorithmBindingPolicy(X509AlgorithmBindingPolicy.Mode.EXPLICIT,
|
||||||
|
Optional.empty(), Optional.empty(), Optional.of(new X509AlgorithmBindingPolicy.BindingReference(
|
||||||
|
binding.bindingId(), binding.semanticCommitment() + "-changed")), Optional.empty());
|
||||||
|
CertificateProfileDefinition invalid = new CertificateProfileDefinition(base.certificateType(),
|
||||||
|
"server-tls-changed", base.profileVersion(), base.formatId(), "Changed server TLS",
|
||||||
|
base.maximumValidity(), base.subjectPolicy(), base.certificatePolicy(), changed);
|
||||||
|
assertThrows(PkiException.class,
|
||||||
|
() -> service.importProfile(CertificateProfileDocumentCodec.writeCanonical(invalid)));
|
||||||
|
}
|
||||||
|
System.out.println("...binding=" + binding.bindingId());
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void persistedDeployerBindingRequiresIdenticalRegistryAfterReopen(@TempDir Path directory) throws Exception {
|
||||||
|
System.out.println("persistedDeployerBindingRequiresIdenticalRegistryAfterReopen");
|
||||||
|
List<X509AlgorithmBinding> descriptors = new java.util.ArrayList<>(StandardX509Bindings.descriptors());
|
||||||
|
descriptors.addAll(ZeroEchoPrivateX509Bindings.descriptors());
|
||||||
|
descriptors.addAll(new TestX509AlgorithmBindingProvider().bindings());
|
||||||
|
X509AlgorithmBindingRegistry deployerRegistry = ImmutableX509AlgorithmBindingRegistry.create(descriptors);
|
||||||
|
X509AlgorithmBinding binding = deployerRegistry.find("test.ed25519.certificate.v1").orElseThrow();
|
||||||
|
CertificateProfileDefinition base = builtIn("server-tls").definition();
|
||||||
|
X509AlgorithmBindingPolicy policy = new X509AlgorithmBindingPolicy(X509AlgorithmBindingPolicy.Mode.EXPLICIT,
|
||||||
|
Optional.empty(), Optional.empty(), Optional.of(new X509AlgorithmBindingPolicy.BindingReference(
|
||||||
|
binding.bindingId(), binding.semanticCommitment())), Optional.empty());
|
||||||
|
CertificateProfileDefinition explicit = new CertificateProfileDefinition(base.certificateType(),
|
||||||
|
"server-tls-deployer", base.profileVersion(), base.formatId(), "Deployer server TLS",
|
||||||
|
base.maximumValidity(), base.subjectPolicy(), base.certificatePolicy(), policy);
|
||||||
|
Path root = directory.resolve("store");
|
||||||
|
try (FilesystemPkiStore store = new FilesystemPkiStore(root, FsPkiStoreOptions.defaults(), CLOCK)) {
|
||||||
|
DefaultProfileService service = service(store, deployerRegistry);
|
||||||
|
service.importProfile(CertificateProfileDocumentCodec.writeCanonical(explicit));
|
||||||
|
service.activateProfile(explicit.profileId(), explicit.profileVersion());
|
||||||
|
}
|
||||||
|
try (FilesystemPkiStore store = new FilesystemPkiStore(root, FsPkiStoreOptions.defaults(), CLOCK)) {
|
||||||
|
assertEquals(explicit.profileId(), service(store, deployerRegistry)
|
||||||
|
.requireActiveProfile(explicit.profileId()).reference().profileId());
|
||||||
|
assertThrows(PkiException.class, () -> service(store).requireActiveProfile(explicit.profileId()));
|
||||||
|
}
|
||||||
|
System.out.println("...binding=" + binding.bindingId());
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void lifecycleOperationsSanitizeStoreDiagnosticsAndAuditCodes() {
|
void lifecycleOperationsSanitizeStoreDiagnosticsAndAuditCodes() {
|
||||||
String sentinel = "unsafe-profile-store-sentinel";
|
String sentinel = "unsafe-profile-store-sentinel";
|
||||||
@@ -248,6 +320,11 @@ final class DefaultProfileServiceTest {
|
|||||||
return new DefaultProfileService(store, CLOCK, new InMemoryAuditSink());
|
return new DefaultProfileService(store, CLOCK, new InMemoryAuditSink());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static DefaultProfileService service(FilesystemPkiStore store,
|
||||||
|
X509AlgorithmBindingRegistry algorithmBindings) {
|
||||||
|
return new DefaultProfileService(store, CLOCK, new InMemoryAuditSink(), algorithmBindings);
|
||||||
|
}
|
||||||
|
|
||||||
private static BuiltInCertificateProfileTemplate builtIn(String profileId) {
|
private static BuiltInCertificateProfileTemplate builtIn(String profileId) {
|
||||||
return BuiltInCertificateProfileCatalog.load(DefaultProfileServiceTest.class.getClassLoader()).stream()
|
return BuiltInCertificateProfileCatalog.load(DefaultProfileServiceTest.class.getClassLoader()).stream()
|
||||||
.filter(template -> profileId.equals(template.definition().profileId())).findFirst().orElseThrow();
|
.filter(template -> profileId.equals(template.definition().profileId())).findFirst().orElseThrow();
|
||||||
|
|||||||
@@ -38,22 +38,59 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.KeyPairGenerator;
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.security.Security;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
|
||||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||||
import org.bouncycastle.asn1.DERNull;
|
import org.bouncycastle.asn1.DERNull;
|
||||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
|
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
|
||||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name;
|
||||||
|
import org.bouncycastle.cert.X509CRLHolder;
|
||||||
|
import org.bouncycastle.cert.X509CertificateHolder;
|
||||||
|
import org.bouncycastle.cert.X509v2CRLBuilder;
|
||||||
|
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||||
|
import org.bouncycastle.operator.ContentSigner;
|
||||||
|
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
|
||||||
|
import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder;
|
||||||
|
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import zeroecho.core.alg.BootstrapAlgorithmIdentities;
|
import zeroecho.core.alg.BootstrapAlgorithmIdentities;
|
||||||
import zeroecho.core.spec.AlgorithmIdentity;
|
import zeroecho.core.spec.AlgorithmIdentity;
|
||||||
import zeroecho.core.io.ImmutableByteContent;
|
import zeroecho.core.io.ImmutableByteContent;
|
||||||
|
import zeroecho.core.io.RepeatableContent;
|
||||||
|
import zeroecho.core.alg.sphincsplus.SphincsPlusKeyGenSpec;
|
||||||
|
import zeroecho.sdk.ZeroEchoSession;
|
||||||
|
import zeroecho.pki.api.algorithm.X509AlgorithmBinding;
|
||||||
|
import zeroecho.pki.api.EncodedObject;
|
||||||
|
import zeroecho.pki.api.Encoding;
|
||||||
|
import zeroecho.pki.api.FormatId;
|
||||||
|
import zeroecho.pki.api.issuance.VerificationPolicy;
|
||||||
|
import zeroecho.pki.api.request.CertificationRequest;
|
||||||
|
import zeroecho.pki.api.request.ParsedCertificationRequest;
|
||||||
|
import zeroecho.pki.api.request.ProofOfPossessionStatus;
|
||||||
|
import zeroecho.pki.impl.framework.x509.bc.BcX509CertificationRequestParser;
|
||||||
|
import zeroecho.pki.impl.framework.x509.bc.BcX509ProofOfPossessionVerifier;
|
||||||
|
import zeroecho.pki.impl.framework.x509.bc.BcX509SignedObjectValidator;
|
||||||
|
import zeroecho.pki.impl.framework.x509.bc.BcX509PublicKeyAdapter;
|
||||||
|
import zeroecho.pki.testkit.TestX509AlgorithmBindingProvider;
|
||||||
import zeroecho.core.spec.AlgorithmSuite;
|
import zeroecho.core.spec.AlgorithmSuite;
|
||||||
import zeroecho.core.spi.AlgorithmExecutionCapability;
|
import zeroecho.core.spi.AlgorithmExecutionCapability;
|
||||||
import zeroecho.core.spi.AlgorithmExecutionCapabilityProvider;
|
import zeroecho.core.spi.AlgorithmExecutionCapabilityProvider;
|
||||||
@@ -65,6 +102,136 @@ import zeroecho.pki.impl.framework.x509.bc.BcX509VerificationExecutor;
|
|||||||
*/
|
*/
|
||||||
public final class X509BindingPhaseATest {
|
public final class X509BindingPhaseATest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void zeroEchoPrivateAssignmentsAreExactAndLocked() {
|
||||||
|
System.out.println("zeroEchoPrivateAssignmentsAreExactAndLocked");
|
||||||
|
List<X509AlgorithmBinding> bindings = ZeroEchoPrivateX509Bindings.descriptors();
|
||||||
|
assertEquals(List.of("1.3.6.1.4.1.31424.1.1.1", "1.3.6.1.4.1.31424.1.1.2",
|
||||||
|
"1.3.6.1.4.1.31424.1.1.3", "1.3.6.1.4.1.31424.1.1.4"),
|
||||||
|
bindings.stream().map(X509AlgorithmBinding::oid).toList());
|
||||||
|
assertEquals(4, bindings.stream().map(X509AlgorithmBinding::bindingId).distinct().count());
|
||||||
|
assertThrows(UnsupportedOperationException.class, () -> bindings.add(bindings.get(0)));
|
||||||
|
assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> ImmutableX509AlgorithmBindingRegistry.create(List.of(bindings.get(0), bindings.get(0))));
|
||||||
|
System.out.println("...assignment-count=" + bindings.size());
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void productionSphincsPrivateBindingSignsAndVerifies() throws Exception {
|
||||||
|
System.out.println("productionSphincsPrivateBindingSignsAndVerifies");
|
||||||
|
Security.addProvider(new BouncyCastlePQCProvider());
|
||||||
|
KeyPair keyPair = new ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("SPHINCS+",
|
||||||
|
SphincsPlusKeyGenSpec.defaultSpec());
|
||||||
|
BcX509VerificationExecutor verifier = new BcX509VerificationExecutor();
|
||||||
|
X509AuthoritySnapshot authority = X509AuthoritySnapshot.compose(List.of(), List.of(verifier),
|
||||||
|
List.of(X509AuthoritySnapshot.bindExecutor(BcX509VerificationExecutor.IMPLEMENTATION_ID,
|
||||||
|
AlgorithmExecutionCapability.Direction.VERIFY, verifier)), policy(true, "private-test-v1"));
|
||||||
|
String signatureBinding = "zeroecho.private.sphincs-plus-default.certificate-signature.v1";
|
||||||
|
String publicKeyBinding = "zeroecho.private.sphincs-plus-default.spki.v1";
|
||||||
|
X509ExecutionPlan<BcX509VerificationExecutor> plan = authority.plan(
|
||||||
|
BootstrapAlgorithmIdentities.SPHINCS_PLUS_SIGNATURE,
|
||||||
|
BootstrapAlgorithmIdentities.SPHINCS_PLUS_PUBLIC_KEY,
|
||||||
|
AlgorithmExecutionCapability.Direction.VERIFY, Optional.empty(), "explicit", signatureBinding,
|
||||||
|
BcX509VerificationExecutor.class);
|
||||||
|
SubjectPublicKeyInfo wrapped = new BcX509PublicKeyAdapter(authority.bindings()).wrap(
|
||||||
|
keyPair.getPublic().getEncoded(), BootstrapAlgorithmIdentities.SPHINCS_PLUS_PUBLIC_KEY,
|
||||||
|
publicKeyBinding);
|
||||||
|
AlgorithmIdentifier identifier = BcX509AlgorithmAdapter.toBc(authority.bindings().resolve(signatureBinding,
|
||||||
|
BootstrapAlgorithmIdentities.SPHINCS_PLUS_SIGNATURE, X509AlgorithmRole.SIGNATURE_ALGORITHM));
|
||||||
|
byte[] payload = "ZeroEcho private X.509 binding".getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
Signature signer = Signature.getInstance("SPHINCS+", new BouncyCastlePQCProvider());
|
||||||
|
signer.initSign(keyPair.getPrivate());
|
||||||
|
signer.update(payload);
|
||||||
|
byte[] signature = signer.sign();
|
||||||
|
try (RepeatableContent content = content(payload)) {
|
||||||
|
assertTrue(verifier.verify(authority, plan, wrapped, identifier, content, signature));
|
||||||
|
}
|
||||||
|
|
||||||
|
AlgorithmIdentifier csrIdentifier = privateIdentifier(authority,
|
||||||
|
"zeroecho.private.sphincs-plus-default.csr-signature.v1");
|
||||||
|
PKCS10CertificationRequest csr = new PKCS10CertificationRequestBuilder(new X500Name("CN=Private Request"),
|
||||||
|
wrapped).build(contentSigner(keyPair.getPrivate(), csrIdentifier));
|
||||||
|
ParsedCertificationRequest parsed = new BcX509CertificationRequestParser().parse(new CertificationRequest(
|
||||||
|
new FormatId("x509"), new EncodedObject(Encoding.DER, csr.getEncoded())));
|
||||||
|
assertEquals(ProofOfPossessionStatus.VERIFIED,
|
||||||
|
new BcX509ProofOfPossessionVerifier(authority, verifier)
|
||||||
|
.verify(parsed, new VerificationPolicy(true, Optional.empty())).status());
|
||||||
|
|
||||||
|
Instant notBefore = Instant.parse("2026-08-04T00:00:00Z");
|
||||||
|
X500Name issuer = new X500Name("CN=Private Root");
|
||||||
|
X509CertificateHolder certificate = new X509v3CertificateBuilder(issuer, BigInteger.ONE,
|
||||||
|
Date.from(notBefore), Date.from(notBefore.plusSeconds(3600)), issuer, wrapped)
|
||||||
|
.build(contentSigner(keyPair.getPrivate(), identifier));
|
||||||
|
try (RepeatableContent certificateContent = content(certificate.getEncoded())) {
|
||||||
|
BcX509SignedObjectValidator.CertificateBindings validated = new BcX509SignedObjectValidator(authority)
|
||||||
|
.validateCertificate(certificateContent,
|
||||||
|
Optional.of(BootstrapAlgorithmIdentities.SPHINCS_PLUS_SIGNATURE),
|
||||||
|
zeroecho.core.io.CancellationSignal.NONE);
|
||||||
|
assertEquals(BootstrapAlgorithmIdentities.SPHINCS_PLUS_PUBLIC_KEY, validated.subjectPublicKey());
|
||||||
|
}
|
||||||
|
try (RepeatableContent tbs = content(certificate.toASN1Structure().getTBSCertificate().getEncoded())) {
|
||||||
|
assertTrue(verifier.verify(authority, plan, wrapped, identifier, tbs, certificate.getSignature()));
|
||||||
|
}
|
||||||
|
|
||||||
|
AlgorithmIdentifier crlIdentifier = privateIdentifier(authority,
|
||||||
|
"zeroecho.private.sphincs-plus-default.crl-signature.v1");
|
||||||
|
X509CRLHolder crl = new X509v2CRLBuilder(issuer, Date.from(notBefore))
|
||||||
|
.setNextUpdate(Date.from(notBefore.plusSeconds(1800)))
|
||||||
|
.build(contentSigner(keyPair.getPrivate(), crlIdentifier));
|
||||||
|
X509ExecutionPlan<BcX509VerificationExecutor> crlPlan = authority.plan(
|
||||||
|
BootstrapAlgorithmIdentities.SPHINCS_PLUS_SIGNATURE,
|
||||||
|
BootstrapAlgorithmIdentities.SPHINCS_PLUS_PUBLIC_KEY,
|
||||||
|
AlgorithmExecutionCapability.Direction.VERIFY, Optional.empty(), "explicit",
|
||||||
|
"zeroecho.private.sphincs-plus-default.crl-signature.v1", BcX509VerificationExecutor.class);
|
||||||
|
try (RepeatableContent tbs = content(crl.toASN1Structure().getTBSCertList().getEncoded())) {
|
||||||
|
assertTrue(verifier.verify(authority, crlPlan, wrapped, crlIdentifier, tbs,
|
||||||
|
crl.toASN1Structure().getSignature().getOctets()));
|
||||||
|
}
|
||||||
|
assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> new BcX509AlgorithmAdapter(authority.bindings()).decode(
|
||||||
|
new AlgorithmIdentifier(identifier.getAlgorithm(), DERNull.INSTANCE),
|
||||||
|
X509AlgorithmRole.SIGNATURE_ALGORITHM));
|
||||||
|
System.out.println("...oid=" + identifier.getAlgorithm().getId() + ", signature-bytes=" + signature.length);
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deployerProviderBindingSignsAndVerifiesEndToEnd() throws Exception {
|
||||||
|
System.out.println("deployerProviderBindingSignsAndVerifiesEndToEnd");
|
||||||
|
TestX509AlgorithmBindingProvider deployer = new TestX509AlgorithmBindingProvider();
|
||||||
|
X509BindingRuleProvider provider = new X509BindingRuleProvider() {
|
||||||
|
@Override
|
||||||
|
public List<X509BindingRule> rules() {
|
||||||
|
return deployer.rules();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
BcX509VerificationExecutor verifier = new BcX509VerificationExecutor();
|
||||||
|
X509AuthoritySnapshot authority = X509AuthoritySnapshot.compose(List.of(provider), List.of(verifier),
|
||||||
|
List.of(X509AuthoritySnapshot.bindExecutor(BcX509VerificationExecutor.IMPLEMENTATION_ID,
|
||||||
|
AlgorithmExecutionCapability.Direction.VERIFY, verifier)), policy(true, "deployer-test-v1"));
|
||||||
|
KeyPairGenerator generator = KeyPairGenerator.getInstance("Ed25519");
|
||||||
|
KeyPair pair = generator.generateKeyPair();
|
||||||
|
SubjectPublicKeyInfo spki = SubjectPublicKeyInfo.getInstance(pair.getPublic().getEncoded());
|
||||||
|
String bindingId = "test.ed25519.certificate.v1";
|
||||||
|
AlgorithmIdentifier identifier = BcX509AlgorithmAdapter.toBc(authority.bindings().resolve(bindingId,
|
||||||
|
BootstrapAlgorithmIdentities.ED25519_SIGNATURE, X509AlgorithmRole.SIGNATURE_ALGORITHM));
|
||||||
|
X500Name name = new X500Name("CN=Deployer Binding");
|
||||||
|
Instant start = Instant.parse("2026-08-04T00:00:00Z");
|
||||||
|
X509CertificateHolder certificate = new X509v3CertificateBuilder(name, BigInteger.TWO, Date.from(start),
|
||||||
|
Date.from(start.plusSeconds(3600)), name, spki)
|
||||||
|
.build(contentSigner(pair.getPrivate(), identifier, "Ed25519"));
|
||||||
|
X509ExecutionPlan<BcX509VerificationExecutor> plan = authority.plan(
|
||||||
|
BootstrapAlgorithmIdentities.ED25519_SIGNATURE, BootstrapAlgorithmIdentities.ED25519_PUBLIC_KEY,
|
||||||
|
AlgorithmExecutionCapability.Direction.VERIFY, Optional.empty(), "explicit", bindingId,
|
||||||
|
BcX509VerificationExecutor.class);
|
||||||
|
try (RepeatableContent tbs = content(certificate.toASN1Structure().getTBSCertificate().getEncoded())) {
|
||||||
|
assertTrue(verifier.verify(authority, plan, spki, identifier, tbs, certificate.getSignature()));
|
||||||
|
}
|
||||||
|
System.out.println("...oid=" + identifier.getAlgorithm().getId());
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void bootstrapMatrixIsExactAndSymmetric() {
|
void bootstrapMatrixIsExactAndSymmetric() {
|
||||||
System.out.println("bootstrapMatrixIsExactAndSymmetric");
|
System.out.println("bootstrapMatrixIsExactAndSymmetric");
|
||||||
@@ -374,6 +541,71 @@ public final class X509BindingPhaseATest {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static RepeatableContent content(byte[] bytes) {
|
||||||
|
byte[] snapshot = bytes.clone();
|
||||||
|
return new RepeatableContent() {
|
||||||
|
@Override
|
||||||
|
public ByteArrayInputStream openStream() {
|
||||||
|
return new ByteArrayInputStream(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OptionalLong length() {
|
||||||
|
return OptionalLong.of(snapshot.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String contentId() {
|
||||||
|
return "private-binding-test-content";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
// immutable in-memory test content owns no external resource
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AlgorithmIdentifier privateIdentifier(X509AuthoritySnapshot authority, String bindingId) {
|
||||||
|
return BcX509AlgorithmAdapter.toBc(authority.bindings().resolve(bindingId,
|
||||||
|
BootstrapAlgorithmIdentities.SPHINCS_PLUS_SIGNATURE, X509AlgorithmRole.SIGNATURE_ALGORITHM));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ContentSigner contentSigner(PrivateKey privateKey, AlgorithmIdentifier identifier) {
|
||||||
|
return contentSigner(privateKey, identifier, "SPHINCS+");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ContentSigner contentSigner(PrivateKey privateKey, AlgorithmIdentifier identifier,
|
||||||
|
String jcaAlgorithm) {
|
||||||
|
return new ContentSigner() {
|
||||||
|
private final ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AlgorithmIdentifier getAlgorithmIdentifier() {
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutputStream getOutputStream() {
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getSignature() {
|
||||||
|
try {
|
||||||
|
Signature signature = "SPHINCS+".equals(jcaAlgorithm)
|
||||||
|
? Signature.getInstance(jcaAlgorithm, new BouncyCastlePQCProvider())
|
||||||
|
: Signature.getInstance(jcaAlgorithm);
|
||||||
|
signature.initSign(privateKey);
|
||||||
|
signature.update(output.toByteArray());
|
||||||
|
return signature.sign();
|
||||||
|
} catch (java.security.GeneralSecurityException failure) {
|
||||||
|
throw new IllegalStateException("Test signing failed", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test-only conflicting extension rule.
|
* Test-only conflicting extension rule.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -229,8 +229,9 @@ public final class H7ProfileDocuments {
|
|||||||
List<String> sanRules, boolean noKeyEncipherment, List<String> eku) {
|
List<String> sanRules, boolean noKeyEncipherment, List<String> eku) {
|
||||||
String keyUsage = noKeyEncipherment ? "[\"DIGITAL_SIGNATURE\"]"
|
String keyUsage = noKeyEncipherment ? "[\"DIGITAL_SIGNATURE\"]"
|
||||||
: "[\"DIGITAL_SIGNATURE\",\"KEY_ENCIPHERMENT\"]";
|
: "[\"DIGITAL_SIGNATURE\",\"KEY_ENCIPHERMENT\"]";
|
||||||
String json = "{\"schemaVersion\":2,\"certificateType\":\"END_ENTITY\",\"profileId\":\"" + id
|
String json = "{\"schemaVersion\":3,\"certificateType\":\"END_ENTITY\",\"profileId\":\"" + id
|
||||||
+ "\",\"profileVersion\":1,\"formatId\":\"" + formatId + "\",\"displayName\":\"H7 Test Profile\","
|
+ "\",\"profileVersion\":1,\"formatId\":\"" + formatId + "\",\"displayName\":\"H7 Test Profile\","
|
||||||
|
+ "\"algorithmBindings\":{\"mode\":\"STANDARD_ONLY\"},"
|
||||||
+ "\"maxValidity\":\"PT8760H\",\"subject\":{\"allowEmpty\":" + allowEmpty + ",\"rules\":["
|
+ "\"maxValidity\":\"PT8760H\",\"subject\":{\"allowEmpty\":" + allowEmpty + ",\"rules\":["
|
||||||
+ String.join(",", subjectRules) + "]}," + "\"subjectAlternativeNames\":{\"minimumTotal\":"
|
+ String.join(",", subjectRules) + "]}," + "\"subjectAlternativeNames\":{\"minimumTotal\":"
|
||||||
+ minimumTotal + ",\"maximumTotal\":" + maximumTotal + ",\"serviceIdentityRequired\":" + serviceIdentity
|
+ minimumTotal + ",\"maximumTotal\":" + maximumTotal + ",\"serviceIdentityRequired\":" + serviceIdentity
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* 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.testkit;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import zeroecho.core.alg.BootstrapAlgorithmIdentities;
|
||||||
|
import zeroecho.core.spec.AlgorithmIdentity;
|
||||||
|
import zeroecho.pki.api.algorithm.X509AlgorithmBinding;
|
||||||
|
import zeroecho.pki.impl.framework.x509.X509AlgorithmIdentifier;
|
||||||
|
import zeroecho.pki.impl.framework.x509.X509AlgorithmRole;
|
||||||
|
import zeroecho.pki.impl.framework.x509.X509BindingRule;
|
||||||
|
import zeroecho.pki.spi.algorithm.X509AlgorithmBindingProvider;
|
||||||
|
|
||||||
|
/** Deterministic deployer binding provider used only by service-loading tests. */
|
||||||
|
public final class TestX509AlgorithmBindingProvider implements X509AlgorithmBindingProvider {
|
||||||
|
/** Stable provider identity. */
|
||||||
|
public static final String ID = "test.deployer.bindings";
|
||||||
|
/** Deployer-owned PEN test root. */
|
||||||
|
public static final String PEN_ROOT = "1.3.6.1.4.1.55555.42";
|
||||||
|
/** Egothor deployment-local test root. */
|
||||||
|
public static final String EGOTHOR_ROOT = "1.3.6.1.4.1.31424.1.2.4242";
|
||||||
|
|
||||||
|
private static final X509AlgorithmBinding CERTIFICATE = binding("test.ed25519.certificate.v1",
|
||||||
|
X509AlgorithmBinding.Role.CERTIFICATE_SIGNATURE, PEN_ROOT + ".1");
|
||||||
|
private static final X509AlgorithmBinding CRL = binding("test.ed25519.crl.v1",
|
||||||
|
X509AlgorithmBinding.Role.CRL_SIGNATURE, EGOTHOR_ROOT + ".1");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String providerId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String bindingSetVersion() {
|
||||||
|
return "test-bindings-v1";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<X509AlgorithmBinding> bindings() {
|
||||||
|
return List.of(CERTIFICATE, CRL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<X509BindingRule> rules() {
|
||||||
|
return List.of(new Rule(CERTIFICATE), new Rule(CRL));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static X509AlgorithmBinding binding(String id, X509AlgorithmBinding.Role role, String oid) {
|
||||||
|
return new X509AlgorithmBinding(id, BootstrapAlgorithmIdentities.ED25519_SIGNATURE, role, oid,
|
||||||
|
X509AlgorithmBinding.Origin.DEPLOYER_PRIVATE, 1, X509AlgorithmBinding.ParameterRule.ABSENT,
|
||||||
|
X509AlgorithmBinding.PublicKeyEncoding.NOT_APPLICABLE,
|
||||||
|
X509AlgorithmBinding.SignatureEncoding.OPAQUE,
|
||||||
|
X509AlgorithmBinding.Interoperability.DEPLOYER_ECOSYSTEM, Optional.of(ID), id + "|v1");
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Rule(X509AlgorithmBinding binding) implements X509BindingRule {
|
||||||
|
@Override
|
||||||
|
public String id() {
|
||||||
|
return binding.bindingId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public X509AlgorithmRole role() {
|
||||||
|
return X509AlgorithmRole.SIGNATURE_ALGORITHM;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String oid() {
|
||||||
|
return binding.oid();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String semanticFingerprint() {
|
||||||
|
return binding.semanticCommitment();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SignatureEncoding signatureEncoding() {
|
||||||
|
return SignatureEncoding.OPAQUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PublicKeyEncoding publicKeyEncoding() {
|
||||||
|
return PublicKeyEncoding.NOT_APPLICABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<X509AlgorithmBinding> bindings() {
|
||||||
|
return List.of(binding);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<X509AlgorithmIdentifier> encode(AlgorithmIdentity identity) {
|
||||||
|
return binding.algorithmIdentity().equals(identity)
|
||||||
|
? Optional.of(X509AlgorithmIdentifier.absent(binding.oid())) : Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<AlgorithmIdentity> decode(X509AlgorithmIdentifier identifier) {
|
||||||
|
if (!binding.oid().equals(identifier.oid())) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
if (identifier.parameterForm() != X509AlgorithmIdentifier.ParameterForm.ABSENT) {
|
||||||
|
throw new IllegalArgumentException("Test binding parameters must be absent");
|
||||||
|
}
|
||||||
|
return Optional.of(binding.algorithmIdentity());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
zeroecho.pki.testkit.TestX509AlgorithmBindingProvider
|
||||||
Reference in New Issue
Block a user