From 5420c19d08b106a797999bd79c14342fb6284582 Mon Sep 17 00:00:00 2001
From: Leo Galambos
Date: Sat, 1 Aug 2026 16:45:14 +0200
Subject: [PATCH] wip(pki): checkpoint Phase A metadata foundation
Checkpoint the current pre-release Phase A work before production
persistence integration continues.
Includes the consolidated transactional metadata SPI, POSIX append-only
metadata log, recovery epochs, mutation codec, state reducer, internal
transaction engine, transactional adapter, staged-content foundations,
and the related current lib/pki changes.
Validated baseline:
- lib tests pass
- focused metadata tests pass
- PMD passes with zero findings
- JavaDoc passes
- app compilation passes
- pki retains exactly 31 independently classified failures:
2 credential snapshot/model cases and 29 revocation fixture/reference
cases
This is a work-in-progress safety checkpoint, not a release-ready
milestone.
---
.../alg/BootstrapAlgorithmIdentities.java | 226 ++++
.../common/sig/SignatureInteropProfiles.java | 28 +-
.../zeroecho/core/io/CancellationSignal.java | 73 ++
.../java/zeroecho/core/io/ContentDigests.java | 86 ++
.../java/zeroecho/core/io/ContentSlice.java | 161 +++
.../core/io/ImmutableByteContent.java | 107 ++
.../java/zeroecho/core/io/OneShotContent.java | 75 ++
.../zeroecho/core/io/RepeatableContent.java | 97 ++
.../zeroecho/core/spec/AlgorithmIdentity.java | 589 +++++++++
.../core/spec/AlgorithmIdentityCatalog.java | 184 +++
.../core/spec/AlgorithmIdentityCodec.java | 70 ++
.../zeroecho/core/spec/AlgorithmSuite.java | 79 ++
.../spi/AlgorithmExecutionCapabilities.java | 141 +++
.../spi/AlgorithmExecutionCapability.java | 84 ++
.../AlgorithmExecutionCapabilityProvider.java | 56 +
.../spec/AlgorithmIdentityPhaseATest.java | 150 +++
.../zeroecho/pki/api/ca/CaImportCommand.java | 4 +-
.../api/content/DeploymentResourcePolicy.java | 74 ++
.../pki/api/content/DurableContentOwner.java | 119 ++
.../api/content/DurableContentReference.java | 117 ++
.../pki/api/content/ResourceLimit.java | 108 ++
.../pki/api/credential/Credential.java | 10 +-
.../pki/api/credential/CredentialBundle.java | 4 +-
.../zeroecho/pki/api/status/StatusObject.java | 10 +-
.../core/CaCertificateProfileValidator.java | 9 +-
.../zeroecho/pki/impl/core/CaProofGate.java | 132 ++-
.../core/CertificateProfileValidator.java | 54 +-
.../pki/impl/core/CredentialContent.java | 111 ++
.../pki/impl/core/CredentialSnapshots.java | 10 +-
.../pki/impl/core/DefaultCaService.java | 57 +-
.../pki/impl/core/DefaultIssuanceService.java | 27 +-
.../impl/core/DefaultRevocationService.java | 14 +-
.../impl/core/DefaultStatusObjectService.java | 250 +++-
.../pki/impl/core/async/PkiSigningBus.java | 502 ++++++--
.../ZeroEchoLibSignatureWorkflow.java | 104 +-
.../ZeroEchoLibSignatureWorkflowProvider.java | 44 +-
.../framework/x509/StandardX509Bindings.java | 619 ++++++++++
.../framework/x509/StreamingDerReader.java | 681 +++++++++++
.../framework/x509/StreamingDerWriter.java | 155 +++
.../x509/X509AlgorithmIdentifier.java | 167 +++
.../framework/x509/X509AlgorithmResolver.java | 243 ++++
.../framework/x509/X509AlgorithmRole.java | 44 +
.../framework/x509/X509AuthoritySnapshot.java | 635 ++++++++++
.../framework/x509/X509BindingCatalog.java | 216 ++++
.../impl/framework/x509/X509BindingRule.java | 137 +++
.../x509/X509BindingRuleProvider.java | 109 ++
.../framework/x509/X509BuiltInDefaults.java | 89 ++
.../framework/x509/X509ComponentCatalog.java | 253 ++++
.../framework/x509/X509ExecutionPlan.java | 97 ++
.../framework/x509/X509SecurityFloor.java | 79 ++
.../x509/X509SignedObjectCompletion.java | 79 ++
.../x509/X509SuiteCompatibility.java | 81 ++
.../x509/bc/BcX509AlgorithmAdapter.java | 168 +++
.../bc/BcX509CertificationRequestParser.java | 1 +
.../x509/bc/BcX509CredentialFramework.java | 29 +-
.../bc/BcX509CredentialFrameworkProvider.java | 11 +-
.../bc/BcX509CredentialIssuerBackend.java | 98 +-
.../bc/BcX509ProofOfPossessionVerifier.java | 66 +-
.../x509/bc/BcX509SignedObjectValidator.java | 305 +++++
.../x509/bc/BcX509StatusObjectGenerator.java | 525 ++++++---
.../x509/bc/BcX509VerificationExecutor.java | 151 +++
.../framework/x509/bc/OidAlgorithmMapper.java | 83 +-
.../x509/bc/PkiBusContentSigner.java | 126 +-
.../bc/WorkflowProofOfPossessionVerifier.java | 55 +-
.../impl/fs/CredentialContentTransaction.java | 563 +++++++++
.../pki/impl/fs/DurableMetadataFiles.java | 417 +++++++
.../pki/impl/fs/FilesystemPkiStore.java | 283 ++++-
.../impl/fs/FilesystemStagedContentStore.java | 909 ++++++++++++++
.../fs/FilesystemTemporaryUniqueIndex.java | 305 +++++
.../java/zeroecho/pki/impl/fs/FsCodec.java | 129 +-
.../java/zeroecho/pki/impl/fs/FsPaths.java | 8 +
.../pki/impl/fs/FsSnapshotExporter.java | 1 +
.../pki/impl/fs/MetadataFrameCodec.java | 445 +++++++
.../impl/fs/MetadataMutationPayloadCodec.java | 591 ++++++++++
.../pki/impl/fs/MetadataStateIndex.java | 471 ++++++++
.../fs/PosixMetadataAdapterLifecycle.java | 306 +++++
.../pki/impl/fs/PosixMetadataLog.java | 987 ++++++++++++++++
.../pki/impl/fs/PosixMetadataLogScanner.java | 1050 +++++++++++++++++
.../impl/fs/PosixMetadataSnapshotSupport.java | 559 +++++++++
.../pki/impl/fs/PosixMetadataStoreEngine.java | 820 +++++++++++++
.../fs/PosixMetadataTransactionSupport.java | 692 +++++++++++
.../fs/PosixTransactionalMetadataStore.java | 255 ++++
.../pki/spi/crypto/SignatureWorkflow.java | 48 +-
.../framework/CredentialIssuerBackend.java | 7 +-
.../pki/spi/framework/CrlEntrySource.java | 102 ++
.../spi/framework/StatusObjectGenerator.java | 8 +-
.../zeroecho/pki/spi/publish/Publisher.java | 4 +-
.../zeroecho/pki/spi/store/ContentSink.java | 92 ++
.../pki/spi/store/MetadataCommitResult.java | 67 ++
.../pki/spi/store/MetadataCursor.java | 46 +
.../zeroecho/pki/spi/store/MetadataKey.java | 163 +++
.../pki/spi/store/MetadataSnapshot.java | 215 ++++
.../spi/store/MetadataStoreCapabilities.java | 63 +
.../pki/spi/store/MetadataStoreException.java | 44 +
.../pki/spi/store/MetadataStoreId.java | 27 +
.../pki/spi/store/MetadataTransaction.java | 105 ++
.../pki/spi/store/MetadataTransactionId.java | 53 +
.../java/zeroecho/pki/spi/store/PkiStore.java | 14 +-
.../pki/spi/store/RevocationSnapshot.java | 120 ++
.../pki/spi/store/StagedContentStore.java | 201 ++++
.../pki/spi/store/TemporaryUniqueIndex.java | 95 ++
.../spi/store/TransactionalMetadataStore.java | 93 ++
...e.spi.AlgorithmExecutionCapabilityProvider | 1 +
.../e2e/CaProfileIssuanceEnforcementTest.java | 162 +--
.../pki/e2e/H7EndEntityAcceptanceE2eTest.java | 52 +-
.../e2e/H7EndEntityCsrRejectionE2eTest.java | 4 +-
.../java/zeroecho/pki/e2e/PkiCoreE2eTest.java | 11 +-
.../zeroecho/pki/e2e/PkiProofGateE2eTest.java | 189 +--
.../DefaultStatusObjectServiceCrlTest.java | 156 ++-
.../impl/core/H7ProfileEnforcementTest.java | 21 +-
...EffectiveCredentialStatusResolverTest.java | 27 +-
.../core/async/PkiSigningBusFailureTest.java | 186 ++-
.../PkiSigningBusOperatorApprovalTest.java | 22 +-
.../async/PkiSigningBusResilienceTest.java | 14 +-
.../ZeroEchoLibKeyRefParsingTest.java | 8 +-
...hoLibSignatureWorkflowPersistenceTest.java | 20 +-
...gnatureWorkflowVerifyEncodedEcdsaTest.java | 7 +-
...LibSignatureWorkflowVerifyEncodedTest.java | 7 +-
.../x509/StreamingDerReaderTest.java | 207 ++++
.../framework/x509/X509BindingPhaseATest.java | 422 +++++++
.../bc/PkiBusContentSignerCleanupTest.java | 42 +-
...WorkflowProofOfPossessionVerifierTest.java | 11 +-
.../pki/impl/fs/DurableMetadataFilesTest.java | 118 ++
...ystemCredentialContentTransactionTest.java | 239 ++++
.../pki/impl/fs/FilesystemPkiStoreTest.java | 51 +-
.../fs/FilesystemRevocationJournalTest.java | 16 +-
.../fs/FilesystemSignWorkflowStoreTest.java | 112 +-
.../fs/FilesystemStagedContentStoreTest.java | 411 +++++++
.../zeroecho/pki/impl/fs/FsCodecTest.java | 29 +-
.../pki/impl/fs/MetadataFrameCodecTest.java | 669 +++++++++++
.../fs/MetadataMutationPayloadCodecTest.java | 469 ++++++++
.../pki/impl/fs/MetadataStateIndexTest.java | 220 ++++
.../impl/fs/PosixMetadataLogScannerTest.java | 579 +++++++++
.../pki/impl/fs/PosixMetadataLogTest.java | 409 +++++++
.../impl/fs/PosixMetadataStoreEngineTest.java | 633 ++++++++++
.../PosixTransactionalMetadataStoreTest.java | 919 +++++++++++++++
.../pki/spi/bootstrap/PkiBootstrapTest.java | 3 +-
.../spi/crypto/SignRequestCleanupTest.java | 21 +-
.../pki/spi/store/CommitOutcomeTest.java | 54 +
.../InMemoryTransactionalMetadataStore.java | 746 ++++++++++++
.../pki/spi/store/MetadataKeyTest.java | 116 ++
...ransactionalMetadataStoreContractTest.java | 565 +++++++++
.../DurableDelayedSignatureWorkflow.java | 13 +-
...ableOperatorApprovalSignatureWorkflow.java | 13 +-
.../testkit/InMemorySignatureWorkflow.java | 28 +-
.../OperatorApprovalSignatureWorkflow.java | 5 +-
.../zeroecho/pki/testkit/PkiTestRuntime.java | 226 +++-
147 files changed, 26788 insertions(+), 1071 deletions(-)
create mode 100644 lib/src/main/java/zeroecho/core/alg/BootstrapAlgorithmIdentities.java
create mode 100644 lib/src/main/java/zeroecho/core/io/CancellationSignal.java
create mode 100644 lib/src/main/java/zeroecho/core/io/ContentDigests.java
create mode 100644 lib/src/main/java/zeroecho/core/io/ContentSlice.java
create mode 100644 lib/src/main/java/zeroecho/core/io/ImmutableByteContent.java
create mode 100644 lib/src/main/java/zeroecho/core/io/OneShotContent.java
create mode 100644 lib/src/main/java/zeroecho/core/io/RepeatableContent.java
create mode 100644 lib/src/main/java/zeroecho/core/spec/AlgorithmIdentity.java
create mode 100644 lib/src/main/java/zeroecho/core/spec/AlgorithmIdentityCatalog.java
create mode 100644 lib/src/main/java/zeroecho/core/spec/AlgorithmIdentityCodec.java
create mode 100644 lib/src/main/java/zeroecho/core/spec/AlgorithmSuite.java
create mode 100644 lib/src/main/java/zeroecho/core/spi/AlgorithmExecutionCapabilities.java
create mode 100644 lib/src/main/java/zeroecho/core/spi/AlgorithmExecutionCapability.java
create mode 100644 lib/src/main/java/zeroecho/core/spi/AlgorithmExecutionCapabilityProvider.java
create mode 100644 lib/src/test/java/zeroecho/core/spec/AlgorithmIdentityPhaseATest.java
create mode 100644 pki/src/main/java/zeroecho/pki/api/content/DeploymentResourcePolicy.java
create mode 100644 pki/src/main/java/zeroecho/pki/api/content/DurableContentOwner.java
create mode 100644 pki/src/main/java/zeroecho/pki/api/content/DurableContentReference.java
create mode 100644 pki/src/main/java/zeroecho/pki/api/content/ResourceLimit.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/core/CredentialContent.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/framework/x509/StandardX509Bindings.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/framework/x509/StreamingDerReader.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/framework/x509/StreamingDerWriter.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/framework/x509/X509AlgorithmIdentifier.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/framework/x509/X509AlgorithmResolver.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/framework/x509/X509AlgorithmRole.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/framework/x509/X509AuthoritySnapshot.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/framework/x509/X509BindingCatalog.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/framework/x509/X509BindingRule.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/framework/x509/X509BindingRuleProvider.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/framework/x509/X509BuiltInDefaults.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/framework/x509/X509ComponentCatalog.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/framework/x509/X509ExecutionPlan.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/framework/x509/X509SecurityFloor.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/framework/x509/X509SignedObjectCompletion.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/framework/x509/X509SuiteCompatibility.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509AlgorithmAdapter.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509SignedObjectValidator.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/framework/x509/bc/BcX509VerificationExecutor.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/fs/CredentialContentTransaction.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/fs/DurableMetadataFiles.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/fs/FilesystemStagedContentStore.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/fs/FilesystemTemporaryUniqueIndex.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/fs/MetadataFrameCodec.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/fs/MetadataMutationPayloadCodec.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/fs/MetadataStateIndex.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/fs/PosixMetadataAdapterLifecycle.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/fs/PosixMetadataLog.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/fs/PosixMetadataLogScanner.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/fs/PosixMetadataSnapshotSupport.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/fs/PosixMetadataStoreEngine.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/fs/PosixMetadataTransactionSupport.java
create mode 100644 pki/src/main/java/zeroecho/pki/impl/fs/PosixTransactionalMetadataStore.java
create mode 100644 pki/src/main/java/zeroecho/pki/spi/framework/CrlEntrySource.java
create mode 100644 pki/src/main/java/zeroecho/pki/spi/store/ContentSink.java
create mode 100644 pki/src/main/java/zeroecho/pki/spi/store/MetadataCommitResult.java
create mode 100644 pki/src/main/java/zeroecho/pki/spi/store/MetadataCursor.java
create mode 100644 pki/src/main/java/zeroecho/pki/spi/store/MetadataKey.java
create mode 100644 pki/src/main/java/zeroecho/pki/spi/store/MetadataSnapshot.java
create mode 100644 pki/src/main/java/zeroecho/pki/spi/store/MetadataStoreCapabilities.java
create mode 100644 pki/src/main/java/zeroecho/pki/spi/store/MetadataStoreException.java
create mode 100644 pki/src/main/java/zeroecho/pki/spi/store/MetadataStoreId.java
create mode 100644 pki/src/main/java/zeroecho/pki/spi/store/MetadataTransaction.java
create mode 100644 pki/src/main/java/zeroecho/pki/spi/store/MetadataTransactionId.java
create mode 100644 pki/src/main/java/zeroecho/pki/spi/store/RevocationSnapshot.java
create mode 100644 pki/src/main/java/zeroecho/pki/spi/store/StagedContentStore.java
create mode 100644 pki/src/main/java/zeroecho/pki/spi/store/TemporaryUniqueIndex.java
create mode 100644 pki/src/main/java/zeroecho/pki/spi/store/TransactionalMetadataStore.java
create mode 100644 pki/src/main/resources/META-INF/services/zeroecho.core.spi.AlgorithmExecutionCapabilityProvider
create mode 100644 pki/src/test/java/zeroecho/pki/impl/framework/x509/StreamingDerReaderTest.java
create mode 100644 pki/src/test/java/zeroecho/pki/impl/framework/x509/X509BindingPhaseATest.java
create mode 100644 pki/src/test/java/zeroecho/pki/impl/fs/DurableMetadataFilesTest.java
create mode 100644 pki/src/test/java/zeroecho/pki/impl/fs/FilesystemCredentialContentTransactionTest.java
create mode 100644 pki/src/test/java/zeroecho/pki/impl/fs/FilesystemStagedContentStoreTest.java
create mode 100644 pki/src/test/java/zeroecho/pki/impl/fs/MetadataFrameCodecTest.java
create mode 100644 pki/src/test/java/zeroecho/pki/impl/fs/MetadataMutationPayloadCodecTest.java
create mode 100644 pki/src/test/java/zeroecho/pki/impl/fs/MetadataStateIndexTest.java
create mode 100644 pki/src/test/java/zeroecho/pki/impl/fs/PosixMetadataLogScannerTest.java
create mode 100644 pki/src/test/java/zeroecho/pki/impl/fs/PosixMetadataLogTest.java
create mode 100644 pki/src/test/java/zeroecho/pki/impl/fs/PosixMetadataStoreEngineTest.java
create mode 100644 pki/src/test/java/zeroecho/pki/impl/fs/PosixTransactionalMetadataStoreTest.java
create mode 100644 pki/src/test/java/zeroecho/pki/spi/store/CommitOutcomeTest.java
create mode 100644 pki/src/test/java/zeroecho/pki/spi/store/InMemoryTransactionalMetadataStore.java
create mode 100644 pki/src/test/java/zeroecho/pki/spi/store/MetadataKeyTest.java
create mode 100644 pki/src/test/java/zeroecho/pki/spi/store/TransactionalMetadataStoreContractTest.java
diff --git a/lib/src/main/java/zeroecho/core/alg/BootstrapAlgorithmIdentities.java b/lib/src/main/java/zeroecho/core/alg/BootstrapAlgorithmIdentities.java
new file mode 100644
index 0000000..2d92151
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/alg/BootstrapAlgorithmIdentities.java
@@ -0,0 +1,226 @@
+/*******************************************************************************
+ * 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.core.alg;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+import zeroecho.core.spec.AlgorithmIdentity;
+import zeroecho.core.spec.AlgorithmIdentityCatalog;
+import zeroecho.core.spec.AlgorithmSuite;
+
+/**
+ * Immutable bootstrap identities required by current ZeroEcho PKI behavior.
+ *
+ *
+ * These identities are mandatory defaults, not a permanent maximum algorithm
+ * set. Trusted installed extensions may add identities in other namespaces.
+ * Provider aliases are accepted only by {@link #fromCompatibilityAlias(String)}
+ * and never become canonical identity data.
+ *
+ */
+public final class BootstrapAlgorithmIdentities {
+
+ private static final AlgorithmIdentity.Family SHA2_256 = family("sha2-256");
+ private static final AlgorithmIdentity.Family SHA2_384 = family("sha2-384");
+ private static final AlgorithmIdentity.Family SHA2_512 = family("sha2-512");
+ private static final AlgorithmIdentity.Family MGF1_FAMILY = family("mgf1");
+ private static final AlgorithmIdentity.Family RSA_PKCS1 = family("rsa-pkcs1-v1_5");
+ private static final AlgorithmIdentity.Family RSA_PSS = family("rsa-pss");
+ 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 RSA_KEY = family("rsa");
+ private static final AlgorithmIdentity.Family EC_KEY = family("ec");
+
+ /** SHA-256 digest identity. */
+ public static final AlgorithmIdentity SHA256 = identity(AlgorithmIdentity.Kind.DIGEST, SHA2_256,
+ AlgorithmIdentity.NoParameters.INSTANCE);
+ /** SHA-384 digest identity. */
+ public static final AlgorithmIdentity SHA384 = identity(AlgorithmIdentity.Kind.DIGEST, SHA2_384,
+ AlgorithmIdentity.NoParameters.INSTANCE);
+ /** SHA-512 digest identity. */
+ public static final AlgorithmIdentity SHA512 = identity(AlgorithmIdentity.Kind.DIGEST, SHA2_512,
+ AlgorithmIdentity.NoParameters.INSTANCE);
+ /** MGF1 mask-generation identity. */
+ public static final AlgorithmIdentity MGF1 = identity(AlgorithmIdentity.Kind.MASK_GENERATION, MGF1_FAMILY,
+ AlgorithmIdentity.NoParameters.INSTANCE);
+
+ /** RSA PKCS#1 v1.5 with SHA-256. */
+ public static final AlgorithmIdentity RSA_PKCS1_SHA256 = digestSignature(RSA_PKCS1, SHA256);
+ /** RSA PKCS#1 v1.5 with SHA-384. */
+ public static final AlgorithmIdentity RSA_PKCS1_SHA384 = digestSignature(RSA_PKCS1, SHA384);
+ /** RSA PKCS#1 v1.5 with SHA-512. */
+ public static final AlgorithmIdentity RSA_PKCS1_SHA512 = digestSignature(RSA_PKCS1, SHA512);
+ /** RSA-PSS SHA-256/MGF1-SHA-256/salt-32/trailer-1 bootstrap identity. */
+ public static final AlgorithmIdentity RSA_PSS_SHA256 = rsaPss(SHA256, SHA256, 32);
+ /** ECDSA with SHA-256, independent of the EC curve. */
+ public static final AlgorithmIdentity ECDSA_SHA256 = digestSignature(ECDSA, SHA256);
+ /** ECDSA with SHA-384, independent of the EC curve. */
+ public static final AlgorithmIdentity ECDSA_SHA384 = digestSignature(ECDSA, SHA384);
+ /** ECDSA with SHA-512, independent of the EC curve. */
+ public static final AlgorithmIdentity ECDSA_SHA512 = digestSignature(ECDSA, SHA512);
+ /** Ed25519 signature identity. */
+ public static final AlgorithmIdentity ED25519_SIGNATURE = identity(AlgorithmIdentity.Kind.SIGNATURE,
+ ED25519_FAMILY, AlgorithmIdentity.NoParameters.INSTANCE);
+ /** Ed448 signature identity. */
+ public static final AlgorithmIdentity ED448_SIGNATURE = identity(AlgorithmIdentity.Kind.SIGNATURE, ED448_FAMILY,
+ AlgorithmIdentity.NoParameters.INSTANCE);
+
+ /** RSA public-key identity. */
+ public static final AlgorithmIdentity RSA_PUBLIC_KEY = identity(AlgorithmIdentity.Kind.PUBLIC_KEY, RSA_KEY,
+ AlgorithmIdentity.NoParameters.INSTANCE);
+ /** EC P-256 public-key identity. */
+ public static final AlgorithmIdentity EC_P256_PUBLIC_KEY = namedKey(EC_KEY, "p-256");
+ /** EC P-384 public-key identity. */
+ public static final AlgorithmIdentity EC_P384_PUBLIC_KEY = namedKey(EC_KEY, "p-384");
+ /** EC P-521 public-key identity. */
+ public static final AlgorithmIdentity EC_P521_PUBLIC_KEY = namedKey(EC_KEY, "p-521");
+ /** Ed25519 public-key identity. */
+ public static final AlgorithmIdentity ED25519_PUBLIC_KEY = identity(AlgorithmIdentity.Kind.PUBLIC_KEY,
+ ED25519_FAMILY, AlgorithmIdentity.NoParameters.INSTANCE);
+ /** Ed448 public-key identity. */
+ public static final AlgorithmIdentity ED448_PUBLIC_KEY = identity(AlgorithmIdentity.Kind.PUBLIC_KEY, ED448_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);
+ /** Current ECDSA P-384 signing suite. */
+ public static final AlgorithmSuite ECDSA_SHA384_P384 = new AlgorithmSuite(ECDSA_SHA384, EC_P384_PUBLIC_KEY);
+ /** Current ECDSA P-521 signing suite. */
+ public static final AlgorithmSuite ECDSA_SHA512_P521 = new AlgorithmSuite(ECDSA_SHA512, EC_P521_PUBLIC_KEY);
+ /** Current immutable PKI signing default. */
+ public static final AlgorithmSuite PKI_SIGNATURE_DEFAULT_V1 = new AlgorithmSuite(RSA_PKCS1_SHA256, RSA_PUBLIC_KEY);
+
+ private static final List 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);
+
+ private static final AlgorithmIdentityCatalog CATALOG = AlgorithmIdentityCatalog.builtIn(IDENTITIES);
+
+ private static final Map ALIASES = Map.ofEntries(
+ Map.entry("SHA256withRSA", RSA_PKCS1_SHA256),
+ Map.entry("SHA384withRSA", RSA_PKCS1_SHA384),
+ Map.entry("SHA512withRSA", RSA_PKCS1_SHA512),
+ Map.entry("SHA256withRSAandMGF1", RSA_PSS_SHA256),
+ Map.entry("SHA256withECDSA", ECDSA_SHA256),
+ Map.entry("SHA384withECDSA", ECDSA_SHA384),
+ Map.entry("SHA512withECDSA", ECDSA_SHA512),
+ Map.entry("Ed25519", ED25519_SIGNATURE),
+ Map.entry("Ed448", ED448_SIGNATURE));
+
+ private BootstrapAlgorithmIdentities() {
+ }
+
+ /**
+ * Returns the immutable bootstrap identity catalog.
+ *
+ * @return built-in catalog
+ */
+ public static AlgorithmIdentityCatalog catalog() {
+ return CATALOG;
+ }
+
+ /**
+ * Creates an exact RSA-PSS identity without requiring a central enum entry.
+ *
+ * @param hash message digest
+ * @param mgfHash MGF1 digest
+ * @param saltLength salt length in bytes
+ * @return exact RSA-PSS identity
+ * @throws IllegalArgumentException if the tuple is contradictory
+ */
+ public static AlgorithmIdentity rsaPss(AlgorithmIdentity hash, AlgorithmIdentity mgfHash, int saltLength) {
+ return identity(AlgorithmIdentity.Kind.SIGNATURE, RSA_PSS,
+ new AlgorithmIdentity.RsaPssParameters(hash, MGF1, mgfHash, saltLength, 1));
+ }
+
+ /**
+ * Resolves a finite legacy provider alias at the compatibility boundary.
+ *
+ *
+ * SHA-1 and unknown aliases are rejected. The returned identity, rather than
+ * the alias, is authoritative.
+ *
+ *
+ * @param alias legacy provider spelling
+ * @return exact bootstrap identity, or empty when unknown or forbidden
+ */
+ public static Optional fromCompatibilityAlias(String alias) {
+ Objects.requireNonNull(alias, "alias");
+ return Optional.ofNullable(ALIASES.get(alias));
+ }
+
+ /**
+ * Returns the immutable finite built-in compatibility aliases.
+ *
+ * @return alias-to-exact-identity map
+ */
+ public static Map compatibilityAliases() {
+ return ALIASES;
+ }
+
+ /**
+ * Resolves either a canonical identity or an approved compatibility alias.
+ *
+ * @param value canonical identity or finite legacy alias
+ * @return exact identity, or empty when unsupported
+ */
+ public static Optional resolve(String value) {
+ Objects.requireNonNull(value, "value");
+ Optional canonical = CATALOG.resolve(value);
+ return canonical.isPresent() ? canonical : fromCompatibilityAlias(value);
+ }
+
+ private static AlgorithmIdentity digestSignature(AlgorithmIdentity.Family family, AlgorithmIdentity digest) {
+ return identity(AlgorithmIdentity.Kind.SIGNATURE, family, new AlgorithmIdentity.DigestParameters(digest));
+ }
+
+ private static AlgorithmIdentity namedKey(AlgorithmIdentity.Family family, String name) {
+ return identity(AlgorithmIdentity.Kind.PUBLIC_KEY, family,
+ new AlgorithmIdentity.NamedParameters(new AlgorithmIdentity.Family("zeroecho", name)));
+ }
+
+ private static AlgorithmIdentity identity(AlgorithmIdentity.Kind kind, AlgorithmIdentity.Family family,
+ AlgorithmIdentity.Parameters parameters) {
+ return new AlgorithmIdentity(kind, family, parameters);
+ }
+
+ private static AlgorithmIdentity.Family family(String name) {
+ return new AlgorithmIdentity.Family("zeroecho", name);
+ }
+}
diff --git a/lib/src/main/java/zeroecho/core/alg/common/sig/SignatureInteropProfiles.java b/lib/src/main/java/zeroecho/core/alg/common/sig/SignatureInteropProfiles.java
index 6a770f6..b6cd0a9 100644
--- a/lib/src/main/java/zeroecho/core/alg/common/sig/SignatureInteropProfiles.java
+++ b/lib/src/main/java/zeroecho/core/alg/common/sig/SignatureInteropProfiles.java
@@ -38,8 +38,10 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import zeroecho.core.alg.BootstrapAlgorithmIdentities;
import zeroecho.core.alg.ecdsa.EcdsaCurveSpec;
import zeroecho.core.alg.rsa.RsaSigSpec;
+import zeroecho.core.spec.AlgorithmIdentity;
import zeroecho.core.spec.VoidSpec;
/**
@@ -96,6 +98,10 @@ public final class SignatureInteropProfiles {
new SignatureInteropProfile("SHA512withRSA", "RSA", "RSA",
RsaSigSpec.pkcs1v15(RsaSigSpec.Hash.SHA512),
SignatureInteropProfile.SignatureRepresentation.IDENTITY, 0)),
+ Map.entry("SHA256withRSAandMGF1",
+ new SignatureInteropProfile("SHA256withRSAandMGF1", "RSA", "RSA",
+ RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32),
+ SignatureInteropProfile.SignatureRepresentation.IDENTITY, 0)),
Map.entry("SHA256withECDSA", new SignatureInteropProfile("SHA256withECDSA", "ECDSA", "ECDSA", // NOPMD
EcdsaCurveSpec.P256,
SignatureInteropProfile.SignatureRepresentation.ECDSA_DER_EXTERNAL_P1363_INTERNAL,
@@ -114,6 +120,17 @@ public final class SignatureInteropProfiles {
Map.entry("Ed448", new SignatureInteropProfile("Ed448", "Ed448", "Ed448", VoidSpec.INSTANCE,
SignatureInteropProfile.SignatureRepresentation.IDENTITY, 0)));
+ private static final Map CANONICAL_PROFILES = Map.ofEntries(
+ canonical(BootstrapAlgorithmIdentities.RSA_PKCS1_SHA256, "SHA256withRSA"),
+ canonical(BootstrapAlgorithmIdentities.RSA_PKCS1_SHA384, "SHA384withRSA"),
+ canonical(BootstrapAlgorithmIdentities.RSA_PKCS1_SHA512, "SHA512withRSA"),
+ canonical(BootstrapAlgorithmIdentities.RSA_PSS_SHA256, "SHA256withRSAandMGF1"),
+ canonical(BootstrapAlgorithmIdentities.ECDSA_SHA256, "SHA256withECDSA"),
+ canonical(BootstrapAlgorithmIdentities.ECDSA_SHA384, "SHA384withECDSA"),
+ canonical(BootstrapAlgorithmIdentities.ECDSA_SHA512, "SHA512withECDSA"),
+ canonical(BootstrapAlgorithmIdentities.ED25519_SIGNATURE, "Ed25519"),
+ canonical(BootstrapAlgorithmIdentities.ED448_SIGNATURE, "Ed448"));
+
private SignatureInteropProfiles() {
}
@@ -129,7 +146,8 @@ public final class SignatureInteropProfiles {
if (algorithmId.isBlank()) {
return Optional.empty();
}
- return Optional.ofNullable(PROFILES.get(algorithmId));
+ SignatureInteropProfile profile = CANONICAL_PROFILES.get(algorithmId);
+ return Optional.ofNullable(profile == null ? PROFILES.get(algorithmId) : profile);
}
/**
@@ -139,7 +157,9 @@ public final class SignatureInteropProfiles {
* @return immutable set of supported standard signature names
*/
public static Set algorithmIds() {
- return PROFILES.keySet();
+ Set identifiers = new java.util.LinkedHashSet<>(PROFILES.keySet());
+ identifiers.addAll(CANONICAL_PROFILES.keySet());
+ return Set.copyOf(identifiers);
}
/**
@@ -164,4 +184,8 @@ public final class SignatureInteropProfiles {
}
return algorithmId;
}
+
+ private static Map.Entry canonical(AlgorithmIdentity identity, String alias) {
+ return Map.entry(identity.canonicalForm(), PROFILES.get(alias));
+ }
}
diff --git a/lib/src/main/java/zeroecho/core/io/CancellationSignal.java b/lib/src/main/java/zeroecho/core/io/CancellationSignal.java
new file mode 100644
index 0000000..8754112
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/io/CancellationSignal.java
@@ -0,0 +1,73 @@
+/*******************************************************************************
+ * 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.core.io;
+
+import java.io.InterruptedIOException;
+
+/**
+ * Provider-independent cancellation signal for streaming operations.
+ *
+ *
+ * The signal carries no operation content, key material or executor state.
+ * Implementations should be immutable views over runtime-owned cancellation
+ * state. Streaming readers and writers are expected to call
+ * {@link #throwIfCancelled()} between bounded I/O operations.
+ *
+ */
+@FunctionalInterface
+public interface CancellationSignal {
+
+ /**
+ * A signal that never requests cancellation.
+ */
+ CancellationSignal NONE = () -> false;
+
+ /**
+ * Reports whether cancellation was requested.
+ *
+ * @return {@code true} when the operation should stop
+ */
+ boolean isCancelled();
+
+ /**
+ * Fails the current streaming operation when cancellation was requested.
+ *
+ * @throws InterruptedIOException when cancellation was requested
+ */
+ default void throwIfCancelled() throws InterruptedIOException {
+ if (isCancelled()) {
+ throw new InterruptedIOException("Streaming operation cancelled");
+ }
+ }
+}
diff --git a/lib/src/main/java/zeroecho/core/io/ContentDigests.java b/lib/src/main/java/zeroecho/core/io/ContentDigests.java
new file mode 100644
index 0000000..54696b6
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/io/ContentDigests.java
@@ -0,0 +1,86 @@
+/*******************************************************************************
+ * 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.core.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HexFormat;
+import java.util.Objects;
+
+/**
+ * Streaming integrity helpers for repeatable content.
+ */
+public final class ContentDigests {
+
+ private static final int BUFFER_BYTES = 16 * 1024;
+
+ private ContentDigests() {
+ throw new AssertionError("No instances");
+ }
+
+ /**
+ * Computes a SHA-256 fingerprint without materializing the aggregate content.
+ *
+ * @param content repeatable content
+ * @param cancellation runtime cancellation signal
+ * @return lowercase hexadecimal SHA-256 fingerprint
+ * @throws IOException if the content cannot be read or cancellation is
+ * requested
+ */
+ public static String sha256(RepeatableContent content, CancellationSignal cancellation) throws IOException {
+ Objects.requireNonNull(content, "content");
+ Objects.requireNonNull(cancellation, "cancellation");
+ MessageDigest digest;
+ try {
+ digest = MessageDigest.getInstance("SHA-256");
+ } catch (NoSuchAlgorithmException ex) {
+ throw new IllegalStateException("SHA-256 is unavailable", ex);
+ }
+ byte[] buffer = new byte[BUFFER_BYTES];
+ try (InputStream input = content.openStream()) {
+ int read;
+ while ((read = input.read(buffer)) >= 0) {
+ cancellation.throwIfCancelled();
+ if (read > 0) {
+ digest.update(buffer, 0, read);
+ }
+ }
+ } finally {
+ java.util.Arrays.fill(buffer, (byte) 0);
+ }
+ return HexFormat.of().formatHex(digest.digest());
+ }
+}
diff --git a/lib/src/main/java/zeroecho/core/io/ContentSlice.java b/lib/src/main/java/zeroecho/core/io/ContentSlice.java
new file mode 100644
index 0000000..61e844d
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/io/ContentSlice.java
@@ -0,0 +1,161 @@
+/*******************************************************************************
+ * 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.core.io;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Objects;
+import java.util.OptionalLong;
+
+/**
+ * Immutable repeatable bounded view over another repeatable content source.
+ *
+ *
+ * A slice opens a fresh source pass and skips incrementally; it never copies the
+ * represented bytes. Closing the slice does not close its source because source
+ * ownership remains with the creator.
+ *
+ */
+public final class ContentSlice implements RepeatableContent {
+
+ private static final long EMPTY_LENGTH = 0L;
+ private final RepeatableContent source;
+ private final long offset;
+ private final long length;
+ private final String contentId;
+
+ /**
+ * Creates a repeatable slice.
+ *
+ * @param source repeatable source
+ * @param offset non-negative source offset
+ * @param length non-negative slice length
+ * @throws IllegalArgumentException if a range is negative or exceeds a known
+ * source length
+ */
+ public ContentSlice(RepeatableContent source, long offset, long length) {
+ this.source = Objects.requireNonNull(source, "source");
+ if (offset < 0L || length < 0L) {
+ throw new IllegalArgumentException("Content slice range must not be negative");
+ }
+ long end = Math.addExact(offset, length);
+ OptionalLong sourceLength = source.length();
+ if (sourceLength.isPresent() && end > sourceLength.getAsLong()) {
+ throw new IllegalArgumentException("Content slice exceeds source");
+ }
+ this.offset = offset;
+ this.length = length;
+ this.contentId = source.contentId() + "#slice:" + offset + ':' + length;
+ }
+
+ @Override
+ public InputStream openStream() throws IOException {
+ InputStream input = source.openStream();
+ try {
+ skipExactly(input, offset);
+ return new LimitedInputStream(input, length);
+ } catch (IOException failure) {
+ input.close();
+ throw failure;
+ }
+ }
+
+ @Override
+ public OptionalLong length() {
+ return OptionalLong.of(length);
+ }
+
+ @Override
+ public String contentId() {
+ return contentId;
+ }
+
+ @Override
+ public void close() {
+ // Source ownership remains with the creator.
+ }
+
+ private static void skipExactly(InputStream input, long count) throws IOException {
+ long remaining = count;
+ while (remaining != EMPTY_LENGTH) {
+ long skipped = input.skip(remaining);
+ if (skipped > EMPTY_LENGTH) {
+ remaining -= skipped;
+ } else if (input.read() < 0) {
+ throw new IOException("Content slice source is truncated");
+ } else {
+ remaining--;
+ }
+ }
+ }
+
+ /** Exact-length stream view that fails when its underlying source truncates. */
+ private static final class LimitedInputStream extends FilterInputStream {
+ private long remaining;
+
+ private LimitedInputStream(InputStream input, long remaining) {
+ super(input);
+ this.remaining = remaining;
+ }
+
+ @Override
+ public int read() throws IOException {
+ if (remaining == EMPTY_LENGTH) {
+ return -1;
+ }
+ int value = super.read();
+ if (value < 0) {
+ throw new IOException("Content slice source is truncated");
+ }
+ remaining--;
+ return value;
+ }
+
+ @Override
+ public int read(byte[] bytes, int offset, int count) throws IOException {
+ Objects.checkFromIndexSize(offset, count, bytes.length);
+ if (remaining == EMPTY_LENGTH) {
+ return -1;
+ }
+ int requested = (int) Math.min(remaining, count);
+ int read = super.read(bytes, offset, requested);
+ if (read < 0) {
+ throw new IOException("Content slice source is truncated");
+ }
+ remaining -= read;
+ return read;
+ }
+ }
+}
diff --git a/lib/src/main/java/zeroecho/core/io/ImmutableByteContent.java b/lib/src/main/java/zeroecho/core/io/ImmutableByteContent.java
new file mode 100644
index 0000000..dc9165b
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/io/ImmutableByteContent.java
@@ -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.core.io;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HexFormat;
+import java.util.Objects;
+import java.util.OptionalLong;
+
+/**
+ * Explicit small-value adapter from immutable bytes to repeatable content.
+ *
+ *
+ * This adapter intentionally materializes its individual value. It is suitable
+ * for bounded signatures, public-key fields and external small-object inputs. It
+ * must not be used as the authoritative representation of aggregate CRLs, TBS
+ * objects or streamed entry sequences.
+ *
+ */
+public final class ImmutableByteContent implements RepeatableContent {
+
+ private final byte[] bytes;
+ private final String contentId;
+
+ /**
+ * Creates an owned immutable byte value.
+ *
+ * @param bytes individual value, possibly empty
+ * @throws NullPointerException if {@code bytes} is {@code null}
+ */
+ public ImmutableByteContent(byte[] bytes) {
+ byte[] source = Objects.requireNonNull(bytes, "bytes");
+ this.bytes = source.clone();
+ this.contentId = "sha256:" + digest(this.bytes);
+ }
+
+ @Override
+ public InputStream openStream() {
+ return new ByteArrayInputStream(bytes);
+ }
+
+ @Override
+ public OptionalLong length() {
+ return OptionalLong.of(bytes.length);
+ }
+
+ @Override
+ public String contentId() {
+ return contentId;
+ }
+
+ /**
+ * Returns a defensive copy for an explicitly bounded provider adapter.
+ *
+ * @return newly allocated bytes
+ */
+ public byte[] copyBytes() {
+ return bytes.clone();
+ }
+
+ @Override
+ public void close() {
+ // Immutable caller-visible values own no external resources.
+ }
+
+ private static String digest(byte[] value) {
+ try {
+ return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value));
+ } catch (NoSuchAlgorithmException ex) {
+ throw new IllegalStateException("SHA-256 is unavailable", ex);
+ }
+ }
+}
diff --git a/lib/src/main/java/zeroecho/core/io/OneShotContent.java b/lib/src/main/java/zeroecho/core/io/OneShotContent.java
new file mode 100644
index 0000000..e80d9c7
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/io/OneShotContent.java
@@ -0,0 +1,75 @@
+/*******************************************************************************
+ * 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.core.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.OptionalLong;
+
+/**
+ * Provider-independent one-shot streaming input.
+ *
+ *
+ * A one-shot input is not repeatable and therefore must be staged before signing
+ * recovery, canonical comparison, postcondition validation or publication that
+ * needs another pass. The returned stream is owned by the caller. Implementations
+ * must reject a second call to {@link #openStream()}.
+ *
+ */
+public interface OneShotContent extends AutoCloseable {
+
+ /**
+ * Opens the only sequential reader.
+ *
+ * @return content stream
+ * @throws IOException if the source cannot be opened
+ * @throws IllegalStateException if the source was already opened
+ */
+ InputStream openStream() throws IOException;
+
+ /**
+ * Returns the known source length when available.
+ *
+ * @return non-negative length or empty
+ */
+ OptionalLong length();
+
+ /**
+ * Releases the source.
+ *
+ * @throws IOException if cleanup fails
+ */
+ @Override
+ void close() throws IOException;
+}
diff --git a/lib/src/main/java/zeroecho/core/io/RepeatableContent.java b/lib/src/main/java/zeroecho/core/io/RepeatableContent.java
new file mode 100644
index 0000000..e392d44
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/io/RepeatableContent.java
@@ -0,0 +1,97 @@
+/*******************************************************************************
+ * 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.core.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.OptionalLong;
+
+/**
+ * Immutable, provider-independent source of repeatable operation content.
+ *
+ *
+ * Every call to {@link #openStream()} returns a new sequential reader positioned
+ * at the first byte. Implementations may be backed by files, object storage,
+ * databases or explicitly small immutable byte values. Callers own and must close
+ * each returned stream. Closing the content releases its implementation-owned
+ * resources but does not close streams already returned unless the implementation
+ * documents a stronger local rule.
+ *
+ *
+ *
+ * The contract does not impose an aggregate content-size limit. Completion remains
+ * subject to available storage, I/O, technical representability and explicitly
+ * injected deployment policy. Content never carries key material or cryptographic
+ * provider authority.
+ *
+ */
+public interface RepeatableContent extends AutoCloseable {
+
+ /**
+ * Opens a new sequential reader.
+ *
+ * @return newly opened content stream
+ * @throws IOException if the immutable content cannot be opened or its
+ * integrity cannot be established
+ */
+ InputStream openStream() throws IOException;
+
+ /**
+ * Returns the known aggregate length when cheaply and authoritatively
+ * available.
+ *
+ * @return non-negative length, or empty when the length is unknown
+ */
+ OptionalLong length();
+
+ /**
+ * Returns a stable, non-secret identifier for integrity and durable provenance.
+ *
+ *
+ * The identifier is metadata, not authorization, and must not expose a
+ * temporary physical path.
+ *
+ *
+ * @return stable non-blank identifier
+ */
+ String contentId();
+
+ /**
+ * Releases implementation-owned resources.
+ *
+ * @throws IOException if cleanup fails
+ */
+ @Override
+ void close() throws IOException;
+}
diff --git a/lib/src/main/java/zeroecho/core/spec/AlgorithmIdentity.java b/lib/src/main/java/zeroecho/core/spec/AlgorithmIdentity.java
new file mode 100644
index 0000000..c9a26ca
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/spec/AlgorithmIdentity.java
@@ -0,0 +1,589 @@
+/*******************************************************************************
+ * 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.core.spec;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/**
+ * Provider-independent identity of one exact cryptographic operation or key
+ * type.
+ *
+ *
+ * An identity contains no provider, implementation class, key material, or
+ * X.509 representation. Families are namespaced so trusted installed
+ * extensions can add typed parameter models without modifying a central enum.
+ * The parameter object is responsible for family-specific validation and a
+ * deterministic canonical component.
+ *
+ *
+ *
+ * Instances are immutable. Equality and hashing use the complete canonical
+ * semantics, including the role, family, and parameters.
+ *
+ */
+public final class AlgorithmIdentity {
+
+ private static final Pattern COMPONENT = Pattern.compile("[a-z][a-z0-9._-]{0,63}");
+ private static final AlgorithmIdentityCodec BUILTIN_CODEC = new BuiltInCodec();
+ private static final String NO_PARAMETERS = "none";
+ private static final int RSA_PSS_COMPONENT_COUNT = 5;
+ private static final int REQUIRED_TRAILER_FIELD = 1;
+ private static final int UTF8_ONE_BYTE_LIMIT = 0x7f;
+ private static final int UTF8_TWO_BYTE_LIMIT = 0x7ff;
+
+ private final Kind kind;
+ private final Family family;
+ private final AlgorithmIdentityCodec codec;
+ private final byte[] parameterSnapshot;
+ private final String canonicalForm;
+
+ /**
+ * Semantic role represented by an identity.
+ */
+ public enum Kind {
+ /** Message digest. */
+ DIGEST,
+ /** Mask-generation function. */
+ MASK_GENERATION,
+ /** Signature scheme, independent of a particular key parameter set. */
+ SIGNATURE,
+ /** Public-key algorithm and its exact key parameter set. */
+ PUBLIC_KEY,
+ /** Key-encapsulation mechanism. */
+ KEM,
+ /** Key-agreement mechanism. */
+ AGREEMENT
+ }
+
+ /**
+ * Stable namespaced algorithm family name.
+ *
+ * @param namespace namespace owned by the built-in catalog or trusted
+ * extension
+ * @param name family name within that namespace
+ */
+ public record Family(String namespace, String name) {
+
+ /**
+ * Creates a validated family name.
+ *
+ * @throws IllegalArgumentException if either component is not a lowercase
+ * canonical identifier
+ */
+ public Family {
+ namespace = requireComponent(namespace, "namespace");
+ name = requireComponent(name, "name");
+ }
+
+ /**
+ * Returns the deterministic family representation.
+ *
+ * @return namespace and family separated by {@code /}
+ */
+ public String canonicalForm() {
+ return namespace + "/" + name;
+ }
+ }
+
+ /**
+ * Typed, immutable family parameters.
+ *
+ *
+ * Implementations supplied by trusted code must validate their complete
+ * family-specific semantics during construction. The canonical component is
+ * persistent identity data and therefore must never depend on a provider,
+ * locale, insertion order, or display alias.
+ *
+ * Trusted extension implementations must not return mutable caller-owned
+ * state. Immutable records may return {@code this}.
+ *
+ *
+ * @return immutable owned parameters
+ */
+ Parameters immutableCopy();
+ }
+
+ /**
+ * Parameters for an unparameterized family.
+ */
+ public enum NoParameters implements Parameters {
+ /** Singleton empty-parameter value. */
+ INSTANCE;
+
+ @Override
+ public String canonicalForm() {
+ return "none";
+ }
+
+ @Override
+ public Parameters immutableCopy() {
+ return this;
+ }
+ }
+
+ /**
+ * Digest-qualified signature parameters.
+ *
+ * @param digest exact digest identity
+ */
+ public record DigestParameters(AlgorithmIdentity digest) implements Parameters {
+
+ /**
+ * Creates digest-qualified parameters.
+ *
+ * @throws IllegalArgumentException if {@code digest} is not a digest
+ * identity
+ */
+ public DigestParameters {
+ Objects.requireNonNull(digest, "digest");
+ if (digest.kind() != Kind.DIGEST) {
+ throw new IllegalArgumentException("digest must have DIGEST kind");
+ }
+ }
+
+ @Override
+ public String canonicalForm() {
+ return "digest=" + digest.family().namespace() + "." + digest.family().name() + "."
+ + digest.parameters().canonicalForm();
+ }
+
+ @Override
+ public Parameters immutableCopy() {
+ return this;
+ }
+ }
+
+ /**
+ * Exact RSA-PSS parameters.
+ *
+ * @param hash message digest identity
+ * @param mask mask-generation identity
+ * @param maskHash mask-generation digest identity
+ * @param saltLength non-negative salt length in bytes
+ * @param trailerField trailer field; PKCS#1 currently defines value {@code 1}
+ */
+ public record RsaPssParameters(AlgorithmIdentity hash, AlgorithmIdentity mask, AlgorithmIdentity maskHash,
+ int saltLength, int trailerField) implements Parameters {
+
+ /**
+ * Creates a validated exact RSA-PSS parameter tuple.
+ *
+ * @throws IllegalArgumentException if roles or numeric parameters are
+ * contradictory
+ */
+ public RsaPssParameters {
+ Objects.requireNonNull(hash, "hash");
+ Objects.requireNonNull(mask, "mask");
+ Objects.requireNonNull(maskHash, "maskHash");
+ if (hash.kind() != Kind.DIGEST || maskHash.kind() != Kind.DIGEST) {
+ throw new IllegalArgumentException("RSA-PSS hashes must have DIGEST kind");
+ }
+ if (mask.kind() != Kind.MASK_GENERATION) {
+ throw new IllegalArgumentException("RSA-PSS mask must have MASK_GENERATION kind");
+ }
+ if (saltLength < 0) {
+ throw new IllegalArgumentException("RSA-PSS salt length must not be negative");
+ }
+ if (trailerField != REQUIRED_TRAILER_FIELD) {
+ throw new IllegalArgumentException("RSA-PSS trailer field must be 1");
+ }
+ }
+
+ @Override
+ public String canonicalForm() {
+ return "hash=" + shortName(hash) + ",mask=" + shortName(mask) + ",maskhash=" + shortName(maskHash)
+ + ",salt=" + saltLength + ",trailer=" + trailerField;
+ }
+
+ @Override
+ public Parameters immutableCopy() {
+ return this;
+ }
+ }
+
+ /**
+ * Exact named parameter set, such as an elliptic-curve name.
+ *
+ * @param parameterSet stable namespaced parameter-set identifier
+ */
+ public record NamedParameters(Family parameterSet) implements Parameters {
+
+ /**
+ * Creates named parameters.
+ *
+ * @throws NullPointerException if {@code parameterSet} is {@code null}
+ */
+ public NamedParameters {
+ Objects.requireNonNull(parameterSet, "parameterSet");
+ }
+
+ @Override
+ public String canonicalForm() {
+ return "set=" + parameterSet.namespace() + "." + parameterSet.name();
+ }
+
+ @Override
+ public Parameters immutableCopy() {
+ return this;
+ }
+ }
+
+ /**
+ * Creates one exact algorithm identity.
+ *
+ * @param kind semantic role
+ * @param family stable namespaced family
+ * @param parameters validated typed parameters
+ * @throws NullPointerException if an argument is {@code null}
+ * @throws IllegalArgumentException if the parameter canonical form is not
+ * deterministic syntax
+ */
+ public AlgorithmIdentity(Kind kind, Family family, Parameters parameters) {
+ this(kind, family, parameters, BUILTIN_CODEC);
+ }
+
+ /**
+ * Creates one exact algorithm identity using a trusted typed codec.
+ *
+ * @param kind semantic role
+ * @param family stable namespaced family
+ * @param parameters validated typed parameters
+ * @param codec immutable canonical parameter codec
+ */
+ public AlgorithmIdentity(Kind kind, Family family, Parameters parameters, AlgorithmIdentityCodec codec) {
+ this.kind = Objects.requireNonNull(kind, "kind");
+ this.family = Objects.requireNonNull(family, "family");
+ this.codec = Objects.requireNonNull(codec, "codec");
+ requireComponent(codec.id(), "codec.id");
+ byte[] encoded = codec.encode(Objects.requireNonNull(parameters, "parameters"));
+ this.parameterSnapshot = Objects.requireNonNull(encoded, "encoded parameters").clone();
+ Parameters decoded = Objects.requireNonNull(codec.decode(parameterSnapshot.clone()), "decoded parameters");
+ byte[] roundTrip = codec.encode(decoded);
+ if (!java.util.Arrays.equals(parameterSnapshot, roundTrip)) {
+ throw new IllegalArgumentException("Algorithm parameter codec is not canonical");
+ }
+ this.canonicalForm = "zealg:2:" + field(kind.name().toLowerCase(Locale.ROOT)) + field(family.namespace())
+ + field(family.name()) + field(codec.id())
+ + field(Base64.getUrlEncoder().withoutPadding().encodeToString(parameterSnapshot));
+ }
+
+ /**
+ * Returns the semantic identity kind.
+ *
+ * @return identity kind
+ */
+ public Kind kind() {
+ return kind;
+ }
+
+ /**
+ * Returns the stable family.
+ *
+ * @return namespaced family
+ */
+ public Family family() {
+ return family;
+ }
+
+ /**
+ * Returns the immutable typed parameters.
+ *
+ * @return family parameters
+ */
+ public Parameters parameters() {
+ return codec.decode(parameterSnapshot.clone());
+ }
+
+ /**
+ * Parses a complete version-two canonical identity with installed codecs.
+ *
+ * @param canonicalForm canonical identity
+ * @param codecs trusted installed codecs
+ * @return exact decoded identity
+ * @throws IllegalArgumentException if syntax, version, codec, or canonical
+ * round-trip validation fails
+ */
+ public static AlgorithmIdentity parse(String canonicalForm, Collection codecs) {
+ Objects.requireNonNull(canonicalForm, "canonicalForm");
+ Objects.requireNonNull(codecs, "codecs");
+ if (!canonicalForm.startsWith("zealg:2:")) {
+ throw new IllegalArgumentException("Unsupported canonical algorithm identity version");
+ }
+ Map byId = new HashMap<>();
+ byId.put(BUILTIN_CODEC.id(), BUILTIN_CODEC);
+ for (AlgorithmIdentityCodec candidate : codecs) {
+ AlgorithmIdentityCodec previous = byId.putIfAbsent(candidate.id(), candidate);
+ if (previous != null && !previous.id().equals(candidate.id())) {
+ throw new IllegalArgumentException("Algorithm identity codec collision");
+ }
+ }
+ Cursor cursor = new Cursor(canonicalForm, "zealg:2:".length());
+ Kind parsedKind;
+ try {
+ parsedKind = Kind.valueOf(cursor.field().toUpperCase(Locale.ROOT));
+ } catch (IllegalArgumentException exception) {
+ throw new IllegalArgumentException("Unknown algorithm identity kind", exception);
+ }
+ Family parsedFamily = new Family(cursor.field(), cursor.field());
+ String codecId = cursor.field();
+ String encodedParameters = cursor.field();
+ cursor.requireEnd();
+ AlgorithmIdentityCodec selected = byId.get(codecId);
+ if (selected == null) {
+ throw new IllegalArgumentException("Unknown algorithm identity codec");
+ }
+ byte[] bytes;
+ try {
+ bytes = Base64.getUrlDecoder().decode(encodedParameters);
+ } catch (IllegalArgumentException malformed) {
+ throw new IllegalArgumentException("Malformed canonical algorithm parameters", malformed);
+ }
+ AlgorithmIdentity identity = new AlgorithmIdentity(parsedKind, parsedFamily, selected.decode(bytes), selected);
+ if (!canonicalForm.equals(identity.canonicalForm())) {
+ throw new IllegalArgumentException("Non-canonical algorithm identity");
+ }
+ return identity;
+ }
+
+ /**
+ * Returns the deterministic provider-independent representation.
+ *
+ * @return complete canonical identity
+ */
+ public String canonicalForm() {
+ return canonicalForm;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof AlgorithmIdentity identity && canonicalForm.equals(identity.canonicalForm);
+ }
+
+ @Override
+ public int hashCode() {
+ return canonicalForm.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return canonicalForm;
+ }
+
+ private static String requireComponent(String value, String field) {
+ Objects.requireNonNull(value, field);
+ if (!COMPONENT.matcher(value).matches()) {
+ throw new IllegalArgumentException(field + " must be a lowercase canonical identifier");
+ }
+ return value;
+ }
+
+ private static String shortName(AlgorithmIdentity identity) {
+ return identity.family.namespace() + "." + identity.family.name() + "."
+ + identity.parameters().canonicalForm();
+ }
+
+ private static String field(String value) {
+ byte[] utf8 = value.getBytes(StandardCharsets.UTF_8);
+ return utf8.length + ":" + value;
+ }
+
+ /** Strict cursor for the length-prefixed canonical representation. */
+ private static final class Cursor {
+
+ private final String value;
+ private int offset;
+
+ private Cursor(String value, int offset) {
+ this.value = value;
+ this.offset = offset;
+ }
+
+ private String field() {
+ int separator = value.indexOf(':', offset);
+ if (separator < 0 || separator == offset) {
+ throw new IllegalArgumentException("Malformed canonical algorithm identity");
+ }
+ int length;
+ try {
+ length = Integer.parseInt(value.substring(offset, separator));
+ } catch (NumberFormatException exception) {
+ throw new IllegalArgumentException("Malformed canonical algorithm identity length", exception);
+ }
+ if (length < 0) {
+ throw new IllegalArgumentException("Negative canonical algorithm identity length");
+ }
+ int start = separator + 1;
+ int index = start;
+ int bytes = 0;
+ while (index < value.length() && bytes < length) {
+ int codePoint = value.codePointAt(index);
+ bytes += utf8Length(codePoint);
+ index += Character.charCount(codePoint);
+ }
+ if (bytes != length) {
+ throw new IllegalArgumentException("Truncated canonical algorithm identity field");
+ }
+ offset = index;
+ return value.substring(start, index);
+ }
+
+ private void requireEnd() {
+ if (offset != value.length()) {
+ throw new IllegalArgumentException("Trailing canonical algorithm identity data");
+ }
+ }
+
+ private static int utf8Length(int codePoint) {
+ if (codePoint <= UTF8_ONE_BYTE_LIMIT) {
+ return 1;
+ }
+ if (codePoint <= UTF8_TWO_BYTE_LIMIT) {
+ return 2;
+ }
+ return codePoint <= 0xffff ? 3 : 4;
+ }
+ }
+
+ /** Canonical codec for the built-in closed parameter records. */
+ private static final class BuiltInCodec implements AlgorithmIdentityCodec {
+
+ @Override
+ public String id() {
+ return "zeroecho.builtin";
+ }
+
+ @Override
+ public byte[] encode(Parameters parameters) {
+ if (!isKnown(parameters)) {
+ throw new IllegalArgumentException("Built-in codec cannot encode extension parameters");
+ }
+ return parameters.canonicalForm().getBytes(StandardCharsets.US_ASCII);
+ }
+
+ @Override
+ public Parameters decode(byte[] encoded) {
+ String value = new String(encoded.clone(), StandardCharsets.US_ASCII);
+ if (!java.util.Arrays.equals(encoded, value.getBytes(StandardCharsets.US_ASCII))) {
+ throw new IllegalArgumentException("Built-in parameters are not ASCII");
+ }
+ if (NO_PARAMETERS.equals(value)) {
+ return NoParameters.INSTANCE;
+ }
+ if (value.startsWith("digest=")) {
+ return new DigestParameters(parseShort(value.substring(7), Kind.DIGEST));
+ }
+ if (value.startsWith("set=")) {
+ return new NamedParameters(parseFamily(value.substring(4)));
+ }
+ if (value.startsWith("hash=")) {
+ String[] components = value.split(",");
+ if (components.length != RSA_PSS_COMPONENT_COUNT) {
+ throw new IllegalArgumentException("Malformed RSA-PSS parameters");
+ }
+ AlgorithmIdentity hash = parseShort(requirePair(components[0], "hash"), Kind.DIGEST);
+ AlgorithmIdentity mask = parseShort(requirePair(components[1], "mask"), Kind.MASK_GENERATION);
+ AlgorithmIdentity maskHash = parseShort(requirePair(components[2], "maskhash"), Kind.DIGEST);
+ int salt = parseInteger(requirePair(components[3], "salt"));
+ int trailer = parseInteger(requirePair(components[4], "trailer"));
+ return new RsaPssParameters(hash, mask, maskHash, salt, trailer);
+ }
+ throw new IllegalArgumentException("Unknown built-in algorithm parameters");
+ }
+
+ private static boolean isKnown(Parameters parameters) {
+ return parameters instanceof NoParameters || parameters instanceof DigestParameters
+ || parameters instanceof RsaPssParameters || parameters instanceof NamedParameters;
+ }
+
+ private static AlgorithmIdentity parseShort(String value, Kind kind) {
+ int first = value.indexOf('.');
+ int second = value.indexOf('.', first + 1);
+ if (first <= 0 || second <= first + 1) {
+ throw new IllegalArgumentException("Malformed nested algorithm identity");
+ }
+ Family family = new Family(value.substring(0, first), value.substring(first + 1, second));
+ String parameters = value.substring(second + 1);
+ return new AlgorithmIdentity(kind, family, decodeStatic(parameters));
+ }
+
+ private static Parameters decodeStatic(String value) {
+ return new BuiltInCodec().decode(value.getBytes(StandardCharsets.US_ASCII));
+ }
+
+ private static Family parseFamily(String value) {
+ int separator = value.indexOf('.');
+ if (separator <= 0 || separator == value.length() - 1) {
+ throw new IllegalArgumentException("Malformed named parameter set");
+ }
+ return new Family(value.substring(0, separator), value.substring(separator + 1));
+ }
+
+ private static String requirePair(String value, String name) {
+ String prefix = name + "=";
+ if (!value.startsWith(prefix)) {
+ throw new IllegalArgumentException("Malformed RSA-PSS parameters");
+ }
+ return value.substring(prefix.length());
+ }
+
+ private static int parseInteger(String value) {
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException exception) {
+ throw new IllegalArgumentException("Malformed integer algorithm parameter", exception);
+ }
+ }
+ }
+}
diff --git a/lib/src/main/java/zeroecho/core/spec/AlgorithmIdentityCatalog.java b/lib/src/main/java/zeroecho/core/spec/AlgorithmIdentityCatalog.java
new file mode 100644
index 0000000..b231496
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/spec/AlgorithmIdentityCatalog.java
@@ -0,0 +1,184 @@
+/*******************************************************************************
+ * 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.core.spec;
+
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Deeply immutable snapshot of installed algorithm identities.
+ *
+ *
+ * Built-in identities reserve the {@code zeroecho} namespace. Trusted installed
+ * code may contribute identities under another namespace. Duplicate canonical
+ * representations fail closed; registration order never supplies precedence.
+ * Administrative configuration is not a registration mechanism.
+ *
+ */
+public final class AlgorithmIdentityCatalog {
+
+ /** Namespace reserved for immutable built-in identities. */
+ public static final String BUILTIN_NAMESPACE = "zeroecho";
+
+ private final Map identities;
+
+ private AlgorithmIdentityCatalog(Map identities) {
+ this.identities = Map.copyOf(identities);
+ }
+
+ /**
+ * Creates the immutable built-in catalog.
+ *
+ * @param identities built-in identities
+ * @return immutable catalog
+ * @throws IllegalArgumentException if an identity is outside the reserved
+ * namespace or collides
+ */
+ public static AlgorithmIdentityCatalog builtIn(Collection identities) {
+ return create(identities, true);
+ }
+
+ /**
+ * Creates a trusted extension catalog.
+ *
+ * @param identities extension identities
+ * @return immutable catalog
+ * @throws IllegalArgumentException if an extension uses the built-in namespace
+ * or contains a collision
+ */
+ public static AlgorithmIdentityCatalog extension(Collection identities) {
+ return create(identities, false);
+ }
+
+ /**
+ * Returns a new additive snapshot containing this catalog and all extensions.
+ *
+ * @param extensions trusted installed extension catalogs
+ * @return immutable merged snapshot
+ * @throws IllegalArgumentException if any canonical identity collides
+ */
+ public AlgorithmIdentityCatalog merge(List extensions) {
+ Objects.requireNonNull(extensions, "extensions");
+ Map merged = new LinkedHashMap<>(identities);
+ for (AlgorithmIdentityCatalog extension : extensions) {
+ Objects.requireNonNull(extension, "extension");
+ for (AlgorithmIdentity identity : extension.identities.values()) {
+ if (BUILTIN_NAMESPACE.equals(identity.family().namespace())
+ && identities.values().stream().noneMatch(builtIn -> builtIn.kind() == identity.kind()
+ && builtIn.family().equals(identity.family()))) {
+ throw new IllegalArgumentException("Extension identity uses unknown reserved family");
+ }
+ AlgorithmIdentity previous = merged.putIfAbsent(identity.canonicalForm(), identity);
+ if (previous != null && !previous.equals(identity)) {
+ throw new IllegalArgumentException("Algorithm identity collision");
+ }
+ if (previous != null) {
+ throw new IllegalArgumentException("Duplicate algorithm identity");
+ }
+ }
+ }
+ return new AlgorithmIdentityCatalog(merged);
+ }
+
+ /**
+ * Adds exact parameter combinations to this authority snapshot.
+ *
+ *
+ * A trusted extension may add a tuple within an existing reserved family, but
+ * cannot introduce a new family under the built-in namespace.
+ *
+ *
+ * @param additions exact additive identities
+ * @return new immutable catalog
+ */
+ public AlgorithmIdentityCatalog add(Collection additions) {
+ Objects.requireNonNull(additions, "additions");
+ Map merged = new LinkedHashMap<>(identities);
+ for (AlgorithmIdentity identity : additions) {
+ Objects.requireNonNull(identity, "identity");
+ if (BUILTIN_NAMESPACE.equals(identity.family().namespace())
+ && identities.values().stream().noneMatch(builtIn -> builtIn.kind() == identity.kind()
+ && builtIn.family().equals(identity.family()))) {
+ throw new IllegalArgumentException("Extension identity uses unknown reserved family");
+ }
+ if (merged.putIfAbsent(identity.canonicalForm(), identity) != null) {
+ throw new IllegalArgumentException("Duplicate algorithm identity");
+ }
+ }
+ return new AlgorithmIdentityCatalog(merged);
+ }
+
+ /**
+ * Resolves a canonical identity without provider alias fallback.
+ *
+ * @param canonicalForm complete canonical representation
+ * @return registered exact identity, or empty when unknown
+ */
+ public Optional resolve(String canonicalForm) {
+ Objects.requireNonNull(canonicalForm, "canonicalForm");
+ return Optional.ofNullable(identities.get(canonicalForm));
+ }
+
+ /**
+ * Returns identities in deterministic canonical order.
+ *
+ * @return immutable identity list
+ */
+ public List identities() {
+ return identities.values().stream().sorted((left, right) -> left.canonicalForm()
+ .compareTo(right.canonicalForm())).toList();
+ }
+
+ private static AlgorithmIdentityCatalog create(Collection source, boolean builtIn) {
+ Objects.requireNonNull(source, "identities");
+ Map result = new LinkedHashMap<>();
+ for (AlgorithmIdentity identity : source) {
+ Objects.requireNonNull(identity, "identity");
+ boolean reserved = BUILTIN_NAMESPACE.equals(identity.family().namespace());
+ if (builtIn != reserved) {
+ throw new IllegalArgumentException(
+ builtIn ? "Built-in identity must use reserved namespace"
+ : "Extension identity must not use reserved namespace");
+ }
+ if (result.putIfAbsent(identity.canonicalForm(), identity) != null) {
+ throw new IllegalArgumentException("Duplicate algorithm identity");
+ }
+ }
+ return new AlgorithmIdentityCatalog(result);
+ }
+}
diff --git a/lib/src/main/java/zeroecho/core/spec/AlgorithmIdentityCodec.java b/lib/src/main/java/zeroecho/core/spec/AlgorithmIdentityCodec.java
new file mode 100644
index 0000000..e178c03
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/spec/AlgorithmIdentityCodec.java
@@ -0,0 +1,70 @@
+/*******************************************************************************
+ * 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.core.spec;
+
+/**
+ * Trusted-code canonical codec for one typed algorithm-parameter schema.
+ *
+ *
+ * Encoding takes an immediate immutable snapshot. Decoding must return fresh
+ * values or intrinsically immutable values and must reject malformed,
+ * incomplete, or contradictory input. Codec identifiers are stable semantic
+ * identity and cannot be redefined by catalog ordering or configuration.
+ *
+ */
+public interface AlgorithmIdentityCodec {
+
+ /**
+ * Returns the stable namespaced codec identifier.
+ *
+ * @return canonical codec identifier
+ */
+ String id();
+
+ /**
+ * Encodes complete typed parameters.
+ *
+ * @param parameters typed parameters
+ * @return independently owned canonical bytes
+ */
+ byte[] encode(AlgorithmIdentity.Parameters parameters);
+
+ /**
+ * Decodes complete canonical bytes.
+ *
+ * @param encoded canonical bytes
+ * @return fresh validated typed parameters
+ */
+ AlgorithmIdentity.Parameters decode(byte[] encoded);
+}
diff --git a/lib/src/main/java/zeroecho/core/spec/AlgorithmSuite.java b/lib/src/main/java/zeroecho/core/spec/AlgorithmSuite.java
new file mode 100644
index 0000000..fa4d8dd
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/spec/AlgorithmSuite.java
@@ -0,0 +1,79 @@
+/*******************************************************************************
+ * 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.core.spec;
+
+import java.util.Objects;
+
+/**
+ * Immutable composition of a signature scheme and a compatible public-key
+ * identity.
+ *
+ *
+ * The suite keeps signature and key semantics separate. In particular, an
+ * ECDSA signature identity contains its digest while the public-key identity
+ * contains the named curve. Compatibility is evaluated by capability and
+ * policy, not inferred from an X.509 signature OID.
+ *
+ *
+ * @param signature exact signature-scheme identity
+ * @param publicKey exact public-key identity
+ */
+public record AlgorithmSuite(AlgorithmIdentity signature, AlgorithmIdentity publicKey) {
+
+ /**
+ * Creates a validated signature suite.
+ *
+ * @throws NullPointerException if an identity is {@code null}
+ * @throws IllegalArgumentException if an identity has the wrong semantic kind
+ */
+ public AlgorithmSuite {
+ Objects.requireNonNull(signature, "signature");
+ Objects.requireNonNull(publicKey, "publicKey");
+ if (signature.kind() != AlgorithmIdentity.Kind.SIGNATURE) {
+ throw new IllegalArgumentException("signature must have SIGNATURE kind");
+ }
+ if (publicKey.kind() != AlgorithmIdentity.Kind.PUBLIC_KEY) {
+ throw new IllegalArgumentException("publicKey must have PUBLIC_KEY kind");
+ }
+ }
+
+ /**
+ * Returns the deterministic suite representation.
+ *
+ * @return signature and key canonical identities in a length-independent form
+ */
+ public String canonicalForm() {
+ return "zesuite:1:" + signature.canonicalForm() + "|" + publicKey.canonicalForm();
+ }
+}
diff --git a/lib/src/main/java/zeroecho/core/spi/AlgorithmExecutionCapabilities.java b/lib/src/main/java/zeroecho/core/spi/AlgorithmExecutionCapabilities.java
new file mode 100644
index 0000000..3ba003b
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/spi/AlgorithmExecutionCapabilities.java
@@ -0,0 +1,141 @@
+/*******************************************************************************
+ * 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.core.spi;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.ServiceLoader;
+import java.util.Set;
+
+import zeroecho.core.spec.AlgorithmIdentity;
+import zeroecho.core.spec.AlgorithmSuite;
+
+/**
+ * Immutable deterministic snapshot of installed execution capabilities.
+ *
+ *
+ * Multiple implementations may support one semantic identity. Callers must
+ * select an implementation explicitly when more than one remains after policy;
+ * classpath or provider order never supplies precedence.
+ *
+ */
+public final class AlgorithmExecutionCapabilities {
+
+ private final List capabilities;
+
+ /**
+ * Creates a validated immutable snapshot.
+ *
+ * @param capabilities installed trusted-code capabilities
+ * @throws IllegalArgumentException if implementation identifiers collide or a
+ * fingerprint is blank
+ */
+ public AlgorithmExecutionCapabilities(List capabilities) {
+ Objects.requireNonNull(capabilities, "capabilities");
+ List copy = new ArrayList<>(capabilities);
+ copy.sort(Comparator.comparing(AlgorithmExecutionCapability::implementationId));
+ Set identifiers = new HashSet<>();
+ for (AlgorithmExecutionCapability capability : copy) {
+ Objects.requireNonNull(capability, "capability");
+ if (capability.implementationId() == null || capability.implementationId().isBlank()
+ || !identifiers.add(capability.implementationId())) {
+ throw new IllegalArgumentException("Execution capability identifier collision");
+ }
+ if (capability.domainFingerprint() == null || capability.domainFingerprint().isBlank()) {
+ throw new IllegalArgumentException("Execution capability fingerprint must not be blank");
+ }
+ }
+ this.capabilities = List.copyOf(copy);
+ }
+
+ /**
+ * Discovers installed providers using the existing ServiceLoader convention.
+ *
+ * @return deterministic immutable capability snapshot
+ */
+ public static AlgorithmExecutionCapabilities installed() {
+ List providers = new ArrayList<>();
+ ServiceLoader.load(AlgorithmExecutionCapabilityProvider.class).forEach(providers::add);
+ return fromProviders(providers);
+ }
+
+ /**
+ * Builds one deterministic snapshot from already selected trusted providers.
+ *
+ * @param providers provider instances belonging to the runtime graph
+ * @return immutable capability snapshot
+ */
+ public static AlgorithmExecutionCapabilities fromProviders(
+ Collection extends AlgorithmExecutionCapabilityProvider> providers) {
+ Objects.requireNonNull(providers, "providers");
+ List discovered = new ArrayList<>();
+ List ordered = new ArrayList<>(providers);
+ ordered.sort(Comparator.comparing(provider -> provider.getClass().getName()));
+ for (AlgorithmExecutionCapabilityProvider provider : ordered) {
+ List contribution = Objects.requireNonNull(provider.capabilities(),
+ "provider capabilities");
+ discovered.addAll(contribution);
+ }
+ return new AlgorithmExecutionCapabilities(discovered);
+ }
+
+ /**
+ * Finds all installed implementations supporting an exact tuple.
+ *
+ * @param identity exact operation identity
+ * @param suite complete suite
+ * @param direction operation direction
+ * @return deterministic immutable matching list
+ */
+ public List supporting(AlgorithmIdentity identity, AlgorithmSuite suite,
+ AlgorithmExecutionCapability.Direction direction) {
+ Objects.requireNonNull(identity, "identity");
+ Objects.requireNonNull(suite, "suite");
+ Objects.requireNonNull(direction, "direction");
+ return capabilities.stream().filter(capability -> capability.supports(identity, suite, direction)).toList();
+ }
+
+ /**
+ * Returns the immutable installed snapshot.
+ *
+ * @return capabilities sorted by implementation identifier
+ */
+ public List all() {
+ return capabilities;
+ }
+}
diff --git a/lib/src/main/java/zeroecho/core/spi/AlgorithmExecutionCapability.java b/lib/src/main/java/zeroecho/core/spi/AlgorithmExecutionCapability.java
new file mode 100644
index 0000000..3a031ff
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/spi/AlgorithmExecutionCapability.java
@@ -0,0 +1,84 @@
+/*******************************************************************************
+ * 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.core.spi;
+
+import zeroecho.core.spec.AlgorithmIdentity;
+import zeroecho.core.spec.AlgorithmSuite;
+
+/**
+ * Trusted-code declaration of an installed cryptographic execution domain.
+ *
+ *
+ * A capability describes implementation availability; it never defines
+ * algorithm identity, X.509 semantics, defaults, or policy. Implementations must
+ * provide a stable semantic fingerprint for deterministic conflict diagnostics.
+ * Administrative configuration cannot provide implementation classes.
+ *
+ */
+public interface AlgorithmExecutionCapability {
+
+ /**
+ * Supported execution direction.
+ */
+ enum Direction {
+ /** Signature generation. */
+ SIGN,
+ /** Signature verification. */
+ VERIFY
+ }
+
+ /**
+ * Returns a stable installed implementation identifier.
+ *
+ * @return namespaced provider implementation identifier
+ */
+ String implementationId();
+
+ /**
+ * Returns a deterministic description of the supported typed domain.
+ *
+ * @return stable non-secret domain fingerprint
+ */
+ String domainFingerprint();
+
+ /**
+ * Tests whether this implementation supports an exact identity and suite.
+ *
+ * @param identity requested exact operation identity
+ * @param suite complete key and signature suite
+ * @param direction requested direction
+ * @return {@code true} only for tuples implemented exactly
+ */
+ boolean supports(AlgorithmIdentity identity, AlgorithmSuite suite, Direction direction);
+}
diff --git a/lib/src/main/java/zeroecho/core/spi/AlgorithmExecutionCapabilityProvider.java b/lib/src/main/java/zeroecho/core/spi/AlgorithmExecutionCapabilityProvider.java
new file mode 100644
index 0000000..bc4e894
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/spi/AlgorithmExecutionCapabilityProvider.java
@@ -0,0 +1,56 @@
+/*******************************************************************************
+ * 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.core.spi;
+
+import java.util.List;
+
+/**
+ * Service-provider contract for trusted installed execution capabilities.
+ *
+ *
+ * Providers are deployment code discovered using the existing ServiceLoader
+ * convention. Configuration may select an installed capability but cannot name
+ * or load an implementation class.
+ *
+ */
+@FunctionalInterface
+public interface AlgorithmExecutionCapabilityProvider {
+
+ /**
+ * Returns an immutable capability contribution.
+ *
+ * @return installed capabilities; never {@code null}
+ */
+ List capabilities();
+}
diff --git a/lib/src/test/java/zeroecho/core/spec/AlgorithmIdentityPhaseATest.java b/lib/src/test/java/zeroecho/core/spec/AlgorithmIdentityPhaseATest.java
new file mode 100644
index 0000000..2696362
--- /dev/null
+++ b/lib/src/test/java/zeroecho/core/spec/AlgorithmIdentityPhaseATest.java
@@ -0,0 +1,150 @@
+/*******************************************************************************
+ * 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.core.spec;
+
+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.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import zeroecho.core.alg.BootstrapAlgorithmIdentities;
+import zeroecho.core.spi.AlgorithmExecutionCapabilities;
+import zeroecho.core.spi.AlgorithmExecutionCapability;
+
+/**
+ * Phase A regression tests for provider-independent identity and capability
+ * contracts.
+ */
+public final class AlgorithmIdentityPhaseATest {
+
+ @Test
+ void exactIdentityAlgebraAndCanonicalRoundTrip() {
+ System.out.println("exactIdentityAlgebraAndCanonicalRoundTrip");
+ AlgorithmIdentity first = BootstrapAlgorithmIdentities.rsaPss(BootstrapAlgorithmIdentities.SHA384,
+ BootstrapAlgorithmIdentities.SHA512, 40);
+ AlgorithmIdentity second = BootstrapAlgorithmIdentities.rsaPss(BootstrapAlgorithmIdentities.SHA384,
+ BootstrapAlgorithmIdentities.SHA512, 40);
+ AlgorithmIdentity different = BootstrapAlgorithmIdentities.rsaPss(BootstrapAlgorithmIdentities.SHA384,
+ BootstrapAlgorithmIdentities.SHA384, 40);
+ AlgorithmIdentityCatalog extension = AlgorithmIdentityCatalog.extension(List.of(
+ new AlgorithmIdentity(AlgorithmIdentity.Kind.SIGNATURE,
+ new AlgorithmIdentity.Family("example", "signature"),
+ new AlgorithmIdentity.DigestParameters(BootstrapAlgorithmIdentities.SHA384))));
+ AlgorithmIdentityCatalog merged = BootstrapAlgorithmIdentities.catalog().merge(List.of(extension));
+
+ assertEquals(first, second);
+ assertEquals(first.hashCode(), second.hashCode());
+ assertNotEquals(first, different);
+ assertEquals(BootstrapAlgorithmIdentities.RSA_PKCS1_SHA256,
+ merged.resolve(BootstrapAlgorithmIdentities.RSA_PKCS1_SHA256.canonicalForm()).orElseThrow());
+ assertFalse(first.canonicalForm().contains("BC"));
+ assertFalse(first.canonicalForm().contains("Sun"));
+ System.out.println("...canonical=" + abbreviate(first.canonicalForm()));
+ System.out.println("...ok");
+ }
+
+ @Test
+ void roleAndParameterContradictionsFailClosed() {
+ System.out.println("roleAndParameterContradictionsFailClosed");
+ assertThrows(IllegalArgumentException.class,
+ () -> new AlgorithmSuite(BootstrapAlgorithmIdentities.SHA256,
+ BootstrapAlgorithmIdentities.RSA_PUBLIC_KEY));
+ assertThrows(IllegalArgumentException.class,
+ () -> new AlgorithmIdentity.RsaPssParameters(BootstrapAlgorithmIdentities.RSA_PUBLIC_KEY,
+ BootstrapAlgorithmIdentities.MGF1, BootstrapAlgorithmIdentities.SHA256, 32, 1));
+ assertThrows(IllegalArgumentException.class,
+ () -> BootstrapAlgorithmIdentities.rsaPss(BootstrapAlgorithmIdentities.SHA256,
+ BootstrapAlgorithmIdentities.SHA256, -1));
+ assertThrows(IllegalArgumentException.class,
+ () -> AlgorithmIdentityCatalog.extension(List.of(BootstrapAlgorithmIdentities.SHA256)));
+ assertTrue(BootstrapAlgorithmIdentities.fromCompatibilityAlias("SHA1withRSA").isEmpty());
+ assertTrue(BootstrapAlgorithmIdentities.fromCompatibilityAlias("provider-specific").isEmpty());
+ System.out.println("...rejections=6");
+ System.out.println("...ok");
+ }
+
+ @Test
+ void parameterizedCapabilityDomainIsProviderMetadataOnly() {
+ System.out.println("parameterizedCapabilityDomainIsProviderMetadataOnly");
+ AlgorithmIdentity pss = BootstrapAlgorithmIdentities.rsaPss(BootstrapAlgorithmIdentities.SHA384,
+ BootstrapAlgorithmIdentities.SHA512, 40);
+ AlgorithmSuite suite = new AlgorithmSuite(pss, BootstrapAlgorithmIdentities.RSA_PUBLIC_KEY);
+ AlgorithmExecutionCapability capability = new TestPssCapability();
+ AlgorithmExecutionCapabilities capabilities = new AlgorithmExecutionCapabilities(List.of(capability));
+
+ assertEquals(1,
+ capabilities.supporting(pss, suite, AlgorithmExecutionCapability.Direction.VERIFY).size());
+ assertTrue(capabilities.supporting(pss, suite, AlgorithmExecutionCapability.Direction.SIGN).isEmpty());
+ assertEquals(pss, suite.signature());
+ System.out.println("...implementation=" + capability.implementationId());
+ System.out.println("...ok");
+ }
+
+ private static String abbreviate(String value) {
+ return value.length() <= 30 ? value : value.substring(0, 27) + "...";
+ }
+
+ /**
+ * Typed test-only parameter domain proving that a central enum is unnecessary.
+ */
+ private static final class TestPssCapability implements AlgorithmExecutionCapability {
+
+ @Override
+ public String implementationId() {
+ return "test.rsa-pss-verify";
+ }
+
+ @Override
+ public String domainFingerprint() {
+ return "rsa-pss|sha384|mgf1-sha512|salt=0..64|verify";
+ }
+
+ @Override
+ public boolean supports(AlgorithmIdentity identity, AlgorithmSuite suite, Direction direction) {
+ if (!(identity.parameters() instanceof AlgorithmIdentity.RsaPssParameters parameters)) {
+ return false;
+ }
+ return identity.equals(suite.signature())
+ && BootstrapAlgorithmIdentities.RSA_PUBLIC_KEY.equals(suite.publicKey())
+ && BootstrapAlgorithmIdentities.SHA384.equals(parameters.hash())
+ && BootstrapAlgorithmIdentities.SHA512.equals(parameters.maskHash())
+ && parameters.saltLength() <= 64 && direction == Direction.VERIFY;
+ }
+ }
+}
diff --git a/pki/src/main/java/zeroecho/pki/api/ca/CaImportCommand.java b/pki/src/main/java/zeroecho/pki/api/ca/CaImportCommand.java
index 7920189..22dbf5c 100644
--- a/pki/src/main/java/zeroecho/pki/api/ca/CaImportCommand.java
+++ b/pki/src/main/java/zeroecho/pki/api/ca/CaImportCommand.java
@@ -33,11 +33,11 @@
******************************************************************************/
package zeroecho.pki.api.ca;
-import zeroecho.pki.api.EncodedObject;
import zeroecho.pki.api.FormatId;
import zeroecho.pki.api.KeyRef;
import zeroecho.pki.api.SubjectRef;
import zeroecho.pki.api.attr.AttributeSet;
+import zeroecho.pki.api.content.DurableContentReference;
/**
* Command to import an existing root CA credential into PKI inventory.
@@ -55,7 +55,7 @@ import zeroecho.pki.api.attr.AttributeSet;
* @param attributes universal attributes (may be empty but not null)
*/
public record CaImportCommand(FormatId formatId, SubjectRef subjectRef, String profileId, KeyRef keyRef,
- EncodedObject existingCaCredential, AttributeSet attributes) {
+ DurableContentReference existingCaCredential, AttributeSet attributes) {
/**
* Creates a CA import command.
diff --git a/pki/src/main/java/zeroecho/pki/api/content/DeploymentResourcePolicy.java b/pki/src/main/java/zeroecho/pki/api/content/DeploymentResourcePolicy.java
new file mode 100644
index 0000000..fa2acb0
--- /dev/null
+++ b/pki/src/main/java/zeroecho/pki/api/content/DeploymentResourcePolicy.java
@@ -0,0 +1,74 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Leo Galambos
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * 3. All advertising materials mentioning features or use of this software must
+ * display the following acknowledgement:
+ * This product includes software developed by the Egothor project.
+ *
+ * 4. Neither the name of the copyright holder nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ ******************************************************************************/
+package zeroecho.pki.api.content;
+
+import java.util.Objects;
+
+/**
+ * Immutable deployment-owned resource policy for streaming PKI operations.
+ *
+ *
+ * ZeroEcho provides no built-in operational values. A deployment explicitly
+ * supplies every dimension as either unrestricted by deployment or positively
+ * limited. Checks are incremental and do not alter algorithm, certificate or
+ * binding semantics.
+ *
+ *
+ * @param acceptedSourceBytes accepted input bytes
+ * @param generatedObjectBytes generated signed-object bytes
+ * @param entryCount streamed entry count
+ * @param stagedContentBytes durable staged-content bytes
+ * @param temporaryStorageBytes temporary spool bytes
+ * @param publicationBytes bytes supplied to a publisher
+ * @param openStagedObjects concurrently open staged objects
+ */
+public record DeploymentResourcePolicy(ResourceLimit acceptedSourceBytes, ResourceLimit generatedObjectBytes,
+ ResourceLimit entryCount, ResourceLimit stagedContentBytes, ResourceLimit temporaryStorageBytes,
+ ResourceLimit publicationBytes, ResourceLimit openStagedObjects) {
+
+ /**
+ * Creates a deployment resource policy.
+ *
+ * @throws NullPointerException if a dimension is {@code null}
+ */
+ public DeploymentResourcePolicy {
+ Objects.requireNonNull(acceptedSourceBytes, "acceptedSourceBytes");
+ Objects.requireNonNull(generatedObjectBytes, "generatedObjectBytes");
+ Objects.requireNonNull(entryCount, "entryCount");
+ Objects.requireNonNull(stagedContentBytes, "stagedContentBytes");
+ Objects.requireNonNull(temporaryStorageBytes, "temporaryStorageBytes");
+ Objects.requireNonNull(publicationBytes, "publicationBytes");
+ Objects.requireNonNull(openStagedObjects, "openStagedObjects");
+ }
+}
diff --git a/pki/src/main/java/zeroecho/pki/api/content/DurableContentOwner.java b/pki/src/main/java/zeroecho/pki/api/content/DurableContentOwner.java
new file mode 100644
index 0000000..5aecfa0
--- /dev/null
+++ b/pki/src/main/java/zeroecho/pki/api/content/DurableContentOwner.java
@@ -0,0 +1,119 @@
+/*******************************************************************************
+ * 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.content;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+
+import zeroecho.pki.api.PkiId;
+import zeroecho.pki.api.orch.SigningSubmissionId;
+
+/**
+ * Typed immutable identity of a durable business owner of staged content.
+ *
+ *
+ * Owner identities contain no path, content, key material, provider name, or
+ * implementation class. The staged-content store is the sole authority that
+ * persists retain and release transitions. Constructing this value does not
+ * itself retain content.
+ *
+ *
+ * @param category closed durable-owner category
+ * @param identifier canonical category-specific identifier
+ */
+public record DurableContentOwner(Category category, String identifier) {
+
+ private static final int MAX_IDENTIFIER_BYTES = 512;
+
+ /** Creates and validates one typed owner identity. */
+ public DurableContentOwner {
+ Objects.requireNonNull(category, "category");
+ String exact = Objects.requireNonNull(identifier, "identifier");
+ if (exact.isBlank() || !exact.equals(exact.strip())
+ || exact.getBytes(StandardCharsets.UTF_8).length > MAX_IDENTIFIER_BYTES) {
+ throw new IllegalArgumentException("Durable content owner identifier is not canonical");
+ }
+ for (int index = 0; index < exact.length(); index++) {
+ char value = exact.charAt(index);
+ if (value < 0x21 || value > 0x7e || value == '/' || value == '\\') {
+ throw new IllegalArgumentException("Durable content owner identifier is not canonical");
+ }
+ }
+ if (category == Category.SIGNING_OPERATION) {
+ SigningSubmissionId.parse(new PkiId(exact));
+ }
+ }
+
+ /**
+ * Creates the canonical owner for one signing operation.
+ *
+ * @param operationId canonical signing submission identifier
+ * @return signing-operation owner
+ * @throws IllegalArgumentException if the identifier is not a canonical
+ * signing submission identifier
+ */
+ public static DurableContentOwner signingOperation(PkiId operationId) {
+ Objects.requireNonNull(operationId, "operationId");
+ SigningSubmissionId parsed = SigningSubmissionId.parse(operationId);
+ return new DurableContentOwner(Category.SIGNING_OPERATION, parsed.id().value());
+ }
+
+ /**
+ * Creates the canonical owner for one persisted credential record.
+ *
+ * @param credentialId canonical credential identifier
+ * @return credential-record owner
+ * @throws IllegalArgumentException if the identifier cannot be represented as
+ * a canonical durable owner identifier
+ */
+ public static DurableContentOwner credentialRecord(PkiId credentialId) {
+ Objects.requireNonNull(credentialId, "credentialId");
+ return new DurableContentOwner(Category.CREDENTIAL_RECORD, credentialId.value());
+ }
+
+ /** Returns a deterministic persistence token containing no path or payload. */
+ public String canonicalForm() {
+ return category.name() + ":" + identifier;
+ }
+
+ /** Closed durable-owner categories reserved by the Phase A lifecycle. */
+ public enum Category {
+ /** Pending or recoverable signing operation. */
+ SIGNING_OPERATION,
+ /** Persisted credential record; integration is deferred. */
+ CREDENTIAL_RECORD,
+ /** Persisted status-object record; integration is deferred. */
+ STATUS_OBJECT_RECORD
+ }
+}
diff --git a/pki/src/main/java/zeroecho/pki/api/content/DurableContentReference.java b/pki/src/main/java/zeroecho/pki/api/content/DurableContentReference.java
new file mode 100644
index 0000000..26cdfe9
--- /dev/null
+++ b/pki/src/main/java/zeroecho/pki/api/content/DurableContentReference.java
@@ -0,0 +1,117 @@
+/*******************************************************************************
+ * 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.content;
+
+import zeroecho.pki.api.Encoding;
+
+/**
+ * Stable, payload-free reference to immutable staged content.
+ *
+ *
+ * The reference is durable provenance rather than a live file handle. It carries
+ * no physical path, payload, key material or runtime authorization token. It may
+ * be resolved only through the staged-content store whose immutable identifier
+ * matches {@link #storeId()}.
+ *
+ *
+ * A durable content reference is authoritative only when issued and validated
+ * by its owning staged-content store. This read-only interface deliberately has
+ * no public construction factory: callers cannot turn arbitrary metadata into
+ * an authoritative reference. Persistence decoders must restore references
+ * through the owning store, which rejects foreign stores and metadata mismatch.
+ * Sealing and reference issuance do not imply durable business-object ownership.
+ *
+ *
+ * Implementations are immutable and expose no physical path, temporary name,
+ * payload, key material, open handle, or mutable lifecycle state. Earlier
+ * pre-release persistence forms are not accepted or migrated. Phase B key
+ * isolation is outside this contract.
+ *
+ */
+public interface DurableContentReference {
+
+ /**
+ * Returns the canonical logical identity of the owning store.
+ *
+ * @return path-independent store identity
+ */
+ String storeId();
+
+ /**
+ * Returns the opaque canonical content identity issued by the store.
+ *
+ * @return path-independent content identity
+ */
+ String contentId();
+
+ /**
+ * Returns the semantic transport encoding.
+ *
+ * @return content encoding
+ */
+ Encoding encoding();
+
+ /**
+ * Returns the exact checked byte length.
+ *
+ * @return non-negative content length
+ */
+ long length();
+
+ /**
+ * Returns the canonical lowercase SHA-256 integrity value.
+ *
+ * @return integrity value; callers must not treat it as issuance authority
+ */
+ String sha256();
+
+ /**
+ * Returns the immutable content purpose classification.
+ *
+ * @return lifecycle classification; not durable ownership state
+ */
+ Lifecycle lifecycle();
+
+ /**
+ * Durable ownership classification.
+ */
+ enum Lifecycle {
+ /** Content retained while a durable operation is pending. */
+ OPERATION,
+ /** Content owned by an immutable persisted PKI object. */
+ PERSISTED,
+ /** Content eligible for release after its immediate operation. */
+ TEMPORARY
+ }
+}
diff --git a/pki/src/main/java/zeroecho/pki/api/content/ResourceLimit.java b/pki/src/main/java/zeroecho/pki/api/content/ResourceLimit.java
new file mode 100644
index 0000000..2c4d4b1
--- /dev/null
+++ b/pki/src/main/java/zeroecho/pki/api/content/ResourceLimit.java
@@ -0,0 +1,108 @@
+/*******************************************************************************
+ * 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.content;
+
+/**
+ * Explicit deployment-owned resource constraint.
+ *
+ *
+ * This type distinguishes absence of a deployment limit from a positive finite
+ * limit without nullable numbers or sentinel values. It never changes PKI,
+ * algorithm or X.509 binding semantics.
+ *
+ */
+public sealed interface ResourceLimit permits ResourceLimit.UnrestrictedByDeployment, ResourceLimit.LimitedTo {
+ /** Smallest valid observed aggregate value. */
+ long EMPTY = 0L;
+
+ /**
+ * Checks an incrementally observed non-negative value.
+ *
+ * @param observed observed aggregate count or byte length
+ * @throws IllegalArgumentException if {@code observed} is negative
+ * @throws ResourceLimitExceededException if the configured limit is exceeded
+ */
+ void requireAllows(long observed);
+
+ /**
+ * Explicit absence of a deployment-owned limit.
+ */
+ record UnrestrictedByDeployment() implements ResourceLimit {
+ @Override
+ public void requireAllows(long observed) {
+ if (observed < EMPTY) {
+ throw new IllegalArgumentException("Observed resource value must not be negative");
+ }
+ }
+ }
+
+ /**
+ * Positive finite deployment-owned limit.
+ *
+ * @param maximum inclusive maximum
+ */
+ record LimitedTo(long maximum) implements ResourceLimit {
+ /**
+ * Creates a finite limit.
+ *
+ * @throws IllegalArgumentException if {@code maximum} is not positive
+ */
+ public LimitedTo {
+ if (maximum <= EMPTY) {
+ throw new IllegalArgumentException("Deployment resource limit must be positive");
+ }
+ }
+
+ @Override
+ public void requireAllows(long observed) {
+ if (observed < EMPTY) {
+ throw new IllegalArgumentException("Observed resource value must not be negative");
+ }
+ if (observed > maximum) {
+ throw new ResourceLimitExceededException();
+ }
+ }
+ }
+
+ /**
+ * Stable non-sensitive failure for deployment resource rejection.
+ */
+ final class ResourceLimitExceededException extends IllegalStateException {
+ private static final long serialVersionUID = 5780535874172354935L;
+
+ private ResourceLimitExceededException() {
+ super("Deployment resource limit exceeded: code=DEPLOYMENT_RESOURCE_LIMIT_EXCEEDED");
+ }
+ }
+}
diff --git a/pki/src/main/java/zeroecho/pki/api/credential/Credential.java b/pki/src/main/java/zeroecho/pki/api/credential/Credential.java
index fab9f7b..3fb8ad9 100644
--- a/pki/src/main/java/zeroecho/pki/api/credential/Credential.java
+++ b/pki/src/main/java/zeroecho/pki/api/credential/Credential.java
@@ -33,13 +33,13 @@
******************************************************************************/
package zeroecho.pki.api.credential;
-import zeroecho.pki.api.EncodedObject;
import zeroecho.pki.api.FormatId;
import zeroecho.pki.api.IssuerRef;
import zeroecho.pki.api.PkiId;
import zeroecho.pki.api.SubjectRef;
import zeroecho.pki.api.Validity;
import zeroecho.pki.api.attr.AttributeSet;
+import zeroecho.pki.api.content.DurableContentReference;
/**
* Issued credential with mandatory core metadata and universal attributes.
@@ -70,12 +70,12 @@ import zeroecho.pki.api.attr.AttributeSet;
* current revocation state and evaluation time are
* external runtime inputs. Security-sensitive callers
* must use {@link EffectiveCredentialStatusResolver}.
- * @param encoded encoded credential bytes
+ * @param content immutable store-owned credential content
* @param attributes universal attribute set
*/
public record Credential(PkiId credentialId, FormatId formatId, IssuerRef issuerRef, SubjectRef subjectRef,
Validity validity, String serialOrUniqueId, PkiId publicKeyId, CredentialProfileBinding profileBinding,
- CredentialStatus status, EncodedObject encoded, AttributeSet attributes) {
+ CredentialStatus status, DurableContentReference content, AttributeSet attributes) {
/**
* Creates a credential record.
@@ -110,8 +110,8 @@ public record Credential(PkiId credentialId, FormatId formatId, IssuerRef issuer
if (status == null) {
throw new IllegalArgumentException("status must not be null");
}
- if (encoded == null) {
- throw new IllegalArgumentException("encoded must not be null");
+ if (content == null) {
+ throw new IllegalArgumentException("content must not be null");
}
if (attributes == null) {
throw new IllegalArgumentException("attributes must not be null");
diff --git a/pki/src/main/java/zeroecho/pki/api/credential/CredentialBundle.java b/pki/src/main/java/zeroecho/pki/api/credential/CredentialBundle.java
index cd86bfe..493e9b4 100644
--- a/pki/src/main/java/zeroecho/pki/api/credential/CredentialBundle.java
+++ b/pki/src/main/java/zeroecho/pki/api/credential/CredentialBundle.java
@@ -35,7 +35,7 @@ package zeroecho.pki.api.credential;
import java.util.List;
-import zeroecho.pki.api.EncodedObject;
+import zeroecho.pki.api.content.DurableContentReference;
/**
* Bundle of a primary credential and supporting objects.
@@ -49,7 +49,7 @@ import zeroecho.pki.api.EncodedObject;
* @param credential primary credential
* @param supportingObjects supporting artifacts (framework-defined ordering)
*/
-public record CredentialBundle(Credential credential, List supportingObjects) {
+public record CredentialBundle(Credential credential, List supportingObjects) {
/**
* Creates a bundle.
diff --git a/pki/src/main/java/zeroecho/pki/api/status/StatusObject.java b/pki/src/main/java/zeroecho/pki/api/status/StatusObject.java
index c54f353..4388a6c 100644
--- a/pki/src/main/java/zeroecho/pki/api/status/StatusObject.java
+++ b/pki/src/main/java/zeroecho/pki/api/status/StatusObject.java
@@ -36,10 +36,10 @@ package zeroecho.pki.api.status;
import java.time.Instant;
import java.util.Optional;
-import zeroecho.pki.api.EncodedObject;
import zeroecho.pki.api.FormatId;
import zeroecho.pki.api.PkiId;
import zeroecho.pki.api.attr.AttributeSet;
+import zeroecho.pki.api.content.DurableContentReference;
/**
* Generated status object used for revocation distribution.
@@ -57,12 +57,12 @@ import zeroecho.pki.api.attr.AttributeSet;
* @param type status object type
* @param thisUpdate time of issuance/publication baseline
* @param nextUpdate optional next update timestamp
- * @param encoded encoded payload
+ * @param content immutable store-owned encoded content reference
* @param attributes universal attributes describing the object (must not
* contain secrets)
*/
public record StatusObject(PkiId statusObjectId, FormatId formatId, PkiId issuerCaId, StatusObjectType type,
- Instant thisUpdate, Optional nextUpdate, EncodedObject encoded, AttributeSet attributes) {
+ Instant thisUpdate, Optional nextUpdate, DurableContentReference content, AttributeSet attributes) {
/**
* Creates a status object.
@@ -89,8 +89,8 @@ public record StatusObject(PkiId statusObjectId, FormatId formatId, PkiId issuer
if (nextUpdate == null) {
throw new IllegalArgumentException("nextUpdate must not be null");
}
- if (encoded == null) {
- throw new IllegalArgumentException("encoded must not be null");
+ if (content == null) {
+ throw new IllegalArgumentException("content must not be null");
}
if (attributes == null) {
throw new IllegalArgumentException("attributes must not be null");
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/CaCertificateProfileValidator.java b/pki/src/main/java/zeroecho/pki/impl/core/CaCertificateProfileValidator.java
index 2debf62..1b2bbd3 100644
--- a/pki/src/main/java/zeroecho/pki/impl/core/CaCertificateProfileValidator.java
+++ b/pki/src/main/java/zeroecho/pki/impl/core/CaCertificateProfileValidator.java
@@ -80,12 +80,12 @@ final class CaCertificateProfileValidator {
ActiveCertificateProfile activeProfile, CertificateProfileKind expectedKind, FormatId formatId,
PkiId issuerCaId, PkiId subjectCaId, SubjectRef requestedSubject, EncodedObject exactPublicKey,
Optional requestedValidity, Instant evaluationTime, Optional issuerNotAfter,
- BigInteger serial) {
+ BigInteger serial, zeroecho.pki.impl.framework.x509.X509AuthoritySnapshot authority) {
CertificateProfileDefinition definition = activeProfile.definition();
requireProfileShape(definition, expectedKind, formatId);
List approvedSubject = validateSubject(requestedSubject, definition);
return validateApprovedSubject(operation, activeProfile, expectedKind, formatId, issuerCaId, subjectCaId,
- approvedSubject, exactPublicKey, requestedValidity, evaluationTime, issuerNotAfter, serial);
+ approvedSubject, exactPublicKey, requestedValidity, evaluationTime, issuerNotAfter, serial, authority);
}
/*
@@ -97,12 +97,13 @@ final class CaCertificateProfileValidator {
ValidatedCaCertificateRequest.Operation operation, ActiveCertificateProfile activeProfile,
CertificateProfileKind expectedKind, FormatId formatId, PkiId issuerCaId, PkiId subjectCaId,
List approvedSubject, EncodedObject exactPublicKey, Optional requestedValidity,
- Instant evaluationTime, Optional issuerNotAfter, BigInteger serial) {
+ Instant evaluationTime, Optional issuerNotAfter, BigInteger serial,
+ zeroecho.pki.impl.framework.x509.X509AuthoritySnapshot authority) {
CertificateProfileDefinition definition = activeProfile.definition();
requireProfileShape(definition, expectedKind, formatId);
List subjectSnapshot = requireApprovedSubject(approvedSubject);
CertificateProfileValidator.requireSubjectKeyAllowed(exactPublicKey,
- definition.caPolicy().allowedSubjectKeyAlgorithmIds());
+ definition.caPolicy().allowedSubjectKeyAlgorithmIds(), authority);
Validity validity = approvedValidity(requestedValidity, definition, evaluationTime, issuerNotAfter,
operation == ValidatedCaCertificateRequest.Operation.IMPORT_ROOT);
SubjectRef canonicalSubject = new SubjectRef(BcX509ProfileSupport.subject(subjectSnapshot).toString());
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/CaProofGate.java b/pki/src/main/java/zeroecho/pki/impl/core/CaProofGate.java
index 78c5a2d..b1535c9 100644
--- a/pki/src/main/java/zeroecho/pki/impl/core/CaProofGate.java
+++ b/pki/src/main/java/zeroecho/pki/impl/core/CaProofGate.java
@@ -33,7 +33,6 @@
******************************************************************************/
package zeroecho.pki.impl.core;
-import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.MessageDigest;
@@ -49,24 +48,30 @@ import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.operator.ContentSigner;
-import org.bouncycastle.operator.ContentVerifier;
-import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder;
-import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
+import zeroecho.core.spec.AlgorithmIdentity;
+import zeroecho.core.io.ImmutableByteContent;
+import zeroecho.core.spi.AlgorithmExecutionCapability;
import zeroecho.pki.api.EncodedObject;
import zeroecho.pki.api.Encoding;
import zeroecho.pki.api.FormatId;
import zeroecho.pki.api.KeyRef;
import zeroecho.pki.api.PkiException;
import zeroecho.pki.api.PkiId;
+import zeroecho.pki.api.content.DurableContentReference;
import zeroecho.pki.api.audit.AccessContext;
import zeroecho.pki.api.audit.AuditEvent;
import zeroecho.pki.api.audit.Principal;
import zeroecho.pki.api.audit.Purpose;
import zeroecho.pki.impl.core.async.PkiSigningBus;
+import zeroecho.pki.impl.framework.x509.X509AlgorithmRole;
+import zeroecho.pki.impl.framework.x509.X509ExecutionPlan;
+import zeroecho.pki.impl.framework.x509.bc.BcX509VerificationExecutor;
+import zeroecho.pki.impl.framework.x509.bc.BcX509AlgorithmAdapter;
import zeroecho.pki.spi.audit.AuditSink;
import zeroecho.pki.util.async.AsyncState;
import zeroecho.pki.util.async.AsyncStatus;
+import zeroecho.pki.spi.store.ContentSink;
/**
* Internal fail-closed proof gate for CA signing keys.
@@ -85,20 +90,27 @@ final class CaProofGate {
private final PublicKeyInfoResolver publicKeyResolver;
private final PkiSigningBus signingBus;
private final AuditSink auditSink;
- private final String signatureAlgorithmId;
+ private final AlgorithmIdentity signatureIdentity;
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);
+ }
+
+ /* default */ CaProofGate(PublicKeyInfoResolver publicKeyResolver, PkiSigningBus signingBus, AuditSink auditSink,
+ AlgorithmIdentity signatureIdentity, Duration signingTtl) {
this.publicKeyResolver = Objects.requireNonNull(publicKeyResolver, "publicKeyResolver");
this.signingBus = Objects.requireNonNull(signingBus, "signingBus");
this.auditSink = Objects.requireNonNull(auditSink, "auditSink");
- this.signatureAlgorithmId = Objects.requireNonNull(signatureAlgorithmId, "signatureAlgorithmId");
+ this.signatureIdentity = Objects.requireNonNull(signatureIdentity, "signatureIdentity");
this.signingTtl = Objects.requireNonNull(signingTtl, "signingTtl");
}
/* default */ ContentSigner signer(ManagedKeyProof proof) {
- return new BusBackedContentSigner(signingBus, proof.keyRef(), signatureAlgorithmId, signingTtl);
+ return new BusBackedContentSigner(signingBus, proof.keyRef(), signatureIdentity, signingTtl);
}
/* default */ SubjectPublicKeyInfo parseRootSpki(EncodedObject spki, FormatId formatId) {
@@ -115,8 +127,24 @@ final class CaProofGate {
}
try {
byte[] embeddedSpki = certificate.getSubjectPublicKeyInfo().getEncoded();
- return MessageDigest.isEqual(expectedSpki.bytes(), embeddedSpki) && certificate.isSignatureValid(
- new JcaContentVerifierProviderBuilder().build(certificate.getSubjectPublicKeyInfo()));
+ BcX509AlgorithmAdapter adapter = new BcX509AlgorithmAdapter(signingBus.authority().bindings());
+ AlgorithmIdentity outer = adapter.decode(certificate.getSignatureAlgorithm(),
+ X509AlgorithmRole.SIGNATURE_ALGORITHM);
+ AlgorithmIdentity inner = adapter.decode(
+ certificate.toASN1Structure().getTBSCertificate().getSignature(),
+ X509AlgorithmRole.SIGNATURE_ALGORITHM);
+ AlgorithmIdentity key = adapter.decode(certificate.getSubjectPublicKeyInfo().getAlgorithm(),
+ X509AlgorithmRole.SUBJECT_PUBLIC_KEY_ALGORITHM);
+ X509ExecutionPlan plan = signingBus.authority().plan(outer, key,
+ AlgorithmExecutionCapability.Direction.VERIFY,
+ Optional.of(BcX509VerificationExecutor.IMPLEMENTATION_ID), "root-proof",
+ BcX509VerificationExecutor.class);
+ BcX509VerificationExecutor executor = plan.executor();
+ boolean verified = executor.verify(signingBus.authority(), plan, certificate.getSubjectPublicKeyInfo(),
+ certificate.getSignatureAlgorithm(),
+ new ImmutableByteContent(certificate.toASN1Structure().getTBSCertificate().getEncoded()),
+ certificate.getSignature());
+ return MessageDigest.isEqual(expectedSpki.bytes(), embeddedSpki) && verified && outer.equals(inner);
} catch (Exception ex) {
return false;
}
@@ -174,7 +202,7 @@ final class CaProofGate {
}
private byte[] signManagedKeyChallenge(KeyRef keyRef, byte[] challenge) {
- ContentSigner contentSigner = new BusBackedContentSigner(signingBus, keyRef, signatureAlgorithmId, signingTtl);
+ ContentSigner contentSigner = new BusBackedContentSigner(signingBus, keyRef, signatureIdentity, signingTtl);
try {
contentSigner.getOutputStream().write(challenge);
} catch (IOException ex) {
@@ -186,10 +214,17 @@ final class CaProofGate {
private boolean verifyChallenge(byte[] spkiDer, byte[] challenge, byte[] signature) {
try {
SubjectPublicKeyInfo spki = SubjectPublicKeyInfo.getInstance(spkiDer);
- ContentVerifier verifier = new JcaContentVerifierProviderBuilder().build(spki)
- .get(new DefaultSignatureAlgorithmIdentifierFinder().find(signatureAlgorithmId));
- verifier.getOutputStream().write(challenge);
- return verifier.verify(signature);
+ BcX509AlgorithmAdapter adapter = new BcX509AlgorithmAdapter(signingBus.authority().bindings());
+ AlgorithmIdentity keyIdentity = adapter.decode(spki.getAlgorithm(),
+ X509AlgorithmRole.SUBJECT_PUBLIC_KEY_ALGORITHM);
+ X509ExecutionPlan plan = signingBus.authority().plan(signatureIdentity,
+ keyIdentity, AlgorithmExecutionCapability.Direction.VERIFY,
+ Optional.of(BcX509VerificationExecutor.IMPLEMENTATION_ID), "managed-key-proof",
+ BcX509VerificationExecutor.class);
+ BcX509VerificationExecutor executor = plan.executor();
+ return executor.verify(signingBus.authority(), plan, spki,
+ adapter.encode(signatureIdentity, X509AlgorithmRole.SIGNATURE_ALGORITHM),
+ new ImmutableByteContent(challenge), signature);
} catch (Exception ex) {
return false;
}
@@ -231,21 +266,30 @@ final class CaProofGate {
private final PkiSigningBus bus;
private final KeyRef keyRef;
- private final String algorithmId;
+ private final AlgorithmIdentity algorithmIdentity;
private final Duration ttl;
- private final ByteArrayOutputStream output;
+ private final ContentSink sink;
+ private final OutputStream output;
- private BusBackedContentSigner(PkiSigningBus bus, KeyRef keyRef, String algorithmId, Duration ttl) {
+ private BusBackedContentSigner(PkiSigningBus bus, KeyRef keyRef, AlgorithmIdentity algorithmIdentity,
+ Duration ttl) {
this.bus = bus;
this.keyRef = keyRef;
- this.algorithmId = algorithmId;
+ this.algorithmIdentity = algorithmIdentity;
this.ttl = ttl;
- this.output = new ByteArrayOutputStream();
+ this.sink = bus.beginSigningContent(Encoding.BINARY);
+ try {
+ this.output = sink.outputStream();
+ } catch (IOException exception) {
+ closeSinkPreserving(exception);
+ throw new PkiException("Signing content staging failed: code=SPOOL_STORAGE_FAILED", exception);
+ }
}
@Override
public AlgorithmIdentifier getAlgorithmIdentifier() {
- return new DefaultSignatureAlgorithmIdentifierFinder().find(algorithmId);
+ return new BcX509AlgorithmAdapter(bus.authority().bindings()).encode(algorithmIdentity,
+ X509AlgorithmRole.SIGNATURE_ALGORITHM);
}
@Override
@@ -255,24 +299,40 @@ final class CaProofGate {
@Override
public byte[] getSignature() {
- byte[] tbs = output.toByteArray();
+ DurableContentReference content;
try {
- Principal owner = new Principal("SYSTEM", "pki");
- PkiId opId = bus.newSubmissionId();
- EncodedObject payload = new EncodedObject(Encoding.BINARY, tbs);
- AccessContext accessContext = new AccessContext(owner, new Purpose("X509_SIGN"), Optional.empty(),
- Optional.empty());
- PkiSigningBus.SignContinuation continuation = new PkiSigningBus.SignContinuation(accessContext,
- algorithmId, payload, keyRef, Encoding.BINARY, Optional.empty());
- try {
- bus.submitSign(opId, owner, keyRef, algorithmId, payload, ttl, Optional.of(continuation.encode()));
- } catch (RuntimeException failure) { // NOPMD - delete state if submission partially persisted it
- deletePreservingFailure(opId);
- throw failure;
- }
- return awaitSignature(opId);
+ output.close();
+ content = sink.complete();
+ } catch (IOException exception) {
+ closeSinkPreserving(exception);
+ throw new PkiException("Signing content staging failed: code=SPOOL_STORAGE_FAILED", exception);
+ }
+ Principal owner = new Principal("SYSTEM", "pki");
+ PkiId opId = bus.newSubmissionId();
+ AccessContext accessContext = new AccessContext(owner, new Purpose("X509_SIGN"), Optional.empty(),
+ Optional.empty());
+ String canonicalIdentity = algorithmIdentity.canonicalForm();
+ PkiSigningBus.SignContinuation continuation = new PkiSigningBus.SignContinuation(accessContext,
+ canonicalIdentity, content, keyRef, Encoding.BINARY, Optional.empty());
+ boolean submitted = false;
+ try {
+ bus.submitSign(opId, owner, keyRef, canonicalIdentity, content, ttl,
+ Optional.of(continuation.encode()));
+ submitted = true;
} finally {
- Arrays.fill(tbs, (byte) 0);
+ if (!submitted) {
+ deletePreservingFailure(opId);
+ bus.releaseContent(content);
+ }
+ }
+ return awaitSignature(opId);
+ }
+
+ private void closeSinkPreserving(IOException primaryFailure) {
+ try {
+ sink.close();
+ } catch (IOException cleanupFailure) {
+ primaryFailure.addSuppressed(cleanupFailure);
}
}
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/CertificateProfileValidator.java b/pki/src/main/java/zeroecho/pki/impl/core/CertificateProfileValidator.java
index 006b249..88d6f1a 100644
--- a/pki/src/main/java/zeroecho/pki/impl/core/CertificateProfileValidator.java
+++ b/pki/src/main/java/zeroecho/pki/impl/core/CertificateProfileValidator.java
@@ -49,12 +49,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
-import org.bouncycastle.asn1.ASN1ObjectIdentifier;
-import org.bouncycastle.asn1.DERNull;
-import org.bouncycastle.asn1.edec.EdECObjectIdentifiers;
-import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
-import org.bouncycastle.asn1.x9.X9ObjectIdentifiers;
import zeroecho.pki.api.Encoding;
import zeroecho.pki.api.PkiException;
@@ -74,6 +69,11 @@ import zeroecho.pki.api.request.ParsedCertificationRequest;
import zeroecho.pki.api.request.SubjectAlternativeName;
import zeroecho.pki.api.request.SubjectRdn;
import zeroecho.pki.impl.framework.x509.bc.BcX509Attributes;
+import zeroecho.pki.impl.framework.x509.bc.BcX509AlgorithmAdapter;
+import zeroecho.pki.impl.framework.x509.X509AlgorithmRole;
+import zeroecho.pki.impl.framework.x509.X509AuthoritySnapshot;
+import zeroecho.core.alg.BootstrapAlgorithmIdentities;
+import zeroecho.core.spec.AlgorithmIdentity;
import zeroecho.pki.impl.framework.x509.bc.BcX509ProfileSupport;
/**
@@ -83,18 +83,20 @@ import zeroecho.pki.impl.framework.x509.bc.BcX509ProfileSupport;
@SuppressWarnings("PMD.CyclomaticComplexity")
final class CertificateProfileValidator {
+ private static final String EC_FAMILY = "ec";
+
private CertificateProfileValidator() {
}
/* package */ static ValidatedCertificateRequest validate(VerifiedIssuanceCandidate candidate,
CertificateProfile profile, CertificateProfileRef profileReference, Credential issuerCredential,
- Instant evaluationTime) {
+ Instant evaluationTime, X509AuthoritySnapshot authority) {
ParsedCertificationRequest request = candidate.request();
LeafCertificatePolicy policy = profile.leafPolicy();
requireCanonicalRequestAttributes(request);
List approvedSubject = validateSubject(request, profile);
List approvedSans = validateSans(request, policy, approvedSubject.isEmpty());
- requireSubjectKeyAllowed(candidate.exactPublicKey(), policy.allowedSubjectKeyAlgorithmIds());
+ requireSubjectKeyAllowed(candidate.exactPublicKey(), policy.allowedSubjectKeyAlgorithmIds(), authority);
Validity validity = approvedValidity(candidate, request, profile, issuerCredential, evaluationTime);
boolean sanCritical = approvedSubject.isEmpty()
|| policy.subjectAlternativeNamePolicy().criticalWithNonemptySubject();
@@ -225,15 +227,24 @@ final class CertificateProfileValidator {
// The public exception deliberately redacts ASN.1 parser details.
@SuppressWarnings({ "PMD.PreserveStackTrace", "PMD.AvoidRethrowingException" })
/* package */ static void requireSubjectKeyAllowed(zeroecho.pki.api.EncodedObject exactPublicKey,
- Set allowedAlgorithms) {
+ Set allowedAlgorithms, X509AuthoritySnapshot authority) {
if (exactPublicKey.encoding() != Encoding.DER) {
throw reject("SUBJECT_KEY_UNSUPPORTED");
}
byte[] encoded = exactPublicKey.bytes();
try {
SubjectPublicKeyInfo spki = SubjectPublicKeyInfo.getInstance(encoded);
- SubjectKeyAlgorithm algorithm = subjectKeyAlgorithm(spki.getAlgorithm().getAlgorithm());
- requireSupportedParameters(spki, algorithm);
+ AlgorithmIdentity identity;
+ try {
+ identity = new BcX509AlgorithmAdapter(authority.bindings()).decode(spki.getAlgorithm(),
+ X509AlgorithmRole.SUBJECT_PUBLIC_KEY_ALGORITHM);
+ } catch (IllegalArgumentException invalidBinding) {
+ boolean knownOid = authority.bindings().rules().stream()
+ .filter(rule -> rule.role() == X509AlgorithmRole.SUBJECT_PUBLIC_KEY_ALGORITHM)
+ .anyMatch(rule -> rule.oid().equals(spki.getAlgorithm().getAlgorithm().getId()));
+ throw reject(knownOid ? "SUBJECT_KEY_PARAMETERS_UNSUPPORTED" : "SUBJECT_KEY_ALGORITHM_UNKNOWN");
+ }
+ SubjectKeyAlgorithm algorithm = subjectKeyAlgorithm(identity);
if (!allowedAlgorithms.contains(algorithm.profileId())) {
throw reject("SUBJECT_KEY_ALGORITHM_FORBIDDEN");
}
@@ -258,35 +269,22 @@ final class CertificateProfileValidator {
}
}
- private static SubjectKeyAlgorithm subjectKeyAlgorithm(ASN1ObjectIdentifier oid) {
- if (PKCSObjectIdentifiers.rsaEncryption.equals(oid)) {
+ private static SubjectKeyAlgorithm subjectKeyAlgorithm(AlgorithmIdentity identity) {
+ if (identity.equals(BootstrapAlgorithmIdentities.RSA_PUBLIC_KEY)) {
return new SubjectKeyAlgorithm("RSA", "RSA");
}
- if (X9ObjectIdentifiers.id_ecPublicKey.equals(oid)) {
+ if (EC_FAMILY.equals(identity.family().name())) {
return new SubjectKeyAlgorithm("ECDSA", "EC");
}
- if (EdECObjectIdentifiers.id_Ed25519.equals(oid)) {
+ if (identity.equals(BootstrapAlgorithmIdentities.ED25519_PUBLIC_KEY)) {
return new SubjectKeyAlgorithm("Ed25519", "Ed25519");
}
- if (EdECObjectIdentifiers.id_Ed448.equals(oid)) {
+ if (identity.equals(BootstrapAlgorithmIdentities.ED448_PUBLIC_KEY)) {
return new SubjectKeyAlgorithm("Ed448", "Ed448");
}
throw reject("SUBJECT_KEY_ALGORITHM_UNKNOWN");
}
- private static void requireSupportedParameters(SubjectPublicKeyInfo spki, SubjectKeyAlgorithm algorithm) {
- org.bouncycastle.asn1.ASN1Encodable parameters = spki.getAlgorithm().getParameters();
- boolean supported = switch (algorithm.profileId()) {
- case "RSA" -> DERNull.INSTANCE.equals(parameters);
- case "ECDSA" -> parameters instanceof ASN1ObjectIdentifier;
- case "Ed25519", "Ed448" -> parameters == null;
- default -> false;
- };
- if (!supported) {
- throw reject("SUBJECT_KEY_PARAMETERS_UNSUPPORTED");
- }
- }
-
// The public exception deliberately redacts temporal arithmetic details.
@SuppressWarnings("PMD.PreserveStackTrace")
private static Validity approvedValidity(VerifiedIssuanceCandidate candidate, ParsedCertificationRequest request,
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/CredentialContent.java b/pki/src/main/java/zeroecho/pki/impl/core/CredentialContent.java
new file mode 100644
index 0000000..d31ee34
--- /dev/null
+++ b/pki/src/main/java/zeroecho/pki/impl/core/CredentialContent.java
@@ -0,0 +1,111 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Leo Galambos
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * 3. All advertising materials mentioning features or use of this software must
+ * display the following acknowledgement:
+ * This product includes software developed by the Egothor project.
+ *
+ * 4. Neither the name of the copyright holder nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ ******************************************************************************/
+package zeroecho.pki.impl.core;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Optional;
+
+import zeroecho.core.io.CancellationSignal;
+import zeroecho.core.io.RepeatableContent;
+import zeroecho.core.spec.AlgorithmIdentity;
+import zeroecho.pki.api.Encoding;
+import zeroecho.pki.api.PkiException;
+import zeroecho.pki.api.content.DurableContentReference;
+import zeroecho.pki.impl.framework.x509.X509AuthoritySnapshot;
+import zeroecho.pki.impl.framework.x509.bc.BcX509SignedObjectValidator;
+import zeroecho.pki.spi.store.ContentSink;
+import zeroecho.pki.spi.store.PkiStore;
+
+/**
+ * Internal individual-certificate adapter around store-owned content.
+ */
+final class CredentialContent {
+ private CredentialContent() {
+ }
+
+ /* default */ static DurableContentReference stage(PkiStore store, byte[] encoded) {
+ try (ContentSink sink = store.stagedContent().beginContent(Encoding.DER, DurableContentReference.Lifecycle.PERSISTED);
+ OutputStream output = sink.outputStream()) {
+ output.write(encoded);
+ return sink.complete();
+ } catch (IOException exception) {
+ throw new PkiException("Credential staging failed: code=SPOOL_STORAGE_FAILED", exception);
+ }
+ }
+
+ /* default */ static byte[] materializeForBc(PkiStore store, DurableContentReference reference) {
+ if (reference.length() > Integer.MAX_VALUE) {
+ throw new PkiException("Credential exceeds BC adapter element domain: code=ADAPTER_ELEMENT_LIMIT_EXCEEDED");
+ }
+ byte[] result = new byte[(int) reference.length()];
+ try {
+ readExact(store, reference, result);
+ return result;
+ } catch (IOException exception) {
+ java.util.Arrays.fill(result, (byte) 0);
+ throw new PkiException("Credential content failed: code=CONTENT_IO_FAILED", exception);
+ }
+ }
+
+ /* default */ static BcX509SignedObjectValidator.CertificateBindings validateCertificate(PkiStore store,
+ DurableContentReference reference, X509AuthoritySnapshot authority,
+ Optional expectedSignature) {
+ try (RepeatableContent content = store.stagedContent().openContent(reference)) {
+ return new BcX509SignedObjectValidator(authority).validateCertificate(content, expectedSignature,
+ CancellationSignal.NONE);
+ } catch (IOException | IllegalArgumentException exception) {
+ throw new PkiException("Certificate validation failed: code=NON_CANONICAL_DER", exception);
+ }
+ }
+
+ private static void readExact(PkiStore store, DurableContentReference reference, byte[] result)
+ throws IOException {
+ try (RepeatableContent content = store.stagedContent().openContent(reference);
+ InputStream input = content.openStream()) {
+ int offset = 0;
+ while (offset != result.length) {
+ int count = input.read(result, offset, result.length - offset);
+ if (count < 0) {
+ throw new IOException("Credential content is truncated");
+ }
+ offset += count;
+ }
+ if (input.read() >= 0) {
+ throw new IOException("Credential content length changed");
+ }
+ }
+ }
+}
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/CredentialSnapshots.java b/pki/src/main/java/zeroecho/pki/impl/core/CredentialSnapshots.java
index c5eaa34..9b23049 100644
--- a/pki/src/main/java/zeroecho/pki/impl/core/CredentialSnapshots.java
+++ b/pki/src/main/java/zeroecho/pki/impl/core/CredentialSnapshots.java
@@ -36,7 +36,6 @@ package zeroecho.pki.impl.core;
import java.util.ArrayList;
import java.util.List;
-import zeroecho.pki.api.EncodedObject;
import zeroecho.pki.api.attr.AttributeId;
import zeroecho.pki.api.attr.AttributeSet;
import zeroecho.pki.api.attr.AttributeValue;
@@ -54,18 +53,13 @@ final class CredentialSnapshots {
/* default */ static CredentialBundle copy(CredentialBundle source) {
Credential credential = copy(source.credential());
- List supporting = source.supportingObjects().stream().map(CredentialSnapshots::copy).toList();
- return new CredentialBundle(credential, supporting);
+ return new CredentialBundle(credential, List.copyOf(source.supportingObjects()));
}
/* default */ static Credential copy(Credential source) {
return new Credential(source.credentialId(), source.formatId(), source.issuerRef(), source.subjectRef(),
source.validity(), source.serialOrUniqueId(), source.publicKeyId(), source.profileBinding(),
- source.status(), copy(source.encoded()), copy(source.attributes()));
- }
-
- private static EncodedObject copy(EncodedObject source) {
- return new EncodedObject(source.encoding(), source.bytes().clone());
+ source.status(), source.content(), copy(source.attributes()));
}
private static AttributeSet copy(AttributeSet source) {
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/DefaultCaService.java b/pki/src/main/java/zeroecho/pki/impl/core/DefaultCaService.java
index 7297892..7bcbecf 100644
--- a/pki/src/main/java/zeroecho/pki/impl/core/DefaultCaService.java
+++ b/pki/src/main/java/zeroecho/pki/impl/core/DefaultCaService.java
@@ -60,6 +60,8 @@ import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
+import zeroecho.core.spec.AlgorithmIdentity;
+import zeroecho.core.spi.AlgorithmExecutionCapability;
import zeroecho.pki.api.CaService;
import zeroecho.pki.api.EncodedObject;
import zeroecho.pki.api.Encoding;
@@ -91,6 +93,8 @@ import zeroecho.pki.api.credential.EffectiveCredentialStatusResolver;
import zeroecho.pki.api.profile.ActiveCertificateProfile;
import zeroecho.pki.api.profile.CertificateProfileKind;
import zeroecho.pki.impl.core.async.PkiSigningBus;
+import zeroecho.pki.impl.framework.x509.X509ExecutionPlan;
+import zeroecho.pki.spi.crypto.SignatureWorkflow;
import zeroecho.pki.impl.core.attr.SimpleAttributeSet;
import zeroecho.pki.spi.audit.AuditSink;
import zeroecho.pki.spi.framework.CredentialFramework;
@@ -167,10 +171,12 @@ public final class DefaultCaService implements CaService {
private final CredentialFramework framework;
private final CredentialIssuerBackend issuerBackend;
private final CaProofGate proofGate;
+ private final zeroecho.pki.impl.framework.x509.X509AuthoritySnapshot authority;
private final AuditSink auditSink;
private final EffectiveCredentialStatusResolver statusResolver;
private final ProfileService profileService;
private final Clock clock;
+ private final AlgorithmIdentity signatureIdentity;
/**
* Creates a CA service bound to a specific store, credential framework, and
@@ -232,6 +238,7 @@ public final class DefaultCaService implements CaService {
this.issuerBackend = Objects.requireNonNull(issuerBackend, "issuerBackend");
Objects.requireNonNull(publicKeyResolver, "publicKeyResolver");
Objects.requireNonNull(signingBus, "signingBus");
+ this.authority = signingBus.authority();
this.auditSink = Objects.requireNonNull(auditSink, "auditSink");
this.statusResolver = Objects.requireNonNull(statusResolver, "statusResolver");
this.profileService = Objects.requireNonNull(profileService, "profileService");
@@ -242,7 +249,12 @@ public final class DefaultCaService implements CaService {
if (signingTtl == null || signingTtl.isZero() || signingTtl.isNegative()) {
throw new IllegalArgumentException("signingTtl must be positive");
}
- this.proofGate = new CaProofGate(publicKeyResolver, signingBus, auditSink, signatureAlgorithmId, signingTtl);
+ AlgorithmIdentity signatureIdentity = signingBus.authority().resolveIdentity(signatureAlgorithmId);
+ X509ExecutionPlan plan = 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);
}
/**
@@ -294,7 +306,7 @@ public final class DefaultCaService implements CaService {
ValidatedCaCertificateRequest request = CaCertificateProfileValidator.validate(
ValidatedCaCertificateRequest.Operation.CREATE_ROOT, activeProfile, CertificateProfileKind.ROOT_CA,
command.formatId(), new PkiId("ca:pending-root"), new PkiId("ca:pending-root"), command.subjectRef(),
- spki, Optional.empty(), evaluationTime, Optional.empty(), serial);
+ spki, Optional.empty(), evaluationTime, Optional.empty(), serial, authority);
SubjectPublicKeyInfo rootPublicKeyInfo = proofGate.parseRootSpki(spki, command.formatId());
CaProofGate.ManagedKeyProof proof = proofGate.proveManagedKey(keyRef, command.formatId(), CREATE_ROOT_REJECTED,
Optional.empty());
@@ -342,7 +354,7 @@ public final class DefaultCaService implements CaService {
Credential credential = new Credential(credId, command.formatId(), new IssuerRef(caId), request.subjectRef(),
validity, serial.toString(), publicKeyId, new CaProfileBinding(request.profileReference()),
- CredentialStatus.ISSUED, new EncodedObject(Encoding.DER, certDer),
+ CredentialStatus.ISSUED, CredentialContent.stage(store, certDer),
SimpleAttributeSet.builder().build());
CredentialProfileBindings.requireCaBinding(credential.profileBinding(), request.profileReference());
@@ -394,7 +406,8 @@ public final class DefaultCaService implements CaService {
throw new PkiException("Only DER import supported by this runtime");
}
- byte[] certDer = command.existingCaCredential().bytes().clone();
+ CredentialContent.validateCertificate(store, command.existingCaCredential(), authority, Optional.empty());
+ byte[] certDer = CredentialContent.materializeForBc(store, command.existingCaCredential());
X509CertificateHolder holder;
try {
holder = new X509CertificateHolder(certDer);
@@ -423,10 +436,10 @@ public final class DefaultCaService implements CaService {
ValidatedCaCertificateRequest request = CaCertificateProfileValidator.validate(
ValidatedCaCertificateRequest.Operation.IMPORT_ROOT, activeProfile, CertificateProfileKind.ROOT_CA,
command.formatId(), caId, caId, command.subjectRef(), spki, Optional.of(validity), evaluationTime,
- Optional.empty(), serial);
+ Optional.empty(), serial, authority);
Credential credential = new Credential(credId, command.formatId(), new IssuerRef(caId), request.subjectRef(),
validity, serial.toString(), publicKeyId, new CaProfileBinding(request.profileReference()),
- CredentialStatus.ISSUED, new EncodedObject(Encoding.DER, certDer),
+ CredentialStatus.ISSUED, command.existingCaCredential(),
SimpleAttributeSet.builder().build());
CredentialProfileBindings.requireCaBinding(credential.profileBinding(), request.profileReference());
requireCaCertificateMatches(credential, credential, request, caId, IMPORT_ROOT_REJECTED,
@@ -511,7 +524,7 @@ public final class DefaultCaService implements CaService {
ValidatedCaCertificateRequest.Operation.CREATE_INTERMEDIATE, activeProfile,
CertificateProfileKind.INTERMEDIATE_CA, command.formatId(), command.issuerCaId(), caId, approvedSubject,
subjectSpki, Optional.empty(), evaluationTime, Optional.of(issuerCredential.validity().notAfter()),
- CertificateSerialAllocator.allocate());
+ CertificateSerialAllocator.allocate(), authority);
CaProofGate.ManagedKeyProof subjectProof = proofGate.proveManagedKey(command.keyRef().get(), command.formatId(),
CREATE_INT_REJECTED, Optional.of(caId));
requireSameManagedKey(subjectSpki, subjectProof.exactPublicKey(), CREATE_INT_REJECTED, command.formatId(),
@@ -520,7 +533,7 @@ public final class DefaultCaService implements CaService {
Credential backendCredential;
try {
- backendCredential = issuerBackend.issueIntermediateCertificate(issue, issuerCredential.encoded(),
+ backendCredential = issuerBackend.issueIntermediateCertificate(issue, issuerCredential.content(),
issuer.issuerKeyRef());
} catch (RuntimeException ex) { // NOPMD - reject malformed or mutable framework output
throw proofGate.rejection(CREATE_INT_REJECTED, command.formatId(), Optional.of(caId),
@@ -602,7 +615,8 @@ public final class DefaultCaService implements CaService {
ValidatedCaCertificateRequest.Operation.ISSUE_INTERMEDIATE, activeProfile,
CertificateProfileKind.INTERMEDIATE_CA, command.formatId(), command.issuerCaId(), command.subjectCaId(),
approvedSubject, subjectSpki, command.requestedValidity(), evaluationTime,
- Optional.of(issuerCredential.validity().notAfter()), CertificateSerialAllocator.allocate());
+ Optional.of(issuerCredential.validity().notAfter()), CertificateSerialAllocator.allocate(),
+ authority);
CaProofGate.ManagedKeyProof subjectProof = proofGate.proveManagedKey(subject.issuerKeyRef(), command.formatId(),
ISSUE_INT_REJECTED, Optional.of(subject.caId()));
requireSameManagedKey(subjectSpki, subjectProof.exactPublicKey(), ISSUE_INT_REJECTED, command.formatId(),
@@ -612,7 +626,7 @@ public final class DefaultCaService implements CaService {
Credential backendCredential;
try {
- backendCredential = issuerBackend.issueIntermediateCertificate(gated, issuerCredential.encoded(),
+ backendCredential = issuerBackend.issueIntermediateCertificate(gated, issuerCredential.content(),
issuer.issuerKeyRef());
} catch (RuntimeException ex) { // NOPMD - reject malformed or mutable framework output
throw proofGate.rejection(ISSUE_INT_REJECTED, command.formatId(), Optional.of(subject.caId()),
@@ -833,10 +847,12 @@ public final class DefaultCaService implements CaService {
private void requireIssuerKeyBinding(CaRecord issuer, Credential credential, FormatId formatId, String action,
Optional objectId) {
try {
- if (credential.encoded().encoding() != Encoding.DER) {
+ if (credential.content().encoding() != Encoding.DER) {
throw proofGate.rejection(action, formatId, objectId, "ISSUER_CREDENTIAL_INVALID");
}
- X509CertificateHolder holder = new X509CertificateHolder(credential.encoded().bytes());
+ CredentialContent.validateCertificate(store, credential.content(), authority, Optional.empty());
+ X509CertificateHolder holder = new X509CertificateHolder(CredentialContent.materializeForBc(store,
+ credential.content()));
CaProofGate.ManagedKeyProof proof = proofGate.proveManagedKey(issuer.issuerKeyRef(), formatId, action,
objectId);
if (!MessageDigest.isEqual(proof.exactPublicKey().bytes(), holder.getSubjectPublicKeyInfo().getEncoded())) {
@@ -855,8 +871,13 @@ public final class DefaultCaService implements CaService {
if (!matchesCaCredentialEnvelope(credential, request, subjectCaId)) {
throw proofGate.rejection(action, framework.formatId(), Optional.of(subjectCaId), mismatchCode);
}
- X509CertificateHolder holder = new X509CertificateHolder(credential.encoded().bytes());
- X509CertificateHolder issuerHolder = new X509CertificateHolder(issuerCredential.encoded().bytes());
+ CredentialContent.validateCertificate(store, credential.content(), authority,
+ Optional.of(signatureIdentity));
+ CredentialContent.validateCertificate(store, issuerCredential.content(), authority, Optional.empty());
+ byte[] credentialDer = CredentialContent.materializeForBc(store, credential.content());
+ X509CertificateHolder holder = new X509CertificateHolder(credentialDer);
+ X509CertificateHolder issuerHolder = new X509CertificateHolder(CredentialContent.materializeForBc(store,
+ issuerCredential.content()));
byte[] actualSpki = holder.getSubjectPublicKeyInfo().getEncoded();
Extension constraintsExtension = holder.getExtension(Extension.basicConstraints);
BasicConstraints constraints = constraintsExtension == null ? null
@@ -872,7 +893,7 @@ public final class DefaultCaService implements CaService {
actualSpki)
|| !matchesCaCertificatePolicy(holder, request, constraintsExtension, constraints,
keyUsageExtension, keyUsage)
- || !matchesCaCredentialMetadata(credential, holder, request, actualSpki)) {
+ || !matchesCaCredentialMetadata(credential, holder, request, actualSpki, credentialDer)) {
throw proofGate.rejection(action, framework.formatId(), Optional.of(subjectCaId), mismatchCode);
}
CredentialProfileBindings.requireCaBinding(credential.profileBinding(), request.profileReference());
@@ -885,7 +906,7 @@ public final class DefaultCaService implements CaService {
private boolean matchesCaCredentialEnvelope(Credential credential, ValidatedCaCertificateRequest request,
PkiId subjectCaId) {
- return framework.formatId().equals(credential.formatId()) && credential.encoded().encoding() == Encoding.DER
+ return framework.formatId().equals(credential.formatId()) && credential.content().encoding() == Encoding.DER
&& credential.status() == CredentialStatus.ISSUED
&& credential.subjectRef().equals(request.subjectRef())
&& credential.issuerRef()
@@ -915,9 +936,9 @@ public final class DefaultCaService implements CaService {
}
private static boolean matchesCaCredentialMetadata(Credential credential, X509CertificateHolder holder,
- ValidatedCaCertificateRequest request, byte[] actualSpki) {
+ ValidatedCaCertificateRequest request, byte[] actualSpki, byte[] credentialDer) {
return credential.publicKeyId().equals(new PkiId("spki:" + sha256Hex(actualSpki)))
- && credential.credentialId().equals(new PkiId("x509:" + sha256Hex(credential.encoded().bytes())))
+ && credential.credentialId().equals(new PkiId("x509:" + sha256Hex(credentialDer)))
&& credential.serialOrUniqueId().equals(holder.getSerialNumber().toString())
&& credential.validity().notBefore().getEpochSecond() == holder.getNotBefore().toInstant()
.getEpochSecond()
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/DefaultIssuanceService.java b/pki/src/main/java/zeroecho/pki/impl/core/DefaultIssuanceService.java
index 8d94b2e..3878015 100644
--- a/pki/src/main/java/zeroecho/pki/impl/core/DefaultIssuanceService.java
+++ b/pki/src/main/java/zeroecho/pki/impl/core/DefaultIssuanceService.java
@@ -78,6 +78,9 @@ import zeroecho.pki.api.request.ParsedCertificationRequest;
import zeroecho.pki.api.request.ProofOfPossessionResult;
import zeroecho.pki.api.request.ProofOfPossessionStatus;
import zeroecho.pki.impl.framework.x509.bc.BcX509Attributes;
+import zeroecho.pki.impl.framework.x509.bc.BcX509CredentialFramework;
+import zeroecho.pki.impl.framework.x509.X509AuthoritySnapshot;
+import zeroecho.pki.impl.framework.x509.X509BuiltInDefaults;
import zeroecho.pki.impl.framework.x509.bc.BcX509ProfileSupport;
import zeroecho.pki.spi.audit.AuditSink;
import zeroecho.pki.spi.framework.CredentialFramework;
@@ -157,6 +160,8 @@ public final class DefaultIssuanceService implements IssuanceService {
private final EffectiveCredentialStatusResolver statusResolver;
private final ProfileService profileService;
private final Clock clock;
+ private final X509AuthoritySnapshot authority;
+ private final zeroecho.core.spec.AlgorithmIdentity expectedSignature;
/**
* Creates the issuance service bound to the supplied persistence and framework
@@ -185,6 +190,11 @@ public final class DefaultIssuanceService implements IssuanceService {
this.statusResolver = Objects.requireNonNull(statusResolver, "statusResolver");
this.profileService = Objects.requireNonNull(profileService, "profileService");
this.clock = Objects.requireNonNull(clock, "clock");
+ if (!(framework instanceof BcX509CredentialFramework x509Framework)) {
+ throw new IllegalArgumentException("X.509 issuance requires an algorithm authority");
+ }
+ this.authority = x509Framework.authority();
+ this.expectedSignature = authority.resolveDefault(X509BuiltInDefaults.PKI_SIGNATURE_DEFAULT_V1).signature();
}
/**
@@ -250,7 +260,7 @@ public final class DefaultIssuanceService implements IssuanceService {
ValidatedCertificateRequest validated;
try {
validated = CertificateProfileValidator.validate(candidate, profile, active.reference(), issuerCred,
- evaluationTime);
+ evaluationTime, authority);
} catch (PkiException exception) {
throw rejection(candidate.request(), statusCode(exception));
}
@@ -259,7 +269,7 @@ public final class DefaultIssuanceService implements IssuanceService {
CredentialBundle bundle;
try {
bundle = CredentialSnapshots
- .copy(issuerBackend.issueEndEntity(validated, issuerCred.encoded(), issuer.issuerKeyRef(), serial));
+ .copy(issuerBackend.issueEndEntity(validated, issuerCred.content(), issuer.issuerKeyRef(), serial));
} catch (RuntimeException ex) { // NOPMD - framework output must cross the snapshot boundary
throw rejection(candidate.request(), "BACKEND_CREDENTIAL_MISMATCH");
}
@@ -441,14 +451,19 @@ public final class DefaultIssuanceService implements IssuanceService {
try {
CredentialProfileBindings.requireEndEntityBinding(credential.profileBinding(),
validated.profileReference());
- if (!framework.formatId().equals(credential.formatId()) || credential.encoded().encoding() != Encoding.DER
+ if (!framework.formatId().equals(credential.formatId()) || credential.content().encoding() != Encoding.DER
|| !credential.subjectRef().equals(validated.subjectRef())
|| !credential.issuerRef().equals(new zeroecho.pki.api.IssuerRef(validated.issuerCaId()))
|| credential.status() != CredentialStatus.ISSUED) {
throw rejection(auditRequest, "BACKEND_CREDENTIAL_MISMATCH");
}
- X509CertificateHolder holder = new X509CertificateHolder(credential.encoded().bytes());
- X509CertificateHolder issuerHolder = new X509CertificateHolder(issuerCredential.encoded().bytes());
+ CredentialContent.validateCertificate(store, credential.content(), authority,
+ Optional.of(expectedSignature));
+ CredentialContent.validateCertificate(store, issuerCredential.content(), authority, Optional.empty());
+ byte[] credentialDer = CredentialContent.materializeForBc(store, credential.content());
+ X509CertificateHolder holder = new X509CertificateHolder(credentialDer);
+ X509CertificateHolder issuerHolder = new X509CertificateHolder(
+ CredentialContent.materializeForBc(store, issuerCredential.content()));
byte[] actualSpki = holder.getSubjectPublicKeyInfo().getEncoded();
if (!MessageDigest.isEqual(validated.exactPublicKey().bytes(), actualSpki)
|| !holder.getSubject().equals(BcX509ProfileSupport.subject(validated.subjectRdns()))
@@ -457,7 +472,7 @@ public final class DefaultIssuanceService implements IssuanceService {
new JcaContentVerifierProviderBuilder().build(issuerHolder.getSubjectPublicKeyInfo()))
|| !holder.getSerialNumber().equals(allocatedSerial)
|| !credential.publicKeyId().equals(new PkiId("spki:" + sha256Hex(actualSpki)))
- || !credential.credentialId().equals(new PkiId("x509:" + sha256Hex(credential.encoded().bytes())))
+ || !credential.credentialId().equals(new PkiId("x509:" + sha256Hex(credentialDer)))
|| !credential.serialOrUniqueId().equals(holder.getSerialNumber().toString())
|| !credential.validity().equals(validated.validity())
|| validated.validity().notBefore().getEpochSecond() != holder.getNotBefore().toInstant()
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/DefaultRevocationService.java b/pki/src/main/java/zeroecho/pki/impl/core/DefaultRevocationService.java
index 937d23c..ad2ad7d 100644
--- a/pki/src/main/java/zeroecho/pki/impl/core/DefaultRevocationService.java
+++ b/pki/src/main/java/zeroecho/pki/impl/core/DefaultRevocationService.java
@@ -112,8 +112,18 @@ public final class DefaultRevocationService implements RevocationService {
@SuppressWarnings("PMD.AvoidCatchingGenericException")
public List search(RevocationQuery query) {
Objects.requireNonNull(query, "query");
- try {
- return store.listRevocationJournals().stream().filter(journal -> matches(journal, query)).toList();
+ try (zeroecho.pki.spi.store.RevocationSnapshot snapshot = store.openRevocationSnapshot();
+ zeroecho.pki.spi.store.RevocationSnapshot.Cursor cursor = snapshot.openCursor()) {
+ List matching = new java.util.ArrayList<>();
+ while (cursor.next()) {
+ RevocationJournal journal = cursor.current();
+ if (matches(journal, query)) {
+ matching.add(journal);
+ }
+ }
+ return List.copyOf(matching);
+ } catch (java.io.IOException failure) {
+ throw new PkiException("Revocation snapshot failed: code=STORE_FAILED", failure);
} catch (RuntimeException failure) {
throw sanitized(failure);
}
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/DefaultStatusObjectService.java b/pki/src/main/java/zeroecho/pki/impl/core/DefaultStatusObjectService.java
index 032856b..78ac0fc 100644
--- a/pki/src/main/java/zeroecho/pki/impl/core/DefaultStatusObjectService.java
+++ b/pki/src/main/java/zeroecho/pki/impl/core/DefaultStatusObjectService.java
@@ -33,17 +33,19 @@
******************************************************************************/
package zeroecho.pki.impl.core;
+import java.io.IOException;
import java.math.BigInteger;
import java.time.Instant;
import java.util.Arrays;
-import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
-import java.util.Set;
+import java.util.OptionalLong;
import org.bouncycastle.cert.X509CertificateHolder;
+import zeroecho.core.io.CancellationSignal;
+import zeroecho.core.io.RepeatableContent;
import zeroecho.pki.api.Encoding;
import zeroecho.pki.api.PkiException;
import zeroecho.pki.api.PkiId;
@@ -51,6 +53,7 @@ import zeroecho.pki.api.StatusObjectService;
import zeroecho.pki.api.attr.AttributeValue;
import zeroecho.pki.api.ca.CaRecord;
import zeroecho.pki.api.ca.CaState;
+import zeroecho.pki.api.content.DurableContentReference;
import zeroecho.pki.api.credential.Credential;
import zeroecho.pki.api.credential.CredentialUse;
import zeroecho.pki.api.credential.EffectiveCredentialStatus;
@@ -65,11 +68,18 @@ import zeroecho.pki.api.status.StatusObjectQuery;
import zeroecho.pki.api.status.StatusObjectType;
import zeroecho.pki.impl.core.attr.SimpleAttributeSet;
import zeroecho.pki.impl.framework.x509.bc.BcX509Attributes;
+import zeroecho.pki.impl.framework.x509.bc.BcX509SignedObjectValidator;
+import zeroecho.pki.impl.framework.x509.X509AuthoritySnapshot;
+import zeroecho.pki.impl.framework.x509.X509ExecutionPlan;
+import zeroecho.pki.impl.framework.x509.X509SignedObjectCompletion;
+import zeroecho.pki.spi.crypto.SignatureWorkflow;
import zeroecho.pki.impl.framework.x509.bc.BcX509CredentialFramework;
import zeroecho.pki.spi.audit.AuditSink;
import zeroecho.pki.spi.framework.CredentialFramework;
import zeroecho.pki.spi.framework.CrlEntry;
+import zeroecho.pki.spi.framework.CrlEntrySource;
import zeroecho.pki.spi.store.PkiStore;
+import zeroecho.pki.spi.store.RevocationSnapshot;
/**
* Default implementation of {@link StatusObjectService}.
@@ -126,6 +136,7 @@ public final class DefaultStatusObjectService implements StatusObjectService {
private final CredentialFramework framework;
private final AuditSink auditSink;
private final EffectiveCredentialStatusResolver statusResolver;
+ private final X509AuthoritySnapshot authority;
/**
* Creates a status object service bound to the supplied persistence and
@@ -141,11 +152,12 @@ public final class DefaultStatusObjectService implements StatusObjectService {
* @throws NullPointerException if an argument is {@code null}
*/
public DefaultStatusObjectService(PkiStore store, CredentialFramework framework, AuditSink auditSink,
- EffectiveCredentialStatusResolver statusResolver) {
+ EffectiveCredentialStatusResolver statusResolver, X509AuthoritySnapshot authority) {
this.store = Objects.requireNonNull(store, "store");
this.framework = Objects.requireNonNull(framework, "framework");
this.auditSink = Objects.requireNonNull(auditSink, "auditSink");
this.statusResolver = Objects.requireNonNull(statusResolver, "statusResolver");
+ this.authority = Objects.requireNonNull(authority, "authority");
}
/**
@@ -193,6 +205,7 @@ public final class DefaultStatusObjectService implements StatusObjectService {
* generated status object fails
*/
@Override
+ @SuppressWarnings({ "PMD.AvoidCatchingGenericException", "PMD.PreserveStackTrace" })
public StatusObject generate(StatusObjectGenerateCommand command) {
if (command == null) {
throw new IllegalArgumentException("command must not be null");
@@ -206,59 +219,79 @@ public final class DefaultStatusObjectService implements StatusObjectService {
}
EffectiveCredentialStatusResolver.Evaluation statusEvaluation = statusResolver.beginEvaluation();
Credential issuerCred = selectIssuerCredential(ca, command, statusEvaluation);
- List crlEntries = command.type() == StatusObjectType.CRL
- ? collectCrlEntries(command.issuerCaId(), statusEvaluation.evaluationTime())
- : List.of();
SimpleAttributeSet.Builder b = SimpleAttributeSet.builder();
b.putAll(command.attributes());
- b.put(BcX509Attributes.ISSUER_CERT_DER, new AttributeValue.BytesValue(issuerCred.encoded().bytes()));
+ b.put(BcX509Attributes.ISSUER_CERT_DER,
+ new AttributeValue.BytesValue(CredentialContent.materializeForBc(store, issuerCred.content())));
b.put(BcX509Attributes.ISSUER_KEYREF, new AttributeValue.StringValue(ca.issuerKeyRef().value()));
StatusObjectGenerateCommand wired = new StatusObjectGenerateCommand(command.issuerCaId(), command.type(),
command.formatId(), b.build());
- if (command.type() == StatusObjectType.CRL) {
- return generateAndPersistCrl(wired, crlEntries);
- }
- StatusObject obj = framework.statusObjectGenerator().generate(wired, crlEntries);
- store.putStatusObject(obj);
- return obj;
- }
-
- // Framework, signing, and store failures may carry provider or persisted
- // material. CRL generation deliberately replaces the complete boundary with
- // one fresh cause-free and suppressed-free exception.
- @SuppressWarnings({ "PMD.AvoidCatchingGenericException", "PMD.PreserveStackTrace" })
- private StatusObject generateAndPersistCrl(StatusObjectGenerateCommand command, List entries) {
- try {
- StatusObject generated = framework.statusObjectGenerator().generate(command, entries);
- store.putStatusObject(generated);
- return generated;
- } catch (RuntimeException exception) {
- throw crlGenerationFailure();
- }
- }
-
- // Store and parser failures may contain persisted material; the complete
- // collection boundary deliberately replaces every cause with one stable code.
- @SuppressWarnings({ "PMD.AvoidCatchingGenericException", "PMD.PreserveStackTrace" })
- private List collectCrlEntries(PkiId issuerCaId, Instant evaluationTime) {
- try {
- List journals = Objects.requireNonNull(store.listRevocationJournals(),
- "revocation journals");
- List entries = new java.util.ArrayList<>();
- Set serials = new HashSet<>();
- for (RevocationJournal journal : journals) {
- collectCrlEntry(issuerCaId, evaluationTime, journal, serials).ifPresent(entries::add);
+ try (CrlEntrySource entries = command.type() == StatusObjectType.CRL
+ ? openCrlEntries(command.issuerCaId(), statusEvaluation.evaluationTime())
+ : new EmptyCrlEntrySource()) {
+ if (command.type() == StatusObjectType.CRL) {
+ return generateAndPersistCrl(wired, entries, issuerCred);
}
- return List.copyOf(entries);
- } catch (RuntimeException exception) {
+ return generateAndPersistOther();
+ } catch (IOException | RuntimeException exception) {
throw crlGenerationFailure();
}
}
- private Optional collectCrlEntry(PkiId issuerCaId, Instant evaluationTime, RevocationJournal journal,
- Set serials) {
+ private StatusObject generateAndPersistCrl(StatusObjectGenerateCommand command, CrlEntrySource entries,
+ Credential issuer) {
+ X509SignedObjectCompletion completion = framework.statusObjectGenerator().generate(command, entries);
+ StatusObject generated = authority.requireStatusCompletion(completion);
+ X509ExecutionPlan signingPlan = authority.requireStatusSigningPlan(completion);
+ requirePersistableContent(generated.content());
+ boolean accepted = false;
+ try {
+ byte[] issuerDer = CredentialContent.materializeForBc(store, issuer.content());
+ try (RepeatableContent content = store.stagedContent().openContent(generated.content())) {
+ X509CertificateHolder holder = new X509CertificateHolder(issuerDer);
+ new BcX509SignedObjectValidator(authority).validateGeneratedCrl(content, signingPlan,
+ holder.getSubjectPublicKeyInfo(), CancellationSignal.NONE);
+ } finally {
+ Arrays.fill(issuerDer, (byte) 0);
+ }
+ store.putStatusObject(generated);
+ accepted = true;
+ return generated;
+ } catch (IOException exception) {
+ throw new PkiException("Status postcondition validation failed: code=CONTENT_IO_FAILED", exception);
+ } finally {
+ if (!accepted) {
+ releaseRejectedContent(generated.content());
+ }
+ }
+ }
+
+ private StatusObject generateAndPersistOther() {
+ throw new PkiException("Unsupported status object type");
+ }
+
+ private void requirePersistableContent(DurableContentReference content) {
+ if (content.lifecycle() != DurableContentReference.Lifecycle.PERSISTED
+ || !store.stagedContent().contentStoreId().equals(content.storeId())) {
+ throw new PkiException("Status content lifecycle invalid: code=STAGED_CONTENT_FOREIGN_RUNTIME");
+ }
+ }
+
+ private void releaseRejectedContent(DurableContentReference content) {
+ try {
+ store.stagedContent().retireUnownedContent(content);
+ } catch (IOException cleanupFailure) {
+ throw new PkiException("Rejected status content cleanup failed: code=CONTENT_IO_FAILED", cleanupFailure);
+ }
+ }
+
+ private CrlEntrySource openCrlEntries(PkiId issuerCaId, Instant evaluationTime) {
+ return new JournalCrlEntrySource(store.openRevocationSnapshot(), issuerCaId, evaluationTime);
+ }
+
+ private Optional collectCrlEntry(PkiId issuerCaId, Instant evaluationTime, RevocationJournal journal) {
Objects.requireNonNull(journal, "journal");
RevocationTransition latest = Objects.requireNonNull(journal.latest(), "latest transition");
if (latest.time().isAfter(evaluationTime)) {
@@ -273,13 +306,10 @@ public final class DefaultStatusObjectService implements StatusObjectService {
return Optional.empty();
}
if (!BcX509CredentialFramework.FORMAT_ID.equals(credential.formatId())
- || credential.encoded().encoding() != Encoding.DER) {
+ || credential.content().encoding() != Encoding.DER) {
throw crlGenerationFailure();
}
BigInteger serial = certificateSerial(credential);
- if (!serials.add(serial)) {
- throw crlGenerationFailure();
- }
RevocationReason reason = switch (latest.state()) {
case HELD -> RevocationReason.CERTIFICATE_HOLD;
case PERMANENTLY_REVOKED ->
@@ -289,11 +319,131 @@ public final class DefaultStatusObjectService implements StatusObjectService {
return Optional.of(new CrlEntry(serial, latest.time(), reason));
}
+ /** Stable restartable view over one revocation-store snapshot. */
+ private final class JournalCrlEntrySource implements CrlEntrySource {
+ private final RevocationSnapshot snapshot;
+ private final PkiId issuerCaId;
+ private final Instant evaluationTime;
+
+ private JournalCrlEntrySource(RevocationSnapshot snapshot, PkiId issuerCaId, Instant evaluationTime) {
+ this.snapshot = snapshot;
+ this.issuerCaId = issuerCaId;
+ this.evaluationTime = evaluationTime;
+ }
+
+ @Override
+ public Cursor openCursor() throws IOException {
+ return new JournalCrlCursor(snapshot.openCursor(), issuerCaId, evaluationTime);
+ }
+
+ @Override
+ public OptionalLong count() {
+ return OptionalLong.empty();
+ }
+
+ @Override
+ public void close() throws IOException {
+ snapshot.close();
+ }
+ }
+
+ /** Bounded cursor translating authoritative journals into CRL entries. */
+ private final class JournalCrlCursor implements CrlEntrySource.Cursor {
+ private final RevocationSnapshot.Cursor cursor;
+ private final PkiId issuerCaId;
+ private final Instant evaluationTime;
+ private CrlEntry current;
+ private long ordinal = -1L;
+
+ private JournalCrlCursor(RevocationSnapshot.Cursor cursor, PkiId issuerCaId, Instant evaluationTime) {
+ this.cursor = cursor;
+ this.issuerCaId = issuerCaId;
+ this.evaluationTime = evaluationTime;
+ }
+
+ @Override
+ public boolean next() throws IOException {
+ while (cursor.next()) {
+ Optional candidate = collectCrlEntry(issuerCaId, evaluationTime, cursor.current());
+ if (candidate.isPresent()) {
+ current = candidate.orElseThrow();
+ ordinal = Math.addExact(ordinal, 1L);
+ return true;
+ }
+ }
+ current = null;
+ return false;
+ }
+
+ @Override
+ public CrlEntry current() {
+ if (current == null) {
+ throw new IllegalStateException("CRL entry cursor is not positioned");
+ }
+ return current;
+ }
+
+ @Override
+ public long ordinal() {
+ if (current == null) {
+ throw new IllegalStateException("CRL entry cursor is not positioned");
+ }
+ return ordinal;
+ }
+
+ @Override
+ public void close() throws IOException {
+ cursor.close();
+ current = null;
+ }
+ }
+
+ /** Empty source used for status formats without revocation entries. */
+ private static final class EmptyCrlEntrySource implements CrlEntrySource {
+ @Override
+ public Cursor openCursor() {
+ return new EmptyCrlCursor();
+ }
+
+ @Override
+ public OptionalLong count() {
+ return OptionalLong.of(0L);
+ }
+
+ @Override
+ public void close() {
+ // No resources.
+ }
+ }
+
+ /** Resource-free cursor for an empty status-entry source. */
+ private static final class EmptyCrlCursor implements CrlEntrySource.Cursor {
+ @Override
+ public boolean next() {
+ return false;
+ }
+
+ @Override
+ public CrlEntry current() {
+ throw new IllegalStateException("Empty CRL cursor has no entry");
+ }
+
+ @Override
+ public long ordinal() {
+ throw new IllegalStateException("Empty CRL cursor has no ordinal");
+ }
+
+ @Override
+ public void close() {
+ // No resources.
+ }
+ }
+
// Parser failures can contain persisted certificate details; the original
// cause is intentionally removed at this public service boundary.
@SuppressWarnings("PMD.PreserveStackTrace")
- private static BigInteger certificateSerial(Credential credential) {
- byte[] der = credential.encoded().bytes();
+ private BigInteger certificateSerial(Credential credential) {
+ byte[] der = CredentialContent.materializeForBc(store, credential.content());
BigInteger serial;
try {
serial = new X509CertificateHolder(der).getSerialNumber();
diff --git a/pki/src/main/java/zeroecho/pki/impl/core/async/PkiSigningBus.java b/pki/src/main/java/zeroecho/pki/impl/core/async/PkiSigningBus.java
index ab0e6fc..2ab7c07 100644
--- a/pki/src/main/java/zeroecho/pki/impl/core/async/PkiSigningBus.java
+++ b/pki/src/main/java/zeroecho/pki/impl/core/async/PkiSigningBus.java
@@ -40,6 +40,7 @@ import java.time.Instant;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -48,18 +49,27 @@ import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
+import zeroecho.core.spec.AlgorithmIdentity;
+import zeroecho.core.spi.AlgorithmExecutionCapability;
+import zeroecho.core.io.RepeatableContent;
import zeroecho.pki.api.EncodedObject;
import zeroecho.pki.api.Encoding;
import zeroecho.pki.api.KeyRef;
import zeroecho.pki.api.PkiException;
import zeroecho.pki.api.PkiId;
+import zeroecho.pki.api.content.DurableContentReference;
+import zeroecho.pki.api.content.DurableContentOwner;
import zeroecho.pki.api.audit.Principal;
import zeroecho.pki.api.orch.OrchestrationDurabilityPolicy;
import zeroecho.pki.api.orch.SigningSubmissionId;
import zeroecho.pki.api.orch.WorkflowStateRecord;
+import zeroecho.pki.impl.framework.x509.X509AuthoritySnapshot;
+import zeroecho.pki.impl.framework.x509.X509ExecutionPlan;
import zeroecho.pki.spi.crypto.SignatureWorkflow;
import zeroecho.pki.spi.store.PkiStore;
import zeroecho.pki.spi.store.SignWorkflowStore;
+import zeroecho.pki.spi.store.ContentSink;
+import zeroecho.pki.spi.store.TemporaryUniqueIndex;
import zeroecho.pki.util.async.AsyncEndpoint;
import zeroecho.pki.util.async.AsyncState;
import zeroecho.pki.util.async.AsyncStatus;
@@ -112,6 +122,7 @@ public final class PkiSigningBus implements AutoCloseable {
private final PkiStore store;
private final DurableAsyncBus bus;
private final SignatureWorkflow signer;
+ private final X509AuthoritySnapshot authority;
private final SecureRandom random;
private final String namespace;
private final OperationCoordinator coordinator;
@@ -122,58 +133,70 @@ public final class PkiSigningBus implements AutoCloseable {
private final OrchestrationDurabilityPolicy durabilityPolicy;
/**
- * Creates a signing bus.
- *
- * @param store persistent store (source of truth for
- * continuation state)
- * @param signer signature workflow
- * @param durableLineStorePath path to append-only line store file
- */
- public PkiSigningBus(PkiStore store, SignatureWorkflow signer, Path durableLineStorePath) {
- this(store, signer, durableLineStorePath, resolveDisplaySuffixMaxLen(Optional.empty()),
- OrchestrationDurabilityPolicy.DURABLE_MIN_STATE);
- }
-
- /**
- * Creates a signing bus.
+ * Creates a signing bus in an explicitly composed runtime authority graph.
*
*
- * The {@code displaySuffixMaxLen} parameter controls the maximum number of
- * characters appended after {@code '#'} in operation identifiers returned by
- * {@link #canonicalizeOperationId(PkiId, Principal)}. If
- * {@code displaySuffixMaxLen} is not positive, the constructor fails.
+ * The authority must already bind every signing identity declared by
+ * {@code signer} to that exact workflow instance for the {@code SIGN}
+ * direction. This constructor validates ownership before registering the
+ * workflow or activating durable state. It never constructs an internal
+ * authority, accepts an authority from another runtime, or handles private
+ * key material.
*
*
- * @param store persistent store (source of truth for
- * continuation state)
- * @param signer signature workflow
- * @param durableLineStorePath path to append-only line store file
- * @param displaySuffixMaxLen maximum number of characters after {@code '#'}
+ * @param store persistent store
+ * @param signer exact signing workflow owned by
+ * {@code authority}
+ * @param durableLineStorePath durable bus path
+ * @param authority shared immutable runtime authority
+ * @throws NullPointerException if any argument is {@code null}
+ * @throws IllegalArgumentException if the authority does not own the exact
+ * workflow for every declared signing
+ * identity and the {@code SIGN} direction
*/
- public PkiSigningBus(PkiStore store, SignatureWorkflow signer, Path durableLineStorePath, int displaySuffixMaxLen) {
- this(store, signer, durableLineStorePath, displaySuffixMaxLen, OrchestrationDurabilityPolicy.DURABLE_MIN_STATE);
+ public PkiSigningBus(PkiStore store, SignatureWorkflow signer, Path durableLineStorePath,
+ X509AuthoritySnapshot authority) {
+ this(store, signer, durableLineStorePath, resolveDisplaySuffixMaxLen(Optional.empty()),
+ OrchestrationDurabilityPolicy.DURABLE_MIN_STATE, authority);
}
/**
- * Creates a signing bus with explicit workflow continuation durability policy.
+ * Creates a signing bus bound to one immutable algorithm authority snapshot.
*
- * @param store persistent store (source of truth for
- * continuation state)
- * @param signer signature workflow
- * @param durableLineStorePath path to append-only line store file
- * @param displaySuffixMaxLen maximum number of characters after {@code '#'}
- * @param durabilityPolicy durability policy used when persisting workflow
- * state
+ *
+ * The authority must already bind every signing identity declared by
+ * {@code signer} to that exact workflow instance for the {@code SIGN}
+ * direction. Validation occurs before registration or durable-state
+ * activation. The constructor does not derive an authority from provider
+ * strings, accept cross-runtime workflow ownership, or access private key
+ * material.
+ *
+ *
+ * @param store persistent store
+ * @param signer exact workflow represented by the snapshot
+ * capability
+ * @param durableLineStorePath durable bus path
+ * @param displaySuffixMaxLen display suffix bound
+ * @param durabilityPolicy continuation durability policy
+ * @param authority immutable runtime authority
+ * @throws NullPointerException if any reference argument is {@code null}
+ * @throws IllegalArgumentException if the display suffix bound is invalid or
+ * the authority does not own the exact
+ * workflow for every declared signing
+ * identity and the {@code SIGN} direction
*/
public PkiSigningBus(PkiStore store, SignatureWorkflow signer, Path durableLineStorePath, int displaySuffixMaxLen,
- OrchestrationDurabilityPolicy durabilityPolicy) {
+ OrchestrationDurabilityPolicy durabilityPolicy, X509AuthoritySnapshot authority) {
Objects.requireNonNull(store, "store");
Objects.requireNonNull(signer, "signer");
Objects.requireNonNull(durableLineStorePath, "durableLineStorePath");
Objects.requireNonNull(durabilityPolicy, "durabilityPolicy");
+ X509AuthoritySnapshot exactAuthority = Objects.requireNonNull(authority, "authority");
+ validateSigningAuthority(signer, exactAuthority);
this.store = store;
this.signer = signer;
+ this.authority = exactAuthority;
this.random = new SecureRandom();
this.namespace = store.signingNamespace() + "." + signer.id();
signer.validateSigningDomain(this.namespace, store.signingHorizon(), store.signingPermittedSkew());
@@ -189,13 +212,21 @@ public final class PkiSigningBus implements AutoCloseable {
for (WorkflowStateRecord state : store.listWorkflowStates()) {
if (TYPE_SIGN.equals(state.type()) && state.payload().isPresent()) {
try {
- SignContinuation.decode(state.payload().orElseThrow());
+ SignContinuation continuation = SignContinuation.decode(state.payload().orElseThrow(),
+ store.stagedContent());
+ X509ExecutionPlan plan = authority.planSigning(continuation.algorithmId,
+ workflowImplementationId(signer), SignatureWorkflow.class);
+ authority.authorize(plan, signer, AlgorithmExecutionCapability.Direction.SIGN);
+ AlgorithmIdentity identity = plan.selection().requested();
+ if (!identity.canonicalForm().equals(continuation.algorithmId)) {
+ throw new IllegalArgumentException("Persisted sign continuation is not canonical");
+ }
} catch (RuntimeException ex) { // NOPMD - malformed persisted state must fail closed
throw new PkiException("Invalid persisted sign continuation: code=CONTINUATION_INVALID");
}
}
}
- this.endpoint = new SignatureWorkflowEndpoint(store, signer, coordinator, externalActions);
+ this.endpoint = new SignatureWorkflowEndpoint(store, signer, coordinator, externalActions, authority);
this.bus.registerEndpoint(ENDPOINT_SIGNER, endpoint);
this.signerRegistration = signer.register(endpoint::onProviderStatusChanged);
for (SignWorkflowStore.Record record : store.listSignRecords()) {
@@ -206,6 +237,109 @@ public final class PkiSigningBus implements AutoCloseable {
}
}
+ /**
+ * Returns the immutable authority used by this workflow graph.
+ *
+ * @return authority snapshot
+ */
+ public X509AuthoritySnapshot authority() {
+ return authority;
+ }
+
+ /**
+ * Begins runtime-owned durable staging for one signing input.
+ *
+ * @param encoding content encoding
+ * @return atomic staged-content sink
+ */
+ public ContentSink beginSigningContent(Encoding encoding) {
+ return beginContent(encoding, DurableContentReference.Lifecycle.OPERATION);
+ }
+
+ /**
+ * Begins atomic runtime-owned content staging.
+ *
+ *
+ * The sink writes to the injected staged-content store and never retains an
+ * aggregate payload in the bus. ZeroEcho core imposes no arbitrary
+ * product-wide aggregate size limit.
+ *
+ *
+ * @param encoding content encoding
+ * @param lifecycle ownership and retirement class
+ * @return atomic streamed sink
+ * @throws PkiException if staging cannot begin
+ */
+ public ContentSink beginContent(Encoding encoding, DurableContentReference.Lifecycle lifecycle) {
+ try {
+ return store.stagedContent().beginContent(Objects.requireNonNull(encoding, "encoding"),
+ Objects.requireNonNull(lifecycle, "lifecycle"));
+ } catch (java.io.IOException ex) {
+ throw new PkiException("Content staging failed: code=SPOOL_STORAGE_FAILED");
+ }
+ }
+
+ /**
+ * Opens immutable repeatable content owned by this runtime.
+ *
+ * @param reference opaque durable content reference
+ * @return repeatable content
+ * @throws PkiException if the content is missing, incomplete, corrupt, or
+ * belongs to another runtime store
+ */
+ public RepeatableContent openContent(DurableContentReference reference) {
+ try {
+ return store.stagedContent().openContent(Objects.requireNonNull(reference, "reference"));
+ } catch (java.io.IOException ex) {
+ throw new PkiException("Content open failed: code=CONTENT_INTEGRITY_FAILED");
+ }
+ }
+
+ /**
+ * Releases staged content from this runtime.
+ *
+ * @param reference content reference
+ * @throws PkiException if cleanup fails
+ */
+ public void releaseContent(DurableContentReference reference) {
+ try {
+ store.stagedContent().retireUnownedContent(Objects.requireNonNull(reference, "reference"));
+ } catch (java.io.IOException ex) {
+ throw new PkiException("Content cleanup failed: code=SPOOL_STORAGE_FAILED");
+ }
+ }
+
+ /**
+ * Begins a runtime-owned file-backed uniqueness index.
+ *
+ * @return temporary uniqueness index
+ * @throws PkiException if temporary storage cannot be created
+ */
+ public TemporaryUniqueIndex beginUniqueIndex() {
+ try {
+ return store.stagedContent().beginUniqueIndex();
+ } catch (java.io.IOException ex) {
+ throw new PkiException("Temporary index failed: code=SPOOL_STORAGE_FAILED");
+ }
+ }
+
+ private static String workflowImplementationId(SignatureWorkflow workflow) {
+ return "workflow." + workflow.id();
+ }
+
+ private static void validateSigningAuthority(SignatureWorkflow workflow, X509AuthoritySnapshot authority) {
+ Set supportedAlgorithms = Set.copyOf(
+ Objects.requireNonNull(workflow.supportedAlgorithms(), "workflow.supportedAlgorithms"));
+ if (supportedAlgorithms.isEmpty()) {
+ throw new IllegalArgumentException("Signature workflow must declare a signing identity");
+ }
+ for (String algorithm : supportedAlgorithms) {
+ X509ExecutionPlan plan = authority.planSigning(algorithm,
+ workflowImplementationId(workflow), SignatureWorkflow.class);
+ authority.authorize(plan, workflow, AlgorithmExecutionCapability.Direction.SIGN);
+ }
+ }
+
/**
* Builds a canonical, globally unique operation identifier derived from tuple
* (owner, clientOpId).
@@ -242,24 +376,29 @@ public final class PkiSigningBus implements AutoCloseable {
* @param owner owner principal
* @param keyRef signing key reference
* @param algorithmId signature algorithm id
- * @param payload bytes to sign
+ * @param content durable repeatable content to sign
* @param ttl time-to-live
* @param workflowPayload minimal continuation payload
*/
- public void submitSign(PkiId opId, Principal owner, KeyRef keyRef, String algorithmId, EncodedObject payload,
+ public void submitSign(PkiId opId, Principal owner, KeyRef keyRef, String algorithmId,
+ DurableContentReference content,
Duration ttl, Optional workflowPayload) {
Objects.requireNonNull(opId, "opId");
Objects.requireNonNull(owner, "owner");
Objects.requireNonNull(keyRef, "keyRef");
Objects.requireNonNull(algorithmId, "algorithmId");
- Objects.requireNonNull(payload, "payload");
+ Objects.requireNonNull(content, "content");
Objects.requireNonNull(ttl, "ttl");
Objects.requireNonNull(workflowPayload, "workflowPayload");
if (algorithmId.isBlank()) {
throw new IllegalArgumentException("algorithmId must not be blank");
}
+ X509ExecutionPlan submittedPlan = authority.planSigning(algorithmId,
+ workflowImplementationId(signer), SignatureWorkflow.class);
+ authority.authorize(submittedPlan, signer, AlgorithmExecutionCapability.Direction.SIGN);
+ AlgorithmIdentity submittedIdentity = submittedPlan.selection().requested();
if (ttl.isZero() || ttl.isNegative()) {
throw new IllegalArgumentException("ttl must be positive");
}
@@ -272,27 +411,52 @@ public final class PkiSigningBus implements AutoCloseable {
try (OperationCoordinator.Lease ignored = coordinator.acquire(baseOpId)) {
SigningSubmissionId parsed = SigningSubmissionId.parse(baseOpId);
Instant deadline = parsed.createdAt().plus(ttl);
- SignContinuation continuation = SignContinuation.decode(workflowPayload.get());
+ SignContinuation continuation = SignContinuation.decode(workflowPayload.get(), store.stagedContent());
+ X509ExecutionPlan continuationPlan = authority.planSigning(continuation.algorithmId,
+ workflowImplementationId(signer), SignatureWorkflow.class);
+ authority.authorize(continuationPlan, signer, AlgorithmExecutionCapability.Direction.SIGN);
+ AlgorithmIdentity continuationIdentity = continuationPlan.selection().requested();
if (!owner.equals(continuation.accessContext.principal()) || !keyRef.equals(continuation.keyRef)
- || !algorithmId.equals(continuation.algorithmId)
- || payload.encoding() != continuation.payload.encoding()
- || !java.util.Arrays.equals(payload.bytes(), continuation.payload.bytes())) {
+ || !submittedIdentity.equals(continuationIdentity)
+ || !content.equals(continuation.content())) {
throw new IllegalArgumentException("Sign continuation does not match the submitted request");
}
+ continuation = continuation.withAlgorithmId(submittedIdentity.canonicalForm());
String fingerprint = continuation.semanticFingerprint(namespace, deadline);
EncodedObject persistedRequest = continuation.withSignerOpId(baseOpId).encode();
SignWorkflowStore.Record intent = new SignWorkflowStore.Record(baseOpId, namespace, fingerprint, owner,
parsed.createdAt(), deadline, persistedRequest, SignWorkflowStore.State.INTENT, 0L, 0L,
Optional.empty(), Optional.of("INTENT"), Optional.empty(), Optional.empty());
- SignWorkflowStore.CreateResult created = store.createSignIntent(intent);
- if (created == SignWorkflowStore.CreateResult.CONFLICT) {
- throw new PkiException("Signing submission identifier conflicts with a different request");
+ DurableContentOwner contentOwner = DurableContentOwner.signingOperation(baseOpId);
+ boolean retained = false;
+ boolean recordVisible = false;
+ try {
+ retained = store.stagedContent().retainContent(content, contentOwner);
+ SignWorkflowStore.CreateResult created = store.createSignIntent(intent);
+ if (created == SignWorkflowStore.CreateResult.CONFLICT) {
+ throw new PkiException("Signing submission identifier conflicts with a different request");
+ }
+ recordVisible = true;
+ authoritative = store.getSignRecord(baseOpId).orElseThrow();
+ } catch (java.io.IOException exception) {
+ throw new PkiException("Signing content retention failed: code=SPOOL_STORAGE_FAILED", exception);
+ } finally {
+ if (retained && !recordVisible) {
+ rollbackSigningOwner(content, contentOwner);
+ }
}
- authoritative = store.getSignRecord(baseOpId).orElseThrow();
}
project(authoritative);
}
+ private void rollbackSigningOwner(DurableContentReference content, DurableContentOwner owner) {
+ try {
+ store.stagedContent().releaseContent(content, owner);
+ } catch (java.io.IOException exception) {
+ throw new PkiException("Signing content rollback failed: code=SPOOL_STORAGE_FAILED", exception);
+ }
+ }
+
/**
* Returns current status if known.
*/
@@ -400,8 +564,15 @@ public final class PkiSigningBus implements AutoCloseable {
if (!isTerminalSignState(state.state())) {
return;
}
+ Optional releaseReference = Optional.empty();
+ if (state.state() != SignWorkflowStore.State.RETIRED) {
+ releaseReference = Optional.of(SignContinuation.decode(state.request(), store.stagedContent()).content());
+ }
state = confirmRetirement(baseOpId, state);
store.deleteWorkflowState(baseOpId);
+ if (releaseReference.isPresent()) {
+ releaseRetiredOperationContent(baseOpId, releaseReference.get());
+ }
}
AsyncState advisoryState = state.result().isPresent() ? AsyncState.SUCCEEDED : AsyncState.CANCELLED;
bus.update(baseOpId, new AsyncStatus(advisoryState, store.signingNow(), Optional.of("RETIRED"),
@@ -409,6 +580,17 @@ public final class PkiSigningBus implements AutoCloseable {
bus.retire(baseOpId);
}
+ private void releaseRetiredOperationContent(PkiId operationId, DurableContentReference reference) {
+ if (reference.lifecycle() != DurableContentReference.Lifecycle.OPERATION) {
+ return;
+ }
+ try {
+ store.stagedContent().releaseContent(reference, DurableContentOwner.signingOperation(operationId));
+ } catch (java.io.IOException exception) {
+ throw new PkiException("Signing content retirement failed: code=SPOOL_STORAGE_FAILED", exception);
+ }
+ }
+
private void reconcileExpiredOperations() {
Instant current = store.signingNow();
for (SignWorkflowStore.Record candidate : store.listSignRecords()) {
@@ -813,6 +995,7 @@ public final class PkiSigningBus implements AutoCloseable {
private final SignatureWorkflow signer;
private final OperationCoordinator coordinator;
private final ExternalActionCoordinator externalActions;
+ private final X509AuthoritySnapshot authority;
private final ConcurrentMap pendingAdvisories;
private final AtomicInteger pendingAdvisoryCount;
private final AtomicBoolean closed;
@@ -826,11 +1009,12 @@ public final class PkiSigningBus implements AutoCloseable {
* operation and to query its status; must not be {@code null}
*/
private SignatureWorkflowEndpoint(PkiStore store, SignatureWorkflow signer, OperationCoordinator coordinator,
- ExternalActionCoordinator externalActions) {
+ ExternalActionCoordinator externalActions, X509AuthoritySnapshot authority) {
this.store = store;
this.signer = signer;
this.coordinator = coordinator;
this.externalActions = externalActions;
+ this.authority = authority;
this.pendingAdvisories = new ConcurrentHashMap<>();
this.pendingAdvisoryCount = new AtomicInteger();
this.closed = new AtomicBoolean();
@@ -877,7 +1061,8 @@ public final class PkiSigningBus implements AutoCloseable {
SubmissionCall call = prepared.get();
PkiId returned;
try (ExternalActionCoordinator.Reservation ignored = call.reservation()) {
- returned = signer.submitSign(call.request());
+ authority.authorize(call.plan(), signer, AlgorithmExecutionCapability.Direction.SIGN);
+ returned = call.plan().executor().submitSign(call.request());
} catch (RuntimeException ambiguousFailure) { // NOPMD - provider acceptance is unknown
return;
}
@@ -917,17 +1102,27 @@ public final class PkiSigningBus implements AutoCloseable {
ExternalActionCoordinator.Reservation reservation = reserved.get();
boolean reservationTransferred = false;
try {
- SignContinuation continuation = SignContinuation.decode(claimed.request());
+ SignContinuation continuation = SignContinuation.decode(claimed.request(), store.stagedContent());
+ DurableContentOwner contentOwner = DurableContentOwner.signingOperation(opId);
+ requireSigningOwner(continuation.content(), contentOwner);
+ X509ExecutionPlan plan = authority.planSigning(continuation.algorithmId,
+ workflowImplementationId(signer), SignatureWorkflow.class);
+ authority.authorize(plan, signer, AlgorithmExecutionCapability.Direction.SIGN);
+ AlgorithmIdentity persistedIdentity = plan.selection().requested();
+ if (!persistedIdentity.canonicalForm().equals(continuation.algorithmId)) {
+ throw new IllegalArgumentException("Persisted sign continuation is not canonical");
+ }
+ RepeatableContent content = openContent(continuation.content());
SignatureWorkflow.SignRequest request = SignatureWorkflow.SignRequest.create(opId,
claimed.namespace(), claimed.fence(), continuation.accessContext, continuation.keyRef,
- continuation.algorithmId, continuation.payload,
- Optional.of(continuation.preferredSignatureEncoding), Optional.of(claimed.deadline()));
+ continuation.algorithmId, content, Optional.of(continuation.preferredSignatureEncoding),
+ Optional.of(claimed.deadline()));
if (!constantTimeAsciiEquals(claimed.fingerprint(), request.semanticFingerprint())) {
store.transitionSign(opId, claimed.revision(), claimed.fence(), SignWorkflowStore.State.FAILED,
Optional.of("REQUEST_INTEGRITY_FAILURE"), Optional.empty(), Optional.empty());
return Optional.empty();
}
- SubmissionCall call = new SubmissionCall(claimed, request, reservation);
+ SubmissionCall call = new SubmissionCall(claimed, request, reservation, plan);
reservationTransferred = true;
return Optional.of(call);
} finally {
@@ -938,6 +1133,24 @@ public final class PkiSigningBus implements AutoCloseable {
}
}
+ private void requireSigningOwner(DurableContentReference reference, DurableContentOwner owner) {
+ try {
+ if (!store.stagedContent().contentOwners(reference).contains(owner)) {
+ throw new PkiException("Signing content owner missing: code=STAGED_CONTENT_INCOMPLETE");
+ }
+ } catch (java.io.IOException exception) {
+ throw new PkiException("Signing content ownership failed: code=CONTENT_INTEGRITY_FAILED", exception);
+ }
+ }
+
+ private RepeatableContent openContent(DurableContentReference reference) {
+ try {
+ return store.stagedContent().openContent(reference);
+ } catch (java.io.IOException ex) {
+ throw new PkiException("Signing content unavailable: code=STAGED_CONTENT_MISSING");
+ }
+ }
+
private static boolean constantTimeAsciiEquals(String left, String right) {
byte[] leftBytes = left.getBytes(java.nio.charset.StandardCharsets.US_ASCII);
byte[] rightBytes = right.getBytes(java.nio.charset.StandardCharsets.US_ASCII);
@@ -970,7 +1183,7 @@ public final class PkiSigningBus implements AutoCloseable {
}
private record SubmissionCall(SignWorkflowStore.Record record, SignatureWorkflow.SignRequest request,
- ExternalActionCoordinator.Reservation reservation) {
+ ExternalActionCoordinator.Reservation reservation, X509ExecutionPlan plan) {
}
/**
@@ -1260,7 +1473,8 @@ public final class PkiSigningBus implements AutoCloseable {
*
Encoding model
*
* Instances are encoded into a compact binary representation through
- * {@link #encode()} and reconstructed through {@link #decode(EncodedObject)}.
+ * {@link #encode()} and reconstructed through
+ * {@link #decode(EncodedObject, StagedContentStore)}.
* The binary format is versioned by {@link #VERSION}. The current version
* persists the algorithm identifier, payload encoding and bytes, key reference,
* preferred signature encoding, and the optional downstream signer operation
@@ -1271,7 +1485,7 @@ public final class PkiSigningBus implements AutoCloseable {
*
* The current binary encoding does not persist the original
* {@link zeroecho.pki.api.audit.AccessContext} losslessly. During
- * {@link #decode(EncodedObject)}, a synthetic system access context is created
+ * {@link #decode(EncodedObject, StagedContentStore)}, a synthetic system access context is created
* instead. This is sufficient for the current continuation flow, but callers
* must not assume that {@code decode(encode(x))} preserves the original access
* context exactly.
@@ -1284,11 +1498,13 @@ public final class PkiSigningBus implements AutoCloseable {
*/
public static final class SignContinuation {
- private static final byte VERSION = 2;
+ private static final byte VERSION = 4;
+ private static final long MINIMUM_CONTENT_LENGTH = 0L;
private final zeroecho.pki.api.audit.AccessContext accessContext;
private final String algorithmId;
- private final EncodedObject payload;
+ private final Optional content;
+ private final ContentCommitment commitment;
private final KeyRef keyRef;
private final Encoding preferredSignatureEncoding;
private final Optional signerOpId;
@@ -1300,7 +1516,7 @@ public final class PkiSigningBus implements AutoCloseable {
* sign request; must not be {@code null}
* @param algorithmId non-blank signature algorithm identifier;
* must not be {@code null} or blank
- * @param payload to-be-signed payload; must not be
+ * @param content durable to-be-signed content reference
* {@code null}
* @param keyRef signing key reference; must not be
* {@code null}
@@ -1313,10 +1529,13 @@ public final class PkiSigningBus implements AutoCloseable {
* @throws IllegalArgumentException if {@code algorithmId} is blank
*/
public SignContinuation(zeroecho.pki.api.audit.AccessContext accessContext, String algorithmId,
- EncodedObject payload, KeyRef keyRef, Encoding preferredSignatureEncoding, Optional signerOpId) {
+ DurableContentReference content, KeyRef keyRef, Encoding preferredSignatureEncoding,
+ Optional signerOpId) {
this.accessContext = Objects.requireNonNull(accessContext, "accessContext");
this.algorithmId = Objects.requireNonNull(algorithmId, "algorithmId");
- this.payload = Objects.requireNonNull(payload, "payload");
+ DurableContentReference exactContent = Objects.requireNonNull(content, "content");
+ this.content = Optional.of(exactContent);
+ this.commitment = ContentCommitment.of(exactContent);
this.keyRef = Objects.requireNonNull(keyRef, "keyRef");
this.preferredSignatureEncoding = Objects.requireNonNull(preferredSignatureEncoding,
"preferredSignatureEncoding");
@@ -1326,6 +1545,22 @@ public final class PkiSigningBus implements AutoCloseable {
}
}
+ private SignContinuation(zeroecho.pki.api.audit.AccessContext accessContext, String algorithmId,
+ Optional content, ContentCommitment commitment, KeyRef keyRef,
+ Encoding preferredSignatureEncoding, Optional signerOpId) {
+ this.accessContext = Objects.requireNonNull(accessContext, "accessContext");
+ this.algorithmId = Objects.requireNonNull(algorithmId, "algorithmId");
+ this.content = Objects.requireNonNull(content, "content");
+ this.commitment = Objects.requireNonNull(commitment, "commitment");
+ this.keyRef = Objects.requireNonNull(keyRef, "keyRef");
+ this.preferredSignatureEncoding = Objects.requireNonNull(preferredSignatureEncoding,
+ "preferredSignatureEncoding");
+ this.signerOpId = Objects.requireNonNull(signerOpId, "signerOpId");
+ if (algorithmId.isBlank() || content.isPresent() && !commitment.matches(content.get())) {
+ throw new IllegalArgumentException("Invalid sign continuation content commitment");
+ }
+ }
+
/**
* Returns a new continuation with the downstream signer workflow operation
* identifier assigned.
@@ -1343,8 +1578,19 @@ public final class PkiSigningBus implements AutoCloseable {
* @throws NullPointerException if {@code opId} is {@code null}
*/
public SignContinuation withSignerOpId(PkiId opId) {
- return new SignContinuation(accessContext, algorithmId, payload, keyRef, preferredSignatureEncoding,
- Optional.of(opId));
+ return new SignContinuation(accessContext, algorithmId, content, commitment, keyRef,
+ preferredSignatureEncoding, Optional.of(opId));
+ }
+
+ private SignContinuation withAlgorithmId(String canonicalAlgorithmId) {
+ return new SignContinuation(accessContext, canonicalAlgorithmId, content, commitment, keyRef,
+ preferredSignatureEncoding, signerOpId);
+ }
+
+ /** Returns a terminal continuation retaining only immutable content commitment metadata. */
+ public SignContinuation withoutLiveContent() {
+ return new SignContinuation(accessContext, algorithmId, Optional.empty(), commitment, keyRef,
+ preferredSignatureEncoding, signerOpId);
}
/**
@@ -1363,6 +1609,31 @@ public final class PkiSigningBus implements AutoCloseable {
return signerOpId;
}
+ /**
+ * Returns the payload-free durable reference used for recovery.
+ *
+ * @return staged content reference; never a live handle or payload
+ */
+ public DurableContentReference content() {
+ return content.orElseThrow(() -> new IllegalStateException("Retired continuation has no live content"));
+ }
+
+ /** Returns whether this continuation still requires live staged content. */
+ public boolean hasLiveContent() {
+ return content.isPresent();
+ }
+
+ /** Restores the committed reference through its owning store for delayed cleanup. */
+ public DurableContentReference restoreContent(zeroecho.pki.spi.store.StagedContentStore stagedContent)
+ throws java.io.IOException {
+ Objects.requireNonNull(stagedContent, "stagedContent");
+ if (content.isPresent()) {
+ return content.get();
+ }
+ return stagedContent.restoreReference(commitment.storeId(), commitment.contentId(), commitment.encoding(),
+ commitment.length(), commitment.sha256(), commitment.lifecycle());
+ }
+
/**
* Computes the canonical semantic fingerprint for this persisted request.
*
@@ -1380,7 +1651,8 @@ public final class PkiSigningBus implements AutoCloseable {
*/
public String semanticFingerprint(String namespace, Instant deadline) {
Objects.requireNonNull(deadline, "deadline");
- return SignatureWorkflow.SignRequest.fingerprint(namespace, accessContext, keyRef, algorithmId, payload,
+ return SignatureWorkflow.SignRequest.fingerprint(namespace, accessContext, keyRef, algorithmId,
+ new ReferenceContent(commitment),
Optional.of(preferredSignatureEncoding), Optional.of(deadline));
}
@@ -1411,7 +1683,7 @@ public final class PkiSigningBus implements AutoCloseable {
*
*
format version,
*
algorithm identifier,
- *
payload encoding and bytes,
+ *
store-issued staged-content reference metadata,
*
key reference,
*
preferred signature encoding,
*
presence marker and optional downstream signer workflow operation
@@ -1427,7 +1699,6 @@ public final class PkiSigningBus implements AutoCloseable {
*/
public EncodedObject encode() {
WipeableByteArrayOutputStream bytes = new WipeableByteArrayOutputStream();
- byte[] payloadBytes = payload.bytes();
byte[] encoded = null;
try {
try (java.io.DataOutputStream output = new java.io.DataOutputStream(bytes)) {
@@ -1445,9 +1716,13 @@ public final class PkiSigningBus implements AutoCloseable {
}
output.writeUTF(algorithmId);
output.writeUTF(keyRef.value());
- output.writeByte(payload.encoding().ordinal());
- output.writeInt(payloadBytes.length);
- output.write(payloadBytes);
+ output.writeBoolean(content.isPresent());
+ output.writeUTF(commitment.storeId());
+ output.writeUTF(commitment.contentId());
+ output.writeByte(commitment.encoding().ordinal());
+ output.writeLong(commitment.length());
+ output.writeUTF(commitment.sha256());
+ output.writeByte(commitment.lifecycle().ordinal());
output.writeByte(preferredSignatureEncoding.ordinal());
output.writeBoolean(signerOpId.isPresent());
if (signerOpId.isPresent()) {
@@ -1459,7 +1734,6 @@ public final class PkiSigningBus implements AutoCloseable {
} catch (java.io.IOException ex) {
throw new PkiException("Failed to encode sign continuation: code=CONTINUATION_ENCODE_FAILED");
} finally {
- java.util.Arrays.fill(payloadBytes, (byte) 0);
if (encoded != null) {
java.util.Arrays.fill(encoded, (byte) 0);
}
@@ -1489,6 +1763,8 @@ public final class PkiSigningBus implements AutoCloseable {
*
*
* @param obj binary encoded continuation payload; must not be {@code null}
+ * @param stagedContent owning store used to restore and validate the persisted
+ * content reference
* @return decoded continuation instance
* @throws NullPointerException if {@code obj} is {@code null}
* @throws IllegalArgumentException if {@code obj} does not use
@@ -1496,13 +1772,14 @@ public final class PkiSigningBus implements AutoCloseable {
* format version is not supported, or if the
* binary payload is malformed
*/
- public static SignContinuation decode(EncodedObject obj) {
+ public static SignContinuation decode(EncodedObject obj,
+ zeroecho.pki.spi.store.StagedContentStore stagedContent) {
Objects.requireNonNull(obj, "obj");
+ Objects.requireNonNull(stagedContent, "stagedContent");
if (obj.encoding() != Encoding.BINARY) {
throw new IllegalArgumentException("Expected BINARY continuation payload");
}
byte[] encoded = obj.bytes();
- byte[] payloadBytes = null;
try (java.io.DataInputStream input = new java.io.DataInputStream(
new java.io.ByteArrayInputStream(encoded))) {
int version = input.readUnsignedByte();
@@ -1518,30 +1795,85 @@ public final class PkiSigningBus implements AutoCloseable {
: Optional.empty();
String algId = input.readUTF();
KeyRef key = new KeyRef(input.readUTF());
- Encoding payloadEncoding = Encoding.values()[input.readUnsignedByte()];
- int payloadLength = input.readInt();
- if (payloadLength <= 0 || payloadLength > 16 * 1024 * 1024) {
- throw new PkiException("Invalid sign continuation payload length");
- }
- payloadBytes = input.readNBytes(payloadLength);
- if (payloadBytes.length != payloadLength) {
- throw new PkiException("Truncated sign continuation payload");
- }
+ boolean liveContent = input.readBoolean();
+ String storeId = input.readUTF();
+ String contentId = input.readUTF();
+ Encoding contentEncoding = Encoding.values()[input.readUnsignedByte()];
+ long contentLength = input.readLong();
+ String sha256 = input.readUTF();
+ DurableContentReference.Lifecycle lifecycle = DurableContentReference.Lifecycle
+ .values()[input.readUnsignedByte()];
+ ContentCommitment commitment = new ContentCommitment(storeId, contentId, contentEncoding,
+ contentLength, sha256, lifecycle);
+ Optional content = liveContent
+ ? Optional.of(stagedContent.restoreReference(storeId, contentId, contentEncoding,
+ contentLength, sha256, lifecycle))
+ : Optional.empty();
Encoding preferred = Encoding.values()[input.readUnsignedByte()];
Optional signerId = input.readBoolean() ? Optional.of(new PkiId(input.readUTF()))
: Optional.empty();
+ requireCompleteInput(input);
zeroecho.pki.api.audit.AccessContext access = new zeroecho.pki.api.audit.AccessContext(principal,
purpose, objectId, formatId);
- return new SignContinuation(access, algId, new EncodedObject(payloadEncoding, payloadBytes), key,
- preferred, signerId);
+ return new SignContinuation(access, algId, content, commitment, key, preferred, signerId);
} catch (java.io.IOException | IndexOutOfBoundsException ex) {
throw new PkiException("Malformed sign continuation: code=CONTINUATION_MALFORMED");
} finally {
java.util.Arrays.fill(encoded, (byte) 0);
- if (payloadBytes != null) {
- java.util.Arrays.fill(payloadBytes, (byte) 0);
+ }
+ }
+
+ private record ContentCommitment(String storeId, String contentId, Encoding encoding, long length,
+ String sha256, DurableContentReference.Lifecycle lifecycle) {
+ private ContentCommitment {
+ Objects.requireNonNull(storeId, "storeId");
+ Objects.requireNonNull(contentId, "contentId");
+ Objects.requireNonNull(encoding, "encoding");
+ Objects.requireNonNull(sha256, "sha256");
+ Objects.requireNonNull(lifecycle, "lifecycle");
+ if (length < MINIMUM_CONTENT_LENGTH) {
+ throw new IllegalArgumentException("Content length must not be negative");
}
}
+
+ private static ContentCommitment of(DurableContentReference reference) {
+ return new ContentCommitment(reference.storeId(), reference.contentId(), reference.encoding(),
+ reference.length(), reference.sha256(), reference.lifecycle());
+ }
+
+ private boolean matches(DurableContentReference reference) {
+ return storeId.equals(reference.storeId()) && contentId.equals(reference.contentId())
+ && encoding == reference.encoding() && length == reference.length()
+ && sha256.equals(reference.sha256()) && lifecycle == reference.lifecycle();
+ }
+ }
+
+ private static void requireCompleteInput(java.io.DataInputStream input) throws java.io.IOException {
+ if (input.read() >= 0) {
+ throw new java.io.IOException("Trailing sign continuation data");
+ }
+ }
+
+ private record ReferenceContent(ContentCommitment reference) implements RepeatableContent {
+ @Override
+ public java.io.InputStream openStream() throws java.io.IOException {
+ throw new java.io.IOException("Content reference requires staged-content store");
+ }
+
+ @Override
+ public java.util.OptionalLong length() {
+ return java.util.OptionalLong.of(reference.length());
+ }
+
+ @Override
+ public String contentId() {
+ return "sha256:" + reference.sha256();
+ }
+
+ @Override
+ public void close() {
+ // Reference metadata owns no live resource.
+ }
}
/**
diff --git a/pki/src/main/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflow.java b/pki/src/main/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflow.java
index 167f6c9..76a0f81 100644
--- a/pki/src/main/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflow.java
+++ b/pki/src/main/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflow.java
@@ -33,13 +33,11 @@
******************************************************************************/
package zeroecho.pki.impl.crypto.zeroecholib;
-import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
@@ -87,6 +85,8 @@ import zeroecho.core.alg.rsa.RsaPublicKeySpec;
import zeroecho.core.alg.slhdsa.SlhDsaPublicKeySpec;
import zeroecho.core.alg.sphincsplus.SphincsPlusPublicKeySpec;
import zeroecho.core.context.SignatureContext;
+import zeroecho.core.io.CancellationSignal;
+import zeroecho.core.io.RepeatableContent;
import zeroecho.core.io.TailStrippingInputStream;
import zeroecho.core.spec.AlgorithmKeySpec;
import zeroecho.core.spec.ContextSpec;
@@ -266,7 +266,7 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
private static final OperationStatus UNKNOWN_OPERATION_STATUS = new OperationStatus(State.FAILED, Instant.EPOCH,
Optional.of(DC_UNKNOWN_OPERATION), Optional.empty());
- private static final int OPERATION_RECORD_VERSION = 3;
+ private static final int OPERATION_RECORD_VERSION = 4;
private static final long MIN_FENCING_TOKEN = 1L;
private final String id;
@@ -488,7 +488,6 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
return opId;
}
- byte[] payloadBytes = null;
byte[] signatureBytes = null;
try {
KeyRefParts parts = parseKeyRefOrThrow(request.keyRef(), true);
@@ -514,8 +513,9 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
completeSign(request, expiredStatus());
return opId;
}
- payloadBytes = request.payload().bytes();
- signatureBytes = signStreaming(request.algorithmId(), prv.key(), pub.key(), payloadBytes);
+ request.cancellation().throwIfCancelled();
+ signatureBytes = signStreaming(request.algorithmId(), prv.key(), pub.key(), request.content(),
+ request.cancellation());
Encoding outEnc = request.preferredSignatureEncoding().orElse(Encoding.BINARY);
EncodedObject signature = encodeSignatureOrThrow(outEnc, signatureBytes);
@@ -553,7 +553,6 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
logSafeFailure("SIGN", DC_CRYPTO_FAILURE, ex);
return opId;
} finally {
- clearOwned("sign-payload", payloadBytes);
clearOwned("sign-result-copy", signatureBytes);
}
}
@@ -670,7 +669,6 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
putStatus(opId, new OperationStatus(State.RUNNING, now(), Optional.of(DC_SUBMITTED), Optional.empty()));
byte[] signatureBytes = null;
- byte[] payloadBytes = null;
try {
if (request.algorithmId() == null || request.algorithmId().isBlank()) {
throw new InvalidRequestException(DC_INVALID_ALGORITHM_ID);
@@ -683,8 +681,9 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
putStatus(opId, expiredStatus());
return opId;
}
- payloadBytes = request.payload().bytes();
- boolean ok = verifyStreaming(request.algorithmId(), pub, payloadBytes, signatureBytes);
+ request.cancellation().throwIfCancelled();
+ boolean ok = verifyStreaming(request.algorithmId(), pub, request.content(), signatureBytes,
+ request.cancellation());
Instant completedAt = now();
if (deadlineReached(request.deadline(), completedAt)) {
@@ -716,7 +715,6 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
logSafeFailure("VERIFY", DC_CRYPTO_FAILURE, ex);
return opId;
} finally {
- clearOwned("verify-payload", payloadBytes);
clearOwned("verify-signature", signatureBytes);
}
}
@@ -1061,7 +1059,8 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
return a;
}
- private byte[] signStreaming(String algorithmId, PrivateKey prv, PublicKey pub, byte[] msg)
+ private byte[] signStreaming(String algorithmId, PrivateKey prv, PublicKey pub, RepeatableContent content,
+ CancellationSignal cancellation)
throws GeneralSecurityException, IOException {
Optional profile = SignatureInteropProfiles.resolve(algorithmId);
@@ -1075,14 +1074,14 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
try (SignatureContext signer = session.createContext(contextAlgorithmId, KeyUsage.SIGN, prv, contextSpec)) {
final byte[][] sigHolder = new byte[1][];
- try (InputStream in = new TailStrippingInputStream(signer.wrap(new ByteArrayInputStream(msg)), sigLen,
- 8192) {
+ try (InputStream source = content.openStream();
+ InputStream in = new TailStrippingInputStream(signer.wrap(source), sigLen, 8192) {
@Override
protected void processTail(byte[] tail) throws IOException {
sigHolder[0] = (tail == null) ? null : tail.clone();
}
}) {
- in.transferTo(OutputStream.nullOutputStream());
+ consume(in, cancellation);
}
byte[] internalSignature = sigHolder[0];
@@ -1101,7 +1100,8 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
}
}
- private boolean verifyStreaming(String algorithmId, PublicKey pub, byte[] msg, byte[] signature)
+ private boolean verifyStreaming(String algorithmId, PublicKey pub, RepeatableContent content, byte[] signature,
+ CancellationSignal cancellation)
throws GeneralSecurityException, IOException {
Optional profile = SignatureInteropProfiles.resolve(algorithmId);
@@ -1116,8 +1116,8 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
try (SignatureContext verifier = session.createContext(contextAlgorithmId, KeyUsage.VERIFY, pub, contextSpec)) {
verifier.setExpectedTag(internalSignature);
- try (InputStream in = verifier.wrap(new ByteArrayInputStream(msg))) {
- in.transferTo(OutputStream.nullOutputStream());
+ try (InputStream source = content.openStream(); InputStream in = verifier.wrap(source)) {
+ consume(in, cancellation);
}
return true;
} catch (Exception mismatch) {
@@ -1129,6 +1129,17 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
}
}
+ private static void consume(InputStream input, CancellationSignal cancellation) throws IOException {
+ byte[] buffer = new byte[16 * 1024];
+ try {
+ while (input.read(buffer) >= 0) {
+ cancellation.throwIfCancelled();
+ }
+ } finally {
+ Arrays.fill(buffer, (byte) 0);
+ }
+ }
+
private EncodedObject encodeSignatureOrThrow(Encoding encoding, byte[] sigBytes) throws InvalidRequestException {
if (encoding == Encoding.BINARY || encoding == Encoding.DER) {
return new EncodedObject(encoding, sigBytes);
@@ -1482,14 +1493,8 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
}
output.writeUTF(request.keyRef().value());
output.writeUTF(request.algorithmId());
- output.writeInt(encodingCode(request.payload().encoding()));
- byte[] payload = request.payload().bytes();
- try {
- output.writeInt(payload.length);
- output.write(payload);
- } finally {
- clearOwned("persisted-request-payload", payload);
- }
+ output.writeUTF(request.content().contentId());
+ output.writeLong(request.content().length().orElse(-1L));
output.writeBoolean(request.preferredSignatureEncoding().isPresent());
if (request.preferredSignatureEncoding().isPresent()) {
output.writeInt(encodingCode(request.preferredSignatureEncoding().orElseThrow()));
@@ -1516,25 +1521,38 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow {
objectId, formatId);
KeyRef keyRef = new KeyRef(input.readUTF());
String algorithmId = input.readUTF();
- Encoding payloadEncoding = encodingFromCode(input.readInt());
- int payloadLength = input.readInt();
- if (payloadLength < 1 || payloadLength > 16 * 1024 * 1024) {
- throw new IllegalStateException("Invalid persisted signing payload length");
+ String contentId = input.readUTF();
+ long contentLength = input.readLong();
+ if (contentId.isBlank() || contentLength < -1L) {
+ throw new IllegalStateException("Invalid persisted signing content metadata");
}
- byte[] payload = input.readNBytes(payloadLength);
- if (payload.length != payloadLength) {
- throw new IllegalStateException("Truncated persisted signing payload");
+ Optional preferred = input.readBoolean() ? Optional.of(encodingFromCode(input.readInt()))
+ : Optional.empty();
+ Optional deadline = input.readBoolean()
+ ? Optional.of(Instant.ofEpochSecond(input.readLong(), input.readInt()))
+ : Optional.empty();
+ return SignRequest.create(submissionId, namespace, 1L, access, keyRef, algorithmId,
+ new RecoveredContentMetadata(contentId, contentLength), preferred, deadline);
+ }
+
+ /**
+ * Metadata-only view retained for terminal request identity after restart.
+ * Recovered non-terminal operations fail closed before this content can execute.
+ */
+ private record RecoveredContentMetadata(String contentId, long persistedLength) implements RepeatableContent {
+ @Override
+ public InputStream openStream() throws IOException {
+ throw new IOException("Recovered content requires staged-content resolution");
}
- try {
- Optional preferred = input.readBoolean() ? Optional.of(encodingFromCode(input.readInt()))
- : Optional.empty();
- Optional deadline = input.readBoolean()
- ? Optional.of(Instant.ofEpochSecond(input.readLong(), input.readInt()))
- : Optional.empty();
- return SignRequest.create(submissionId, namespace, 1L, access, keyRef, algorithmId,
- new EncodedObject(payloadEncoding, payload), preferred, deadline);
- } finally {
- clearOwned("loaded-request-payload", payload);
+
+ @Override
+ public java.util.OptionalLong length() {
+ return persistedLength < 0L ? java.util.OptionalLong.empty() : java.util.OptionalLong.of(persistedLength);
+ }
+
+ @Override
+ public void close() {
+ // Metadata owns no live resource.
}
}
diff --git a/pki/src/main/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflowProvider.java b/pki/src/main/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflowProvider.java
index bba310a..7760325 100644
--- a/pki/src/main/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflowProvider.java
+++ b/pki/src/main/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflowProvider.java
@@ -42,6 +42,11 @@ import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
+import zeroecho.core.alg.BootstrapAlgorithmIdentities;
+import zeroecho.core.spec.AlgorithmIdentity;
+import zeroecho.core.spec.AlgorithmSuite;
+import zeroecho.core.spi.AlgorithmExecutionCapability;
+import zeroecho.core.spi.AlgorithmExecutionCapabilityProvider;
import zeroecho.core.spi.KeyringUnlockProvider;
import zeroecho.core.storage.KeyringPassword;
import zeroecho.core.storage.KeyringStore;
@@ -75,7 +80,8 @@ import zeroecho.pki.spi.crypto.SignatureWorkflowRuntimeDependencies;
* performs no value logging.
*
*/
-public final class ZeroEchoLibSignatureWorkflowProvider implements SignatureWorkflowProvider {
+public final class ZeroEchoLibSignatureWorkflowProvider
+ implements SignatureWorkflowProvider, AlgorithmExecutionCapabilityProvider {
/** Stable failure code for a missing explicit keyring unlock provider. */
public static final String DC_KEYRING_UNLOCK_PROVIDER_REQUIRED = "KEYRING_UNLOCK_PROVIDER_REQUIRED";
/** Stable failure code for an unlock-provider acquisition failure. */
@@ -127,6 +133,42 @@ public final class ZeroEchoLibSignatureWorkflowProvider implements SignatureWork
KEY_REQUIRE_SUFFIX);
}
+ /**
+ * Declares the exact classic signature domain implemented by the workflow.
+ *
+ *
+ * This metadata describes the same workflow implementation allocated by this
+ * provider. It does not select an OID, redefine an identity, or claim key
+ * availability.
+ *
+ *
+ * @return immutable execution capability contribution
+ */
+ @Override
+ public java.util.List capabilities() {
+ Set signatures = Set.of(BootstrapAlgorithmIdentities.RSA_PKCS1_SHA256,
+ 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);
+ return java.util.List.of(new AlgorithmExecutionCapability() {
+ @Override
+ public String implementationId() {
+ return "zeroecho-lib.signature-workflow";
+ }
+
+ @Override
+ public String domainFingerprint() {
+ return "zeroecho-lib-signature-v1:bootstrap-classic:sign,verify";
+ }
+
+ @Override
+ public boolean supports(AlgorithmIdentity identity, AlgorithmSuite suite, Direction direction) {
+ return signatures.contains(identity) && identity.equals(suite.signature());
+ }
+ });
+ }
+
/**
* Validates configuration for the ZeroEcho-lib signature workflow provider.
*
diff --git a/pki/src/main/java/zeroecho/pki/impl/framework/x509/StandardX509Bindings.java b/pki/src/main/java/zeroecho/pki/impl/framework/x509/StandardX509Bindings.java
new file mode 100644
index 0000000..eaf0d9f
--- /dev/null
+++ b/pki/src/main/java/zeroecho/pki/impl/framework/x509/StandardX509Bindings.java
@@ -0,0 +1,619 @@
+/*******************************************************************************
+ * 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.ByteArrayOutputStream;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import zeroecho.core.alg.BootstrapAlgorithmIdentities;
+import zeroecho.core.spec.AlgorithmIdentity;
+
+/**
+ * Immutable authoritative standard X.509 bootstrap bindings.
+ *
+ *
+ * Standard OIDs and parameter semantics are code-owned and cannot be overridden
+ * by configuration or extension ordering. RSA-PSS and EC SubjectPublicKeyInfo
+ * are represented by typed parameterized rules.
+ *
+ */
+public final class StandardX509Bindings {
+
+ /** RSA PKCS#1 SHA-256 signature OID. */
+ public static final String OID_RSA_SHA256 = "1.2.840.113549.1.1.11";
+ /** RSA PKCS#1 SHA-384 signature OID. */
+ public static final String OID_RSA_SHA384 = "1.2.840.113549.1.1.12";
+ /** RSA PKCS#1 SHA-512 signature OID. */
+ public static final String OID_RSA_SHA512 = "1.2.840.113549.1.1.13";
+ /** RSA-PSS signature OID. */
+ public static final String OID_RSA_PSS = "1.2.840.113549.1.1.10";
+ /** ECDSA SHA-256 signature OID. */
+ public static final String OID_ECDSA_SHA256 = "1.2.840.10045.4.3.2";
+ /** ECDSA SHA-384 signature OID. */
+ public static final String OID_ECDSA_SHA384 = "1.2.840.10045.4.3.3";
+ /** ECDSA SHA-512 signature OID. */
+ public static final String OID_ECDSA_SHA512 = "1.2.840.10045.4.3.4";
+ /** Ed25519 OID. */
+ public static final String OID_ED25519 = String.join(".", "1", "3", "101", "112");
+ /** Ed448 OID. */
+ public static final String OID_ED448 = String.join(".", "1", "3", "101", "113");
+ /** RSA public-key OID. */
+ public static final String OID_RSA_PUBLIC_KEY = "1.2.840.113549.1.1.1";
+ /** EC public-key OID. */
+ public static final String OID_EC_PUBLIC_KEY = "1.2.840.10045.2.1";
+ /** P-256 named-curve OID. */
+ public static final String OID_P256 = "1.2.840.10045.3.1.7";
+ /** P-384 named-curve OID. */
+ public static final String OID_P384 = "1.3.132.0.34";
+ /** P-521 named-curve OID. */
+ public static final String OID_P521 = "1.3.132.0.35";
+
+ private static final X509BindingCatalog CATALOG = createCatalog(X509ComponentCatalog.builtIn());
+
+ private StandardX509Bindings() {
+ }
+
+ private static X509BindingCatalog createCatalog(X509ComponentCatalog components) {
+ Objects.requireNonNull(components, "components");
+ return X509BindingCatalog.builtIn(List.of(
+ fixed("zeroecho.signature.rsa-pkcs1-sha256", X509AlgorithmRole.SIGNATURE_ALGORITHM,
+ BootstrapAlgorithmIdentities.RSA_PKCS1_SHA256, X509AlgorithmIdentifier.derNull(OID_RSA_SHA256),
+ X509BindingRule.SignatureEncoding.OPAQUE, X509BindingRule.PublicKeyEncoding.NOT_APPLICABLE),
+ fixed("zeroecho.signature.rsa-pkcs1-sha384", X509AlgorithmRole.SIGNATURE_ALGORITHM,
+ BootstrapAlgorithmIdentities.RSA_PKCS1_SHA384, X509AlgorithmIdentifier.derNull(OID_RSA_SHA384),
+ X509BindingRule.SignatureEncoding.OPAQUE, X509BindingRule.PublicKeyEncoding.NOT_APPLICABLE),
+ fixed("zeroecho.signature.rsa-pkcs1-sha512", X509AlgorithmRole.SIGNATURE_ALGORITHM,
+ BootstrapAlgorithmIdentities.RSA_PKCS1_SHA512, X509AlgorithmIdentifier.derNull(OID_RSA_SHA512),
+ X509BindingRule.SignatureEncoding.OPAQUE, X509BindingRule.PublicKeyEncoding.NOT_APPLICABLE),
+ new RsaPssRule(components),
+ fixed("zeroecho.signature.ecdsa-sha256", X509AlgorithmRole.SIGNATURE_ALGORITHM,
+ BootstrapAlgorithmIdentities.ECDSA_SHA256, X509AlgorithmIdentifier.absent(OID_ECDSA_SHA256),
+ X509BindingRule.SignatureEncoding.ECDSA_DER,
+ X509BindingRule.PublicKeyEncoding.NOT_APPLICABLE),
+ fixed("zeroecho.signature.ecdsa-sha384", X509AlgorithmRole.SIGNATURE_ALGORITHM,
+ BootstrapAlgorithmIdentities.ECDSA_SHA384, X509AlgorithmIdentifier.absent(OID_ECDSA_SHA384),
+ X509BindingRule.SignatureEncoding.ECDSA_DER,
+ X509BindingRule.PublicKeyEncoding.NOT_APPLICABLE),
+ fixed("zeroecho.signature.ecdsa-sha512", X509AlgorithmRole.SIGNATURE_ALGORITHM,
+ BootstrapAlgorithmIdentities.ECDSA_SHA512, X509AlgorithmIdentifier.absent(OID_ECDSA_SHA512),
+ X509BindingRule.SignatureEncoding.ECDSA_DER,
+ X509BindingRule.PublicKeyEncoding.NOT_APPLICABLE),
+ fixed("zeroecho.signature.ed25519", X509AlgorithmRole.SIGNATURE_ALGORITHM,
+ BootstrapAlgorithmIdentities.ED25519_SIGNATURE, X509AlgorithmIdentifier.absent(OID_ED25519),
+ X509BindingRule.SignatureEncoding.OPAQUE, X509BindingRule.PublicKeyEncoding.NOT_APPLICABLE),
+ fixed("zeroecho.signature.ed448", X509AlgorithmRole.SIGNATURE_ALGORITHM,
+ BootstrapAlgorithmIdentities.ED448_SIGNATURE, X509AlgorithmIdentifier.absent(OID_ED448),
+ X509BindingRule.SignatureEncoding.OPAQUE, X509BindingRule.PublicKeyEncoding.NOT_APPLICABLE),
+ fixed("zeroecho.spki.rsa", X509AlgorithmRole.SUBJECT_PUBLIC_KEY_ALGORITHM,
+ BootstrapAlgorithmIdentities.RSA_PUBLIC_KEY, X509AlgorithmIdentifier.derNull(OID_RSA_PUBLIC_KEY),
+ X509BindingRule.SignatureEncoding.NOT_APPLICABLE, X509BindingRule.PublicKeyEncoding.RSA_PKCS1_DER),
+ new EcPublicKeyRule(components),
+ fixed("zeroecho.spki.ed25519", X509AlgorithmRole.SUBJECT_PUBLIC_KEY_ALGORITHM,
+ BootstrapAlgorithmIdentities.ED25519_PUBLIC_KEY, X509AlgorithmIdentifier.absent(OID_ED25519),
+ X509BindingRule.SignatureEncoding.NOT_APPLICABLE, X509BindingRule.PublicKeyEncoding.RAW),
+ fixed("zeroecho.spki.ed448", X509AlgorithmRole.SUBJECT_PUBLIC_KEY_ALGORITHM,
+ BootstrapAlgorithmIdentities.ED448_PUBLIC_KEY, X509AlgorithmIdentifier.absent(OID_ED448),
+ X509BindingRule.SignatureEncoding.NOT_APPLICABLE, X509BindingRule.PublicKeyEncoding.RAW)));
+ }
+
+ /**
+ * Returns the immutable built-in standard catalog.
+ *
+ * @return authoritative binding snapshot
+ */
+ public static X509BindingCatalog catalog() {
+ return CATALOG;
+ }
+
+ /**
+ * Creates the built-in rules against an immutable component authority.
+ *
+ * @param components exact component catalog used by parameterized rules
+ * @return immutable built-in binding catalog
+ */
+ public static X509BindingCatalog catalog(X509ComponentCatalog components) {
+ return createCatalog(components);
+ }
+
+ private static X509BindingRule fixed(String id, X509AlgorithmRole role, AlgorithmIdentity identity,
+ X509AlgorithmIdentifier identifier, X509BindingRule.SignatureEncoding signatureEncoding,
+ X509BindingRule.PublicKeyEncoding publicKeyEncoding) {
+ return new FixedRule(id, role, identity, identifier, signatureEncoding, publicKeyEncoding);
+ }
+
+ private record FixedRule(String id, X509AlgorithmRole role, AlgorithmIdentity identity,
+ X509AlgorithmIdentifier identifier, SignatureEncoding signatureEncoding,
+ PublicKeyEncoding publicKeyEncoding) implements X509BindingRule {
+
+ private FixedRule {
+ Objects.requireNonNull(id, "id");
+ Objects.requireNonNull(role, "role");
+ Objects.requireNonNull(identity, "identity");
+ Objects.requireNonNull(identifier, "identifier");
+ Objects.requireNonNull(signatureEncoding, "signatureEncoding");
+ Objects.requireNonNull(publicKeyEncoding, "publicKeyEncoding");
+ }
+
+ @Override
+ public String oid() {
+ return identifier.oid();
+ }
+
+ @Override
+ public String semanticFingerprint() {
+ return id + "|" + role + "|" + identity.canonicalForm() + "|" + identifier.canonicalForm() + "|"
+ + signatureEncoding + "|" + publicKeyEncoding;
+ }
+
+ @Override
+ public Optional encode(AlgorithmIdentity candidate) {
+ return identity.equals(candidate) ? Optional.of(identifier) : Optional.empty();
+ }
+
+ @Override
+ public Optional decode(X509AlgorithmIdentifier candidate) {
+ if (!identifier.oid().equals(candidate.oid())) {
+ return Optional.empty();
+ }
+ if (!identifier.equals(candidate)) {
+ throw new IllegalArgumentException("Non-canonical X.509 algorithm parameters");
+ }
+ return Optional.of(identity);
+ }
+ }
+
+ /** Parameterized authority for RSA-PSS signature identifiers. */
+ private static final class RsaPssRule implements X509BindingRule {
+
+ private final X509ComponentCatalog components;
+
+ private RsaPssRule(X509ComponentCatalog components) {
+ this.components = components;
+ }
+
+ @Override
+ public String id() {
+ return "zeroecho.signature.rsa-pss";
+ }
+
+ @Override
+ public X509AlgorithmRole role() {
+ return X509AlgorithmRole.SIGNATURE_ALGORITHM;
+ }
+
+ @Override
+ public String oid() {
+ return OID_RSA_PSS;
+ }
+
+ @Override
+ public String semanticFingerprint() {
+ return "rsa-pss-v1|sha2-256,sha2-384,sha2-512|mgf1|salt-nonnegative|trailer-1|canonical-explicit";
+ }
+
+ @Override
+ public SignatureEncoding signatureEncoding() {
+ return SignatureEncoding.OPAQUE;
+ }
+
+ @Override
+ public PublicKeyEncoding publicKeyEncoding() {
+ return PublicKeyEncoding.NOT_APPLICABLE;
+ }
+
+ @Override
+ public Optional encode(AlgorithmIdentity identity) {
+ if (identity.kind() != AlgorithmIdentity.Kind.SIGNATURE
+ || !"zeroecho/rsa-pss".equals(identity.family().canonicalForm())) {
+ return Optional.empty();
+ }
+ if (!(identity.parameters() instanceof AlgorithmIdentity.RsaPssParameters parameters)) {
+ throw new IllegalArgumentException("RSA-PSS identity has invalid typed parameters");
+ }
+ byte[] der = encodePss(parameters, components);
+ return Optional.of(X509AlgorithmIdentifier.exact(OID_RSA_PSS, der));
+ }
+
+ @Override
+ public Optional decode(X509AlgorithmIdentifier identifier) {
+ if (!OID_RSA_PSS.equals(identifier.oid())) {
+ return Optional.empty();
+ }
+ if (identifier.parameterForm() != X509AlgorithmIdentifier.ParameterForm.EXACT_DER) {
+ throw new IllegalArgumentException("RSA-PSS parameters must be explicit");
+ }
+ AlgorithmIdentity.RsaPssParameters parameters = decodePss(identifier.parameters(), components);
+ return Optional.of(BootstrapAlgorithmIdentities.rsaPss(parameters.hash(), parameters.maskHash(),
+ parameters.saltLength()));
+ }
+ }
+
+ /** Parameterized authority for named-curve EC public-key identifiers. */
+ private static final class EcPublicKeyRule implements X509BindingRule {
+
+ private final X509ComponentCatalog components;
+
+ private EcPublicKeyRule(X509ComponentCatalog components) {
+ this.components = components;
+ }
+
+ @Override
+ public String id() {
+ return "zeroecho.spki.ec-named-curve";
+ }
+
+ @Override
+ public X509AlgorithmRole role() {
+ return X509AlgorithmRole.SUBJECT_PUBLIC_KEY_ALGORITHM;
+ }
+
+ @Override
+ public String oid() {
+ return OID_EC_PUBLIC_KEY;
+ }
+
+ @Override
+ public String semanticFingerprint() {
+ return "ec-spki-v1|named-only|p-256,p-384,p-521|sec1-point";
+ }
+
+ @Override
+ public SignatureEncoding signatureEncoding() {
+ return SignatureEncoding.NOT_APPLICABLE;
+ }
+
+ @Override
+ public PublicKeyEncoding publicKeyEncoding() {
+ return PublicKeyEncoding.EC_POINT;
+ }
+
+ @Override
+ public Optional encode(AlgorithmIdentity identity) {
+ if (identity.kind() != AlgorithmIdentity.Kind.PUBLIC_KEY
+ || !"zeroecho/ec".equals(identity.family().canonicalForm())) {
+ return Optional.empty();
+ }
+ String curveOid = components.oid(X509ComponentCatalog.Kind.NAMED_CURVE, identity);
+ return Optional.of(X509AlgorithmIdentifier.exact(OID_EC_PUBLIC_KEY, StrictDer.oid(curveOid)));
+ }
+
+ @Override
+ public Optional decode(X509AlgorithmIdentifier identifier) {
+ if (!OID_EC_PUBLIC_KEY.equals(identifier.oid())) {
+ return Optional.empty();
+ }
+ if (identifier.parameterForm() != X509AlgorithmIdentifier.ParameterForm.EXACT_DER) {
+ throw new IllegalArgumentException("EC SubjectPublicKeyInfo requires named-curve parameters");
+ }
+ String curve = StrictDer.decodeOid(identifier.parameters());
+ return Optional.of(components.identity(X509ComponentCatalog.Kind.NAMED_CURVE, curve));
+ }
+ }
+
+ private static byte[] encodePss(AlgorithmIdentity.RsaPssParameters parameters,
+ X509ComponentCatalog components) {
+ String hashOid = requireDigestOid(parameters.hash(), components);
+ String maskHashOid = requireDigestOid(parameters.maskHash(), components);
+ if (!BootstrapAlgorithmIdentities.MGF1.equals(parameters.mask())) {
+ throw new IllegalArgumentException("RSA-PSS requires MGF1");
+ }
+ byte[] hashAlgorithm = StrictDer.sequence(StrictDer.oid(hashOid), StrictDer.nullValue());
+ byte[] maskHashAlgorithm = StrictDer.sequence(StrictDer.oid(maskHashOid), StrictDer.nullValue());
+ byte[] maskAlgorithm = StrictDer.sequence(
+ StrictDer.oid(components.oid(X509ComponentCatalog.Kind.MASK_GENERATION, parameters.mask())),
+ maskHashAlgorithm);
+ return StrictDer.sequence(StrictDer.explicit(0, hashAlgorithm), StrictDer.explicit(1, maskAlgorithm),
+ StrictDer.explicit(2, StrictDer.integer(parameters.saltLength())));
+ }
+
+ private static AlgorithmIdentity.RsaPssParameters decodePss(byte[] encoded, X509ComponentCatalog components) {
+ StrictDer.Reader sequence = StrictDer.reader(encoded).readConstructed(0x30);
+ byte[] hashAlgorithm = sequence.readConstructed(0xa0).readOnlyValue(0x30);
+ byte[] maskAlgorithm = sequence.readConstructed(0xa1).readOnlyValue(0x30);
+ int saltLength = sequence.readConstructed(0xa2).readOnlyInteger();
+ if (sequence.hasRemaining()) {
+ throw new IllegalArgumentException("RSA-PSS DEFAULT trailer must be omitted");
+ }
+ sequence.requireEnd();
+
+ AlgorithmIdentity hash = decodeDigestAlgorithm(hashAlgorithm, components);
+ StrictDer.Reader mask = StrictDer.readerContent(maskAlgorithm);
+ String maskOid = mask.readOid();
+ if (!BootstrapAlgorithmIdentities.MGF1.equals(
+ components.identity(X509ComponentCatalog.Kind.MASK_GENERATION, maskOid))) {
+ throw new IllegalArgumentException("RSA-PSS mask algorithm must be MGF1");
+ }
+ byte[] maskHashAlgorithm = mask.readOnlyValue(0x30);
+ mask.requireEnd();
+ AlgorithmIdentity maskHash = decodeDigestAlgorithm(maskHashAlgorithm, components);
+
+ AlgorithmIdentity.RsaPssParameters parameters = new AlgorithmIdentity.RsaPssParameters(hash,
+ BootstrapAlgorithmIdentities.MGF1, maskHash, saltLength, 1);
+ if (!Arrays.equals(encoded, encodePss(parameters, components))) {
+ throw new IllegalArgumentException("RSA-PSS parameters are not canonical");
+ }
+ return parameters;
+ }
+
+ private static AlgorithmIdentity decodeDigestAlgorithm(byte[] content, X509ComponentCatalog components) {
+ StrictDer.Reader reader = StrictDer.readerContent(content);
+ String oid = reader.readOid();
+ reader.readNull();
+ reader.requireEnd();
+ return components.identity(X509ComponentCatalog.Kind.DIGEST, oid);
+ }
+
+ private static String requireDigestOid(AlgorithmIdentity digest, X509ComponentCatalog components) {
+ return components.oid(X509ComponentCatalog.Kind.DIGEST, digest);
+ }
+
+ /**
+ * Minimal strict DER support for the fixed standard parameter structures.
+ */
+ private enum StrictDer {
+ ;
+
+ private static final int MINIMUM_OID_COMPONENTS = 2;
+ private static final int SHORT_LENGTH_BOUND = 128;
+
+ private static byte[] sequence(byte[]... values) {
+ return tagged(0x30, concatenate(values));
+ }
+
+ private static byte[] explicit(int tag, byte[] value) {
+ return tagged(0xa0 + tag, value);
+ }
+
+ private static byte[] nullValue() {
+ return new byte[] { 0x05, 0x00 };
+ }
+
+ private static byte[] integer(int value) {
+ if (value < 0) {
+ throw new IllegalArgumentException("DER integer must not be negative");
+ }
+ return tagged(0x02, BigInteger.valueOf(value).toByteArray());
+ }
+
+ private static byte[] oid(String dotted) {
+ String[] components = dotted.split("\\.");
+ if (components.length < MINIMUM_OID_COMPONENTS) {
+ throw new IllegalArgumentException("Invalid OID");
+ }
+ int first = Integer.parseInt(components[0]);
+ int second = Integer.parseInt(components[1]);
+ ByteArrayOutputStream content = new ByteArrayOutputStream();
+ writeBase128(content, 40L * first + second);
+ for (int index = 2; index < components.length; index++) {
+ writeBase128(content, Long.parseLong(components[index]));
+ }
+ return tagged(0x06, content.toByteArray());
+ }
+
+ private static String decodeOid(byte[] der) {
+ Reader reader = reader(der);
+ String oid = reader.readOid();
+ reader.requireEnd();
+ if (!Arrays.equals(der, oid(oid))) {
+ throw new IllegalArgumentException("OID is not canonical DER");
+ }
+ return oid;
+ }
+
+ private static byte[] tagged(int tag, byte[] content) {
+ ByteArrayOutputStream output = new ByteArrayOutputStream(content.length + 6);
+ output.write(tag);
+ writeLength(output, content.length);
+ output.writeBytes(content);
+ return output.toByteArray();
+ }
+
+ private static byte[] concatenate(byte[][] values) {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ for (byte[] value : values) {
+ output.writeBytes(value);
+ }
+ return output.toByteArray();
+ }
+
+ private static void writeLength(ByteArrayOutputStream output, int length) {
+ if (length < SHORT_LENGTH_BOUND) {
+ output.write(length);
+ return;
+ }
+ int octets = 0;
+ int current = length;
+ while (current != 0) {
+ octets++;
+ current >>>= 8;
+ }
+ output.write(0x80 | octets);
+ for (int shift = (octets - 1) * 8; shift >= 0; shift -= 8) {
+ output.write(length >>> shift);
+ }
+ }
+
+ private static void writeBase128(ByteArrayOutputStream output, long value) {
+ if (value < 0) {
+ throw new IllegalArgumentException("OID component must not be negative");
+ }
+ int groups = 1;
+ long current = value;
+ while ((current >>>= 7) != 0) {
+ groups++;
+ }
+ for (int group = groups - 1; group >= 0; group--) {
+ int octet = (int) ((value >>> (group * 7)) & 0x7f);
+ output.write(group == 0 ? octet : octet | 0x80);
+ }
+ }
+
+ private static Reader reader(byte[] encoded) {
+ return new Reader(encoded.clone(), 0, encoded.length);
+ }
+
+ private static Reader readerContent(byte[] content) {
+ return new Reader(content.clone(), 0, content.length);
+ }
+
+ /** Bounded cursor over one independently owned DER byte sequence. */
+ private static final class Reader {
+
+ private final byte[] data;
+ private final int end;
+ private int offset;
+
+ private Reader(byte[] data, int offset, int end) {
+ this.data = data;
+ this.offset = offset;
+ this.end = end;
+ }
+
+ private Reader readConstructed(int expectedTag) {
+ byte[] value = readValue(expectedTag);
+ return new Reader(value, 0, value.length);
+ }
+
+ private byte[] readOnlyValue(int expectedTag) {
+ byte[] value = readValue(expectedTag);
+ requireEnd();
+ return value;
+ }
+
+ private int readOnlyInteger() {
+ byte[] value = readValue(0x02);
+ requireEnd();
+ if (value.length == 0 || value.length > 5 || (value[0] & 0x80) != 0) {
+ throw new IllegalArgumentException("Invalid non-negative DER integer");
+ }
+ BigInteger integer = new BigInteger(value);
+ if (!Arrays.equals(value, integer.toByteArray()) || integer.bitLength() > 31) {
+ throw new IllegalArgumentException("Non-canonical or excessive DER integer");
+ }
+ return integer.intValue();
+ }
+
+ private String readOid() {
+ byte[] value = readValue(0x06);
+ if (value.length == 0) {
+ throw new IllegalArgumentException("Empty DER OID");
+ }
+ StringBuilder dotted = new StringBuilder();
+ long component = 0;
+ boolean first = true;
+ boolean continued = false;
+ for (byte octetValue : value) {
+ if (component > (Long.MAX_VALUE >>> 7)) {
+ throw new IllegalArgumentException("DER OID component overflow");
+ }
+ int octet = octetValue & 0xff;
+ component = (component << 7) | (octet & 0x7f);
+ continued = (octet & 0x80) != 0;
+ if (!continued) {
+ if (first) {
+ int firstArc = component < 40 ? 0 : component < 80 ? 1 : 2;
+ dotted.append(firstArc).append('.').append(component - 40L * firstArc);
+ first = false;
+ } else {
+ dotted.append('.').append(component);
+ }
+ component = 0;
+ }
+ }
+ if (continued || first) {
+ throw new IllegalArgumentException("Truncated DER OID");
+ }
+ return dotted.toString();
+ }
+
+ private void readNull() {
+ if (readValue(0x05).length != 0) {
+ throw new IllegalArgumentException("Invalid DER NULL");
+ }
+ }
+
+ private byte[] readValue(int expectedTag) {
+ if (offset >= end || (data[offset++] & 0xff) != expectedTag) {
+ throw new IllegalArgumentException("Unexpected DER tag");
+ }
+ int length = readLength();
+ if (length > end - offset) {
+ throw new IllegalArgumentException("Truncated DER value");
+ }
+ byte[] value = Arrays.copyOfRange(data, offset, offset + length);
+ offset += length;
+ return value;
+ }
+
+ private int readLength() {
+ if (offset >= end) {
+ throw new IllegalArgumentException("Truncated DER length");
+ }
+ int first = data[offset++] & 0xff;
+ if (first < SHORT_LENGTH_BOUND) {
+ return first;
+ }
+ int octets = first & 0x7f;
+ if (octets == 0 || octets > 4 || octets > end - offset) {
+ throw new IllegalArgumentException("Invalid DER length");
+ }
+ int length = 0;
+ for (int index = 0; index < octets; index++) {
+ if (length > (Integer.MAX_VALUE >>> 8)) {
+ throw new IllegalArgumentException("DER length overflow");
+ }
+ length = (length << 8) | (data[offset++] & 0xff);
+ }
+ if (length < SHORT_LENGTH_BOUND) {
+ throw new IllegalArgumentException("Non-canonical DER length");
+ }
+ return length;
+ }
+
+ private void requireEnd() {
+ if (offset != end) {
+ throw new IllegalArgumentException("Trailing DER data");
+ }
+ }
+
+ private boolean hasRemaining() {
+ return offset != end;
+ }
+ }
+ }
+}
diff --git a/pki/src/main/java/zeroecho/pki/impl/framework/x509/StreamingDerReader.java b/pki/src/main/java/zeroecho/pki/impl/framework/x509/StreamingDerReader.java
new file mode 100644
index 0000000..229c38c
--- /dev/null
+++ b/pki/src/main/java/zeroecho/pki/impl/framework/x509/StreamingDerReader.java
@@ -0,0 +1,681 @@
+/*******************************************************************************
+ * 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.Objects;
+
+import zeroecho.core.io.CancellationSignal;
+import zeroecho.core.io.RepeatableContent;
+
+/**
+ * Focused incremental canonical-DER structural validator.
+ *
+ *
+ * Validation consumes exactly one object, rejects indefinite and non-minimal
+ * lengths, and checks canonical primitive forms without materializing aggregate
+ * content. Aggregate offsets and lengths use {@code long}. The fixed structural
+ * depth is an X.509 adapter capability, not an aggregate byte or cardinality
+ * limit.
+ *
+ *
+ *
+ * Canonical SET ordering uses two monotonic comparison readers per active,
+ * fixed-bounded structure depth. Each reader moves forward only, so validation
+ * is {@code O(B)} for encoded size {@code B} with auxiliary heap bounded by the
+ * fixed depth and comparison-buffer size.
+ *
+ */
+public final class StreamingDerReader {
+
+ private static final int BUFFER_BYTES = 16 * 1024;
+ private static final int MAXIMUM_X509_STRUCTURE_DEPTH = 64;
+ private static final int HIGH_TAG_NUMBER = 0x1f;
+ private static final int CONTINUATION_BIT = 0x80;
+ private static final long SHORT_LENGTH_LIMIT = 128L;
+
+ /**
+ * Validates one complete canonical DER object.
+ *
+ * @param content repeatable original content
+ * @param cancellation cancellation signal
+ * @return exact encoded length
+ * @throws IOException if input fails, is malformed, non-canonical, truncated,
+ * or has trailing data
+ */
+ public long validate(RepeatableContent content, CancellationSignal cancellation) throws IOException {
+ Objects.requireNonNull(content, "content");
+ Objects.requireNonNull(cancellation, "cancellation");
+ try (CountedInput input = new CountedInput(content.openStream());
+ OrderingContext ordering = new OrderingContext(content)) {
+ Header root = HeaderReader.read(input);
+ readValue(input, root, 0, cancellation, ordering);
+ if (input.read() >= 0) {
+ throw new IOException("Trailing DER data: code=TRAILING_DER_DATA");
+ }
+ long length = input.position();
+ if (content.length().isPresent() && length != content.length().getAsLong()) {
+ throw new IOException("DER length mismatch: code=NON_CANONICAL_DER");
+ }
+ return length;
+ }
+ }
+
+ /**
+ * Validates and locates the signed portions of one certificate or CRL.
+ *
+ * @param content original repeatable DER
+ * @param kind signed-object grammar
+ * @param cancellation cancellation signal
+ * @return immutable long-offset layout
+ * @throws IOException if structure or canonicality is invalid
+ */
+ public SignedObjectLayout inspectSignedObject(RepeatableContent content, SignedObjectKind kind,
+ CancellationSignal cancellation) throws IOException {
+ validate(content, cancellation);
+ try (CountedInput input = new CountedInput(content.openStream())) {
+ Header outer = HeaderReader.read(input);
+ requireSequence(outer);
+ long outerEnd = Math.addExact(input.position(), outer.length());
+ Header tbs = HeaderReader.read(input);
+ requireSequence(tbs);
+ long tbsTotalLength = tbs.encodedLength();
+ AlgorithmAndKey inner = inspectTbs(content, kind, tbs, cancellation);
+ skip(input, tbs.length(), cancellation);
+ Header outerAlgorithm = HeaderReader.read(input);
+ requireSequence(outerAlgorithm);
+ skip(input, outerAlgorithm.length(), cancellation);
+ Header signature = HeaderReader.read(input);
+ if (!signature.universal() || signature.tagNumber() != 3 || signature.constructed()
+ || signature.length() < 1L) {
+ throw new IOException("Malformed signed-object signature BIT STRING");
+ }
+ int unused = input.readRequired();
+ if (unused != 0) {
+ throw new IOException("Signed-object signature has unused bits");
+ }
+ long signatureOffset = input.position();
+ skip(input, signature.length() - 1L, cancellation);
+ if (input.position() != outerEnd || input.read() >= 0) {
+ throw new IOException("Signed object has trailing data");
+ }
+ return new SignedObjectLayout(tbs.start(), tbsTotalLength, inner.algorithmOffset(),
+ inner.algorithmLength(), outerAlgorithm.start(), outerAlgorithm.encodedLength(),
+ inner.spkiOffset(), inner.spkiLength(), signatureOffset, signature.length() - 1L);
+ }
+ }
+
+ private static AlgorithmAndKey inspectTbs(RepeatableContent content, SignedObjectKind kind, Header tbs,
+ CancellationSignal cancellation) throws IOException {
+ try (CountedInput input = new CountedInput(content.openStream())) {
+ skip(input, tbs.valueOffset(), cancellation);
+ Header child = HeaderReader.read(input);
+ if (kind == SignedObjectKind.CERTIFICATE) {
+ if (child.contextSpecific(0)) {
+ skip(input, child.length(), cancellation);
+ child = HeaderReader.read(input);
+ }
+ requireUniversal(child, 2);
+ skip(input, child.length(), cancellation);
+ child = HeaderReader.read(input);
+ } else if (child.universal() && child.tagNumber() == 2) {
+ skip(input, child.length(), cancellation);
+ child = HeaderReader.read(input);
+ }
+ requireSequence(child);
+ long algorithmOffset = child.start();
+ long algorithmLength = child.encodedLength();
+ if (kind == SignedObjectKind.CRL) {
+ return new AlgorithmAndKey(algorithmOffset, algorithmLength, -1L, 0L);
+ }
+ skip(input, child.length(), cancellation);
+ for (int index = 0; index < 3; index++) {
+ Header field = HeaderReader.read(input);
+ skip(input, field.length(), cancellation);
+ }
+ Header spki = HeaderReader.read(input);
+ requireSequence(spki);
+ return new AlgorithmAndKey(algorithmOffset, algorithmLength, spki.start(), spki.encodedLength());
+ }
+ }
+
+ private static void requireSequence(Header header) throws IOException {
+ requireUniversal(header, 16);
+ if (!header.constructed()) {
+ throw new IOException("Expected constructed DER SEQUENCE");
+ }
+ }
+
+ private static void requireUniversal(Header header, int tagNumber) throws IOException {
+ if (!header.universal() || header.tagNumber() != tagNumber) {
+ throw new IOException("Unexpected signed-object DER field");
+ }
+ }
+
+ private static void readValue(CountedInput input, Header header, int depth, CancellationSignal cancellation,
+ OrderingContext ordering) throws IOException {
+ if (depth >= MAXIMUM_X509_STRUCTURE_DEPTH) {
+ throw new IOException("DER nesting exceeds X.509 adapter capability");
+ }
+ long end = Math.addExact(input.position(), header.length());
+ if (header.constructed()) {
+ requireConstructedForm(header);
+ SetOrdering setOrdering = header.universal() && header.tagNumber() == 17
+ ? ordering.begin(depth, header.valueOffset(), cancellation)
+ : null;
+ while (input.position() < end) {
+ cancellation.throwIfCancelled();
+ Header child = HeaderReader.read(input);
+ long childEnd = Math.addExact(input.position(), child.length());
+ if (childEnd > end) {
+ throw new IOException("DER child exceeds parent: code=MALFORMED_SIGNED_OBJECT");
+ }
+ readValue(input, child, depth + 1, cancellation, ordering);
+ if (setOrdering != null && setOrdering.accept(child.encodedLength(), cancellation) > 0) {
+ throw new IOException("Non-canonical DER SET ordering: code=NON_CANONICAL_DER");
+ }
+ }
+ if (setOrdering != null) {
+ setOrdering.finish(cancellation);
+ }
+ } else {
+ requirePrimitiveForm(header);
+ PrimitiveReader.read(input, header, cancellation);
+ }
+ if (input.position() != end) {
+ throw new IOException("DER value length mismatch: code=MALFORMED_SIGNED_OBJECT");
+ }
+ }
+
+ private static void requireConstructedForm(Header header) throws IOException {
+ if (header.universal() && header.tagNumber() != 16 && header.tagNumber() != 17) {
+ throw new IOException("Constructed primitive is not canonical DER");
+ }
+ }
+
+ private static void requirePrimitiveForm(Header header) throws IOException {
+ if (header.universal() && (header.tagNumber() == 16 || header.tagNumber() == 17)) {
+ throw new IOException("Primitive container is not canonical DER");
+ }
+ }
+
+ private static void skip(CountedInput input, long length, CancellationSignal cancellation) throws IOException {
+ byte[] buffer = new byte[BUFFER_BYTES];
+ long remaining = length;
+ while (remaining != 0L) {
+ cancellation.throwIfCancelled();
+ int read = input.read(buffer, 0, (int) Math.min(buffer.length, remaining));
+ if (read < 0) {
+ throw new IOException("Truncated DER value");
+ }
+ if (read == 0) {
+ input.readRequired();
+ remaining--;
+ } else {
+ remaining -= read;
+ }
+ }
+ }
+
+ /**
+ * Supported signed-object grammar.
+ */
+ public enum SignedObjectKind {
+ /** X.509 Certificate. */
+ CERTIFICATE,
+ /** X.509 CertificateList (CRL). */
+ CRL
+ }
+
+ /**
+ * Exact offsets into the validated original DER.
+ *
+ * @param tbsOffset complete TBS TLV offset
+ * @param tbsLength complete TBS TLV length
+ * @param tbsAlgorithmOffset TBS AlgorithmIdentifier TLV offset
+ * @param tbsAlgorithmLength TBS AlgorithmIdentifier TLV length
+ * @param outerAlgorithmOffset outer AlgorithmIdentifier TLV offset
+ * @param outerAlgorithmLength outer AlgorithmIdentifier TLV length
+ * @param subjectPublicKeyInfoOffset SPKI TLV offset, or {@code -1} for CRLs
+ * @param subjectPublicKeyInfoLength SPKI TLV length, or zero for CRLs
+ * @param signatureOffset signature octets offset
+ * @param signatureLength signature octets length
+ */
+ public record SignedObjectLayout(long tbsOffset, long tbsLength, long tbsAlgorithmOffset,
+ long tbsAlgorithmLength, long outerAlgorithmOffset, long outerAlgorithmLength,
+ long subjectPublicKeyInfoOffset, long subjectPublicKeyInfoLength, long signatureOffset,
+ long signatureLength) {
+ }
+
+ private record AlgorithmAndKey(long algorithmOffset, long algorithmLength, long spkiOffset, long spkiLength) {
+ }
+
+ private record Header(int firstTag, int tagNumber, long length, long start, long valueOffset) {
+ private boolean constructed() {
+ return (firstTag & 0x20) != 0;
+ }
+
+ private boolean universal() {
+ return (firstTag & 0xc0) == 0;
+ }
+
+ private boolean contextSpecific(int expectedTag) {
+ return (firstTag & 0xc0) == 0x80 && tagNumber == expectedTag;
+ }
+
+ private long encodedLength() {
+ return Math.addExact(valueOffset - start, length);
+ }
+ }
+
+ /** Canonical DER tag and length decoder. */
+ private static final class HeaderReader {
+ private static Header read(CountedInput input) throws IOException {
+ long start = input.position();
+ int firstTag = input.readRequired();
+ if (firstTag == 0) {
+ throw new IOException("DER end-of-contents is forbidden");
+ }
+ int tagNumber = readTagNumber(input, firstTag);
+ long length = readLength(input);
+ return new Header(firstTag, tagNumber, length, start, input.position());
+ }
+
+ private static int readTagNumber(CountedInput input, int firstTag) throws IOException {
+ int tagNumber = firstTag & HIGH_TAG_NUMBER;
+ if (tagNumber == HIGH_TAG_NUMBER) {
+ int octet = input.readRequired();
+ if ((octet & 0x7f) == 0) {
+ throw new IOException("Non-minimal DER high tag");
+ }
+ while ((octet & CONTINUATION_BIT) != 0) {
+ octet = input.readRequired();
+ }
+ tagNumber = -1;
+ }
+ return tagNumber;
+ }
+
+ private static long readLength(CountedInput input) throws IOException {
+ int firstLength = input.readRequired();
+ if (firstLength < CONTINUATION_BIT) {
+ return firstLength;
+ }
+ int octets = firstLength & 0x7f;
+ if (octets == 0) {
+ throw new IOException("Indefinite DER length: code=NON_CANONICAL_DER");
+ }
+ if (octets > Long.BYTES) {
+ throw new IOException("DER length is not representable");
+ }
+ int first = input.readRequired();
+ if (first == 0) {
+ throw new IOException("Non-minimal DER length: code=NON_CANONICAL_DER");
+ }
+ long length = first;
+ for (int index = 1; index < octets; index++) {
+ if (length > (Long.MAX_VALUE >>> Byte.SIZE)) {
+ throw new IOException("DER length overflow: code=CONTENT_LENGTH_OVERFLOW");
+ }
+ length = (length << Byte.SIZE) | input.readRequired();
+ }
+ if (length < SHORT_LENGTH_LIMIT) {
+ throw new IOException("Non-minimal DER length: code=NON_CANONICAL_DER");
+ }
+ return length;
+ }
+ }
+
+ /** Fixed-depth owner of monotonic comparison streams for DER SET values. */
+ private static final class OrderingContext implements AutoCloseable {
+ private final RepeatableContent content;
+ private final SetOrdering[] levels = new SetOrdering[MAXIMUM_X509_STRUCTURE_DEPTH];
+
+ private OrderingContext(RepeatableContent content) {
+ this.content = content;
+ }
+
+ private SetOrdering begin(int depth, long valueOffset, CancellationSignal cancellation) throws IOException {
+ SetOrdering ordering = levels[depth];
+ if (ordering == null) {
+ ordering = new SetOrdering(content);
+ levels[depth] = ordering;
+ }
+ ordering.begin(valueOffset, cancellation);
+ return ordering;
+ }
+
+ @Override
+ public void close() throws IOException {
+ IOException failure = null;
+ for (SetOrdering ordering : levels) {
+ if (ordering == null) {
+ continue;
+ }
+ try {
+ ordering.closeStreams();
+ } catch (IOException exception) {
+ if (failure == null) {
+ failure = exception;
+ } else {
+ failure.addSuppressed(exception);
+ }
+ }
+ }
+ if (failure != null) {
+ throw failure;
+ }
+ }
+ }
+
+ /** Adjacent-child comparator whose two source passes only move forward. */
+ private static final class SetOrdering {
+ private static final long NO_PREVIOUS_VALUE = -1L;
+
+ private final CountedInput left;
+ private final CountedInput right;
+ private final byte[] leftBuffer = new byte[BUFFER_BYTES];
+ private final byte[] rightBuffer = new byte[BUFFER_BYTES];
+ private long previousLength = NO_PREVIOUS_VALUE;
+
+ private SetOrdering(RepeatableContent content) throws IOException {
+ left = new CountedInput(content.openStream());
+ try {
+ right = new CountedInput(content.openStream());
+ } catch (IOException exception) {
+ left.close();
+ throw exception;
+ }
+ }
+
+ private void begin(long valueOffset, CancellationSignal cancellation) throws IOException {
+ advanceTo(left, valueOffset, leftBuffer, cancellation);
+ advanceTo(right, valueOffset, rightBuffer, cancellation);
+ previousLength = NO_PREVIOUS_VALUE;
+ }
+
+ private int accept(long currentLength, CancellationSignal cancellation) throws IOException {
+ if (previousLength == NO_PREVIOUS_VALUE) {
+ skipWithBuffer(right, currentLength, rightBuffer, cancellation);
+ previousLength = currentLength;
+ return 0;
+ }
+ int comparison = compare(previousLength, currentLength, cancellation);
+ previousLength = currentLength;
+ return comparison;
+ }
+
+ private void finish(CancellationSignal cancellation) throws IOException {
+ if (previousLength != NO_PREVIOUS_VALUE) {
+ skipWithBuffer(left, previousLength, leftBuffer, cancellation);
+ }
+ }
+
+ private int compare(long leftLength, long rightLength, CancellationSignal cancellation) throws IOException {
+ long common = Math.min(leftLength, rightLength);
+ int comparison = 0;
+ long remaining = common;
+ while (remaining != 0L) {
+ cancellation.throwIfCancelled();
+ int count = (int) Math.min(BUFFER_BYTES, remaining);
+ readExactly(left, leftBuffer, count);
+ readExactly(right, rightBuffer, count);
+ if (comparison == 0) {
+ comparison = compareBuffers(leftBuffer, rightBuffer, count);
+ }
+ remaining -= count;
+ }
+ skipWithBuffer(left, leftLength - common, leftBuffer, cancellation);
+ skipWithBuffer(right, rightLength - common, rightBuffer, cancellation);
+ return comparison == 0 ? Long.compare(leftLength, rightLength) : comparison;
+ }
+
+ private void closeStreams() throws IOException {
+ IOException failure = null;
+ try {
+ left.close();
+ } catch (IOException exception) {
+ failure = exception;
+ }
+ try {
+ right.close();
+ } catch (IOException exception) {
+ if (failure == null) {
+ failure = exception;
+ } else {
+ failure.addSuppressed(exception);
+ }
+ }
+ if (failure != null) {
+ throw failure;
+ }
+ }
+
+ private static void advanceTo(CountedInput input, long position, byte[] buffer,
+ CancellationSignal cancellation) throws IOException {
+ if (input.position() > position) {
+ throw new IOException("DER SET comparison stream moved backward");
+ }
+ skipWithBuffer(input, position - input.position(), buffer, cancellation);
+ }
+
+ private static void skipWithBuffer(InputStream input, long length, byte[] buffer,
+ CancellationSignal cancellation) throws IOException {
+ long remaining = length;
+ while (remaining != 0L) {
+ cancellation.throwIfCancelled();
+ int count = (int) Math.min(buffer.length, remaining);
+ readExactly(input, buffer, count);
+ remaining -= count;
+ }
+ }
+
+ private static void readExactly(InputStream input, byte[] buffer, int length) throws IOException {
+ int offset = 0;
+ while (offset != length) {
+ int count = input.read(buffer, offset, length - offset);
+ if (count < 0) {
+ throw new IOException("Truncated DER value");
+ }
+ if (count == 0) {
+ int value = input.read();
+ if (value < 0) {
+ throw new IOException("Truncated DER value");
+ }
+ buffer[offset] = (byte) value;
+ offset++;
+ } else {
+ offset += count;
+ }
+ }
+ }
+
+ private static int compareBuffers(byte[] left, byte[] right, int length) {
+ for (int index = 0; index < length; index++) {
+ int comparison = Integer.compare(Byte.toUnsignedInt(left[index]), Byte.toUnsignedInt(right[index]));
+ if (comparison != 0) {
+ return comparison;
+ }
+ }
+ return 0;
+ }
+ }
+
+ /**
+ * Input wrapper retaining an overflow-checked long byte position.
+ */
+ private static final class CountedInput extends InputStream {
+ private final InputStream delegate;
+ private long position;
+
+ private CountedInput(InputStream delegate) {
+ super();
+ this.delegate = delegate;
+ }
+
+ @Override
+ public int read() throws IOException {
+ int value = delegate.read();
+ if (value >= 0) {
+ position = Math.addExact(position, 1L);
+ }
+ return value;
+ }
+
+ @Override
+ public int read(byte[] bytes, int offset, int length) throws IOException {
+ int count = delegate.read(bytes, offset, length);
+ if (count > 0) {
+ position = Math.addExact(position, count);
+ }
+ return count;
+ }
+
+ @Override
+ public void close() throws IOException {
+ delegate.close();
+ }
+
+ private int readRequired() throws IOException {
+ int value = read();
+ if (value < 0) {
+ throw new IOException("Truncated DER object");
+ }
+ return value;
+ }
+
+ private long position() {
+ return position;
+ }
+ }
+
+ /**
+ * Canonical validation for primitive DER values.
+ */
+ private static final class PrimitiveReader {
+ private static final long EMPTY_LENGTH = 0L;
+ private static final long SINGLE_OCTET_LENGTH = 1L;
+
+ private static void read(CountedInput input, Header header, CancellationSignal cancellation)
+ throws IOException {
+ if (!header.universal()) {
+ skip(input, header.length(), cancellation);
+ return;
+ }
+ switch (header.tagNumber()) {
+ case 1 -> readBoolean(input, header.length());
+ case 2 -> readInteger(input, header.length(), cancellation);
+ case 3 -> readBitString(input, header.length(), cancellation);
+ case 5 -> readNull(header.length());
+ case 6 -> readOid(input, header.length(), cancellation);
+ default -> skip(input, header.length(), cancellation);
+ }
+ }
+
+ private static void readBoolean(CountedInput input, long length) throws IOException {
+ if (length != SINGLE_OCTET_LENGTH) {
+ throw new IOException("Malformed DER BOOLEAN");
+ }
+ int value = input.readRequired();
+ if (value != 0 && value != 0xff) {
+ throw new IOException("Non-canonical DER BOOLEAN");
+ }
+ }
+
+ private static void readInteger(CountedInput input, long length, CancellationSignal cancellation)
+ throws IOException {
+ if (length == EMPTY_LENGTH) {
+ throw new IOException("Malformed DER INTEGER");
+ }
+ int first = input.readRequired();
+ if (length > SINGLE_OCTET_LENGTH) {
+ int second = input.readRequired();
+ if ((first == 0 && (second & CONTINUATION_BIT) == 0)
+ || (first == 0xff && (second & CONTINUATION_BIT) != 0)) {
+ throw new IOException("Non-canonical DER INTEGER");
+ }
+ skip(input, length - 2L, cancellation);
+ }
+ }
+
+ private static void readBitString(CountedInput input, long length, CancellationSignal cancellation)
+ throws IOException {
+ if (length == EMPTY_LENGTH) {
+ throw new IOException("Malformed DER BIT STRING");
+ }
+ int unused = input.readRequired();
+ if (unused > 7 || (length == SINGLE_OCTET_LENGTH && unused != 0)) {
+ throw new IOException("Malformed DER BIT STRING");
+ }
+ long octets = length - SINGLE_OCTET_LENGTH;
+ if (octets == EMPTY_LENGTH) {
+ return;
+ }
+ int last = 0;
+ for (long index = 0L; index < octets; index++) {
+ cancellation.throwIfCancelled();
+ last = input.readRequired();
+ }
+ if (unused != 0 && (last & ((1 << unused) - 1)) != 0) {
+ throw new IOException("Non-canonical DER BIT STRING");
+ }
+ }
+
+ private static void readNull(long length) throws IOException {
+ if (length != EMPTY_LENGTH) {
+ throw new IOException("Malformed DER NULL");
+ }
+ }
+
+ private static void readOid(CountedInput input, long length, CancellationSignal cancellation)
+ throws IOException {
+ if (length == EMPTY_LENGTH) {
+ throw new IOException("Malformed DER OID");
+ }
+ boolean atComponentStart = true;
+ for (long index = 0L; index < length; index++) {
+ cancellation.throwIfCancelled();
+ int octet = input.readRequired();
+ if (atComponentStart && octet == CONTINUATION_BIT) {
+ throw new IOException("Non-canonical DER OID");
+ }
+ atComponentStart = (octet & CONTINUATION_BIT) == 0;
+ }
+ if (!atComponentStart) {
+ throw new IOException("Truncated DER OID");
+ }
+ }
+ }
+}
diff --git a/pki/src/main/java/zeroecho/pki/impl/framework/x509/StreamingDerWriter.java b/pki/src/main/java/zeroecho/pki/impl/framework/x509/StreamingDerWriter.java
new file mode 100644
index 0000000..692ed05
--- /dev/null
+++ b/pki/src/main/java/zeroecho/pki/impl/framework/x509/StreamingDerWriter.java
@@ -0,0 +1,155 @@
+/*******************************************************************************
+ * 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.io.OutputStream;
+
+import zeroecho.core.io.CancellationSignal;
+
+/**
+ * Focused canonical DER streaming primitives used by signed-object adapters.
+ *
+ *
+ * This is deliberately not a general ASN.1 framework. It writes already
+ * validated current X.509 child encodings into definite-length containers while
+ * accounting in {@code long}. ZeroEcho core imposes no arbitrary product-wide
+ * aggregate CRL-size or revocation-entry limit; completion remains subject to
+ * storage, I/O, technical representability, cancellation, and deployment
+ * policy.
+ *
+ */
+public final class StreamingDerWriter {
+
+ /** DER universal SEQUENCE tag. */
+ public static final int SEQUENCE_TAG = 0x30;
+ /** DER universal BIT STRING tag. */
+ public static final int BIT_STRING_TAG = 0x03;
+
+ private static final int BUFFER_SIZE = 16 * 1024;
+ private static final long EMPTY_LENGTH = 0L;
+ private static final long SHORT_FORM_LIMIT = 128L;
+
+ private StreamingDerWriter() {
+ }
+
+ /**
+ * Returns the encoded size of one tag-length-value object.
+ *
+ * @param valueLength non-negative value length
+ * @return complete encoded length
+ * @throws IllegalArgumentException if the length is negative
+ * @throws ArithmeticException if the result overflows {@code long}
+ */
+ public static long encodedLength(long valueLength) {
+ if (valueLength < EMPTY_LENGTH) {
+ throw new IllegalArgumentException("DER value length must not be negative");
+ }
+ return Math.addExact(Math.addExact(1L, lengthOctets(valueLength)), valueLength);
+ }
+
+ /**
+ * Writes one canonical DER tag and definite length.
+ *
+ * @param output target stream
+ * @param tag one-octet tag
+ * @param valueLength non-negative value length
+ * @throws IOException if writing fails
+ * @throws IllegalArgumentException if the tag or length is invalid
+ */
+ public static void writeTagAndLength(OutputStream output, int tag, long valueLength) throws IOException {
+ if (output == null) {
+ throw new IllegalArgumentException("output must not be null");
+ }
+ if (tag < 0 || tag > 0xff) {
+ throw new IllegalArgumentException("DER tag must fit one octet");
+ }
+ if (valueLength < EMPTY_LENGTH) {
+ throw new IllegalArgumentException("DER value length must not be negative");
+ }
+ output.write(tag);
+ if (valueLength < SHORT_FORM_LIMIT) {
+ output.write((int) valueLength);
+ return;
+ }
+ int octets = significantOctets(valueLength);
+ output.write(0x80 | octets);
+ for (int shift = (octets - 1) * Byte.SIZE; shift >= 0; shift -= Byte.SIZE) {
+ output.write((int) (valueLength >>> shift) & 0xff);
+ }
+ }
+
+ /**
+ * Copies content incrementally and returns the exact byte count.
+ *
+ * @param input source
+ * @param output destination
+ * @param cancellation cancellation signal
+ * @return copied byte count
+ * @throws IOException if reading or writing fails
+ * @throws ArithmeticException if the count overflows
+ */
+ public static long copy(InputStream input, OutputStream output, CancellationSignal cancellation)
+ throws IOException {
+ if (input == null || output == null || cancellation == null) {
+ throw new IllegalArgumentException("DER copy arguments must not be null");
+ }
+ byte[] buffer = new byte[BUFFER_SIZE];
+ long count = 0L;
+ int read;
+ while ((read = input.read(buffer)) >= 0) {
+ cancellation.throwIfCancelled();
+ if (read != 0) {
+ output.write(buffer, 0, read);
+ count = Math.addExact(count, read);
+ }
+ }
+ return count;
+ }
+
+ private static long lengthOctets(long valueLength) {
+ return valueLength < 128L ? 1L : Math.addExact(1L, significantOctets(valueLength));
+ }
+
+ private static int significantOctets(long valueLength) {
+ int octets = 0;
+ long remaining = valueLength;
+ while (remaining != 0L) {
+ octets++;
+ remaining >>>= Byte.SIZE;
+ }
+ return octets;
+ }
+}
diff --git a/pki/src/main/java/zeroecho/pki/impl/framework/x509/X509AlgorithmIdentifier.java b/pki/src/main/java/zeroecho/pki/impl/framework/x509/X509AlgorithmIdentifier.java
new file mode 100644
index 0000000..70268a5
--- /dev/null
+++ b/pki/src/main/java/zeroecho/pki/impl/framework/x509/X509AlgorithmIdentifier.java
@@ -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.impl.framework.x509;
+
+import java.util.Arrays;
+import java.util.HexFormat;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/**
+ * Provider-neutral canonical X.509 {@code AlgorithmIdentifier} representation.
+ *
+ *
+ * Parameters distinguish absent, DER NULL, and exact canonical structured DER.
+ * Parameter bytes are defensively copied. This value contains no Bouncy Castle
+ * object and is immutable.
+ *
+ */
+public final class X509AlgorithmIdentifier {
+
+ private static final Pattern OID = Pattern.compile("[0-2](?:\\.[0-9]+)+");
+ private static final byte[] DER_NULL = { 0x05, 0x00 };
+
+ /**
+ * Exact parameter representation.
+ */
+ public enum ParameterForm {
+ /** Parameters are omitted. */
+ ABSENT,
+ /** Parameters are canonical DER NULL. */
+ DER_NULL,
+ /** Parameters are exact canonical structured DER. */
+ EXACT_DER
+ }
+
+ private final String oid;
+ private final ParameterForm parameterForm;
+ private final byte[] parameters;
+
+ private X509AlgorithmIdentifier(String oid, ParameterForm parameterForm, byte[] parameters) {
+ Objects.requireNonNull(oid, "oid");
+ if (!OID.matcher(oid).matches()) {
+ throw new IllegalArgumentException("Invalid dotted-decimal OID");
+ }
+ this.oid = oid;
+ this.parameterForm = Objects.requireNonNull(parameterForm, "parameterForm");
+ this.parameters = Objects.requireNonNull(parameters, "parameters").clone();
+ if (parameterForm == ParameterForm.ABSENT && parameters.length != 0) {
+ throw new IllegalArgumentException("Absent parameters must have no DER");
+ }
+ if (parameterForm == ParameterForm.DER_NULL && !Arrays.equals(DER_NULL, parameters)) {
+ throw new IllegalArgumentException("DER NULL parameters must be canonical");
+ }
+ if (parameterForm == ParameterForm.EXACT_DER && parameters.length == 0) {
+ throw new IllegalArgumentException("Exact parameters must not be empty");
+ }
+ }
+
+ /**
+ * Creates an identifier with absent parameters.
+ *
+ * @param oid dotted-decimal OID
+ * @return immutable identifier
+ */
+ public static X509AlgorithmIdentifier absent(String oid) {
+ return new X509AlgorithmIdentifier(oid, ParameterForm.ABSENT, new byte[0]);
+ }
+
+ /**
+ * Creates an identifier with canonical DER NULL parameters.
+ *
+ * @param oid dotted-decimal OID
+ * @return immutable identifier
+ */
+ public static X509AlgorithmIdentifier derNull(String oid) {
+ return new X509AlgorithmIdentifier(oid, ParameterForm.DER_NULL, DER_NULL);
+ }
+
+ /**
+ * Creates an identifier with exact canonical structured parameters.
+ *
+ * @param oid dotted-decimal OID
+ * @param parameters complete DER parameter value
+ * @return immutable identifier
+ */
+ public static X509AlgorithmIdentifier exact(String oid, byte[] parameters) {
+ return new X509AlgorithmIdentifier(oid, ParameterForm.EXACT_DER, parameters);
+ }
+
+ /**
+ * Returns the OID.
+ *
+ * @return dotted-decimal OID
+ */
+ public String oid() {
+ return oid;
+ }
+
+ /**
+ * Returns the exact parameter form.
+ *
+ * @return parameter form
+ */
+ public ParameterForm parameterForm() {
+ return parameterForm;
+ }
+
+ /**
+ * Returns independently owned parameter DER.
+ *
+ * @return defensive copy, empty for absent parameters
+ */
+ public byte[] parameters() {
+ return parameters.clone();
+ }
+
+ /**
+ * Returns the deterministic reverse-lookup representation.
+ *
+ * @return OID, form, and exact DER
+ */
+ public String canonicalForm() {
+ return oid + "|" + parameterForm + "|" + HexFormat.of().formatHex(parameters);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof X509AlgorithmIdentifier identifier && oid.equals(identifier.oid)
+ && parameterForm == identifier.parameterForm && Arrays.equals(parameters, identifier.parameters);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * (31 * oid.hashCode() + parameterForm.hashCode()) + Arrays.hashCode(parameters);
+ }
+}
diff --git a/pki/src/main/java/zeroecho/pki/impl/framework/x509/X509AlgorithmResolver.java b/pki/src/main/java/zeroecho/pki/impl/framework/x509/X509AlgorithmResolver.java
new file mode 100644
index 0000000..12d87f1
--- /dev/null
+++ b/pki/src/main/java/zeroecho/pki/impl/framework/x509/X509AlgorithmResolver.java
@@ -0,0 +1,243 @@
+/*******************************************************************************
+ * 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.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import zeroecho.core.spec.AlgorithmIdentity;
+import zeroecho.core.spec.AlgorithmSuite;
+import zeroecho.core.spi.AlgorithmExecutionCapabilities;
+import zeroecho.core.spi.AlgorithmExecutionCapability;
+
+/**
+ * Provider-independent intersection of exact identity, binding, installed
+ * execution capability, immutable security floor, configured policy, and key
+ * compatibility.
+ *
+ *
+ * The resolver returns an immutable effective selection and never falls back to
+ * a default or provider alias. It contains no key material and performs no
+ * cryptographic execution.
+ *
+ */
+public final class X509AlgorithmResolver {
+
+ private static final int UNIQUE_MATCH_COUNT = 1;
+
+ private final X509BindingCatalog bindings;
+ private final AlgorithmExecutionCapabilities capabilities;
+ private final Policy policy;
+
+ /**
+ * Stable resolution failure categories.
+ */
+ public enum Failure {
+ /** No authoritative X.509 binding exists. */
+ NO_BINDING,
+ /** No installed implementation supports the tuple. */
+ NO_CAPABILITY,
+ /** More than one implementation remains without explicit selection. */
+ AMBIGUOUS_IMPLEMENTATION,
+ /** Explicit implementation is unavailable for the tuple. */
+ UNKNOWN_IMPLEMENTATION,
+ /** Semantic capability has no process-local executor binding. */
+ NO_EXECUTOR,
+ /** Non-overridable security floor rejected the identity. */
+ SECURITY_FLOOR,
+ /** Configured policy rejected the suite. */
+ POLICY,
+ /** Signature and key identities are incompatible. */
+ INCOMPATIBLE_KEY
+ }
+
+ /**
+ * Policy decision over exact identity data.
+ */
+ @FunctionalInterface
+ public interface Policy {
+
+ /**
+ * Tests whether configured policy permits the exact operation.
+ *
+ * @param suite complete suite
+ * @param direction execution direction
+ * @return {@code true} when permitted
+ */
+ boolean permits(AlgorithmSuite suite, AlgorithmExecutionCapability.Direction direction);
+
+ /**
+ * Returns stable non-secret policy semantics for snapshot provenance.
+ *
+ * @return deterministic policy fingerprint
+ */
+ default String semanticFingerprint() {
+ return getClass().getName();
+ }
+ }
+
+ /**
+ * Immutable effective selection.
+ *
+ * @param requested exact requested identity
+ * @param suite compatible exact suite
+ * @param binding exact X.509 representation
+ * @param implementation selected execution metadata
+ * @param direction authorized execution direction
+ * @param provenance stable default identifier or {@code explicit}
+ * @param authorityFingerprint authority snapshot fingerprint
+ */
+ public record Selection(AlgorithmIdentity requested, AlgorithmSuite suite, X509AlgorithmIdentifier binding,
+ AlgorithmExecutionCapability implementation, AlgorithmExecutionCapability.Direction direction,
+ String provenance, String authorityFingerprint) {
+
+ /**
+ * Creates an immutable selection.
+ *
+ * @throws NullPointerException if an argument is {@code null}
+ */
+ public Selection {
+ Objects.requireNonNull(requested, "requested");
+ Objects.requireNonNull(suite, "suite");
+ Objects.requireNonNull(binding, "binding");
+ Objects.requireNonNull(implementation, "implementation");
+ Objects.requireNonNull(provenance, "provenance");
+ Objects.requireNonNull(authorityFingerprint, "authorityFingerprint");
+ }
+ }
+
+ /**
+ * Resolution exception with a stable non-sensitive category.
+ */
+ public static final class ResolutionException extends IllegalArgumentException {
+
+ private static final long serialVersionUID = 1L;
+ private final Failure failure;
+
+ /* default */ ResolutionException(Failure failure) {
+ super("X.509 algorithm resolution failed: " + failure);
+ this.failure = failure;
+ }
+
+ private ResolutionException(Failure failure, IllegalArgumentException cause) {
+ super("X.509 algorithm resolution failed: " + failure, cause);
+ this.failure = failure;
+ }
+
+ /**
+ * Returns the stable failure category.
+ *
+ * @return resolution failure
+ */
+ public Failure failure() {
+ return failure;
+ }
+ }
+
+ /**
+ * Creates an immutable resolver snapshot.
+ *
+ * @param bindings authoritative binding snapshot
+ * @param capabilities installed execution snapshot
+ * @param policy configured restrictive policy
+ */
+ public X509AlgorithmResolver(X509BindingCatalog bindings, AlgorithmExecutionCapabilities capabilities,
+ Policy policy) {
+ this.bindings = Objects.requireNonNull(bindings, "bindings");
+ this.capabilities = Objects.requireNonNull(capabilities, "capabilities");
+ this.policy = Objects.requireNonNull(policy, "policy");
+ }
+
+ /**
+ * Resolves an exact explicit selection.
+ *
+ * @param requested exact signature identity
+ * @param key exact public-key identity
+ * @param direction execution direction
+ * @param implementation optional explicit implementation identifier
+ * @param provenance stable default identifier or {@code explicit}
+ * @return immutable effective selection
+ * @throws ResolutionException for a precise fail-closed category
+ */
+ public Selection resolve(AlgorithmIdentity requested, AlgorithmIdentity key,
+ AlgorithmExecutionCapability.Direction direction, Optional implementation, String provenance,
+ String authorityFingerprint) {
+ Objects.requireNonNull(requested, "requested");
+ Objects.requireNonNull(key, "key");
+ Objects.requireNonNull(direction, "direction");
+ Objects.requireNonNull(implementation, "implementation");
+ Objects.requireNonNull(provenance, "provenance");
+ Objects.requireNonNull(authorityFingerprint, "authorityFingerprint");
+ try {
+ X509SecurityFloor.requirePermitted(requested);
+ } catch (IllegalArgumentException rejected) {
+ throw new ResolutionException(Failure.SECURITY_FLOOR, rejected);
+ }
+ AlgorithmSuite suite;
+ try {
+ suite = X509SuiteCompatibility.requireCompatible(requested, key);
+ } catch (IllegalArgumentException incompatible) {
+ throw new ResolutionException(Failure.INCOMPATIBLE_KEY, incompatible);
+ }
+ X509AlgorithmIdentifier binding;
+ try {
+ binding = bindings.resolve(requested, X509AlgorithmRole.SIGNATURE_ALGORITHM);
+ } catch (IllegalArgumentException missing) {
+ throw new ResolutionException(Failure.NO_BINDING, missing);
+ }
+ if (!policy.permits(suite, direction)) {
+ throw new ResolutionException(Failure.POLICY);
+ }
+ List matches = capabilities.supporting(requested, suite, direction);
+ AlgorithmExecutionCapability selected = select(matches, implementation);
+ return new Selection(requested, suite, binding, selected, direction, provenance, authorityFingerprint);
+ }
+
+ private static AlgorithmExecutionCapability select(List matches,
+ Optional requestedImplementation) {
+ if (requestedImplementation.isPresent()) {
+ return matches.stream()
+ .filter(capability -> requestedImplementation.get().equals(capability.implementationId()))
+ .findFirst().orElseThrow(() -> new ResolutionException(Failure.UNKNOWN_IMPLEMENTATION));
+ }
+ if (matches.isEmpty()) {
+ throw new ResolutionException(Failure.NO_CAPABILITY);
+ }
+ if (matches.size() != UNIQUE_MATCH_COUNT) {
+ throw new ResolutionException(Failure.AMBIGUOUS_IMPLEMENTATION);
+ }
+ return matches.get(0);
+ }
+}
diff --git a/pki/src/main/java/zeroecho/pki/impl/framework/x509/X509AlgorithmRole.java b/pki/src/main/java/zeroecho/pki/impl/framework/x509/X509AlgorithmRole.java
new file mode 100644
index 0000000..7e30ec7
--- /dev/null
+++ b/pki/src/main/java/zeroecho/pki/impl/framework/x509/X509AlgorithmRole.java
@@ -0,0 +1,44 @@
+/*******************************************************************************
+ * 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;
+
+/**
+ * Closed semantic role of an algorithm representation in X.509.
+ */
+public enum X509AlgorithmRole {
+ /** Signature algorithm on a certificate, CRL, or certification request. */
+ SIGNATURE_ALGORITHM,
+ /** Public-key algorithm in SubjectPublicKeyInfo. */
+ SUBJECT_PUBLIC_KEY_ALGORITHM
+}
diff --git a/pki/src/main/java/zeroecho/pki/impl/framework/x509/X509AuthoritySnapshot.java b/pki/src/main/java/zeroecho/pki/impl/framework/x509/X509AuthoritySnapshot.java
new file mode 100644
index 0000000..4186d1f
--- /dev/null
+++ b/pki/src/main/java/zeroecho/pki/impl/framework/x509/X509AuthoritySnapshot.java
@@ -0,0 +1,635 @@
+/*******************************************************************************
+ * 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.Collection;
+import java.util.Comparator;
+import java.util.HexFormat;
+import java.util.IdentityHashMap;
+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 zeroecho.core.alg.BootstrapAlgorithmIdentities;
+import zeroecho.core.spec.AlgorithmIdentity;
+import zeroecho.core.spec.AlgorithmIdentityCatalog;
+import zeroecho.core.spec.AlgorithmIdentityCodec;
+import zeroecho.core.spec.AlgorithmSuite;
+import zeroecho.core.spi.AlgorithmExecutionCapabilities;
+import zeroecho.core.spi.AlgorithmExecutionCapability;
+import zeroecho.pki.api.status.StatusObject;
+import zeroecho.pki.spi.crypto.SignatureWorkflow;
+import zeroecho.core.spi.AlgorithmExecutionCapabilityProvider;
+
+/**
+ * One immutable internally consistent runtime authority snapshot.
+ *
+ *
+ * The snapshot composes identities, X.509 components, binding rules, installed
+ * execution capabilities, aliases, immutable defaults, security floor, and
+ * configured policy once. It owns the effective resolver and a stable semantic
+ * fingerprint. It contains no key material and is not persisted in Phase A.
+ *
+ */
+public final class X509AuthoritySnapshot {
+
+ private final AlgorithmIdentityCatalog identities;
+ private final X509ComponentCatalog components;
+ private final X509BindingCatalog bindings;
+ private final AlgorithmExecutionCapabilities capabilities;
+ private final List codecs;
+ private final Map aliases;
+ private final Map defaults;
+ private final X509AlgorithmResolver.Policy policy;
+ private final X509AlgorithmResolver resolver;
+ private final String semanticFingerprint;
+ private final Map executors;
+ private final Object provenanceToken;
+
+ /**
+ * Creates a consistent authority snapshot.
+ *
+ * @param identities exact identity catalog
+ * @param components parameter component catalog
+ * @param bindings role-specific binding catalog
+ * @param capabilities installed execution capabilities
+ * @param policy configured restrictive policy
+ */
+ public X509AuthoritySnapshot(AlgorithmIdentityCatalog identities, X509ComponentCatalog components,
+ X509BindingCatalog bindings, AlgorithmExecutionCapabilities capabilities,
+ X509AlgorithmResolver.Policy policy) {
+ this(identities, components, bindings, capabilities, List.of(),
+ BootstrapAlgorithmIdentities.compatibilityAliases(), X509BuiltInDefaults.snapshot(), policy,
+ List.of());
+ }
+
+ private X509AuthoritySnapshot(AlgorithmIdentityCatalog identities, X509ComponentCatalog components,
+ X509BindingCatalog bindings, AlgorithmExecutionCapabilities capabilities,
+ Collection codecs, Map aliases,
+ Map defaults, X509AlgorithmResolver.Policy policy,
+ Collection executorBindings) {
+ this.identities = Objects.requireNonNull(identities, "identities");
+ this.components = Objects.requireNonNull(components, "components");
+ this.bindings = Objects.requireNonNull(bindings, "bindings");
+ this.capabilities = Objects.requireNonNull(capabilities, "capabilities");
+ this.codecs = immutableCodecs(codecs);
+ this.aliases = immutableAliases(aliases, identities);
+ this.defaults = immutableDefaults(defaults);
+ this.policy = Objects.requireNonNull(policy, "policy");
+ this.resolver = new X509AlgorithmResolver(bindings, capabilities, policy);
+ this.executors = immutableExecutors(executorBindings, capabilities);
+ this.provenanceToken = new Object();
+ this.semanticFingerprint = fingerprint();
+ }
+
+ /**
+ * Creates the installed runtime snapshot with an explicit policy.
+ *
+ * @param policy configured restrictive policy
+ * @return installed snapshot
+ */
+ public static X509AuthoritySnapshot installed(X509AlgorithmResolver.Policy policy) {
+ List bindingProviders = ServiceLoader.load(X509BindingRuleProvider.class).stream()
+ .map(ServiceLoader.Provider::get).sorted(Comparator.comparing(provider -> provider.getClass().getName()))
+ .toList();
+ List capabilityProviders = ServiceLoader
+ .load(AlgorithmExecutionCapabilityProvider.class).stream().map(ServiceLoader.Provider::get)
+ .sorted(Comparator.comparing(provider -> provider.getClass().getName())).toList();
+ return compose(bindingProviders, capabilityProviders, policy);
+ }
+
+ /**
+ * Composes one runtime graph from explicitly selected trusted providers.
+ *
+ * @param bindingProviders binding and identity contributors
+ * @param capabilityProviders execution contributors used by this runtime
+ * @param policy restrictive policy
+ * @return immutable authority snapshot
+ */
+ public static X509AuthoritySnapshot compose(List bindingProviders,
+ List capabilityProviders, X509AlgorithmResolver.Policy policy) {
+ return compose(bindingProviders, capabilityProviders, List.of(), policy);
+ }
+
+ /**
+ * Composes one runtime graph with exact process-local executor bindings.
+ *
+ * @param bindingProviders binding and identity contributors
+ * @param capabilityProviders semantic execution contributors
+ * @param executorBindings actual process-local executors
+ * @param policy restrictive policy
+ * @return immutable authority snapshot
+ */
+ public static X509AuthoritySnapshot compose(List bindingProviders,
+ List capabilityProviders,
+ List executorBindings, X509AlgorithmResolver.Policy policy) {
+ Objects.requireNonNull(bindingProviders, "bindingProviders");
+ Objects.requireNonNull(capabilityProviders, "capabilityProviders");
+ List ordered = bindingProviders.stream()
+ .sorted(Comparator.comparing(provider -> provider.getClass().getName())).toList();
+
+ AlgorithmIdentityCatalog identities = BootstrapAlgorithmIdentities.catalog();
+ List componentExtensions = new ArrayList<>();
+ List bindingExtensions = new ArrayList<>();
+ List codecs = new ArrayList<>();
+ Map aliases = new LinkedHashMap<>(
+ BootstrapAlgorithmIdentities.compatibilityAliases());
+ Map defaults = new LinkedHashMap<>(X509BuiltInDefaults.snapshot());
+ for (X509BindingRuleProvider provider : ordered) {
+ Collection contributedIdentities = List.copyOf(provider.identities());
+ if (!contributedIdentities.isEmpty()) {
+ identities = identities.add(contributedIdentities);
+ }
+ List contributedComponents = List.copyOf(provider.components());
+ if (!contributedComponents.isEmpty()) {
+ componentExtensions.add(X509ComponentCatalog.extension(contributedComponents));
+ }
+ codecs.addAll(List.copyOf(provider.codecs()));
+ mergeAliases(aliases, provider.aliases());
+ mergeDefaults(defaults, provider.defaults());
+ }
+ X509ComponentCatalog components = X509ComponentCatalog.builtIn().merge(componentExtensions);
+ X509BindingCatalog builtInBindings = StandardX509Bindings.catalog(components);
+ for (X509BindingRuleProvider provider : ordered) {
+ List rules = List.copyOf(provider.rules());
+ if (!rules.isEmpty()) {
+ bindingExtensions.add(X509BindingCatalog.extension(rules));
+ }
+ }
+ X509BindingCatalog bindings = builtInBindings.merge(bindingExtensions);
+ AlgorithmExecutionCapabilities capabilities = AlgorithmExecutionCapabilities
+ .fromProviders(capabilityProviders);
+ return new X509AuthoritySnapshot(identities, components, bindings, capabilities, codecs, aliases, defaults,
+ policy, executorBindings);
+ }
+
+ /**
+ * Resolves a canonical identity or finite built-in compatibility alias.
+ *
+ * @param value canonical identity or approved legacy alias
+ * @return exact identity
+ */
+ public AlgorithmIdentity resolveIdentity(String value) {
+ Objects.requireNonNull(value, "value");
+ Optional canonical = identities.resolve(value);
+ if (canonical.isEmpty() && value.startsWith("zealg:2:")) {
+ AlgorithmIdentity parsed = AlgorithmIdentity.parse(value, codecs);
+ canonical = identities.resolve(parsed.canonicalForm());
+ }
+ return canonical.or(() -> Optional.ofNullable(aliases.get(value)))
+ .orElseThrow(() -> new IllegalArgumentException("Unknown algorithm identity"));
+ }
+
+ /**
+ * Resolves an immutable built-in default without extension override.
+ *
+ * @param identifier versioned default identifier
+ * @return exact default suite
+ */
+ public AlgorithmSuite resolveDefault(String identifier) {
+ AlgorithmSuite suite = defaults.get(Objects.requireNonNull(identifier, "identifier"));
+ if (suite == null) {
+ throw new IllegalArgumentException("Unknown default identifier");
+ }
+ return suite;
+ }
+
+ /**
+ * Resolves one effective operation and binds it to this snapshot fingerprint.
+ *
+ * @param signature exact signature identity
+ * @param key exact public-key identity
+ * @param direction execution direction
+ * @param implementation optional implementation selection
+ * @param provenance explicit or default provenance
+ * @return effective immutable selection
+ */
+ public X509AlgorithmResolver.Selection resolve(AlgorithmIdentity signature, AlgorithmIdentity key,
+ AlgorithmExecutionCapability.Direction direction, Optional implementation, String provenance) {
+ return resolver.resolve(signature, key, direction, implementation, provenance, semanticFingerprint);
+ }
+
+ /**
+ * Resolves and authorizes one process-local execution plan.
+ *
+ * @param signature exact signature identity
+ * @param key exact public-key identity
+ * @param direction operation direction
+ * @param implementation optional exact implementation identifier
+ * @param provenance explicit or default provenance
+ * @param executorType required runtime executor type
+ * @param executor type
+ * @return unforgeable process-local plan
+ */
+ public X509ExecutionPlan plan(AlgorithmIdentity signature, AlgorithmIdentity key,
+ AlgorithmExecutionCapability.Direction direction, Optional implementation, String provenance,
+ Class executorType) {
+ X509AlgorithmResolver.Selection selection = resolve(signature, key, direction, implementation, provenance);
+ 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.
+ *
+ * @param value canonical identity or approved finite alias
+ * @param implementation exact implementation identifier
+ * @param executorType required executor type
+ * @param executor type
+ * @return exact process-local signing plan
+ */
+ public X509ExecutionPlan planSigning(String value, String implementation, Class executorType) {
+ AlgorithmIdentity signature = resolveIdentity(value);
+ AlgorithmIdentity key = bootstrapKeyFor(signature);
+ return plan(signature, key, AlgorithmExecutionCapability.Direction.SIGN, Optional.of(implementation),
+ "explicit", executorType);
+ }
+
+ /**
+ * Resolves a signing plan when exactly one implementation is available.
+ *
+ * @param value canonical identity or approved finite alias
+ * @param executorType required executor type
+ * @param executor type
+ * @return exact process-local signing plan
+ */
+ public X509ExecutionPlan planSigning(String value, Class executorType) {
+ AlgorithmIdentity signature = resolveIdentity(value);
+ AlgorithmIdentity key = bootstrapKeyFor(signature);
+ return plan(signature, key, AlgorithmExecutionCapability.Direction.SIGN, Optional.empty(), "explicit",
+ executorType);
+ }
+
+ /**
+ * Validates an execution plan immediately before invoking its executor.
+ *
+ * @param plan plan minted by this snapshot
+ * @param expectedExecutor exact executor reference expected by the boundary
+ * @param direction required operation direction
+ * @throws IllegalArgumentException if provenance, executor, implementation, or
+ * direction differs
+ */
+ public void authorize(X509ExecutionPlan> plan, Object expectedExecutor,
+ AlgorithmExecutionCapability.Direction direction) {
+ Objects.requireNonNull(plan, "plan");
+ Objects.requireNonNull(expectedExecutor, "expectedExecutor");
+ Objects.requireNonNull(direction, "direction");
+ X509AlgorithmResolver.Selection selection = plan.selection();
+ ExecutorKey key = new ExecutorKey(selection.implementation().implementationId(), direction);
+ if (!plan.isOwnedBy(provenanceToken) || !sameInstance(plan.executor(), expectedExecutor)
+ || selection.direction() != direction || !sameInstance(executors.get(key), expectedExecutor)
+ || !semanticFingerprint.equals(selection.authorityFingerprint())) {
+ throw new IllegalArgumentException("X.509 execution plan authority mismatch");
+ }
+ }
+
+ /**
+ * Mints the live completion of an X.509 status-object SIGN operation.
+ *
+ * @param statusObject immutable generated status object
+ * @param signingPlan exact live SIGN plan used to produce its content
+ * @return non-forgeable process-local completion
+ * @throws IllegalArgumentException if the plan is foreign or not an authorized
+ * SIGN plan
+ */
+ public X509SignedObjectCompletion completeStatusObject(StatusObject statusObject,
+ X509ExecutionPlan signingPlan) {
+ Objects.requireNonNull(statusObject, "statusObject");
+ authorize(signingPlan, signingPlan.executor(), AlgorithmExecutionCapability.Direction.SIGN);
+ return new X509SignedObjectCompletion(statusObject, signingPlan, provenanceToken);
+ }
+
+ /**
+ * Validates and unwraps a status completion minted by this live authority.
+ *
+ * @param completion signed-object completion
+ * @return immutable completed status object
+ * @throws IllegalArgumentException if provenance or executor binding differs
+ */
+ public StatusObject requireStatusCompletion(X509SignedObjectCompletion completion) {
+ Objects.requireNonNull(completion, "completion");
+ if (!completion.isOwnedBy(provenanceToken)) {
+ throw new IllegalArgumentException("X.509 status completion authority mismatch");
+ }
+ X509ExecutionPlan plan = completion.signingPlan();
+ authorize(plan, plan.executor(), AlgorithmExecutionCapability.Direction.SIGN);
+ return completion.statusObject();
+ }
+
+ /**
+ * Returns the exact live SIGN plan after validating status completion
+ * provenance.
+ *
+ * @param completion status completion minted by this authority
+ * @return exact authorized SIGN plan
+ * @throws IllegalArgumentException if the completion belongs to another live
+ * runtime
+ */
+ public X509ExecutionPlan requireStatusSigningPlan(X509SignedObjectCompletion completion) {
+ requireStatusCompletion(completion);
+ return completion.signingPlan();
+ }
+
+ /**
+ * Validates a persisted signature identity against the complete runtime
+ * authority intersection.
+ *
+ * @param value canonical identity or approved compatibility alias
+ * @param direction execution direction
+ * @return exact canonical signature identity
+ */
+ public AlgorithmIdentity requireExecutableSignature(String value,
+ AlgorithmExecutionCapability.Direction direction) {
+ AlgorithmIdentity signature = resolveIdentity(value);
+ X509SecurityFloor.requirePermitted(signature);
+ bindings.resolve(signature, X509AlgorithmRole.SIGNATURE_ALGORITHM);
+ List keys = identities.identities().stream()
+ .filter(identity -> identity.kind() == AlgorithmIdentity.Kind.PUBLIC_KEY).toList();
+ for (AlgorithmIdentity key : keys) {
+ try {
+ AlgorithmSuite suite = X509SuiteCompatibility.requireCompatible(signature, key);
+ if (policy.permits(suite, direction)
+ && !capabilities.supporting(signature, suite, direction).isEmpty()) {
+ return signature;
+ }
+ } catch (IllegalArgumentException incompatible) {
+ continue;
+ }
+ }
+ throw new IllegalArgumentException("Signature identity is unavailable for execution");
+ }
+
+ /**
+ * Rejects a selection produced by a semantically different authority snapshot.
+ *
+ * @param selection effective selection
+ */
+ public void requireAuthority(X509AlgorithmResolver.Selection selection) {
+ Objects.requireNonNull(selection, "selection");
+ if (!semanticFingerprint.equals(selection.authorityFingerprint())) {
+ throw new IllegalArgumentException("X.509 authority snapshot mismatch");
+ }
+ }
+
+ /**
+ * Creates one exact process-local executor binding.
+ *
+ * @param implementationId semantic capability implementation identifier
+ * @param direction supported execution direction
+ * @param executor actual runtime executor
+ * @return immutable binding contribution
+ */
+ public static ExecutorBinding bindExecutor(String implementationId,
+ AlgorithmExecutionCapability.Direction direction, Object executor) {
+ return new ExecutorBinding(implementationId, direction, executor);
+ }
+
+ /**
+ * Returns the owned effective resolver.
+ *
+ * @return immutable resolver
+ */
+ public X509AlgorithmResolver resolver() {
+ return resolver;
+ }
+
+ /**
+ * Returns the binding catalog snapshot.
+ *
+ * @return immutable bindings
+ */
+ public X509BindingCatalog bindings() {
+ return bindings;
+ }
+
+ /**
+ * Returns the component catalog snapshot.
+ *
+ * @return immutable components
+ */
+ public X509ComponentCatalog components() {
+ return components;
+ }
+
+ /**
+ * Returns the stable snapshot fingerprint.
+ *
+ * @return SHA-256 hexadecimal semantic fingerprint
+ */
+ public String semanticFingerprint() {
+ return semanticFingerprint;
+ }
+
+ private String fingerprint() {
+ StringBuilder semantic = new StringBuilder();
+ addFields(semantic, "identity", identities.identities().stream().map(AlgorithmIdentity::canonicalForm).toList());
+ addFields(semantic, "codec", codecs.stream().map(AlgorithmIdentityCodec::id).toList());
+ addFields(semantic, "component", List.of(components.semanticFingerprint()));
+ addFields(semantic, "binding",
+ bindings.rules().stream().map(X509BindingRule::semanticFingerprint).sorted().toList());
+ addFields(semantic, "capability", capabilities.all().stream()
+ .map(capability -> capability.implementationId() + ":" + capability.domainFingerprint()).toList());
+ addFields(semantic, "alias", aliases.entrySet().stream().sorted(Map.Entry.comparingByKey())
+ .map(entry -> entry.getKey() + ":" + entry.getValue().canonicalForm()).toList());
+ addFields(semantic, "default", defaults.entrySet().stream().sorted(Map.Entry.comparingByKey())
+ .map(entry -> entry.getKey() + ":" + entry.getValue().canonicalForm()).toList());
+ addFields(semantic, "floor", List.of(X509SecurityFloor.semanticFingerprint()));
+ addFields(semantic, "policy", List.of(policy.semanticFingerprint()));
+ try {
+ return HexFormat.of().formatHex(
+ MessageDigest.getInstance("SHA-256").digest(semantic.toString().getBytes(StandardCharsets.UTF_8)));
+ } catch (NoSuchAlgorithmException exception) {
+ throw new IllegalStateException("SHA-256 unavailable", exception);
+ }
+ }
+
+ private static List immutableCodecs(Collection source) {
+ Map byId = new LinkedHashMap<>();
+ source.stream().sorted(Comparator.comparing(AlgorithmIdentityCodec::id)).forEach(codec -> {
+ if (byId.putIfAbsent(codec.id(), codec) != null) {
+ throw new IllegalArgumentException("Algorithm identity codec collision");
+ }
+ });
+ return List.copyOf(byId.values());
+ }
+
+ private static Map immutableAliases(Map source,
+ AlgorithmIdentityCatalog identities) {
+ Map copy = new LinkedHashMap<>();
+ source.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(entry -> {
+ AlgorithmIdentity identity = Objects.requireNonNull(entry.getValue(), "alias identity");
+ if (entry.getKey().isBlank() || identities.resolve(identity.canonicalForm()).isEmpty()) {
+ throw new IllegalArgumentException("Invalid algorithm alias contribution");
+ }
+ X509SecurityFloor.requirePermitted(identity);
+ copy.put(entry.getKey(), identity);
+ });
+ return Map.copyOf(copy);
+ }
+
+ private static Map immutableDefaults(Map source) {
+ Map copy = new LinkedHashMap<>();
+ source.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(entry -> {
+ if (entry.getKey().isBlank()) {
+ throw new IllegalArgumentException("Invalid default identifier");
+ }
+ X509SecurityFloor.requirePermitted(entry.getValue().signature());
+ copy.put(entry.getKey(), Objects.requireNonNull(entry.getValue(), "default suite"));
+ });
+ return Map.copyOf(copy);
+ }
+
+ private static Map immutableExecutors(Collection source,
+ AlgorithmExecutionCapabilities capabilities) {
+ Map copy = new LinkedHashMap<>();
+ Set known = capabilities.all().stream().map(AlgorithmExecutionCapability::implementationId)
+ .collect(java.util.stream.Collectors.toUnmodifiableSet());
+ source.forEach(binding -> {
+ if (!known.contains(binding.implementationId())) {
+ throw new IllegalArgumentException("Executor binding has no semantic capability");
+ }
+ ExecutorKey key = new ExecutorKey(binding.implementationId(), binding.direction());
+ if (copy.putIfAbsent(key, binding.executor()) != null) {
+ throw new IllegalArgumentException("Duplicate execution binding");
+ }
+ });
+ return Map.copyOf(copy);
+ }
+
+ private static boolean sameInstance(Object first, Object second) {
+ Map