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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
73
lib/src/main/java/zeroecho/core/io/CancellationSignal.java
Normal file
73
lib/src/main/java/zeroecho/core/io/CancellationSignal.java
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
86
lib/src/main/java/zeroecho/core/io/ContentDigests.java
Normal file
86
lib/src/main/java/zeroecho/core/io/ContentDigests.java
Normal 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());
|
||||
}
|
||||
}
|
||||
161
lib/src/main/java/zeroecho/core/io/ContentSlice.java
Normal file
161
lib/src/main/java/zeroecho/core/io/ContentSlice.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
107
lib/src/main/java/zeroecho/core/io/ImmutableByteContent.java
Normal file
107
lib/src/main/java/zeroecho/core/io/ImmutableByteContent.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
75
lib/src/main/java/zeroecho/core/io/OneShotContent.java
Normal file
75
lib/src/main/java/zeroecho/core/io/OneShotContent.java
Normal 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;
|
||||
}
|
||||
97
lib/src/main/java/zeroecho/core/io/RepeatableContent.java
Normal file
97
lib/src/main/java/zeroecho/core/io/RepeatableContent.java
Normal 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;
|
||||
}
|
||||
589
lib/src/main/java/zeroecho/core/spec/AlgorithmIdentity.java
Normal file
589
lib/src/main/java/zeroecho/core/spec/AlgorithmIdentity.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
79
lib/src/main/java/zeroecho/core/spec/AlgorithmSuite.java
Normal file
79
lib/src/main/java/zeroecho/core/spec/AlgorithmSuite.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user