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.
This commit is contained in:
2026-08-01 16:45:14 +02:00
parent 369c626ed8
commit 5420c19d08
147 changed files with 26788 additions and 1071 deletions

View File

@@ -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.
*
* <p>
* 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.
* </p>
*/
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<AlgorithmIdentity> IDENTITIES = List.of(SHA256, SHA384, SHA512, MGF1, RSA_PKCS1_SHA256,
RSA_PKCS1_SHA384, RSA_PKCS1_SHA512, RSA_PSS_SHA256, ECDSA_SHA256, ECDSA_SHA384, ECDSA_SHA512,
ED25519_SIGNATURE, ED448_SIGNATURE, RSA_PUBLIC_KEY, EC_P256_PUBLIC_KEY, EC_P384_PUBLIC_KEY,
EC_P521_PUBLIC_KEY, ED25519_PUBLIC_KEY, ED448_PUBLIC_KEY);
private static final AlgorithmIdentityCatalog CATALOG = AlgorithmIdentityCatalog.builtIn(IDENTITIES);
private static final Map<String, AlgorithmIdentity> 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.
*
* <p>
* SHA-1 and unknown aliases are rejected. The returned identity, rather than
* the alias, is authoritative.
* </p>
*
* @param alias legacy provider spelling
* @return exact bootstrap identity, or empty when unknown or forbidden
*/
public static Optional<AlgorithmIdentity> 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<String, AlgorithmIdentity> 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<AlgorithmIdentity> resolve(String value) {
Objects.requireNonNull(value, "value");
Optional<AlgorithmIdentity> 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);
}
}

View File

@@ -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<String, SignatureInteropProfile> 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<String> algorithmIds() {
return PROFILES.keySet();
Set<String> 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<String, SignatureInteropProfile> canonical(AlgorithmIdentity identity, String alias) {
return Map.entry(identity.canonicalForm(), PROFILES.get(alias));
}
}

View File

@@ -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.
*
* <p>
* 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.
* </p>
*/
@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");
}
}
}

View File

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

View File

@@ -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.
*
* <p>
* 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.
* </p>
*/
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;
}
}
}

View File

@@ -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.
*
* <p>
* 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.
* </p>
*/
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);
}
}
}

View File

@@ -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.
*
* <p>
* 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()}.
* </p>
*/
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;
}

View File

@@ -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.
*
* <p>
* 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.
* </p>
*
* <p>
* 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.
* </p>
*/
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.
*
* <p>
* The identifier is metadata, not authorization, and must not expose a
* temporary physical path.
* </p>
*
* @return stable non-blank identifier
*/
String contentId();
/**
* Releases implementation-owned resources.
*
* @throws IOException if cleanup fails
*/
@Override
void close() throws IOException;
}

View File

@@ -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.
*
* <p>
* 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.
* </p>
*
* <p>
* Instances are immutable. Equality and hashing use the complete canonical
* semantics, including the role, family, and parameters.
* </p>
*/
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.
*
* <p>
* 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.
* </p>
*/
public interface Parameters {
/**
* Returns the deterministic parameter representation.
*
* @return non-blank lowercase canonical component
*/
String canonicalForm();
/**
* Returns an independently owned immutable copy.
*
* <p>
* Trusted extension implementations must not return mutable caller-owned
* state. Immutable records may return {@code this}.
* </p>
*
* @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<AlgorithmIdentityCodec> codecs) {
Objects.requireNonNull(canonicalForm, "canonicalForm");
Objects.requireNonNull(codecs, "codecs");
if (!canonicalForm.startsWith("zealg:2:")) {
throw new IllegalArgumentException("Unsupported canonical algorithm identity version");
}
Map<String, AlgorithmIdentityCodec> 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);
}
}
}
}

View File

@@ -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.
*
* <p>
* 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.
* </p>
*/
public final class AlgorithmIdentityCatalog {
/** Namespace reserved for immutable built-in identities. */
public static final String BUILTIN_NAMESPACE = "zeroecho";
private final Map<String, AlgorithmIdentity> identities;
private AlgorithmIdentityCatalog(Map<String, AlgorithmIdentity> 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<AlgorithmIdentity> 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<AlgorithmIdentity> 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<AlgorithmIdentityCatalog> extensions) {
Objects.requireNonNull(extensions, "extensions");
Map<String, AlgorithmIdentity> 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.
*
* <p>
* A trusted extension may add a tuple within an existing reserved family, but
* cannot introduce a new family under the built-in namespace.
* </p>
*
* @param additions exact additive identities
* @return new immutable catalog
*/
public AlgorithmIdentityCatalog add(Collection<AlgorithmIdentity> additions) {
Objects.requireNonNull(additions, "additions");
Map<String, AlgorithmIdentity> 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<AlgorithmIdentity> 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<AlgorithmIdentity> identities() {
return identities.values().stream().sorted((left, right) -> left.canonicalForm()
.compareTo(right.canonicalForm())).toList();
}
private static AlgorithmIdentityCatalog create(Collection<AlgorithmIdentity> source, boolean builtIn) {
Objects.requireNonNull(source, "identities");
Map<String, AlgorithmIdentity> 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);
}
}

View File

@@ -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.
*
* <p>
* 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.
* </p>
*/
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);
}

View File

@@ -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.
*
* <p>
* 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.
* </p>
*
* @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();
}
}

View File

@@ -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.
*
* <p>
* 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.
* </p>
*/
public final class AlgorithmExecutionCapabilities {
private final List<AlgorithmExecutionCapability> 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<AlgorithmExecutionCapability> capabilities) {
Objects.requireNonNull(capabilities, "capabilities");
List<AlgorithmExecutionCapability> copy = new ArrayList<>(capabilities);
copy.sort(Comparator.comparing(AlgorithmExecutionCapability::implementationId));
Set<String> 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<AlgorithmExecutionCapabilityProvider> 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<AlgorithmExecutionCapability> discovered = new ArrayList<>();
List<AlgorithmExecutionCapabilityProvider> ordered = new ArrayList<>(providers);
ordered.sort(Comparator.comparing(provider -> provider.getClass().getName()));
for (AlgorithmExecutionCapabilityProvider provider : ordered) {
List<AlgorithmExecutionCapability> 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<AlgorithmExecutionCapability> 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<AlgorithmExecutionCapability> all() {
return capabilities;
}
}

View File

@@ -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.
*
* <p>
* 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.
* </p>
*/
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);
}

View File

@@ -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.
*
* <p>
* Providers are deployment code discovered using the existing ServiceLoader
* convention. Configuration may select an installed capability but cannot name
* or load an implementation class.
* </p>
*/
@FunctionalInterface
public interface AlgorithmExecutionCapabilityProvider {
/**
* Returns an immutable capability contribution.
*
* @return installed capabilities; never {@code null}
*/
List<AlgorithmExecutionCapability> capabilities();
}