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
|
||||
presentation; signed status/CRL generation and inspection; and explicit
|
||||
publication registration, processing, retry, reconciliation, inspection, and
|
||||
listing. Presentation limits bound terminal output only; backend revocation and
|
||||
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
|
||||
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
|
||||
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
|
||||
signing-bus location, an algorithm, a finite signing deadline, and the name of an
|
||||
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
|
||||
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
|
||||
typed operations; batch execution opens that service graph once. Backup and
|
||||
import/export commands remain unavailable because the current backend has no
|
||||
production service implementation for them. Final backend release readiness is
|
||||
intentionally deferred until the planned extensible X.509 algorithm/OID binding
|
||||
work and its CLI integration are complete; this CLI does not anticipate or add
|
||||
placeholders for that future work.
|
||||
production service implementation for them.
|
||||
|
||||
|
||||
## Development Status
|
||||
|
||||
@@ -67,7 +67,10 @@ public final class PkiCli {
|
||||
Map.entry("--type", "type"), Map.entry("--publication-id", "publicationId"),
|
||||
Map.entry("--source-type", "sourceType"), Map.entry("--source-id", "sourceId"),
|
||||
Map.entry("--target-type", "targetType"), Map.entry("--target-id", "targetId"),
|
||||
Map.entry("--reason", "reason"), Map.entry("--limit", "limit"));
|
||||
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 PkiCli() {
|
||||
@@ -263,6 +266,9 @@ public final class PkiCli {
|
||||
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("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(" profile.validate --profile-file <file>");
|
||||
writer.println(" profile.register --profile-file <file>");
|
||||
|
||||
@@ -48,10 +48,17 @@ import zeroecho.pki.spi.ProviderConfig;
|
||||
final class PkiCliConfiguration {
|
||||
|
||||
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_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> 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");
|
||||
private static final String STDOUT_PROVIDER = "stdout";
|
||||
|
||||
@@ -64,8 +71,12 @@ final class PkiCliConfiguration {
|
||||
int version = Math.toIntExact(integer(required(root, "version")));
|
||||
if (version == VERSION_ONE) {
|
||||
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 {
|
||||
requireVersionTwoFields(root.fields());
|
||||
throw new IllegalArgumentException("Unsupported CLI configuration version");
|
||||
}
|
||||
ProviderConfig store = provider(required(root, "store"));
|
||||
ProviderConfig audit = provider(required(root, "audit"));
|
||||
@@ -80,23 +91,46 @@ final class PkiCliConfiguration {
|
||||
PkiOperationValue publisherValue = root.fields().get("publishers");
|
||||
List<ProviderConfig> publishers = publisherValue == null ? List.of()
|
||||
: list(publisherValue).values().stream().map(PkiCliConfiguration::provider).toList();
|
||||
return new PkiSessionConfiguration(version, store, audit, signing, publishers);
|
||||
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) {
|
||||
if (!actual.keySet().containsAll(ROOT_FIELDS_V1) || !ROOT_FIELDS_V2.containsAll(actual.keySet())) {
|
||||
private static void requireFields(Map<String, PkiOperationValue> actual, Set<String> allowed) {
|
||||
if (!actual.keySet().containsAll(ROOT_FIELDS_V1) || !allowed.containsAll(actual.keySet())) {
|
||||
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) {
|
||||
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")),
|
||||
provider(required(object, "framework")), text(required(object, "busPath")),
|
||||
text(required(object, "signatureAlgorithm")),
|
||||
Duration.ofSeconds(integer(required(object, "signingTtlSeconds"))),
|
||||
java.util.Optional.of(text(required(object, "unlockEnvironmentVariable"))));
|
||||
java.util.Optional.of(text(required(object, "unlockEnvironmentVariable"))),
|
||||
optionalText(object, "certificateSignatureBinding"), optionalText(object, "crlSignatureBinding"),
|
||||
optionalText(object, "subjectPublicKeyBinding"));
|
||||
}
|
||||
|
||||
private static ProviderConfig provider(PkiOperationValue value) {
|
||||
@@ -140,6 +174,11 @@ final class PkiCliConfiguration {
|
||||
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) {
|
||||
if (value instanceof PkiOperationValue.IntegerValue integer) {
|
||||
return integer.value();
|
||||
|
||||
@@ -68,12 +68,16 @@ final class PkiOperationRegistry {
|
||||
private static final String PROFILE_ID = "profileId";
|
||||
private static final String PROFILE_VERSION = "profileVersion";
|
||||
private static final String REQUEST_ID = "requestId";
|
||||
private static final String BINDING_ID = "bindingId";
|
||||
private static final int MAXIMUM_REQUEST_BYTES = 1024 * 1024;
|
||||
|
||||
private final Map<String, Binding> bindings;
|
||||
|
||||
/* default */ PkiOperationRegistry() {
|
||||
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.ValidateProfile.NAME, this::profile);
|
||||
add(configured, PkiOperation.RegisterProfile.NAME, this::registerProfile);
|
||||
@@ -123,6 +127,29 @@ final class PkiOperationRegistry {
|
||||
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 {
|
||||
requireFields(arguments, Set.of(PROFILE_FILE));
|
||||
Path path = resolve(baseDirectory, text(arguments, PROFILE_FILE));
|
||||
@@ -296,6 +323,11 @@ final class PkiOperationRegistry {
|
||||
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) {
|
||||
return PkiCliConfiguration.integer(PkiCliConfiguration.required(arguments, name));
|
||||
}
|
||||
|
||||
@@ -168,6 +168,33 @@ class PkiCliTest {
|
||||
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
|
||||
void versionTwoConfigurationKeepsCapabilitiesExplicit() throws IOException {
|
||||
System.out.println("versionTwoConfigurationKeepsCapabilitiesExplicit");
|
||||
@@ -393,6 +420,40 @@ class PkiCliTest {
|
||||
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
|
||||
public zeroecho.pki.application.PkiOperationExecutor operations() {
|
||||
return (operation, cancellation) -> outcome(operation);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 2,
|
||||
"version": 3,
|
||||
"store": {
|
||||
"provider": "fs",
|
||||
"properties": {
|
||||
|
||||
@@ -7,6 +7,21 @@
|
||||
"operation": "configuration.validate",
|
||||
"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",
|
||||
"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 ED25519_FAMILY = family("ed25519");
|
||||
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 EC_KEY = family("ec");
|
||||
|
||||
@@ -99,6 +100,9 @@ public final class BootstrapAlgorithmIdentities {
|
||||
/** Ed448 signature identity. */
|
||||
public static final AlgorithmIdentity ED448_SIGNATURE = identity(AlgorithmIdentity.Kind.SIGNATURE, ED448_FAMILY,
|
||||
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. */
|
||||
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. */
|
||||
public static final AlgorithmIdentity ED448_PUBLIC_KEY = identity(AlgorithmIdentity.Kind.PUBLIC_KEY, ED448_FAMILY,
|
||||
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. */
|
||||
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,
|
||||
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,
|
||||
EC_P521_PUBLIC_KEY, ED25519_PUBLIC_KEY, ED448_PUBLIC_KEY);
|
||||
ED25519_SIGNATURE, ED448_SIGNATURE, SPHINCS_PLUS_SIGNATURE, RSA_PUBLIC_KEY, EC_P256_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);
|
||||
|
||||
@@ -141,7 +149,8 @@ public final class BootstrapAlgorithmIdentities {
|
||||
Map.entry("SHA384withECDSA", ECDSA_SHA384),
|
||||
Map.entry("SHA512withECDSA", ECDSA_SHA512),
|
||||
Map.entry("Ed25519", ED25519_SIGNATURE),
|
||||
Map.entry("Ed448", ED448_SIGNATURE));
|
||||
Map.entry("Ed448", ED448_SIGNATURE),
|
||||
Map.entry("SPHINCS+", SPHINCS_PLUS_SIGNATURE));
|
||||
|
||||
private BootstrapAlgorithmIdentities() {
|
||||
}
|
||||
|
||||
@@ -118,7 +118,9 @@ public final class SignatureInteropProfiles {
|
||||
new SignatureInteropProfile("Ed25519", "Ed25519", "Ed25519", VoidSpec.INSTANCE,
|
||||
SignatureInteropProfile.SignatureRepresentation.IDENTITY, 0)),
|
||||
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(
|
||||
canonical(BootstrapAlgorithmIdentities.RSA_PKCS1_SHA256, "SHA256withRSA"),
|
||||
@@ -129,7 +131,8 @@ public final class SignatureInteropProfiles {
|
||||
canonical(BootstrapAlgorithmIdentities.ECDSA_SHA384, "SHA384withECDSA"),
|
||||
canonical(BootstrapAlgorithmIdentities.ECDSA_SHA512, "SHA512withECDSA"),
|
||||
canonical(BootstrapAlgorithmIdentities.ED25519_SIGNATURE, "Ed25519"),
|
||||
canonical(BootstrapAlgorithmIdentities.ED448_SIGNATURE, "Ed448"));
|
||||
canonical(BootstrapAlgorithmIdentities.ED448_SIGNATURE, "Ed448"),
|
||||
canonical(BootstrapAlgorithmIdentities.SPHINCS_PLUS_SIGNATURE, "SPHINCS+"));
|
||||
|
||||
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. */
|
||||
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. */
|
||||
public CaCertificatePolicy {
|
||||
|
||||
@@ -56,9 +56,18 @@ import zeroecho.pki.api.FormatId;
|
||||
* @param maximumValidity positive maximum validity
|
||||
* @param subjectPolicy complete end-entity subject 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,
|
||||
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.
|
||||
@@ -77,7 +86,7 @@ public record CertificateProfile(String profileId, FormatId formatId, String dis
|
||||
throw new IllegalArgumentException("displayName must not be null/blank");
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
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.
|
||||
*
|
||||
* <p>
|
||||
* Schema version 2 replaces the pre-release version 1 shape. The certificate
|
||||
* kind and its closed policy variant are immutable across the definition.
|
||||
* Schema version 3 adds explicit immutable X.509 algorithm-binding selection to
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
@@ -54,13 +55,23 @@ import zeroecho.pki.api.FormatId;
|
||||
* @param maximumValidity positive maximum validity
|
||||
* @param subjectPolicy complete subject policy
|
||||
* @param certificatePolicy closed certificate-specific policy variant
|
||||
* @param algorithmBindingPolicy explicit X.509 representation policy
|
||||
*/
|
||||
public record CertificateProfileDefinition(CertificateProfileKind certificateType, String profileId,
|
||||
long profileVersion, FormatId formatId, String displayName, Duration maximumValidity,
|
||||
SubjectPolicy subjectPolicy, CertificatePolicy certificatePolicy) {
|
||||
SubjectPolicy subjectPolicy, CertificatePolicy certificatePolicy,
|
||||
X509AlgorithmBindingPolicy algorithmBindingPolicy) {
|
||||
|
||||
/** 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.
|
||||
@@ -87,7 +98,7 @@ public record CertificateProfileDefinition(CertificateProfileKind certificateTyp
|
||||
if (maximumValidity == null || maximumValidity.isZero() || maximumValidity.isNegative()) {
|
||||
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");
|
||||
}
|
||||
boolean leaf = certificatePolicy instanceof LeafCertificatePolicy;
|
||||
|
||||
@@ -237,7 +237,7 @@ public final class CertificateProfileDocumentCodec {
|
||||
requireValue(parser, "$." + field);
|
||||
readDocumentField(parser, field, document);
|
||||
}
|
||||
requireAll(document.seen, 8, "$");
|
||||
requireAll(document.seen, 9, "$");
|
||||
if (document.schemaVersion != CertificateProfileDefinition.SCHEMA_VERSION) {
|
||||
throw failure("SCHEMA_VERSION_UNSUPPORTED", "$.schemaVersion");
|
||||
}
|
||||
@@ -286,6 +286,10 @@ public final class CertificateProfileDocumentCodec {
|
||||
document.seen = mark(document.seen, 7, "$.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 "leafCertificate" -> readDocumentLeaf(parser, document);
|
||||
case "caCertificate" -> readDocumentCa(parser, document);
|
||||
@@ -297,7 +301,7 @@ public final class CertificateProfileDocumentCodec {
|
||||
if (document.certificateType != null && document.certificateType != CertificateProfileKind.END_ENTITY) {
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -305,7 +309,7 @@ public final class CertificateProfileDocumentCodec {
|
||||
if (document.certificateType != null && document.certificateType != CertificateProfileKind.END_ENTITY) {
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -313,14 +317,14 @@ public final class CertificateProfileDocumentCodec {
|
||||
if (document.certificateType == CertificateProfileKind.END_ENTITY) {
|
||||
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");
|
||||
}
|
||||
|
||||
private static void requirePolicyFields(CertificateProfileKind certificateType, long seen) {
|
||||
boolean hasSan = isSeen(seen, 8);
|
||||
boolean hasLeaf = isSeen(seen, 9);
|
||||
boolean hasCa = isSeen(seen, 10);
|
||||
boolean hasSan = isSeen(seen, 9);
|
||||
boolean hasLeaf = isSeen(seen, 10);
|
||||
boolean hasCa = isSeen(seen, 11);
|
||||
if (certificateType == CertificateProfileKind.END_ENTITY) {
|
||||
if (hasCa) {
|
||||
throw failure("FIELD_FORBIDDEN_FOR_CERTIFICATE_TYPE", "$.caCertificate");
|
||||
@@ -366,7 +370,7 @@ public final class CertificateProfileDocumentCodec {
|
||||
}
|
||||
return new CertificateProfileDefinition(document.certificateType, document.profileId,
|
||||
document.profileVersion, new FormatId(document.formatId), document.displayName,
|
||||
document.maximumValidity, subjectPolicy, policy);
|
||||
document.maximumValidity, subjectPolicy, policy, document.algorithmBindings);
|
||||
} catch (IllegalArgumentException | ArithmeticException ex) {
|
||||
throw failure("SEMANTIC_INVALID", "$");
|
||||
}
|
||||
@@ -397,6 +401,89 @@ public final class CertificateProfileDocumentCodec {
|
||||
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 {
|
||||
requireToken(parser.currentToken(), JsonToken.START_ARRAY, path);
|
||||
List<SubjectRdnRule> rules = new ArrayList<>();
|
||||
@@ -785,6 +872,7 @@ public final class CertificateProfileDocumentCodec {
|
||||
generator.writeStringProperty("formatId", definition.formatId().value());
|
||||
generator.writeStringProperty("displayName", definition.displayName());
|
||||
generator.writeStringProperty("maxValidity", definition.maximumValidity().toString());
|
||||
writeAlgorithmBindings(generator, definition.algorithmBindingPolicy());
|
||||
writeSubject(generator, definition.subjectPolicy());
|
||||
if (definition.certificateType() == CertificateProfileKind.END_ENTITY) {
|
||||
LeafCertificatePolicy leaf = definition.leafPolicy();
|
||||
@@ -816,6 +904,29 @@ public final class CertificateProfileDocumentCodec {
|
||||
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 {
|
||||
generator.writeObjectPropertyStart("subjectAlternativeNames");
|
||||
generator.writeNumberProperty("minimumTotal", policy.minimumTotal());
|
||||
@@ -1098,7 +1209,8 @@ public final class CertificateProfileDocumentCodec {
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -1124,6 +1236,7 @@ public final class CertificateProfileDocumentCodec {
|
||||
private String formatId;
|
||||
private String displayName;
|
||||
private Duration maximumValidity;
|
||||
private X509AlgorithmBindingPolicy algorithmBindings;
|
||||
private SubjectSection subject;
|
||||
private SanSection san;
|
||||
private LeafSection leaf;
|
||||
|
||||
@@ -52,7 +52,8 @@ public record LeafCertificatePolicy(SubjectAlternativeNamePolicy subjectAlternat
|
||||
boolean extendedKeyUsageCritical, boolean basicConstraintsCritical, Set<String> allowedSubjectKeyAlgorithmIds)
|
||||
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.
|
||||
|
||||
@@ -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.RevocationService;
|
||||
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.credential.CaProfileBinding;
|
||||
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.CertificateProfileRef;
|
||||
import zeroecho.pki.api.profile.ImportedCertificateProfileVersion;
|
||||
import zeroecho.pki.api.profile.X509AlgorithmBindingPolicy;
|
||||
import zeroecho.pki.api.issuance.IssueEndEntityCommand;
|
||||
import zeroecho.pki.api.issuance.VerificationPolicy;
|
||||
import zeroecho.pki.api.publication.PublicationRequest;
|
||||
@@ -101,12 +104,13 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
||||
private final Optional<IssuanceService> issuance;
|
||||
private final Optional<StatusObjectService> statusObjects;
|
||||
private final Optional<PublicationService> publications;
|
||||
private final X509AlgorithmBindingRegistry algorithmBindings;
|
||||
private final Runnable openCheck;
|
||||
|
||||
/* default */ DefaultPkiOperationExecutor(PkiSessionConfiguration configuration, PkiStore store,
|
||||
ProfileService profiles, RevocationService revocations, Runnable openCheck) {
|
||||
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,
|
||||
@@ -114,6 +118,16 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
||||
Optional<CertificationRequestService> requests, Optional<IssuanceService> issuance,
|
||||
Optional<StatusObjectService> statusObjects, Optional<PublicationService> publications,
|
||||
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.store = Objects.requireNonNull(store, "store");
|
||||
this.profiles = Objects.requireNonNull(profiles, "profiles");
|
||||
@@ -123,6 +137,7 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
||||
this.issuance = Objects.requireNonNull(issuance, "issuance");
|
||||
this.statusObjects = Objects.requireNonNull(statusObjects, "statusObjects");
|
||||
this.publications = Objects.requireNonNull(publications, "publications");
|
||||
this.algorithmBindings = Objects.requireNonNull(algorithmBindings, "algorithmBindings");
|
||||
this.openCheck = Objects.requireNonNull(openCheck, "openCheck");
|
||||
}
|
||||
|
||||
@@ -134,6 +149,9 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
||||
try {
|
||||
signal.throwIfCancelled();
|
||||
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.ValidateProfile request -> validateProfile(request);
|
||||
case PkiOperation.RegisterProfile request -> registerProfile(request);
|
||||
@@ -162,7 +180,7 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
||||
case PkiOperation.ProcessPublication request -> processPublication(request);
|
||||
case PkiOperation.RetryPublication request -> retryPublication(request);
|
||||
case PkiOperation.ReconcilePublication request -> reconcilePublication(request);
|
||||
};
|
||||
};
|
||||
return new PkiOperationOutcome.Success(result);
|
||||
} catch (InterruptedIOException failure) {
|
||||
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() {
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
fields.put("version", integer(configuration.version()));
|
||||
@@ -181,8 +254,9 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
||||
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());
|
||||
validateProfileBindings(definition.algorithmBindingPolicy());
|
||||
Map<String, PkiOperationValue> fields = fields();
|
||||
fields.put("profileId", text(definition.profileId()));
|
||||
fields.put("profileVersion", integer(definition.profileVersion()));
|
||||
@@ -191,6 +265,26 @@ final class DefaultPkiOperationExecutor implements PkiOperationExecutor {
|
||||
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) {
|
||||
CertificateProfileRef reference = profiles.importProfile(request.document());
|
||||
return profileReferenceResult(request.name(), reference);
|
||||
|
||||
@@ -36,9 +36,13 @@ package zeroecho.pki.application;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Clock;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@@ -51,11 +55,16 @@ import zeroecho.core.spi.KeyringUnlockProvider;
|
||||
import zeroecho.core.storage.KeyringPassword;
|
||||
import zeroecho.pki.api.CaService;
|
||||
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.PublicationService;
|
||||
import zeroecho.pki.api.ProfileService;
|
||||
import zeroecho.pki.api.RevocationService;
|
||||
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.impl.core.DefaultProfileService;
|
||||
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.DefaultStatusObjectService;
|
||||
import zeroecho.pki.impl.core.StoreBackedEffectiveCredentialStatusResolver;
|
||||
import zeroecho.pki.impl.core.PublicKeyInfoResolver;
|
||||
import zeroecho.pki.impl.core.async.PkiSigningBus;
|
||||
import zeroecho.pki.impl.framework.x509.X509AlgorithmResolver;
|
||||
import zeroecho.pki.impl.framework.x509.X509AuthoritySnapshot;
|
||||
import zeroecho.pki.impl.framework.x509.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.BcX509CredentialIssuerBackend;
|
||||
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.BcX509VerificationExecutor;
|
||||
import zeroecho.pki.spi.ProviderConfig;
|
||||
import zeroecho.pki.spi.algorithm.X509AlgorithmBindingProvider;
|
||||
import zeroecho.pki.spi.audit.AuditSink;
|
||||
import zeroecho.pki.spi.bootstrap.PkiBootstrap;
|
||||
import zeroecho.pki.spi.crypto.PublicKeyInfoSource;
|
||||
@@ -83,6 +99,7 @@ import zeroecho.pki.spi.publish.Publisher;
|
||||
import zeroecho.pki.spi.store.PkiStore;
|
||||
|
||||
/** Default synchronous session composition. */
|
||||
@SuppressWarnings({ "PMD.CouplingBetweenObjects", "PMD.CyclomaticComplexity" })
|
||||
final class DefaultPkiSession implements PkiSession {
|
||||
|
||||
private final PkiStore store;
|
||||
@@ -96,11 +113,13 @@ final class DefaultPkiSession implements PkiSession {
|
||||
private final Optional<PublicationService> publications;
|
||||
private final Optional<PkiSigningBus> signingBus;
|
||||
private final Optional<SignatureWorkflow> signatureWorkflow;
|
||||
private final X509AlgorithmBindingRegistry algorithmBindings;
|
||||
private final PkiOperationExecutor operations;
|
||||
private final AtomicBoolean closed = new AtomicBoolean();
|
||||
|
||||
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.audit = audit;
|
||||
this.profiles = Objects.requireNonNull(profiles, "profiles");
|
||||
@@ -112,8 +131,9 @@ final class DefaultPkiSession implements PkiSession {
|
||||
this.publications = graph.publications();
|
||||
this.signingBus = graph.signingBus();
|
||||
this.signatureWorkflow = graph.signatureWorkflow();
|
||||
this.algorithmBindings = Objects.requireNonNull(algorithmBindings, "algorithmBindings");
|
||||
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) {
|
||||
@@ -146,6 +166,9 @@ final class DefaultPkiSession implements PkiSession {
|
||||
}
|
||||
});
|
||||
exact.publishers().forEach(bootstrap::validatePublisher);
|
||||
BindingComposition bindingComposition = composeBindings(exact);
|
||||
X509AlgorithmBindingRegistry algorithmBindings = bindingComposition.registry();
|
||||
exact.signing().ifPresent(signing -> validateConfiguredBindings(signing, algorithmBindings));
|
||||
|
||||
PkiStore store = null;
|
||||
AuditSink audit = null;
|
||||
@@ -153,10 +176,15 @@ final class DefaultPkiSession implements PkiSession {
|
||||
try {
|
||||
store = Objects.requireNonNull(bootstrap.openStore(exact.store()), "opened store");
|
||||
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);
|
||||
graph = composeGraph(exact, dependencies, store, audit, profiles, clock, bootstrap);
|
||||
return new DefaultPkiSession(exact, store, audit, profiles, revocations, graph);
|
||||
graph = composeGraph(exact, dependencies, store, audit, profiles, clock, bootstrap,
|
||||
bindingComposition.providers());
|
||||
return new DefaultPkiSession(exact, store, audit, profiles, revocations, algorithmBindings, graph);
|
||||
} catch (RuntimeException | Error primary) {
|
||||
closeGraphAfterConstructionFailure(graph, primary);
|
||||
closeAfterConstructionFailure(audit, store, primary);
|
||||
@@ -206,6 +234,12 @@ final class DefaultPkiSession implements PkiSession {
|
||||
return publications;
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509AlgorithmBindingRegistry algorithmBindings() {
|
||||
requireOpen();
|
||||
return algorithmBindings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PkiOperationExecutor operations() {
|
||||
requireOpen();
|
||||
@@ -247,7 +281,7 @@ final class DefaultPkiSession implements PkiSession {
|
||||
@SuppressWarnings({ "PMD.AvoidCatchingGenericException", "PMD.CloseResource", "PMD.ExceptionAsFlowControl" })
|
||||
private static ServiceGraph composeGraph(PkiSessionConfiguration configuration,
|
||||
PkiSessionRuntimeDependencies dependencies, PkiStore store, AuditSink audit, ProfileService profiles,
|
||||
Clock clock, Bootstrap bootstrap) {
|
||||
Clock clock, Bootstrap bootstrap, List<X509BindingRuleProvider> bindingProviders) {
|
||||
Optional<SignatureWorkflow> workflow = Optional.empty();
|
||||
Optional<PkiSigningBus> bus = 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");
|
||||
}
|
||||
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()),
|
||||
authority);
|
||||
bus = Optional.of(openedBus);
|
||||
AlgorithmIdentity signatureIdentity = authority.resolveIdentity(signing.signatureAlgorithm());
|
||||
BcX509CredentialIssuerBackend issuerBackend = new BcX509CredentialIssuerBackend(openedBus,
|
||||
signing.signatureAlgorithm(), signing.signingTtl());
|
||||
signatureIdentity, signing.certificateSignatureBinding(), signing.signingTtl());
|
||||
BcX509StatusObjectGenerator statusGenerator = new BcX509StatusObjectGenerator(openedBus,
|
||||
signing.signatureAlgorithm(), signing.signingTtl());
|
||||
signatureIdentity, signing.crlSignatureBinding(), signing.signingTtl());
|
||||
BcX509CredentialFramework framework = new BcX509CredentialFramework(authority, verification)
|
||||
.wired(statusGenerator, new BcX509ProofOfPossessionVerifier(authority, verification));
|
||||
EffectiveCredentialStatusResolver statusResolver = new StoreBackedEffectiveCredentialStatusResolver(
|
||||
@@ -285,8 +320,8 @@ final class DefaultPkiSession implements PkiSession {
|
||||
statuses = Optional.of(new DefaultStatusObjectService(store, framework, audit, statusResolver,
|
||||
authority));
|
||||
authorities = Optional.of(new DefaultCaService(store, framework, issuerBackend,
|
||||
publicKeys::resolvePublicKeyInfo, openedBus, audit, statusResolver, profiles, clock,
|
||||
signing.signatureAlgorithm(), signing.signingTtl()));
|
||||
publicKeySource(publicKeys, authority, signing), openedBus, audit, statusResolver, profiles, clock,
|
||||
signing.signatureAlgorithm(), signing.certificateSignatureBinding(), signing.signingTtl()));
|
||||
}
|
||||
List<Publisher> configuredPublishers = new ArrayList<>();
|
||||
for (ProviderConfig publisher : configuration.publishers()) {
|
||||
@@ -304,7 +339,7 @@ final class DefaultPkiSession implements PkiSession {
|
||||
}
|
||||
|
||||
private static X509AuthoritySnapshot authority(SignatureWorkflow workflow,
|
||||
BcX509VerificationExecutor verification) {
|
||||
BcX509VerificationExecutor verification, List<X509BindingRuleProvider> bindingProviders) {
|
||||
Set<String> supported = Set.copyOf(workflow.supportedAlgorithms());
|
||||
Set<AlgorithmIdentity> identities = BootstrapAlgorithmIdentities.catalog().identities().stream()
|
||||
.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 X509AuthoritySnapshot.compose(List.of(), List.of(signingProvider, verification),
|
||||
return X509AuthoritySnapshot.compose(bindingProviders, List.of(signingProvider, verification),
|
||||
List.of(X509AuthoritySnapshot.bindExecutor(implementationId,
|
||||
AlgorithmExecutionCapability.Direction.SIGN, workflow),
|
||||
X509AuthoritySnapshot.bindExecutor(BcX509VerificationExecutor.IMPLEMENTATION_ID,
|
||||
AlgorithmExecutionCapability.Direction.VERIFY, verification)), policy);
|
||||
}
|
||||
|
||||
private static 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) {
|
||||
Optional<String> environment = configuration.signing()
|
||||
.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. */
|
||||
private enum ProductionBootstrap implements Bootstrap {
|
||||
INSTANCE;
|
||||
|
||||
@@ -53,6 +53,8 @@ import zeroecho.pki.api.status.StatusObjectType;
|
||||
* explicit type and never by reflection or Java class name.</p>
|
||||
*/
|
||||
public sealed interface PkiOperation permits PkiOperation.ValidateConfiguration, PkiOperation.ValidateProfile,
|
||||
PkiOperation.ListAlgorithmBindings, PkiOperation.InspectAlgorithmBinding,
|
||||
PkiOperation.ValidateAlgorithmBindings,
|
||||
PkiOperation.RegisterProfile, PkiOperation.InspectProfile, PkiOperation.ListProfileVersions,
|
||||
PkiOperation.ActivateProfile, PkiOperation.InspectAuthority, PkiOperation.ListAuthorities,
|
||||
PkiOperation.CreateAuthority, PkiOperation.TransitionAuthority, PkiOperation.InspectRequest,
|
||||
@@ -69,6 +71,81 @@ public sealed interface PkiOperation permits PkiOperation.ValidateConfiguration,
|
||||
/** @return stable semantic operation 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. */
|
||||
record ValidateConfiguration() implements PkiOperation {
|
||||
/** Stable operation name. */
|
||||
|
||||
@@ -42,6 +42,7 @@ import zeroecho.pki.api.PublicationService;
|
||||
import zeroecho.pki.api.ProfileService;
|
||||
import zeroecho.pki.api.RevocationService;
|
||||
import zeroecho.pki.api.StatusObjectService;
|
||||
import zeroecho.pki.api.algorithm.X509AlgorithmBindingRegistry;
|
||||
|
||||
/**
|
||||
* Lifecycle-owned synchronous PKI backend session.
|
||||
@@ -107,6 +108,13 @@ public interface PkiSession extends AutoCloseable {
|
||||
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 */
|
||||
PkiOperationExecutor operations();
|
||||
|
||||
|
||||
@@ -54,14 +54,21 @@ import zeroecho.pki.spi.ProviderConfig;
|
||||
* @param publishers explicitly enabled publication destinations
|
||||
*/
|
||||
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. */
|
||||
public static final int CURRENT_VERSION = 2;
|
||||
public static final int CURRENT_VERSION = 3;
|
||||
|
||||
/** Creates a read-only-compatible version-one configuration. */
|
||||
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. */
|
||||
@@ -74,9 +81,17 @@ public record PkiSessionConfiguration(int version, ProviderConfig store, Provide
|
||||
signing = Objects.requireNonNull(signing, "signing").map(SigningConfiguration::snapshot);
|
||||
publishers = Objects.requireNonNull(publishers, "publishers").stream()
|
||||
.map(PkiSessionConfiguration::snapshot).toList();
|
||||
bindingProviders = List.copyOf(Objects.requireNonNull(bindingProviders, "bindingProviders"));
|
||||
if (version == 1 && (signing.isPresent() || !publishers.isEmpty())) {
|
||||
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) {
|
||||
@@ -93,9 +108,21 @@ public record PkiSessionConfiguration(int version, ProviderConfig store, Provide
|
||||
* @param signingTtl positive synchronous signing deadline
|
||||
* @param unlockEnvironmentVariable optional environment-variable reference used
|
||||
* 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,
|
||||
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. */
|
||||
public SigningConfiguration {
|
||||
@@ -109,6 +136,9 @@ public record PkiSessionConfiguration(int version, ProviderConfig store, Provide
|
||||
}
|
||||
unlockEnvironmentVariable = Objects.requireNonNull(unlockEnvironmentVariable,
|
||||
"unlockEnvironmentVariable");
|
||||
certificateSignatureBinding = binding(certificateSignatureBinding, "certificateSignatureBinding");
|
||||
crlSignatureBinding = binding(crlSignatureBinding, "crlSignatureBinding");
|
||||
subjectPublicKeyBinding = binding(subjectPublicKeyBinding, "subjectPublicKeyBinding");
|
||||
unlockEnvironmentVariable.ifPresent(value -> {
|
||||
if (!value.matches("[A-Z][A-Z0-9_]{0,127}")) {
|
||||
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) {
|
||||
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());
|
||||
return new ValidatedCaCertificateRequest(operation, formatId, issuerCaId, subjectCaId,
|
||||
activeProfile.reference(), expectedKind, canonicalSubject, subjectSnapshot, exactPublicKey, validity,
|
||||
serial, definition.caPolicy());
|
||||
serial, definition.caPolicy(), definition.algorithmBindingPolicy());
|
||||
}
|
||||
|
||||
/* 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.bc.BcX509VerificationExecutor;
|
||||
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.util.async.AsyncState;
|
||||
import zeroecho.pki.util.async.AsyncStatus;
|
||||
@@ -93,26 +94,46 @@ final class CaProofGate {
|
||||
private final PkiSigningBus signingBus;
|
||||
private final AuditSink auditSink;
|
||||
private final AlgorithmIdentity signatureIdentity;
|
||||
private final Optional<String> signatureBindingId;
|
||||
private final Duration signingTtl;
|
||||
|
||||
/* default */ CaProofGate(PublicKeyInfoResolver publicKeyResolver, PkiSigningBus signingBus, AuditSink auditSink,
|
||||
String signatureAlgorithmId, Duration signingTtl) {
|
||||
this(publicKeyResolver, signingBus, auditSink,
|
||||
signingBus.authority().resolveIdentity(Objects.requireNonNull(signatureAlgorithmId,
|
||||
"signatureAlgorithmId")), signingTtl);
|
||||
"signatureAlgorithmId")), Optional.empty(), signingTtl);
|
||||
}
|
||||
|
||||
/* default */ CaProofGate(PublicKeyInfoResolver publicKeyResolver, PkiSigningBus signingBus, AuditSink auditSink,
|
||||
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.signingBus = Objects.requireNonNull(signingBus, "signingBus");
|
||||
this.auditSink = Objects.requireNonNull(auditSink, "auditSink");
|
||||
this.signatureIdentity = Objects.requireNonNull(signatureIdentity, "signatureIdentity");
|
||||
this.signatureBindingId = Objects.requireNonNull(signatureBindingId, "signatureBindingId");
|
||||
this.signingTtl = Objects.requireNonNull(signingTtl, "signingTtl");
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
|
||||
@@ -108,7 +108,7 @@ final class CertificateProfileValidator {
|
||||
return new ValidatedCertificateRequest(candidate.issuerCaId(), profileReference, approvedSubjectRef,
|
||||
approvedSubject, approvedSans, sanCritical, candidate.exactPublicKey(), validity, policy.keyUsages(),
|
||||
policy.extendedKeyUsages(), policy.keyUsageCritical(), policy.extendedKeyUsageCritical(),
|
||||
policy.basicConstraintsCritical());
|
||||
policy.basicConstraintsCritical(), profile.algorithmBindingPolicy());
|
||||
}
|
||||
|
||||
private static void requireCanonicalRequestAttributes(ParsedCertificationRequest request) {
|
||||
@@ -252,14 +252,21 @@ final class CertificateProfileValidator {
|
||||
if (!allowedAlgorithms.contains(algorithm.profileId())) {
|
||||
throw reject("SUBJECT_KEY_ALGORITHM_FORBIDDEN");
|
||||
}
|
||||
PublicKey reconstructed = KeyFactory.getInstance(algorithm.jcaName())
|
||||
.generatePublic(new X509EncodedKeySpec(encoded));
|
||||
SubjectPublicKeyInfo nativeSpki = new zeroecho.pki.impl.framework.x509.bc.BcX509PublicKeyAdapter(
|
||||
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();
|
||||
try {
|
||||
if (canonical == null || !MessageDigest.isEqual(encoded, canonical)) {
|
||||
if (canonical == null || !MessageDigest.isEqual(nativeEncoded, canonical)) {
|
||||
throw reject("SUBJECT_KEY_NOT_CANONICAL");
|
||||
}
|
||||
} finally {
|
||||
java.util.Arrays.fill(nativeEncoded, (byte) 0);
|
||||
if (canonical != null) {
|
||||
java.util.Arrays.fill(canonical, (byte) 0);
|
||||
}
|
||||
@@ -286,6 +293,9 @@ final class CertificateProfileValidator {
|
||||
if (identity.equals(BootstrapAlgorithmIdentities.ED448_PUBLIC_KEY)) {
|
||||
return new SubjectKeyAlgorithm("Ed448", "Ed448");
|
||||
}
|
||||
if (identity.equals(BootstrapAlgorithmIdentities.SPHINCS_PLUS_PUBLIC_KEY)) {
|
||||
return new SubjectKeyAlgorithm("SPHINCS+", "SPHINCS+");
|
||||
}
|
||||
throw reject("SUBJECT_KEY_ALGORITHM_UNKNOWN");
|
||||
}
|
||||
|
||||
|
||||
@@ -232,6 +232,30 @@ public final class DefaultCaService implements CaService {
|
||||
PublicKeyInfoResolver publicKeyResolver, PkiSigningBus signingBus, AuditSink auditSink,
|
||||
EffectiveCredentialStatusResolver statusResolver, ProfileService profileService, Clock clock,
|
||||
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.framework = Objects.requireNonNull(framework, "framework");
|
||||
@@ -250,11 +274,14 @@ public final class DefaultCaService implements CaService {
|
||||
throw new IllegalArgumentException("signingTtl must be positive");
|
||||
}
|
||||
AlgorithmIdentity signatureIdentity = signingBus.authority().resolveIdentity(signatureAlgorithmId);
|
||||
X509ExecutionPlan<SignatureWorkflow> plan = signingBus.authority()
|
||||
.planSigning(signatureIdentity.canonicalForm(), SignatureWorkflow.class);
|
||||
X509ExecutionPlan<SignatureWorkflow> plan = signatureBindingId.isPresent()
|
||||
? 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);
|
||||
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");
|
||||
}
|
||||
|
||||
ContentSigner signer = proofGate.signer(proof);
|
||||
ContentSigner signer = proofGate.signer(proof, request.algorithmBindingPolicy());
|
||||
X509CertificateHolder cert;
|
||||
try {
|
||||
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.CertificateProfileRef;
|
||||
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.Code;
|
||||
import zeroecho.pki.spi.audit.AuditSink;
|
||||
@@ -70,12 +73,42 @@ public final class DefaultProfileService implements ProfileService {
|
||||
private final PkiStore store;
|
||||
private final Clock clock;
|
||||
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. */
|
||||
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.clock = Objects.requireNonNull(clock, "clock");
|
||||
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
|
||||
@@ -83,6 +116,7 @@ public final class DefaultProfileService implements ProfileService {
|
||||
Objects.requireNonNull(jsonDocument, "jsonDocument");
|
||||
return executeSanitized(() -> {
|
||||
CertificateProfileDefinition definition = CertificateProfileDocumentCodec.parse(jsonDocument);
|
||||
validateBindings(definition);
|
||||
byte[] canonical = CertificateProfileDocumentCodec.writeCanonical(definition);
|
||||
return importCanonical(definition, canonical, clock.instant());
|
||||
}, Code.PROFILE_IMPORT_VALIDATION_FAILED);
|
||||
@@ -93,6 +127,7 @@ public final class DefaultProfileService implements ProfileService {
|
||||
Objects.requireNonNull(jsonDocument, "jsonDocument");
|
||||
return executeSanitized(() -> {
|
||||
CertificateProfileDefinition definition = CertificateProfileDocumentCodec.parse(jsonDocument);
|
||||
validateBindings(definition);
|
||||
byte[] canonical = CertificateProfileDocumentCodec.writeCanonical(definition);
|
||||
return importCanonical(definition, canonical, clock.instant());
|
||||
}, Code.PROFILE_IMPORT_VALIDATION_FAILED);
|
||||
@@ -122,6 +157,10 @@ public final class DefaultProfileService implements ProfileService {
|
||||
throw new IllegalArgumentException("profileVersion must be positive");
|
||||
}
|
||||
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);
|
||||
audit("PROFILE_ACTIVATE", result, "SUCCESS");
|
||||
return result;
|
||||
@@ -131,8 +170,12 @@ public final class DefaultProfileService implements ProfileService {
|
||||
@Override
|
||||
public ActiveCertificateProfile requireActiveProfile(String profileId) {
|
||||
requireProfileId(profileId);
|
||||
return executeAudited("PROFILE_RESOLVE", profileId, 0L, Code.PROFILE_STORE_FAILURE,
|
||||
() -> store.requireActiveProfile(profileId));
|
||||
return executeAudited("PROFILE_RESOLVE", profileId, 0L, Code.PROFILE_STORE_FAILURE, () -> {
|
||||
ActiveCertificateProfile active = store.requireActiveProfile(profileId);
|
||||
validateBindings(active.definition());
|
||||
validateRuntimeBindings(active.definition());
|
||||
return active;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -217,6 +260,60 @@ public final class DefaultProfileService implements ProfileService {
|
||||
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
|
||||
* 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.CertificateProfileKind;
|
||||
import zeroecho.pki.api.profile.CertificateProfileRef;
|
||||
import zeroecho.pki.api.profile.X509AlgorithmBindingPolicy;
|
||||
import zeroecho.pki.api.request.SubjectRdn;
|
||||
|
||||
/**
|
||||
@@ -83,6 +84,7 @@ public final class ValidatedCaCertificateRequest {
|
||||
private final Validity validity;
|
||||
private final BigInteger serial;
|
||||
private final CaCertificatePolicy policy;
|
||||
private final X509AlgorithmBindingPolicy algorithmBindingPolicy;
|
||||
|
||||
// The constructor is the single cohesive gate output boundary.
|
||||
@SuppressWarnings("PMD.ExcessiveParameterList")
|
||||
@@ -90,6 +92,15 @@ public final class ValidatedCaCertificateRequest {
|
||||
PkiId subjectCaId, CertificateProfileRef profileReference, CertificateProfileKind certificateType,
|
||||
SubjectRef subjectRef, List<SubjectRdn> subjectRdns, EncodedObject exactPublicKey, Validity validity,
|
||||
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.formatId = Objects.requireNonNull(formatId, "formatId");
|
||||
this.issuerCaId = Objects.requireNonNull(issuerCaId, "issuerCaId");
|
||||
@@ -103,6 +114,7 @@ public final class ValidatedCaCertificateRequest {
|
||||
this.validity = Objects.requireNonNull(validity, "validity");
|
||||
this.serial = Objects.requireNonNull(serial, "serial");
|
||||
this.policy = Objects.requireNonNull(policy, "policy");
|
||||
this.algorithmBindingPolicy = Objects.requireNonNull(algorithmBindingPolicy, "algorithmBindingPolicy");
|
||||
if (serial.signum() <= 0 || serial.toByteArray().length > 20) {
|
||||
throw new IllegalArgumentException("serial must be positive and at most 20 bytes");
|
||||
}
|
||||
@@ -167,4 +179,9 @@ public final class ValidatedCaCertificateRequest {
|
||||
public CaCertificatePolicy 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.ExtendedKeyUsageId;
|
||||
import zeroecho.pki.api.profile.LeafKeyUsage;
|
||||
import zeroecho.pki.api.profile.X509AlgorithmBindingPolicy;
|
||||
import zeroecho.pki.api.request.SubjectAlternativeName;
|
||||
import zeroecho.pki.api.request.SubjectRdn;
|
||||
|
||||
@@ -71,12 +72,25 @@ public final class ValidatedCertificateRequest {
|
||||
private final boolean keyUsageCritical;
|
||||
private final boolean extendedKeyUsageCritical;
|
||||
private final boolean basicConstraintsCritical;
|
||||
private final X509AlgorithmBindingPolicy algorithmBindingPolicy;
|
||||
|
||||
/* 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) {
|
||||
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.profileReference = Objects.requireNonNull(profileReference, "profileReference");
|
||||
this.subjectRef = Objects.requireNonNull(subjectRef, "subjectRef");
|
||||
@@ -90,6 +104,7 @@ public final class ValidatedCertificateRequest {
|
||||
this.keyUsageCritical = keyUsageCritical;
|
||||
this.extendedKeyUsageCritical = extendedKeyUsageCritical;
|
||||
this.basicConstraintsCritical = basicConstraintsCritical;
|
||||
this.algorithmBindingPolicy = Objects.requireNonNull(algorithmBindingPolicy, "algorithmBindingPolicy");
|
||||
}
|
||||
|
||||
/** @return authoritative issuer CA identifier */
|
||||
@@ -161,4 +176,9 @@ public final class ValidatedCertificateRequest {
|
||||
public boolean 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_PSS_SHA256, BootstrapAlgorithmIdentities.ECDSA_SHA256,
|
||||
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() {
|
||||
@Override
|
||||
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.spec.AlgorithmIdentity;
|
||||
import zeroecho.pki.api.algorithm.X509AlgorithmBinding;
|
||||
|
||||
/**
|
||||
* Immutable authoritative standard X.509 bootstrap bindings.
|
||||
@@ -150,6 +151,16 @@ public final class StandardX509Bindings {
|
||||
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,
|
||||
X509AlgorithmIdentifier identifier, X509BindingRule.SignatureEncoding signatureEncoding,
|
||||
X509BindingRule.PublicKeyEncoding publicKeyEncoding) {
|
||||
@@ -180,6 +191,12 @@ public final class StandardX509Bindings {
|
||||
+ signatureEncoding + "|" + publicKeyEncoding;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<X509AlgorithmBinding> bindings() {
|
||||
return descriptors(id, role, identity, identifier, signatureEncoding, publicKeyEncoding,
|
||||
semanticFingerprint());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<X509AlgorithmIdentifier> encode(AlgorithmIdentity candidate) {
|
||||
return identity.equals(candidate) ? Optional.of(identifier) : Optional.empty();
|
||||
@@ -236,6 +253,13 @@ public final class StandardX509Bindings {
|
||||
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
|
||||
public Optional<X509AlgorithmIdentifier> encode(AlgorithmIdentity identity) {
|
||||
if (identity.kind() != AlgorithmIdentity.Kind.SIGNATURE
|
||||
@@ -302,6 +326,16 @@ public final class StandardX509Bindings {
|
||||
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
|
||||
public Optional<X509AlgorithmIdentifier> encode(AlgorithmIdentity identity) {
|
||||
if (identity.kind() != AlgorithmIdentity.Kind.PUBLIC_KEY
|
||||
@@ -341,6 +375,42 @@ public final class StandardX509Bindings {
|
||||
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) {
|
||||
StrictDer.Reader sequence = StrictDer.reader(encoded).readConstructed(0x30);
|
||||
byte[] hashAlgorithm = sequence.readConstructed(0xa0).readOnlyValue(0x30);
|
||||
|
||||
@@ -194,6 +194,14 @@ public final class X509AlgorithmResolver {
|
||||
public Selection resolve(AlgorithmIdentity requested, AlgorithmIdentity key,
|
||||
AlgorithmExecutionCapability.Direction direction, Optional<String> implementation, String provenance,
|
||||
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(key, "key");
|
||||
Objects.requireNonNull(direction, "direction");
|
||||
@@ -213,7 +221,9 @@ public final class X509AlgorithmResolver {
|
||||
}
|
||||
X509AlgorithmIdentifier binding;
|
||||
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) {
|
||||
throw new ResolutionException(Failure.NO_BINDING, missing);
|
||||
}
|
||||
|
||||
@@ -188,7 +188,8 @@ public final class X509AuthoritySnapshot {
|
||||
mergeDefaults(defaults, provider.defaults());
|
||||
}
|
||||
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) {
|
||||
List<X509BindingRule> rules = List.copyOf(provider.rules());
|
||||
if (!rules.isEmpty()) {
|
||||
@@ -248,6 +249,14 @@ public final class X509AuthoritySnapshot {
|
||||
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.
|
||||
*
|
||||
@@ -272,6 +281,20 @@ public final class X509AuthoritySnapshot {
|
||||
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
|
||||
* signature identity.
|
||||
@@ -304,6 +327,14 @@ public final class X509AuthoritySnapshot {
|
||||
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.
|
||||
*
|
||||
@@ -562,6 +593,9 @@ public final class X509AuthoritySnapshot {
|
||||
if (signature.equals(BootstrapAlgorithmIdentities.ED448_SIGNATURE)) {
|
||||
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())
|
||||
|| "rsa-pss".equals(signature.family().name())) {
|
||||
return BootstrapAlgorithmIdentities.RSA_PUBLIC_KEY;
|
||||
|
||||
@@ -130,6 +130,11 @@ public final class X509BindingCatalog {
|
||||
if (rule.role() != role) {
|
||||
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);
|
||||
if (candidate.isPresent()) {
|
||||
if (resolved != null) {
|
||||
@@ -144,6 +149,30 @@ public final class X509BindingCatalog {
|
||||
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.
|
||||
*
|
||||
@@ -167,6 +196,23 @@ public final class X509BindingCatalog {
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -34,8 +34,10 @@
|
||||
package zeroecho.pki.impl.framework.x509;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.List;
|
||||
|
||||
import zeroecho.core.spec.AlgorithmIdentity;
|
||||
import zeroecho.pki.api.algorithm.X509AlgorithmBinding;
|
||||
|
||||
/**
|
||||
* Immutable trusted-code rule between exact ZeroEcho identities and X.509
|
||||
@@ -49,6 +51,19 @@ import zeroecho.core.spec.AlgorithmIdentity;
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@@ -71,6 +86,8 @@ public interface X509BindingRule {
|
||||
EC_POINT,
|
||||
/** Algorithm-defined raw public-key bytes. */
|
||||
RAW,
|
||||
/** Canonical native SubjectPublicKeyInfo DER nested in the BIT STRING. */
|
||||
NESTED_SPKI_DER,
|
||||
/** Not applicable to a signature-only rule. */
|
||||
NOT_APPLICABLE
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ public final class X509SuiteCompatibility {
|
||||
case "ecdsa" -> "ec".equals(keyFamily);
|
||||
case "ed25519" -> "ed25519".equals(keyFamily);
|
||||
case "ed448" -> "ed448".equals(keyFamily);
|
||||
case "sphincs-plus" -> "sphincs-plus".equals(keyFamily);
|
||||
default -> false;
|
||||
};
|
||||
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,
|
||||
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) {
|
||||
throw new IllegalArgumentException("signingBus must not be null");
|
||||
}
|
||||
@@ -176,6 +182,7 @@ public final class BcX509CredentialIssuerBackend implements CredentialIssuerBack
|
||||
}
|
||||
this.signingBus = signingBus;
|
||||
this.signatureIdentity = signatureIdentity;
|
||||
java.util.Objects.requireNonNull(signatureBindingId, "signatureBindingId");
|
||||
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);
|
||||
addLeafExtensions(builder, request);
|
||||
|
||||
PkiBusContentSigner signer = new PkiBusContentSigner(signingBus, issuerKeyRef, signatureIdentity, signingTtl);
|
||||
PkiBusContentSigner signer = signer(issuerKeyRef, request.algorithmBindingPolicy());
|
||||
X509CertificateHolder leaf;
|
||||
try {
|
||||
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) {
|
||||
int bits = 0;
|
||||
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");
|
||||
}
|
||||
|
||||
PkiBusContentSigner signer = new PkiBusContentSigner(signingBus, issuerKeyRef, signatureIdentity, signingTtl);
|
||||
PkiBusContentSigner signer = signer(issuerKeyRef, request.algorithmBindingPolicy());
|
||||
X509CertificateHolder certificate;
|
||||
try {
|
||||
certificate = builder.build(signer);
|
||||
|
||||
@@ -215,6 +215,8 @@ public final class BcX509ProofOfPossessionVerifier implements ProofOfPossessionV
|
||||
X509ExecutionPlan<BcX509VerificationExecutor> plan = authority.plan(signatureIdentity, keyIdentity,
|
||||
AlgorithmExecutionCapability.Direction.VERIFY,
|
||||
Optional.of(BcX509VerificationExecutor.IMPLEMENTATION_ID), "csr-proof",
|
||||
authority.bindings().bindingId(BcX509AlgorithmAdapter.fromBc(csr.getSignatureAlgorithm()),
|
||||
X509AlgorithmRole.SIGNATURE_ALGORITHM),
|
||||
BcX509VerificationExecutor.class);
|
||||
byte[] signedBytes = csr.toASN1Structure().getCertificationRequestInfo().getEncoded();
|
||||
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,
|
||||
AlgorithmExecutionCapability.Direction.VERIFY,
|
||||
Optional.of(BcX509VerificationExecutor.IMPLEMENTATION_ID), "status-postcondition",
|
||||
authority.bindings().bindingId(BcX509AlgorithmAdapter.fromBc(identifier),
|
||||
X509AlgorithmRole.SIGNATURE_ALGORITHM),
|
||||
BcX509VerificationExecutor.class);
|
||||
try (RepeatableContent tbs = new ContentSlice(content, layout.tbsOffset(), layout.tbsLength())) {
|
||||
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 AlgorithmIdentity signatureIdentity;
|
||||
private final Duration signingTtl;
|
||||
private final Optional<String> signatureBindingId;
|
||||
|
||||
/**
|
||||
* Creates the X.509 status object generator.
|
||||
@@ -196,6 +197,12 @@ public final class BcX509StatusObjectGenerator implements StatusObjectGenerator
|
||||
*/
|
||||
public BcX509StatusObjectGenerator(PkiSigningBus signingBus, AlgorithmIdentity signatureIdentity,
|
||||
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) {
|
||||
throw new IllegalArgumentException("signingBus must not be null");
|
||||
}
|
||||
@@ -207,6 +214,7 @@ public final class BcX509StatusObjectGenerator implements StatusObjectGenerator
|
||||
}
|
||||
this.signingBus = signingBus;
|
||||
this.signatureIdentity = signatureIdentity;
|
||||
this.signatureBindingId = java.util.Objects.requireNonNull(signatureBindingId, "signatureBindingId");
|
||||
this.signingTtl = signingTtl;
|
||||
}
|
||||
|
||||
@@ -262,8 +270,10 @@ public final class BcX509StatusObjectGenerator implements StatusObjectGenerator
|
||||
boolean accepted = false;
|
||||
try {
|
||||
entries = encodeEntries(crlEntries);
|
||||
PkiBusContentSigner signer = new PkiBusContentSigner(signingBus, issuerMaterial.keyRef(),
|
||||
signatureIdentity, signingTtl);
|
||||
PkiBusContentSigner signer = signatureBindingId.isPresent()
|
||||
? new PkiBusContentSigner(signingBus, issuerMaterial.keyRef(), signatureIdentity,
|
||||
signatureBindingId.orElseThrow(), signingTtl)
|
||||
: new PkiBusContentSigner(signingBus, issuerMaterial.keyRef(), signatureIdentity, signingTtl);
|
||||
byte[] algorithm = signer.getAlgorithmIdentifier().getEncoded();
|
||||
tbs = encodeTbs(issuerMaterial.issuerHolder(), thisUpdate, nextUpdate, entries, algorithm);
|
||||
copyToSigner(tbs, signer);
|
||||
@@ -321,6 +331,8 @@ public final class BcX509StatusObjectGenerator implements StatusObjectGenerator
|
||||
X509ExecutionPlan<BcX509VerificationExecutor> plan = signingBus.authority().plan(actualSignature,
|
||||
issuerKey, AlgorithmExecutionCapability.Direction.VERIFY,
|
||||
Optional.of(BcX509VerificationExecutor.IMPLEMENTATION_ID), "crl-postcondition",
|
||||
signingBus.authority().bindings().bindingId(BcX509AlgorithmAdapter.fromBc(algorithm),
|
||||
X509AlgorithmRole.SIGNATURE_ALGORITHM),
|
||||
BcX509VerificationExecutor.class);
|
||||
try (RepeatableContent signedContent = new ContentSlice(complete, layout.tbsOffset(),
|
||||
layout.tbsLength())) {
|
||||
|
||||
@@ -37,12 +37,17 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
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.Set;
|
||||
|
||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
|
||||
import org.bouncycastle.operator.ContentVerifier;
|
||||
import org.bouncycastle.operator.OperatorCreationException;
|
||||
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.ECDSA_SHA256, BootstrapAlgorithmIdentities.ECDSA_SHA384,
|
||||
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
|
||||
public List<AlgorithmExecutionCapability> capabilities() {
|
||||
@@ -134,8 +140,16 @@ public final class BcX509VerificationExecutor implements AlgorithmExecutionCapab
|
||||
if (!plan.selection().suite().publicKey().equals(publicKeyIdentity)) {
|
||||
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)
|
||||
.build(publicKeyInfo).get(algorithmIdentifier);
|
||||
.build(nativePublicKey).get(algorithmIdentifier);
|
||||
try (InputStream input = signedContent.openStream(); OutputStream output = verifier.getOutputStream()) {
|
||||
byte[] buffer = new byte[16 * 1024];
|
||||
int read;
|
||||
@@ -148,4 +162,50 @@ public final class BcX509VerificationExecutor implements AlgorithmExecutionCapab
|
||||
}
|
||||
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.impl.core.async.PkiSigningBus;
|
||||
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.spi.crypto.SignatureWorkflow;
|
||||
import zeroecho.pki.spi.store.ContentSink;
|
||||
@@ -156,6 +155,25 @@ public final class PkiBusContentSigner implements ContentSigner {
|
||||
* authoritative signature binding exists
|
||||
*/
|
||||
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) {
|
||||
throw new IllegalArgumentException("bus must not be null");
|
||||
}
|
||||
@@ -170,8 +188,10 @@ public final class PkiBusContentSigner implements ContentSigner {
|
||||
}
|
||||
this.bus = bus;
|
||||
this.keyRef = keyRef;
|
||||
X509ExecutionPlan<SignatureWorkflow> plan = bus.authority()
|
||||
.planSigning(algorithmIdentity.canonicalForm(), SignatureWorkflow.class);
|
||||
X509ExecutionPlan<SignatureWorkflow> plan = bindingId.isPresent()
|
||||
? bus.authority().planSigningWithBinding(algorithmIdentity.canonicalForm(), bindingId.orElseThrow(),
|
||||
SignatureWorkflow.class)
|
||||
: bus.authority().planSigning(algorithmIdentity.canonicalForm(), SignatureWorkflow.class);
|
||||
bus.authority().authorize(plan, plan.executor(),
|
||||
zeroecho.core.spi.AlgorithmExecutionCapability.Direction.SIGN);
|
||||
this.executionPlan = plan;
|
||||
@@ -205,8 +225,7 @@ public final class PkiBusContentSigner implements ContentSigner {
|
||||
*/
|
||||
@Override
|
||||
public AlgorithmIdentifier getAlgorithmIdentifier() {
|
||||
return new BcX509AlgorithmAdapter(bus.authority().bindings()).encode(algorithmIdentity,
|
||||
X509AlgorithmRole.SIGNATURE_ALGORITHM);
|
||||
return BcX509AlgorithmAdapter.toBc(executionPlan.selection().binding());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -225,7 +225,10 @@ public final class WorkflowProofOfPossessionVerifier implements ProofOfPossessio
|
||||
X509AlgorithmRole.SUBJECT_PUBLIC_KEY_ALGORITHM);
|
||||
executionPlan = authority.plan(signatureIdentity, keyIdentity,
|
||||
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) {
|
||||
return new ProofOfPossessionResult(ProofOfPossessionStatus.NOT_SUPPORTED,
|
||||
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
|
||||
void profileDocumentsFailClosedForInvalidSchemaFieldsEncodingAndClassMetadata() {
|
||||
List<byte[]> invalidDocuments = List.of(
|
||||
replace(mainResource(SERVER), "\"schemaVersion\":2", "\"schemaVersion\":1"),
|
||||
replace(mainResource(SERVER), "\"schemaVersion\":2,", "\"schemaVersion\":2,\"active\":true,"),
|
||||
replace(mainResource(SERVER), "\"schemaVersion\":2,",
|
||||
"\"schemaVersion\":2,\"@class\":\"" + InitializationSentinel.CLASS_NAME + "\","),
|
||||
replace(mainResource(SERVER), "\"schemaVersion\":3", "\"schemaVersion\":1"),
|
||||
replace(mainResource(SERVER), "\"schemaVersion\":3,", "\"schemaVersion\":3,\"active\":true,"),
|
||||
replace(mainResource(SERVER), "\"schemaVersion\":3,",
|
||||
"\"schemaVersion\":3,\"@class\":\"" + InitializationSentinel.CLASS_NAME + "\","),
|
||||
malformedUtf8(mainResource(SERVER)));
|
||||
for (byte[] invalid : invalidDocuments) {
|
||||
Map<String, List<byte[]>> resources = baseResources();
|
||||
|
||||
@@ -63,13 +63,14 @@ final class CertificateProfileDocumentCodecTest {
|
||||
|
||||
private static final String VALID_DOCUMENT = """
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"schemaVersion": 3,
|
||||
"certificateType": "END_ENTITY",
|
||||
"profileId": "tls-service",
|
||||
"profileVersion": 7,
|
||||
"formatId": "x509",
|
||||
"displayName": "TLS service",
|
||||
"maxValidity": "PT24H",
|
||||
"algorithmBindings": {"mode":"STANDARD_ONLY"},
|
||||
"subject": {
|
||||
"allowEmpty": false,
|
||||
"rules": [
|
||||
@@ -138,7 +139,7 @@ final class CertificateProfileDocumentCodecTest {
|
||||
void parsesEverySupportedRuleShapeIntoAuthoritativeTypedPolicies() {
|
||||
CertificateProfileDefinition definition = parse(VALID_DOCUMENT);
|
||||
|
||||
assertEquals(2, CertificateProfileDefinition.SCHEMA_VERSION);
|
||||
assertEquals(3, CertificateProfileDefinition.SCHEMA_VERSION);
|
||||
assertEquals(CertificateProfileKind.END_ENTITY, definition.certificateType());
|
||||
assertEquals("tls-service", definition.profileId());
|
||||
assertEquals(7, definition.profileVersion());
|
||||
@@ -311,10 +312,10 @@ final class CertificateProfileDocumentCodecTest {
|
||||
|
||||
@Test
|
||||
void rejectsDuplicateUnknownMissingNullWrongAndNonintegralFields() {
|
||||
assertCode(VALID_DOCUMENT.replace("\"schemaVersion\": 2,", "\"schemaVersion\": 2,\"schemaVersion\": 2,"),
|
||||
assertCode(VALID_DOCUMENT.replace("\"schemaVersion\": 3,", "\"schemaVersion\": 3,\"schemaVersion\": 3,"),
|
||||
"DUPLICATE_FIELD");
|
||||
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("\"oid\": \"2.5.4.3\",", "\"unknown\": true,\"oid\": \"2.5.4.3\","),
|
||||
VALID_DOCUMENT.replace("\"minimumTotal\": 1,", "\"unknown\": true,\"minimumTotal\": 1,"),
|
||||
@@ -330,7 +331,7 @@ final class CertificateProfileDocumentCodecTest {
|
||||
"\"keyUsageCritical\": true,\"keyUsageCritical\": true,"))) {
|
||||
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("\"oid\": \"2.5.4.3\",\n", ""),
|
||||
VALID_DOCUMENT.replace("\"minimumTotal\": 1,\n", ""),
|
||||
@@ -355,7 +356,7 @@ final class CertificateProfileDocumentCodecTest {
|
||||
|
||||
@Test
|
||||
void rejectsUnsupportedVersionsTokensCaseWhitespaceAndNoncanonicalDuration() {
|
||||
assertCode(VALID_DOCUMENT.replace("\"schemaVersion\": 2", "\"schemaVersion\": 1"),
|
||||
assertCode(VALID_DOCUMENT.replace("\"schemaVersion\": 3", "\"schemaVersion\": 1"),
|
||||
"SCHEMA_VERSION_UNSUPPORTED");
|
||||
assertCode(VALID_DOCUMENT.replace("\"certificateType\": \"END_ENTITY\"", "\"certificateType\": \"end_entity\""),
|
||||
"CERTIFICATE_TYPE_UNSUPPORTED");
|
||||
@@ -473,8 +474,8 @@ final class CertificateProfileDocumentCodecTest {
|
||||
@Test
|
||||
void redactsHostileInputAndParserDetailsFromFailures() {
|
||||
String secret = "do-not-disclose-credential";
|
||||
String hostile = VALID_DOCUMENT.replace("\"schemaVersion\": 2,",
|
||||
"\"" + secret + "\": true,\"schemaVersion\": 2,");
|
||||
String hostile = VALID_DOCUMENT.replace("\"schemaVersion\": 3,",
|
||||
"\"" + secret + "\": true,\"schemaVersion\": 3,");
|
||||
|
||||
PkiException exception = assertThrows(PkiException.class, () -> parse(hostile));
|
||||
|
||||
@@ -508,8 +509,8 @@ final class CertificateProfileDocumentCodecTest {
|
||||
logger.addHandler(handler);
|
||||
try {
|
||||
for (String field : hostileFields) {
|
||||
String document = VALID_DOCUMENT.replace("\"schemaVersion\": 2,",
|
||||
"\"" + field + "\":\"" + probeName + "\",\"schemaVersion\": 2,");
|
||||
String document = VALID_DOCUMENT.replace("\"schemaVersion\": 3,",
|
||||
"\"" + field + "\":\"" + probeName + "\",\"schemaVersion\": 3,");
|
||||
PkiException exception = assertThrows(PkiException.class, () -> parse(document));
|
||||
assertTrue(exception.getMessage().contains("code=UNKNOWN_FIELD "));
|
||||
assertFalse(exception.getMessage().contains(field));
|
||||
|
||||
@@ -40,6 +40,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
@@ -48,6 +49,7 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
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.store.PkiStore;
|
||||
import zeroecho.pki.testkit.InMemorySignatureWorkflow;
|
||||
import zeroecho.pki.testkit.TestX509AlgorithmBindingProvider;
|
||||
|
||||
class PkiSessionLifecycleTest {
|
||||
|
||||
@@ -96,6 +99,60 @@ class PkiSessionLifecycleTest {
|
||||
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
|
||||
void validatesBeforeAllocationAndCleansPartialConstruction() {
|
||||
System.out.println("validatesBeforeAllocationAndCleansPartialConstruction");
|
||||
@@ -142,7 +199,7 @@ class PkiSessionLifecycleTest {
|
||||
placeholder, new ProviderConfig("x509-bc", Map.of()),
|
||||
temporaryDirectory.resolve("signing-bus").toString(), "RSA-2048-PSS-SHA256", Duration.ofSeconds(5),
|
||||
Optional.empty());
|
||||
PkiSessionConfiguration configuration = new PkiSessionConfiguration(2,
|
||||
PkiSessionConfiguration configuration = new PkiSessionConfiguration(3,
|
||||
new ProviderConfig("fs", Map.of("root", temporaryDirectory.resolve("store-signing").toString())),
|
||||
new ProviderConfig("memory", Map.of("size", "16")), Optional.of(signing), java.util.List.of());
|
||||
IllegalStateException failure = assertThrows(IllegalStateException.class,
|
||||
@@ -159,7 +216,7 @@ class PkiSessionLifecycleTest {
|
||||
System.out.println("opensExplicitPublisherCapabilityWithoutSigning");
|
||||
ProviderConfig publisher = new ProviderConfig("filesystem",
|
||||
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("memory", Map.of("size", "16")), Optional.empty(), java.util.List.of(publisher));
|
||||
try (PkiSession session = PkiSession.open(configuration)) {
|
||||
@@ -174,7 +231,7 @@ class PkiSessionLifecycleTest {
|
||||
void rejectsUnknownPublisherBeforeStoreAllocation() {
|
||||
System.out.println("rejectsUnknownPublisherBeforeStoreAllocation");
|
||||
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("memory", Map.of("size", "16")), Optional.empty(),
|
||||
java.util.List.of(new ProviderConfig("unknown-publisher", Map.of())));
|
||||
@@ -197,7 +254,7 @@ class PkiSessionLifecycleTest {
|
||||
placeholder, new ProviderConfig("x509-bc", Map.of()),
|
||||
temporaryDirectory.resolve("composed-signing-bus").toString(), "SHA256withRSA",
|
||||
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("memory", Map.of("size", "16")), Optional.of(signing), java.util.List.of());
|
||||
DefaultPkiSession.Bootstrap bootstrap = signingBootstrap(workflow);
|
||||
|
||||
@@ -46,6 +46,7 @@ import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CyclicBarrier;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
@@ -60,13 +61,22 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
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.BuiltInCertificateProfileTemplate;
|
||||
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.fs.FilesystemPkiStore;
|
||||
import zeroecho.pki.impl.fs.FsPkiStoreOptions;
|
||||
import zeroecho.pki.spi.store.PkiStore;
|
||||
import zeroecho.pki.testkit.TestX509AlgorithmBindingProvider;
|
||||
|
||||
/**
|
||||
* 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
|
||||
void lifecycleOperationsSanitizeStoreDiagnosticsAndAuditCodes() {
|
||||
String sentinel = "unsafe-profile-store-sentinel";
|
||||
@@ -248,6 +320,11 @@ final class DefaultProfileServiceTest {
|
||||
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) {
|
||||
return BuiltInCertificateProfileCatalog.load(DefaultProfileServiceTest.class.getClassLoader()).stream()
|
||||
.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.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.security.KeyPair;
|
||||
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.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||
import org.bouncycastle.asn1.DERNull;
|
||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
|
||||
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 zeroecho.core.alg.BootstrapAlgorithmIdentities;
|
||||
import zeroecho.core.spec.AlgorithmIdentity;
|
||||
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.spi.AlgorithmExecutionCapability;
|
||||
import zeroecho.core.spi.AlgorithmExecutionCapabilityProvider;
|
||||
@@ -65,6 +102,136 @@ import zeroecho.pki.impl.framework.x509.bc.BcX509VerificationExecutor;
|
||||
*/
|
||||
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
|
||||
void 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.
|
||||
*/
|
||||
|
||||
@@ -229,8 +229,9 @@ public final class H7ProfileDocuments {
|
||||
List<String> sanRules, boolean noKeyEncipherment, List<String> eku) {
|
||||
String keyUsage = noKeyEncipherment ? "[\"DIGITAL_SIGNATURE\"]"
|
||||
: "[\"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\","
|
||||
+ "\"algorithmBindings\":{\"mode\":\"STANDARD_ONLY\"},"
|
||||
+ "\"maxValidity\":\"PT8760H\",\"subject\":{\"allowEmpty\":" + allowEmpty + ",\"rules\":["
|
||||
+ String.join(",", subjectRules) + "]}," + "\"subjectAlternativeNames\":{\"minimumTotal\":"
|
||||
+ 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