diff --git a/lib/src/main/java/zeroecho/core/alg/BootstrapAlgorithmIdentities.java b/lib/src/main/java/zeroecho/core/alg/BootstrapAlgorithmIdentities.java new file mode 100644 index 0000000..2d92151 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/BootstrapAlgorithmIdentities.java @@ -0,0 +1,226 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import zeroecho.core.spec.AlgorithmIdentity; +import zeroecho.core.spec.AlgorithmIdentityCatalog; +import zeroecho.core.spec.AlgorithmSuite; + +/** + * Immutable bootstrap identities required by current ZeroEcho PKI behavior. + * + *
+ * These identities are mandatory defaults, not a permanent maximum algorithm + * set. Trusted installed extensions may add identities in other namespaces. + * Provider aliases are accepted only by {@link #fromCompatibilityAlias(String)} + * and never become canonical identity data. + *
+ */ +public final class BootstrapAlgorithmIdentities { + + private static final AlgorithmIdentity.Family SHA2_256 = family("sha2-256"); + private static final AlgorithmIdentity.Family SHA2_384 = family("sha2-384"); + private static final AlgorithmIdentity.Family SHA2_512 = family("sha2-512"); + private static final AlgorithmIdentity.Family MGF1_FAMILY = family("mgf1"); + private static final AlgorithmIdentity.Family RSA_PKCS1 = family("rsa-pkcs1-v1_5"); + private static final AlgorithmIdentity.Family RSA_PSS = family("rsa-pss"); + private static final AlgorithmIdentity.Family ECDSA = family("ecdsa"); + private static final AlgorithmIdentity.Family ED25519_FAMILY = family("ed25519"); + private static final AlgorithmIdentity.Family ED448_FAMILY = family("ed448"); + private static final AlgorithmIdentity.Family RSA_KEY = family("rsa"); + private static final AlgorithmIdentity.Family EC_KEY = family("ec"); + + /** SHA-256 digest identity. */ + public static final AlgorithmIdentity SHA256 = identity(AlgorithmIdentity.Kind.DIGEST, SHA2_256, + AlgorithmIdentity.NoParameters.INSTANCE); + /** SHA-384 digest identity. */ + public static final AlgorithmIdentity SHA384 = identity(AlgorithmIdentity.Kind.DIGEST, SHA2_384, + AlgorithmIdentity.NoParameters.INSTANCE); + /** SHA-512 digest identity. */ + public static final AlgorithmIdentity SHA512 = identity(AlgorithmIdentity.Kind.DIGEST, SHA2_512, + AlgorithmIdentity.NoParameters.INSTANCE); + /** MGF1 mask-generation identity. */ + public static final AlgorithmIdentity MGF1 = identity(AlgorithmIdentity.Kind.MASK_GENERATION, MGF1_FAMILY, + AlgorithmIdentity.NoParameters.INSTANCE); + + /** RSA PKCS#1 v1.5 with SHA-256. */ + public static final AlgorithmIdentity RSA_PKCS1_SHA256 = digestSignature(RSA_PKCS1, SHA256); + /** RSA PKCS#1 v1.5 with SHA-384. */ + public static final AlgorithmIdentity RSA_PKCS1_SHA384 = digestSignature(RSA_PKCS1, SHA384); + /** RSA PKCS#1 v1.5 with SHA-512. */ + public static final AlgorithmIdentity RSA_PKCS1_SHA512 = digestSignature(RSA_PKCS1, SHA512); + /** RSA-PSS SHA-256/MGF1-SHA-256/salt-32/trailer-1 bootstrap identity. */ + public static final AlgorithmIdentity RSA_PSS_SHA256 = rsaPss(SHA256, SHA256, 32); + /** ECDSA with SHA-256, independent of the EC curve. */ + public static final AlgorithmIdentity ECDSA_SHA256 = digestSignature(ECDSA, SHA256); + /** ECDSA with SHA-384, independent of the EC curve. */ + public static final AlgorithmIdentity ECDSA_SHA384 = digestSignature(ECDSA, SHA384); + /** ECDSA with SHA-512, independent of the EC curve. */ + public static final AlgorithmIdentity ECDSA_SHA512 = digestSignature(ECDSA, SHA512); + /** Ed25519 signature identity. */ + public static final AlgorithmIdentity ED25519_SIGNATURE = identity(AlgorithmIdentity.Kind.SIGNATURE, + ED25519_FAMILY, AlgorithmIdentity.NoParameters.INSTANCE); + /** Ed448 signature identity. */ + public static final AlgorithmIdentity ED448_SIGNATURE = identity(AlgorithmIdentity.Kind.SIGNATURE, ED448_FAMILY, + AlgorithmIdentity.NoParameters.INSTANCE); + + /** RSA public-key identity. */ + public static final AlgorithmIdentity RSA_PUBLIC_KEY = identity(AlgorithmIdentity.Kind.PUBLIC_KEY, RSA_KEY, + AlgorithmIdentity.NoParameters.INSTANCE); + /** EC P-256 public-key identity. */ + public static final AlgorithmIdentity EC_P256_PUBLIC_KEY = namedKey(EC_KEY, "p-256"); + /** EC P-384 public-key identity. */ + public static final AlgorithmIdentity EC_P384_PUBLIC_KEY = namedKey(EC_KEY, "p-384"); + /** EC P-521 public-key identity. */ + public static final AlgorithmIdentity EC_P521_PUBLIC_KEY = namedKey(EC_KEY, "p-521"); + /** Ed25519 public-key identity. */ + public static final AlgorithmIdentity ED25519_PUBLIC_KEY = identity(AlgorithmIdentity.Kind.PUBLIC_KEY, + ED25519_FAMILY, AlgorithmIdentity.NoParameters.INSTANCE); + /** Ed448 public-key identity. */ + public static final AlgorithmIdentity ED448_PUBLIC_KEY = identity(AlgorithmIdentity.Kind.PUBLIC_KEY, ED448_FAMILY, + AlgorithmIdentity.NoParameters.INSTANCE); + + /** Current ECDSA P-256 signing suite. */ + public static final AlgorithmSuite ECDSA_SHA256_P256 = new AlgorithmSuite(ECDSA_SHA256, EC_P256_PUBLIC_KEY); + /** Current ECDSA P-384 signing suite. */ + public static final AlgorithmSuite ECDSA_SHA384_P384 = new AlgorithmSuite(ECDSA_SHA384, EC_P384_PUBLIC_KEY); + /** Current ECDSA P-521 signing suite. */ + public static final AlgorithmSuite ECDSA_SHA512_P521 = new AlgorithmSuite(ECDSA_SHA512, EC_P521_PUBLIC_KEY); + /** Current immutable PKI signing default. */ + public static final AlgorithmSuite PKI_SIGNATURE_DEFAULT_V1 = new AlgorithmSuite(RSA_PKCS1_SHA256, RSA_PUBLIC_KEY); + + private static final List+ * SHA-1 and unknown aliases are rejected. The returned identity, rather than + * the alias, is authoritative. + *
+ * + * @param alias legacy provider spelling + * @return exact bootstrap identity, or empty when unknown or forbidden + */ + public static Optional+ * The signal carries no operation content, key material or executor state. + * Implementations should be immutable views over runtime-owned cancellation + * state. Streaming readers and writers are expected to call + * {@link #throwIfCancelled()} between bounded I/O operations. + *
+ */ +@FunctionalInterface +public interface CancellationSignal { + + /** + * A signal that never requests cancellation. + */ + CancellationSignal NONE = () -> false; + + /** + * Reports whether cancellation was requested. + * + * @return {@code true} when the operation should stop + */ + boolean isCancelled(); + + /** + * Fails the current streaming operation when cancellation was requested. + * + * @throws InterruptedIOException when cancellation was requested + */ + default void throwIfCancelled() throws InterruptedIOException { + if (isCancelled()) { + throw new InterruptedIOException("Streaming operation cancelled"); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/io/ContentDigests.java b/lib/src/main/java/zeroecho/core/io/ContentDigests.java new file mode 100644 index 0000000..54696b6 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/io/ContentDigests.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.io; + +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.Objects; + +/** + * Streaming integrity helpers for repeatable content. + */ +public final class ContentDigests { + + private static final int BUFFER_BYTES = 16 * 1024; + + private ContentDigests() { + throw new AssertionError("No instances"); + } + + /** + * Computes a SHA-256 fingerprint without materializing the aggregate content. + * + * @param content repeatable content + * @param cancellation runtime cancellation signal + * @return lowercase hexadecimal SHA-256 fingerprint + * @throws IOException if the content cannot be read or cancellation is + * requested + */ + public static String sha256(RepeatableContent content, CancellationSignal cancellation) throws IOException { + Objects.requireNonNull(content, "content"); + Objects.requireNonNull(cancellation, "cancellation"); + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException("SHA-256 is unavailable", ex); + } + byte[] buffer = new byte[BUFFER_BYTES]; + try (InputStream input = content.openStream()) { + int read; + while ((read = input.read(buffer)) >= 0) { + cancellation.throwIfCancelled(); + if (read > 0) { + digest.update(buffer, 0, read); + } + } + } finally { + java.util.Arrays.fill(buffer, (byte) 0); + } + return HexFormat.of().formatHex(digest.digest()); + } +} diff --git a/lib/src/main/java/zeroecho/core/io/ContentSlice.java b/lib/src/main/java/zeroecho/core/io/ContentSlice.java new file mode 100644 index 0000000..61e844d --- /dev/null +++ b/lib/src/main/java/zeroecho/core/io/ContentSlice.java @@ -0,0 +1,161 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.io; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; +import java.util.OptionalLong; + +/** + * Immutable repeatable bounded view over another repeatable content source. + * + *+ * A slice opens a fresh source pass and skips incrementally; it never copies the + * represented bytes. Closing the slice does not close its source because source + * ownership remains with the creator. + *
+ */ +public final class ContentSlice implements RepeatableContent { + + private static final long EMPTY_LENGTH = 0L; + private final RepeatableContent source; + private final long offset; + private final long length; + private final String contentId; + + /** + * Creates a repeatable slice. + * + * @param source repeatable source + * @param offset non-negative source offset + * @param length non-negative slice length + * @throws IllegalArgumentException if a range is negative or exceeds a known + * source length + */ + public ContentSlice(RepeatableContent source, long offset, long length) { + this.source = Objects.requireNonNull(source, "source"); + if (offset < 0L || length < 0L) { + throw new IllegalArgumentException("Content slice range must not be negative"); + } + long end = Math.addExact(offset, length); + OptionalLong sourceLength = source.length(); + if (sourceLength.isPresent() && end > sourceLength.getAsLong()) { + throw new IllegalArgumentException("Content slice exceeds source"); + } + this.offset = offset; + this.length = length; + this.contentId = source.contentId() + "#slice:" + offset + ':' + length; + } + + @Override + public InputStream openStream() throws IOException { + InputStream input = source.openStream(); + try { + skipExactly(input, offset); + return new LimitedInputStream(input, length); + } catch (IOException failure) { + input.close(); + throw failure; + } + } + + @Override + public OptionalLong length() { + return OptionalLong.of(length); + } + + @Override + public String contentId() { + return contentId; + } + + @Override + public void close() { + // Source ownership remains with the creator. + } + + private static void skipExactly(InputStream input, long count) throws IOException { + long remaining = count; + while (remaining != EMPTY_LENGTH) { + long skipped = input.skip(remaining); + if (skipped > EMPTY_LENGTH) { + remaining -= skipped; + } else if (input.read() < 0) { + throw new IOException("Content slice source is truncated"); + } else { + remaining--; + } + } + } + + /** Exact-length stream view that fails when its underlying source truncates. */ + private static final class LimitedInputStream extends FilterInputStream { + private long remaining; + + private LimitedInputStream(InputStream input, long remaining) { + super(input); + this.remaining = remaining; + } + + @Override + public int read() throws IOException { + if (remaining == EMPTY_LENGTH) { + return -1; + } + int value = super.read(); + if (value < 0) { + throw new IOException("Content slice source is truncated"); + } + remaining--; + return value; + } + + @Override + public int read(byte[] bytes, int offset, int count) throws IOException { + Objects.checkFromIndexSize(offset, count, bytes.length); + if (remaining == EMPTY_LENGTH) { + return -1; + } + int requested = (int) Math.min(remaining, count); + int read = super.read(bytes, offset, requested); + if (read < 0) { + throw new IOException("Content slice source is truncated"); + } + remaining -= read; + return read; + } + } +} diff --git a/lib/src/main/java/zeroecho/core/io/ImmutableByteContent.java b/lib/src/main/java/zeroecho/core/io/ImmutableByteContent.java new file mode 100644 index 0000000..dc9165b --- /dev/null +++ b/lib/src/main/java/zeroecho/core/io/ImmutableByteContent.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.io; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.Objects; +import java.util.OptionalLong; + +/** + * Explicit small-value adapter from immutable bytes to repeatable content. + * + *+ * This adapter intentionally materializes its individual value. It is suitable + * for bounded signatures, public-key fields and external small-object inputs. It + * must not be used as the authoritative representation of aggregate CRLs, TBS + * objects or streamed entry sequences. + *
+ */ +public final class ImmutableByteContent implements RepeatableContent { + + private final byte[] bytes; + private final String contentId; + + /** + * Creates an owned immutable byte value. + * + * @param bytes individual value, possibly empty + * @throws NullPointerException if {@code bytes} is {@code null} + */ + public ImmutableByteContent(byte[] bytes) { + byte[] source = Objects.requireNonNull(bytes, "bytes"); + this.bytes = source.clone(); + this.contentId = "sha256:" + digest(this.bytes); + } + + @Override + public InputStream openStream() { + return new ByteArrayInputStream(bytes); + } + + @Override + public OptionalLong length() { + return OptionalLong.of(bytes.length); + } + + @Override + public String contentId() { + return contentId; + } + + /** + * Returns a defensive copy for an explicitly bounded provider adapter. + * + * @return newly allocated bytes + */ + public byte[] copyBytes() { + return bytes.clone(); + } + + @Override + public void close() { + // Immutable caller-visible values own no external resources. + } + + private static String digest(byte[] value) { + try { + return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value)); + } catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException("SHA-256 is unavailable", ex); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/io/OneShotContent.java b/lib/src/main/java/zeroecho/core/io/OneShotContent.java new file mode 100644 index 0000000..e80d9c7 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/io/OneShotContent.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.io; + +import java.io.IOException; +import java.io.InputStream; +import java.util.OptionalLong; + +/** + * Provider-independent one-shot streaming input. + * + *+ * A one-shot input is not repeatable and therefore must be staged before signing + * recovery, canonical comparison, postcondition validation or publication that + * needs another pass. The returned stream is owned by the caller. Implementations + * must reject a second call to {@link #openStream()}. + *
+ */ +public interface OneShotContent extends AutoCloseable { + + /** + * Opens the only sequential reader. + * + * @return content stream + * @throws IOException if the source cannot be opened + * @throws IllegalStateException if the source was already opened + */ + InputStream openStream() throws IOException; + + /** + * Returns the known source length when available. + * + * @return non-negative length or empty + */ + OptionalLong length(); + + /** + * Releases the source. + * + * @throws IOException if cleanup fails + */ + @Override + void close() throws IOException; +} diff --git a/lib/src/main/java/zeroecho/core/io/RepeatableContent.java b/lib/src/main/java/zeroecho/core/io/RepeatableContent.java new file mode 100644 index 0000000..e392d44 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/io/RepeatableContent.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.io; + +import java.io.IOException; +import java.io.InputStream; +import java.util.OptionalLong; + +/** + * Immutable, provider-independent source of repeatable operation content. + * + *+ * Every call to {@link #openStream()} returns a new sequential reader positioned + * at the first byte. Implementations may be backed by files, object storage, + * databases or explicitly small immutable byte values. Callers own and must close + * each returned stream. Closing the content releases its implementation-owned + * resources but does not close streams already returned unless the implementation + * documents a stronger local rule. + *
+ * + *+ * The contract does not impose an aggregate content-size limit. Completion remains + * subject to available storage, I/O, technical representability and explicitly + * injected deployment policy. Content never carries key material or cryptographic + * provider authority. + *
+ */ +public interface RepeatableContent extends AutoCloseable { + + /** + * Opens a new sequential reader. + * + * @return newly opened content stream + * @throws IOException if the immutable content cannot be opened or its + * integrity cannot be established + */ + InputStream openStream() throws IOException; + + /** + * Returns the known aggregate length when cheaply and authoritatively + * available. + * + * @return non-negative length, or empty when the length is unknown + */ + OptionalLong length(); + + /** + * Returns a stable, non-secret identifier for integrity and durable provenance. + * + *+ * The identifier is metadata, not authorization, and must not expose a + * temporary physical path. + *
+ * + * @return stable non-blank identifier + */ + String contentId(); + + /** + * Releases implementation-owned resources. + * + * @throws IOException if cleanup fails + */ + @Override + void close() throws IOException; +} diff --git a/lib/src/main/java/zeroecho/core/spec/AlgorithmIdentity.java b/lib/src/main/java/zeroecho/core/spec/AlgorithmIdentity.java new file mode 100644 index 0000000..c9a26ca --- /dev/null +++ b/lib/src/main/java/zeroecho/core/spec/AlgorithmIdentity.java @@ -0,0 +1,589 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.spec; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collection; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * Provider-independent identity of one exact cryptographic operation or key + * type. + * + *+ * An identity contains no provider, implementation class, key material, or + * X.509 representation. Families are namespaced so trusted installed + * extensions can add typed parameter models without modifying a central enum. + * The parameter object is responsible for family-specific validation and a + * deterministic canonical component. + *
+ * + *+ * Instances are immutable. Equality and hashing use the complete canonical + * semantics, including the role, family, and parameters. + *
+ */ +public final class AlgorithmIdentity { + + private static final Pattern COMPONENT = Pattern.compile("[a-z][a-z0-9._-]{0,63}"); + private static final AlgorithmIdentityCodec BUILTIN_CODEC = new BuiltInCodec(); + private static final String NO_PARAMETERS = "none"; + private static final int RSA_PSS_COMPONENT_COUNT = 5; + private static final int REQUIRED_TRAILER_FIELD = 1; + private static final int UTF8_ONE_BYTE_LIMIT = 0x7f; + private static final int UTF8_TWO_BYTE_LIMIT = 0x7ff; + + private final Kind kind; + private final Family family; + private final AlgorithmIdentityCodec codec; + private final byte[] parameterSnapshot; + private final String canonicalForm; + + /** + * Semantic role represented by an identity. + */ + public enum Kind { + /** Message digest. */ + DIGEST, + /** Mask-generation function. */ + MASK_GENERATION, + /** Signature scheme, independent of a particular key parameter set. */ + SIGNATURE, + /** Public-key algorithm and its exact key parameter set. */ + PUBLIC_KEY, + /** Key-encapsulation mechanism. */ + KEM, + /** Key-agreement mechanism. */ + AGREEMENT + } + + /** + * Stable namespaced algorithm family name. + * + * @param namespace namespace owned by the built-in catalog or trusted + * extension + * @param name family name within that namespace + */ + public record Family(String namespace, String name) { + + /** + * Creates a validated family name. + * + * @throws IllegalArgumentException if either component is not a lowercase + * canonical identifier + */ + public Family { + namespace = requireComponent(namespace, "namespace"); + name = requireComponent(name, "name"); + } + + /** + * Returns the deterministic family representation. + * + * @return namespace and family separated by {@code /} + */ + public String canonicalForm() { + return namespace + "/" + name; + } + } + + /** + * Typed, immutable family parameters. + * + *+ * Implementations supplied by trusted code must validate their complete + * family-specific semantics during construction. The canonical component is + * persistent identity data and therefore must never depend on a provider, + * locale, insertion order, or display alias. + *
+ */ + public interface Parameters { + + /** + * Returns the deterministic parameter representation. + * + * @return non-blank lowercase canonical component + */ + String canonicalForm(); + + /** + * Returns an independently owned immutable copy. + * + *+ * Trusted extension implementations must not return mutable caller-owned + * state. Immutable records may return {@code this}. + *
+ * + * @return immutable owned parameters + */ + Parameters immutableCopy(); + } + + /** + * Parameters for an unparameterized family. + */ + public enum NoParameters implements Parameters { + /** Singleton empty-parameter value. */ + INSTANCE; + + @Override + public String canonicalForm() { + return "none"; + } + + @Override + public Parameters immutableCopy() { + return this; + } + } + + /** + * Digest-qualified signature parameters. + * + * @param digest exact digest identity + */ + public record DigestParameters(AlgorithmIdentity digest) implements Parameters { + + /** + * Creates digest-qualified parameters. + * + * @throws IllegalArgumentException if {@code digest} is not a digest + * identity + */ + public DigestParameters { + Objects.requireNonNull(digest, "digest"); + if (digest.kind() != Kind.DIGEST) { + throw new IllegalArgumentException("digest must have DIGEST kind"); + } + } + + @Override + public String canonicalForm() { + return "digest=" + digest.family().namespace() + "." + digest.family().name() + "." + + digest.parameters().canonicalForm(); + } + + @Override + public Parameters immutableCopy() { + return this; + } + } + + /** + * Exact RSA-PSS parameters. + * + * @param hash message digest identity + * @param mask mask-generation identity + * @param maskHash mask-generation digest identity + * @param saltLength non-negative salt length in bytes + * @param trailerField trailer field; PKCS#1 currently defines value {@code 1} + */ + public record RsaPssParameters(AlgorithmIdentity hash, AlgorithmIdentity mask, AlgorithmIdentity maskHash, + int saltLength, int trailerField) implements Parameters { + + /** + * Creates a validated exact RSA-PSS parameter tuple. + * + * @throws IllegalArgumentException if roles or numeric parameters are + * contradictory + */ + public RsaPssParameters { + Objects.requireNonNull(hash, "hash"); + Objects.requireNonNull(mask, "mask"); + Objects.requireNonNull(maskHash, "maskHash"); + if (hash.kind() != Kind.DIGEST || maskHash.kind() != Kind.DIGEST) { + throw new IllegalArgumentException("RSA-PSS hashes must have DIGEST kind"); + } + if (mask.kind() != Kind.MASK_GENERATION) { + throw new IllegalArgumentException("RSA-PSS mask must have MASK_GENERATION kind"); + } + if (saltLength < 0) { + throw new IllegalArgumentException("RSA-PSS salt length must not be negative"); + } + if (trailerField != REQUIRED_TRAILER_FIELD) { + throw new IllegalArgumentException("RSA-PSS trailer field must be 1"); + } + } + + @Override + public String canonicalForm() { + return "hash=" + shortName(hash) + ",mask=" + shortName(mask) + ",maskhash=" + shortName(maskHash) + + ",salt=" + saltLength + ",trailer=" + trailerField; + } + + @Override + public Parameters immutableCopy() { + return this; + } + } + + /** + * Exact named parameter set, such as an elliptic-curve name. + * + * @param parameterSet stable namespaced parameter-set identifier + */ + public record NamedParameters(Family parameterSet) implements Parameters { + + /** + * Creates named parameters. + * + * @throws NullPointerException if {@code parameterSet} is {@code null} + */ + public NamedParameters { + Objects.requireNonNull(parameterSet, "parameterSet"); + } + + @Override + public String canonicalForm() { + return "set=" + parameterSet.namespace() + "." + parameterSet.name(); + } + + @Override + public Parameters immutableCopy() { + return this; + } + } + + /** + * Creates one exact algorithm identity. + * + * @param kind semantic role + * @param family stable namespaced family + * @param parameters validated typed parameters + * @throws NullPointerException if an argument is {@code null} + * @throws IllegalArgumentException if the parameter canonical form is not + * deterministic syntax + */ + public AlgorithmIdentity(Kind kind, Family family, Parameters parameters) { + this(kind, family, parameters, BUILTIN_CODEC); + } + + /** + * Creates one exact algorithm identity using a trusted typed codec. + * + * @param kind semantic role + * @param family stable namespaced family + * @param parameters validated typed parameters + * @param codec immutable canonical parameter codec + */ + public AlgorithmIdentity(Kind kind, Family family, Parameters parameters, AlgorithmIdentityCodec codec) { + this.kind = Objects.requireNonNull(kind, "kind"); + this.family = Objects.requireNonNull(family, "family"); + this.codec = Objects.requireNonNull(codec, "codec"); + requireComponent(codec.id(), "codec.id"); + byte[] encoded = codec.encode(Objects.requireNonNull(parameters, "parameters")); + this.parameterSnapshot = Objects.requireNonNull(encoded, "encoded parameters").clone(); + Parameters decoded = Objects.requireNonNull(codec.decode(parameterSnapshot.clone()), "decoded parameters"); + byte[] roundTrip = codec.encode(decoded); + if (!java.util.Arrays.equals(parameterSnapshot, roundTrip)) { + throw new IllegalArgumentException("Algorithm parameter codec is not canonical"); + } + this.canonicalForm = "zealg:2:" + field(kind.name().toLowerCase(Locale.ROOT)) + field(family.namespace()) + + field(family.name()) + field(codec.id()) + + field(Base64.getUrlEncoder().withoutPadding().encodeToString(parameterSnapshot)); + } + + /** + * Returns the semantic identity kind. + * + * @return identity kind + */ + public Kind kind() { + return kind; + } + + /** + * Returns the stable family. + * + * @return namespaced family + */ + public Family family() { + return family; + } + + /** + * Returns the immutable typed parameters. + * + * @return family parameters + */ + public Parameters parameters() { + return codec.decode(parameterSnapshot.clone()); + } + + /** + * Parses a complete version-two canonical identity with installed codecs. + * + * @param canonicalForm canonical identity + * @param codecs trusted installed codecs + * @return exact decoded identity + * @throws IllegalArgumentException if syntax, version, codec, or canonical + * round-trip validation fails + */ + public static AlgorithmIdentity parse(String canonicalForm, Collection+ * Built-in identities reserve the {@code zeroecho} namespace. Trusted installed + * code may contribute identities under another namespace. Duplicate canonical + * representations fail closed; registration order never supplies precedence. + * Administrative configuration is not a registration mechanism. + *
+ */ +public final class AlgorithmIdentityCatalog { + + /** Namespace reserved for immutable built-in identities. */ + public static final String BUILTIN_NAMESPACE = "zeroecho"; + + private final Map+ * A trusted extension may add a tuple within an existing reserved family, but + * cannot introduce a new family under the built-in namespace. + *
+ * + * @param additions exact additive identities + * @return new immutable catalog + */ + public AlgorithmIdentityCatalog add(Collection+ * Encoding takes an immediate immutable snapshot. Decoding must return fresh + * values or intrinsically immutable values and must reject malformed, + * incomplete, or contradictory input. Codec identifiers are stable semantic + * identity and cannot be redefined by catalog ordering or configuration. + *
+ */ +public interface AlgorithmIdentityCodec { + + /** + * Returns the stable namespaced codec identifier. + * + * @return canonical codec identifier + */ + String id(); + + /** + * Encodes complete typed parameters. + * + * @param parameters typed parameters + * @return independently owned canonical bytes + */ + byte[] encode(AlgorithmIdentity.Parameters parameters); + + /** + * Decodes complete canonical bytes. + * + * @param encoded canonical bytes + * @return fresh validated typed parameters + */ + AlgorithmIdentity.Parameters decode(byte[] encoded); +} diff --git a/lib/src/main/java/zeroecho/core/spec/AlgorithmSuite.java b/lib/src/main/java/zeroecho/core/spec/AlgorithmSuite.java new file mode 100644 index 0000000..fa4d8dd --- /dev/null +++ b/lib/src/main/java/zeroecho/core/spec/AlgorithmSuite.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.spec; + +import java.util.Objects; + +/** + * Immutable composition of a signature scheme and a compatible public-key + * identity. + * + *+ * The suite keeps signature and key semantics separate. In particular, an + * ECDSA signature identity contains its digest while the public-key identity + * contains the named curve. Compatibility is evaluated by capability and + * policy, not inferred from an X.509 signature OID. + *
+ * + * @param signature exact signature-scheme identity + * @param publicKey exact public-key identity + */ +public record AlgorithmSuite(AlgorithmIdentity signature, AlgorithmIdentity publicKey) { + + /** + * Creates a validated signature suite. + * + * @throws NullPointerException if an identity is {@code null} + * @throws IllegalArgumentException if an identity has the wrong semantic kind + */ + public AlgorithmSuite { + Objects.requireNonNull(signature, "signature"); + Objects.requireNonNull(publicKey, "publicKey"); + if (signature.kind() != AlgorithmIdentity.Kind.SIGNATURE) { + throw new IllegalArgumentException("signature must have SIGNATURE kind"); + } + if (publicKey.kind() != AlgorithmIdentity.Kind.PUBLIC_KEY) { + throw new IllegalArgumentException("publicKey must have PUBLIC_KEY kind"); + } + } + + /** + * Returns the deterministic suite representation. + * + * @return signature and key canonical identities in a length-independent form + */ + public String canonicalForm() { + return "zesuite:1:" + signature.canonicalForm() + "|" + publicKey.canonicalForm(); + } +} diff --git a/lib/src/main/java/zeroecho/core/spi/AlgorithmExecutionCapabilities.java b/lib/src/main/java/zeroecho/core/spi/AlgorithmExecutionCapabilities.java new file mode 100644 index 0000000..3ba003b --- /dev/null +++ b/lib/src/main/java/zeroecho/core/spi/AlgorithmExecutionCapabilities.java @@ -0,0 +1,141 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.spi; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.Set; + +import zeroecho.core.spec.AlgorithmIdentity; +import zeroecho.core.spec.AlgorithmSuite; + +/** + * Immutable deterministic snapshot of installed execution capabilities. + * + *+ * Multiple implementations may support one semantic identity. Callers must + * select an implementation explicitly when more than one remains after policy; + * classpath or provider order never supplies precedence. + *
+ */ +public final class AlgorithmExecutionCapabilities { + + private final List+ * A capability describes implementation availability; it never defines + * algorithm identity, X.509 semantics, defaults, or policy. Implementations must + * provide a stable semantic fingerprint for deterministic conflict diagnostics. + * Administrative configuration cannot provide implementation classes. + *
+ */ +public interface AlgorithmExecutionCapability { + + /** + * Supported execution direction. + */ + enum Direction { + /** Signature generation. */ + SIGN, + /** Signature verification. */ + VERIFY + } + + /** + * Returns a stable installed implementation identifier. + * + * @return namespaced provider implementation identifier + */ + String implementationId(); + + /** + * Returns a deterministic description of the supported typed domain. + * + * @return stable non-secret domain fingerprint + */ + String domainFingerprint(); + + /** + * Tests whether this implementation supports an exact identity and suite. + * + * @param identity requested exact operation identity + * @param suite complete key and signature suite + * @param direction requested direction + * @return {@code true} only for tuples implemented exactly + */ + boolean supports(AlgorithmIdentity identity, AlgorithmSuite suite, Direction direction); +} diff --git a/lib/src/main/java/zeroecho/core/spi/AlgorithmExecutionCapabilityProvider.java b/lib/src/main/java/zeroecho/core/spi/AlgorithmExecutionCapabilityProvider.java new file mode 100644 index 0000000..bc4e894 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/spi/AlgorithmExecutionCapabilityProvider.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.spi; + +import java.util.List; + +/** + * Service-provider contract for trusted installed execution capabilities. + * + *+ * Providers are deployment code discovered using the existing ServiceLoader + * convention. Configuration may select an installed capability but cannot name + * or load an implementation class. + *
+ */ +@FunctionalInterface +public interface AlgorithmExecutionCapabilityProvider { + + /** + * Returns an immutable capability contribution. + * + * @return installed capabilities; never {@code null} + */ + List+ * ZeroEcho provides no built-in operational values. A deployment explicitly + * supplies every dimension as either unrestricted by deployment or positively + * limited. Checks are incremental and do not alter algorithm, certificate or + * binding semantics. + *
+ * + * @param acceptedSourceBytes accepted input bytes + * @param generatedObjectBytes generated signed-object bytes + * @param entryCount streamed entry count + * @param stagedContentBytes durable staged-content bytes + * @param temporaryStorageBytes temporary spool bytes + * @param publicationBytes bytes supplied to a publisher + * @param openStagedObjects concurrently open staged objects + */ +public record DeploymentResourcePolicy(ResourceLimit acceptedSourceBytes, ResourceLimit generatedObjectBytes, + ResourceLimit entryCount, ResourceLimit stagedContentBytes, ResourceLimit temporaryStorageBytes, + ResourceLimit publicationBytes, ResourceLimit openStagedObjects) { + + /** + * Creates a deployment resource policy. + * + * @throws NullPointerException if a dimension is {@code null} + */ + public DeploymentResourcePolicy { + Objects.requireNonNull(acceptedSourceBytes, "acceptedSourceBytes"); + Objects.requireNonNull(generatedObjectBytes, "generatedObjectBytes"); + Objects.requireNonNull(entryCount, "entryCount"); + Objects.requireNonNull(stagedContentBytes, "stagedContentBytes"); + Objects.requireNonNull(temporaryStorageBytes, "temporaryStorageBytes"); + Objects.requireNonNull(publicationBytes, "publicationBytes"); + Objects.requireNonNull(openStagedObjects, "openStagedObjects"); + } +} diff --git a/pki/src/main/java/zeroecho/pki/api/content/DurableContentOwner.java b/pki/src/main/java/zeroecho/pki/api/content/DurableContentOwner.java new file mode 100644 index 0000000..5aecfa0 --- /dev/null +++ b/pki/src/main/java/zeroecho/pki/api/content/DurableContentOwner.java @@ -0,0 +1,119 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.pki.api.content; + +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +import zeroecho.pki.api.PkiId; +import zeroecho.pki.api.orch.SigningSubmissionId; + +/** + * Typed immutable identity of a durable business owner of staged content. + * + *+ * Owner identities contain no path, content, key material, provider name, or + * implementation class. The staged-content store is the sole authority that + * persists retain and release transitions. Constructing this value does not + * itself retain content. + *
+ * + * @param category closed durable-owner category + * @param identifier canonical category-specific identifier + */ +public record DurableContentOwner(Category category, String identifier) { + + private static final int MAX_IDENTIFIER_BYTES = 512; + + /** Creates and validates one typed owner identity. */ + public DurableContentOwner { + Objects.requireNonNull(category, "category"); + String exact = Objects.requireNonNull(identifier, "identifier"); + if (exact.isBlank() || !exact.equals(exact.strip()) + || exact.getBytes(StandardCharsets.UTF_8).length > MAX_IDENTIFIER_BYTES) { + throw new IllegalArgumentException("Durable content owner identifier is not canonical"); + } + for (int index = 0; index < exact.length(); index++) { + char value = exact.charAt(index); + if (value < 0x21 || value > 0x7e || value == '/' || value == '\\') { + throw new IllegalArgumentException("Durable content owner identifier is not canonical"); + } + } + if (category == Category.SIGNING_OPERATION) { + SigningSubmissionId.parse(new PkiId(exact)); + } + } + + /** + * Creates the canonical owner for one signing operation. + * + * @param operationId canonical signing submission identifier + * @return signing-operation owner + * @throws IllegalArgumentException if the identifier is not a canonical + * signing submission identifier + */ + public static DurableContentOwner signingOperation(PkiId operationId) { + Objects.requireNonNull(operationId, "operationId"); + SigningSubmissionId parsed = SigningSubmissionId.parse(operationId); + return new DurableContentOwner(Category.SIGNING_OPERATION, parsed.id().value()); + } + + /** + * Creates the canonical owner for one persisted credential record. + * + * @param credentialId canonical credential identifier + * @return credential-record owner + * @throws IllegalArgumentException if the identifier cannot be represented as + * a canonical durable owner identifier + */ + public static DurableContentOwner credentialRecord(PkiId credentialId) { + Objects.requireNonNull(credentialId, "credentialId"); + return new DurableContentOwner(Category.CREDENTIAL_RECORD, credentialId.value()); + } + + /** Returns a deterministic persistence token containing no path or payload. */ + public String canonicalForm() { + return category.name() + ":" + identifier; + } + + /** Closed durable-owner categories reserved by the Phase A lifecycle. */ + public enum Category { + /** Pending or recoverable signing operation. */ + SIGNING_OPERATION, + /** Persisted credential record; integration is deferred. */ + CREDENTIAL_RECORD, + /** Persisted status-object record; integration is deferred. */ + STATUS_OBJECT_RECORD + } +} diff --git a/pki/src/main/java/zeroecho/pki/api/content/DurableContentReference.java b/pki/src/main/java/zeroecho/pki/api/content/DurableContentReference.java new file mode 100644 index 0000000..26cdfe9 --- /dev/null +++ b/pki/src/main/java/zeroecho/pki/api/content/DurableContentReference.java @@ -0,0 +1,117 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.pki.api.content; + +import zeroecho.pki.api.Encoding; + +/** + * Stable, payload-free reference to immutable staged content. + * + *+ * The reference is durable provenance rather than a live file handle. It carries + * no physical path, payload, key material or runtime authorization token. It may + * be resolved only through the staged-content store whose immutable identifier + * matches {@link #storeId()}. + *
+ * + * A durable content reference is authoritative only when issued and validated + * by its owning staged-content store. This read-only interface deliberately has + * no public construction factory: callers cannot turn arbitrary metadata into + * an authoritative reference. Persistence decoders must restore references + * through the owning store, which rejects foreign stores and metadata mismatch. + * Sealing and reference issuance do not imply durable business-object ownership. + * + *+ * Implementations are immutable and expose no physical path, temporary name, + * payload, key material, open handle, or mutable lifecycle state. Earlier + * pre-release persistence forms are not accepted or migrated. Phase B key + * isolation is outside this contract. + *
+ */ +public interface DurableContentReference { + + /** + * Returns the canonical logical identity of the owning store. + * + * @return path-independent store identity + */ + String storeId(); + + /** + * Returns the opaque canonical content identity issued by the store. + * + * @return path-independent content identity + */ + String contentId(); + + /** + * Returns the semantic transport encoding. + * + * @return content encoding + */ + Encoding encoding(); + + /** + * Returns the exact checked byte length. + * + * @return non-negative content length + */ + long length(); + + /** + * Returns the canonical lowercase SHA-256 integrity value. + * + * @return integrity value; callers must not treat it as issuance authority + */ + String sha256(); + + /** + * Returns the immutable content purpose classification. + * + * @return lifecycle classification; not durable ownership state + */ + Lifecycle lifecycle(); + + /** + * Durable ownership classification. + */ + enum Lifecycle { + /** Content retained while a durable operation is pending. */ + OPERATION, + /** Content owned by an immutable persisted PKI object. */ + PERSISTED, + /** Content eligible for release after its immediate operation. */ + TEMPORARY + } +} diff --git a/pki/src/main/java/zeroecho/pki/api/content/ResourceLimit.java b/pki/src/main/java/zeroecho/pki/api/content/ResourceLimit.java new file mode 100644 index 0000000..2c4d4b1 --- /dev/null +++ b/pki/src/main/java/zeroecho/pki/api/content/ResourceLimit.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.pki.api.content; + +/** + * Explicit deployment-owned resource constraint. + * + *+ * This type distinguishes absence of a deployment limit from a positive finite + * limit without nullable numbers or sentinel values. It never changes PKI, + * algorithm or X.509 binding semantics. + *
+ */ +public sealed interface ResourceLimit permits ResourceLimit.UnrestrictedByDeployment, ResourceLimit.LimitedTo { + /** Smallest valid observed aggregate value. */ + long EMPTY = 0L; + + /** + * Checks an incrementally observed non-negative value. + * + * @param observed observed aggregate count or byte length + * @throws IllegalArgumentException if {@code observed} is negative + * @throws ResourceLimitExceededException if the configured limit is exceeded + */ + void requireAllows(long observed); + + /** + * Explicit absence of a deployment-owned limit. + */ + record UnrestrictedByDeployment() implements ResourceLimit { + @Override + public void requireAllows(long observed) { + if (observed < EMPTY) { + throw new IllegalArgumentException("Observed resource value must not be negative"); + } + } + } + + /** + * Positive finite deployment-owned limit. + * + * @param maximum inclusive maximum + */ + record LimitedTo(long maximum) implements ResourceLimit { + /** + * Creates a finite limit. + * + * @throws IllegalArgumentException if {@code maximum} is not positive + */ + public LimitedTo { + if (maximum <= EMPTY) { + throw new IllegalArgumentException("Deployment resource limit must be positive"); + } + } + + @Override + public void requireAllows(long observed) { + if (observed < EMPTY) { + throw new IllegalArgumentException("Observed resource value must not be negative"); + } + if (observed > maximum) { + throw new ResourceLimitExceededException(); + } + } + } + + /** + * Stable non-sensitive failure for deployment resource rejection. + */ + final class ResourceLimitExceededException extends IllegalStateException { + private static final long serialVersionUID = 5780535874172354935L; + + private ResourceLimitExceededException() { + super("Deployment resource limit exceeded: code=DEPLOYMENT_RESOURCE_LIMIT_EXCEEDED"); + } + } +} diff --git a/pki/src/main/java/zeroecho/pki/api/credential/Credential.java b/pki/src/main/java/zeroecho/pki/api/credential/Credential.java index fab9f7b..3fb8ad9 100644 --- a/pki/src/main/java/zeroecho/pki/api/credential/Credential.java +++ b/pki/src/main/java/zeroecho/pki/api/credential/Credential.java @@ -33,13 +33,13 @@ ******************************************************************************/ package zeroecho.pki.api.credential; -import zeroecho.pki.api.EncodedObject; import zeroecho.pki.api.FormatId; import zeroecho.pki.api.IssuerRef; import zeroecho.pki.api.PkiId; import zeroecho.pki.api.SubjectRef; import zeroecho.pki.api.Validity; import zeroecho.pki.api.attr.AttributeSet; +import zeroecho.pki.api.content.DurableContentReference; /** * Issued credential with mandatory core metadata and universal attributes. @@ -70,12 +70,12 @@ import zeroecho.pki.api.attr.AttributeSet; * current revocation state and evaluation time are * external runtime inputs. Security-sensitive callers * must use {@link EffectiveCredentialStatusResolver}. - * @param encoded encoded credential bytes + * @param content immutable store-owned credential content * @param attributes universal attribute set */ public record Credential(PkiId credentialId, FormatId formatId, IssuerRef issuerRef, SubjectRef subjectRef, Validity validity, String serialOrUniqueId, PkiId publicKeyId, CredentialProfileBinding profileBinding, - CredentialStatus status, EncodedObject encoded, AttributeSet attributes) { + CredentialStatus status, DurableContentReference content, AttributeSet attributes) { /** * Creates a credential record. @@ -110,8 +110,8 @@ public record Credential(PkiId credentialId, FormatId formatId, IssuerRef issuer if (status == null) { throw new IllegalArgumentException("status must not be null"); } - if (encoded == null) { - throw new IllegalArgumentException("encoded must not be null"); + if (content == null) { + throw new IllegalArgumentException("content must not be null"); } if (attributes == null) { throw new IllegalArgumentException("attributes must not be null"); diff --git a/pki/src/main/java/zeroecho/pki/api/credential/CredentialBundle.java b/pki/src/main/java/zeroecho/pki/api/credential/CredentialBundle.java index cd86bfe..493e9b4 100644 --- a/pki/src/main/java/zeroecho/pki/api/credential/CredentialBundle.java +++ b/pki/src/main/java/zeroecho/pki/api/credential/CredentialBundle.java @@ -35,7 +35,7 @@ package zeroecho.pki.api.credential; import java.util.List; -import zeroecho.pki.api.EncodedObject; +import zeroecho.pki.api.content.DurableContentReference; /** * Bundle of a primary credential and supporting objects. @@ -49,7 +49,7 @@ import zeroecho.pki.api.EncodedObject; * @param credential primary credential * @param supportingObjects supporting artifacts (framework-defined ordering) */ -public record CredentialBundle(Credential credential, List- * The {@code displaySuffixMaxLen} parameter controls the maximum number of - * characters appended after {@code '#'} in operation identifiers returned by - * {@link #canonicalizeOperationId(PkiId, Principal)}. If - * {@code displaySuffixMaxLen} is not positive, the constructor fails. + * The authority must already bind every signing identity declared by + * {@code signer} to that exact workflow instance for the {@code SIGN} + * direction. This constructor validates ownership before registering the + * workflow or activating durable state. It never constructs an internal + * authority, accepts an authority from another runtime, or handles private + * key material. *
* - * @param store persistent store (source of truth for - * continuation state) - * @param signer signature workflow - * @param durableLineStorePath path to append-only line store file - * @param displaySuffixMaxLen maximum number of characters after {@code '#'} + * @param store persistent store + * @param signer exact signing workflow owned by + * {@code authority} + * @param durableLineStorePath durable bus path + * @param authority shared immutable runtime authority + * @throws NullPointerException if any argument is {@code null} + * @throws IllegalArgumentException if the authority does not own the exact + * workflow for every declared signing + * identity and the {@code SIGN} direction */ - public PkiSigningBus(PkiStore store, SignatureWorkflow signer, Path durableLineStorePath, int displaySuffixMaxLen) { - this(store, signer, durableLineStorePath, displaySuffixMaxLen, OrchestrationDurabilityPolicy.DURABLE_MIN_STATE); + public PkiSigningBus(PkiStore store, SignatureWorkflow signer, Path durableLineStorePath, + X509AuthoritySnapshot authority) { + this(store, signer, durableLineStorePath, resolveDisplaySuffixMaxLen(Optional.empty()), + OrchestrationDurabilityPolicy.DURABLE_MIN_STATE, authority); } /** - * Creates a signing bus with explicit workflow continuation durability policy. + * Creates a signing bus bound to one immutable algorithm authority snapshot. * - * @param store persistent store (source of truth for - * continuation state) - * @param signer signature workflow - * @param durableLineStorePath path to append-only line store file - * @param displaySuffixMaxLen maximum number of characters after {@code '#'} - * @param durabilityPolicy durability policy used when persisting workflow - * state + *+ * The authority must already bind every signing identity declared by + * {@code signer} to that exact workflow instance for the {@code SIGN} + * direction. Validation occurs before registration or durable-state + * activation. The constructor does not derive an authority from provider + * strings, accept cross-runtime workflow ownership, or access private key + * material. + *
+ * + * @param store persistent store + * @param signer exact workflow represented by the snapshot + * capability + * @param durableLineStorePath durable bus path + * @param displaySuffixMaxLen display suffix bound + * @param durabilityPolicy continuation durability policy + * @param authority immutable runtime authority + * @throws NullPointerException if any reference argument is {@code null} + * @throws IllegalArgumentException if the display suffix bound is invalid or + * the authority does not own the exact + * workflow for every declared signing + * identity and the {@code SIGN} direction */ public PkiSigningBus(PkiStore store, SignatureWorkflow signer, Path durableLineStorePath, int displaySuffixMaxLen, - OrchestrationDurabilityPolicy durabilityPolicy) { + OrchestrationDurabilityPolicy durabilityPolicy, X509AuthoritySnapshot authority) { Objects.requireNonNull(store, "store"); Objects.requireNonNull(signer, "signer"); Objects.requireNonNull(durableLineStorePath, "durableLineStorePath"); Objects.requireNonNull(durabilityPolicy, "durabilityPolicy"); + X509AuthoritySnapshot exactAuthority = Objects.requireNonNull(authority, "authority"); + validateSigningAuthority(signer, exactAuthority); this.store = store; this.signer = signer; + this.authority = exactAuthority; this.random = new SecureRandom(); this.namespace = store.signingNamespace() + "." + signer.id(); signer.validateSigningDomain(this.namespace, store.signingHorizon(), store.signingPermittedSkew()); @@ -189,13 +212,21 @@ public final class PkiSigningBus implements AutoCloseable { for (WorkflowStateRecord state : store.listWorkflowStates()) { if (TYPE_SIGN.equals(state.type()) && state.payload().isPresent()) { try { - SignContinuation.decode(state.payload().orElseThrow()); + SignContinuation continuation = SignContinuation.decode(state.payload().orElseThrow(), + store.stagedContent()); + X509ExecutionPlan+ * The sink writes to the injected staged-content store and never retains an + * aggregate payload in the bus. ZeroEcho core imposes no arbitrary + * product-wide aggregate size limit. + *
+ * + * @param encoding content encoding + * @param lifecycle ownership and retirement class + * @return atomic streamed sink + * @throws PkiException if staging cannot begin + */ + public ContentSink beginContent(Encoding encoding, DurableContentReference.Lifecycle lifecycle) { + try { + return store.stagedContent().beginContent(Objects.requireNonNull(encoding, "encoding"), + Objects.requireNonNull(lifecycle, "lifecycle")); + } catch (java.io.IOException ex) { + throw new PkiException("Content staging failed: code=SPOOL_STORAGE_FAILED"); + } + } + + /** + * Opens immutable repeatable content owned by this runtime. + * + * @param reference opaque durable content reference + * @return repeatable content + * @throws PkiException if the content is missing, incomplete, corrupt, or + * belongs to another runtime store + */ + public RepeatableContent openContent(DurableContentReference reference) { + try { + return store.stagedContent().openContent(Objects.requireNonNull(reference, "reference")); + } catch (java.io.IOException ex) { + throw new PkiException("Content open failed: code=CONTENT_INTEGRITY_FAILED"); + } + } + + /** + * Releases staged content from this runtime. + * + * @param reference content reference + * @throws PkiException if cleanup fails + */ + public void releaseContent(DurableContentReference reference) { + try { + store.stagedContent().retireUnownedContent(Objects.requireNonNull(reference, "reference")); + } catch (java.io.IOException ex) { + throw new PkiException("Content cleanup failed: code=SPOOL_STORAGE_FAILED"); + } + } + + /** + * Begins a runtime-owned file-backed uniqueness index. + * + * @return temporary uniqueness index + * @throws PkiException if temporary storage cannot be created + */ + public TemporaryUniqueIndex beginUniqueIndex() { + try { + return store.stagedContent().beginUniqueIndex(); + } catch (java.io.IOException ex) { + throw new PkiException("Temporary index failed: code=SPOOL_STORAGE_FAILED"); + } + } + + private static String workflowImplementationId(SignatureWorkflow workflow) { + return "workflow." + workflow.id(); + } + + private static void validateSigningAuthority(SignatureWorkflow workflow, X509AuthoritySnapshot authority) { + Set* Instances are encoded into a compact binary representation through - * {@link #encode()} and reconstructed through {@link #decode(EncodedObject)}. + * {@link #encode()} and reconstructed through + * {@link #decode(EncodedObject, StagedContentStore)}. * The binary format is versioned by {@link #VERSION}. The current version * persists the algorithm identifier, payload encoding and bytes, key reference, * preferred signature encoding, and the optional downstream signer operation @@ -1271,7 +1485,7 @@ public final class PkiSigningBus implements AutoCloseable { *
* The current binary encoding does not persist the original
* {@link zeroecho.pki.api.audit.AccessContext} losslessly. During
- * {@link #decode(EncodedObject)}, a synthetic system access context is created
+ * {@link #decode(EncodedObject, StagedContentStore)}, a synthetic system access context is created
* instead. This is sufficient for the current continuation flow, but callers
* must not assume that {@code decode(encode(x))} preserves the original access
* context exactly.
@@ -1284,11 +1498,13 @@ public final class PkiSigningBus implements AutoCloseable {
*/
public static final class SignContinuation {
- private static final byte VERSION = 2;
+ private static final byte VERSION = 4;
+ private static final long MINIMUM_CONTENT_LENGTH = 0L;
private final zeroecho.pki.api.audit.AccessContext accessContext;
private final String algorithmId;
- private final EncodedObject payload;
+ private final Optional
*
+ * This metadata describes the same workflow implementation allocated by this + * provider. It does not select an OID, redefine an identity, or claim key + * availability. + *
+ * + * @return immutable execution capability contribution + */ + @Override + public java.util.List+ * Standard OIDs and parameter semantics are code-owned and cannot be overridden + * by configuration or extension ordering. RSA-PSS and EC SubjectPublicKeyInfo + * are represented by typed parameterized rules. + *
+ */ +public final class StandardX509Bindings { + + /** RSA PKCS#1 SHA-256 signature OID. */ + public static final String OID_RSA_SHA256 = "1.2.840.113549.1.1.11"; + /** RSA PKCS#1 SHA-384 signature OID. */ + public static final String OID_RSA_SHA384 = "1.2.840.113549.1.1.12"; + /** RSA PKCS#1 SHA-512 signature OID. */ + public static final String OID_RSA_SHA512 = "1.2.840.113549.1.1.13"; + /** RSA-PSS signature OID. */ + public static final String OID_RSA_PSS = "1.2.840.113549.1.1.10"; + /** ECDSA SHA-256 signature OID. */ + public static final String OID_ECDSA_SHA256 = "1.2.840.10045.4.3.2"; + /** ECDSA SHA-384 signature OID. */ + public static final String OID_ECDSA_SHA384 = "1.2.840.10045.4.3.3"; + /** ECDSA SHA-512 signature OID. */ + public static final String OID_ECDSA_SHA512 = "1.2.840.10045.4.3.4"; + /** Ed25519 OID. */ + public static final String OID_ED25519 = String.join(".", "1", "3", "101", "112"); + /** Ed448 OID. */ + public static final String OID_ED448 = String.join(".", "1", "3", "101", "113"); + /** RSA public-key OID. */ + public static final String OID_RSA_PUBLIC_KEY = "1.2.840.113549.1.1.1"; + /** EC public-key OID. */ + public static final String OID_EC_PUBLIC_KEY = "1.2.840.10045.2.1"; + /** P-256 named-curve OID. */ + public static final String OID_P256 = "1.2.840.10045.3.1.7"; + /** P-384 named-curve OID. */ + public static final String OID_P384 = "1.3.132.0.34"; + /** P-521 named-curve OID. */ + public static final String OID_P521 = "1.3.132.0.35"; + + private static final X509BindingCatalog CATALOG = createCatalog(X509ComponentCatalog.builtIn()); + + private StandardX509Bindings() { + } + + private static X509BindingCatalog createCatalog(X509ComponentCatalog components) { + Objects.requireNonNull(components, "components"); + return X509BindingCatalog.builtIn(List.of( + fixed("zeroecho.signature.rsa-pkcs1-sha256", X509AlgorithmRole.SIGNATURE_ALGORITHM, + BootstrapAlgorithmIdentities.RSA_PKCS1_SHA256, X509AlgorithmIdentifier.derNull(OID_RSA_SHA256), + X509BindingRule.SignatureEncoding.OPAQUE, X509BindingRule.PublicKeyEncoding.NOT_APPLICABLE), + fixed("zeroecho.signature.rsa-pkcs1-sha384", X509AlgorithmRole.SIGNATURE_ALGORITHM, + BootstrapAlgorithmIdentities.RSA_PKCS1_SHA384, X509AlgorithmIdentifier.derNull(OID_RSA_SHA384), + X509BindingRule.SignatureEncoding.OPAQUE, X509BindingRule.PublicKeyEncoding.NOT_APPLICABLE), + fixed("zeroecho.signature.rsa-pkcs1-sha512", X509AlgorithmRole.SIGNATURE_ALGORITHM, + BootstrapAlgorithmIdentities.RSA_PKCS1_SHA512, X509AlgorithmIdentifier.derNull(OID_RSA_SHA512), + X509BindingRule.SignatureEncoding.OPAQUE, X509BindingRule.PublicKeyEncoding.NOT_APPLICABLE), + new RsaPssRule(components), + fixed("zeroecho.signature.ecdsa-sha256", X509AlgorithmRole.SIGNATURE_ALGORITHM, + BootstrapAlgorithmIdentities.ECDSA_SHA256, X509AlgorithmIdentifier.absent(OID_ECDSA_SHA256), + X509BindingRule.SignatureEncoding.ECDSA_DER, + X509BindingRule.PublicKeyEncoding.NOT_APPLICABLE), + fixed("zeroecho.signature.ecdsa-sha384", X509AlgorithmRole.SIGNATURE_ALGORITHM, + BootstrapAlgorithmIdentities.ECDSA_SHA384, X509AlgorithmIdentifier.absent(OID_ECDSA_SHA384), + X509BindingRule.SignatureEncoding.ECDSA_DER, + X509BindingRule.PublicKeyEncoding.NOT_APPLICABLE), + fixed("zeroecho.signature.ecdsa-sha512", X509AlgorithmRole.SIGNATURE_ALGORITHM, + BootstrapAlgorithmIdentities.ECDSA_SHA512, X509AlgorithmIdentifier.absent(OID_ECDSA_SHA512), + X509BindingRule.SignatureEncoding.ECDSA_DER, + X509BindingRule.PublicKeyEncoding.NOT_APPLICABLE), + fixed("zeroecho.signature.ed25519", X509AlgorithmRole.SIGNATURE_ALGORITHM, + BootstrapAlgorithmIdentities.ED25519_SIGNATURE, X509AlgorithmIdentifier.absent(OID_ED25519), + X509BindingRule.SignatureEncoding.OPAQUE, X509BindingRule.PublicKeyEncoding.NOT_APPLICABLE), + fixed("zeroecho.signature.ed448", X509AlgorithmRole.SIGNATURE_ALGORITHM, + BootstrapAlgorithmIdentities.ED448_SIGNATURE, X509AlgorithmIdentifier.absent(OID_ED448), + X509BindingRule.SignatureEncoding.OPAQUE, X509BindingRule.PublicKeyEncoding.NOT_APPLICABLE), + fixed("zeroecho.spki.rsa", X509AlgorithmRole.SUBJECT_PUBLIC_KEY_ALGORITHM, + BootstrapAlgorithmIdentities.RSA_PUBLIC_KEY, X509AlgorithmIdentifier.derNull(OID_RSA_PUBLIC_KEY), + X509BindingRule.SignatureEncoding.NOT_APPLICABLE, X509BindingRule.PublicKeyEncoding.RSA_PKCS1_DER), + new EcPublicKeyRule(components), + fixed("zeroecho.spki.ed25519", X509AlgorithmRole.SUBJECT_PUBLIC_KEY_ALGORITHM, + BootstrapAlgorithmIdentities.ED25519_PUBLIC_KEY, X509AlgorithmIdentifier.absent(OID_ED25519), + X509BindingRule.SignatureEncoding.NOT_APPLICABLE, X509BindingRule.PublicKeyEncoding.RAW), + fixed("zeroecho.spki.ed448", X509AlgorithmRole.SUBJECT_PUBLIC_KEY_ALGORITHM, + BootstrapAlgorithmIdentities.ED448_PUBLIC_KEY, X509AlgorithmIdentifier.absent(OID_ED448), + X509BindingRule.SignatureEncoding.NOT_APPLICABLE, X509BindingRule.PublicKeyEncoding.RAW))); + } + + /** + * Returns the immutable built-in standard catalog. + * + * @return authoritative binding snapshot + */ + public static X509BindingCatalog catalog() { + return CATALOG; + } + + /** + * Creates the built-in rules against an immutable component authority. + * + * @param components exact component catalog used by parameterized rules + * @return immutable built-in binding catalog + */ + public static X509BindingCatalog catalog(X509ComponentCatalog components) { + return createCatalog(components); + } + + private static X509BindingRule fixed(String id, X509AlgorithmRole role, AlgorithmIdentity identity, + X509AlgorithmIdentifier identifier, X509BindingRule.SignatureEncoding signatureEncoding, + X509BindingRule.PublicKeyEncoding publicKeyEncoding) { + return new FixedRule(id, role, identity, identifier, signatureEncoding, publicKeyEncoding); + } + + private record FixedRule(String id, X509AlgorithmRole role, AlgorithmIdentity identity, + X509AlgorithmIdentifier identifier, SignatureEncoding signatureEncoding, + PublicKeyEncoding publicKeyEncoding) implements X509BindingRule { + + private FixedRule { + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(role, "role"); + Objects.requireNonNull(identity, "identity"); + Objects.requireNonNull(identifier, "identifier"); + Objects.requireNonNull(signatureEncoding, "signatureEncoding"); + Objects.requireNonNull(publicKeyEncoding, "publicKeyEncoding"); + } + + @Override + public String oid() { + return identifier.oid(); + } + + @Override + public String semanticFingerprint() { + return id + "|" + role + "|" + identity.canonicalForm() + "|" + identifier.canonicalForm() + "|" + + signatureEncoding + "|" + publicKeyEncoding; + } + + @Override + public Optional+ * Validation consumes exactly one object, rejects indefinite and non-minimal + * lengths, and checks canonical primitive forms without materializing aggregate + * content. Aggregate offsets and lengths use {@code long}. The fixed structural + * depth is an X.509 adapter capability, not an aggregate byte or cardinality + * limit. + *
+ * + *+ * Canonical SET ordering uses two monotonic comparison readers per active, + * fixed-bounded structure depth. Each reader moves forward only, so validation + * is {@code O(B)} for encoded size {@code B} with auxiliary heap bounded by the + * fixed depth and comparison-buffer size. + *
+ */ +public final class StreamingDerReader { + + private static final int BUFFER_BYTES = 16 * 1024; + private static final int MAXIMUM_X509_STRUCTURE_DEPTH = 64; + private static final int HIGH_TAG_NUMBER = 0x1f; + private static final int CONTINUATION_BIT = 0x80; + private static final long SHORT_LENGTH_LIMIT = 128L; + + /** + * Validates one complete canonical DER object. + * + * @param content repeatable original content + * @param cancellation cancellation signal + * @return exact encoded length + * @throws IOException if input fails, is malformed, non-canonical, truncated, + * or has trailing data + */ + public long validate(RepeatableContent content, CancellationSignal cancellation) throws IOException { + Objects.requireNonNull(content, "content"); + Objects.requireNonNull(cancellation, "cancellation"); + try (CountedInput input = new CountedInput(content.openStream()); + OrderingContext ordering = new OrderingContext(content)) { + Header root = HeaderReader.read(input); + readValue(input, root, 0, cancellation, ordering); + if (input.read() >= 0) { + throw new IOException("Trailing DER data: code=TRAILING_DER_DATA"); + } + long length = input.position(); + if (content.length().isPresent() && length != content.length().getAsLong()) { + throw new IOException("DER length mismatch: code=NON_CANONICAL_DER"); + } + return length; + } + } + + /** + * Validates and locates the signed portions of one certificate or CRL. + * + * @param content original repeatable DER + * @param kind signed-object grammar + * @param cancellation cancellation signal + * @return immutable long-offset layout + * @throws IOException if structure or canonicality is invalid + */ + public SignedObjectLayout inspectSignedObject(RepeatableContent content, SignedObjectKind kind, + CancellationSignal cancellation) throws IOException { + validate(content, cancellation); + try (CountedInput input = new CountedInput(content.openStream())) { + Header outer = HeaderReader.read(input); + requireSequence(outer); + long outerEnd = Math.addExact(input.position(), outer.length()); + Header tbs = HeaderReader.read(input); + requireSequence(tbs); + long tbsTotalLength = tbs.encodedLength(); + AlgorithmAndKey inner = inspectTbs(content, kind, tbs, cancellation); + skip(input, tbs.length(), cancellation); + Header outerAlgorithm = HeaderReader.read(input); + requireSequence(outerAlgorithm); + skip(input, outerAlgorithm.length(), cancellation); + Header signature = HeaderReader.read(input); + if (!signature.universal() || signature.tagNumber() != 3 || signature.constructed() + || signature.length() < 1L) { + throw new IOException("Malformed signed-object signature BIT STRING"); + } + int unused = input.readRequired(); + if (unused != 0) { + throw new IOException("Signed-object signature has unused bits"); + } + long signatureOffset = input.position(); + skip(input, signature.length() - 1L, cancellation); + if (input.position() != outerEnd || input.read() >= 0) { + throw new IOException("Signed object has trailing data"); + } + return new SignedObjectLayout(tbs.start(), tbsTotalLength, inner.algorithmOffset(), + inner.algorithmLength(), outerAlgorithm.start(), outerAlgorithm.encodedLength(), + inner.spkiOffset(), inner.spkiLength(), signatureOffset, signature.length() - 1L); + } + } + + private static AlgorithmAndKey inspectTbs(RepeatableContent content, SignedObjectKind kind, Header tbs, + CancellationSignal cancellation) throws IOException { + try (CountedInput input = new CountedInput(content.openStream())) { + skip(input, tbs.valueOffset(), cancellation); + Header child = HeaderReader.read(input); + if (kind == SignedObjectKind.CERTIFICATE) { + if (child.contextSpecific(0)) { + skip(input, child.length(), cancellation); + child = HeaderReader.read(input); + } + requireUniversal(child, 2); + skip(input, child.length(), cancellation); + child = HeaderReader.read(input); + } else if (child.universal() && child.tagNumber() == 2) { + skip(input, child.length(), cancellation); + child = HeaderReader.read(input); + } + requireSequence(child); + long algorithmOffset = child.start(); + long algorithmLength = child.encodedLength(); + if (kind == SignedObjectKind.CRL) { + return new AlgorithmAndKey(algorithmOffset, algorithmLength, -1L, 0L); + } + skip(input, child.length(), cancellation); + for (int index = 0; index < 3; index++) { + Header field = HeaderReader.read(input); + skip(input, field.length(), cancellation); + } + Header spki = HeaderReader.read(input); + requireSequence(spki); + return new AlgorithmAndKey(algorithmOffset, algorithmLength, spki.start(), spki.encodedLength()); + } + } + + private static void requireSequence(Header header) throws IOException { + requireUniversal(header, 16); + if (!header.constructed()) { + throw new IOException("Expected constructed DER SEQUENCE"); + } + } + + private static void requireUniversal(Header header, int tagNumber) throws IOException { + if (!header.universal() || header.tagNumber() != tagNumber) { + throw new IOException("Unexpected signed-object DER field"); + } + } + + private static void readValue(CountedInput input, Header header, int depth, CancellationSignal cancellation, + OrderingContext ordering) throws IOException { + if (depth >= MAXIMUM_X509_STRUCTURE_DEPTH) { + throw new IOException("DER nesting exceeds X.509 adapter capability"); + } + long end = Math.addExact(input.position(), header.length()); + if (header.constructed()) { + requireConstructedForm(header); + SetOrdering setOrdering = header.universal() && header.tagNumber() == 17 + ? ordering.begin(depth, header.valueOffset(), cancellation) + : null; + while (input.position() < end) { + cancellation.throwIfCancelled(); + Header child = HeaderReader.read(input); + long childEnd = Math.addExact(input.position(), child.length()); + if (childEnd > end) { + throw new IOException("DER child exceeds parent: code=MALFORMED_SIGNED_OBJECT"); + } + readValue(input, child, depth + 1, cancellation, ordering); + if (setOrdering != null && setOrdering.accept(child.encodedLength(), cancellation) > 0) { + throw new IOException("Non-canonical DER SET ordering: code=NON_CANONICAL_DER"); + } + } + if (setOrdering != null) { + setOrdering.finish(cancellation); + } + } else { + requirePrimitiveForm(header); + PrimitiveReader.read(input, header, cancellation); + } + if (input.position() != end) { + throw new IOException("DER value length mismatch: code=MALFORMED_SIGNED_OBJECT"); + } + } + + private static void requireConstructedForm(Header header) throws IOException { + if (header.universal() && header.tagNumber() != 16 && header.tagNumber() != 17) { + throw new IOException("Constructed primitive is not canonical DER"); + } + } + + private static void requirePrimitiveForm(Header header) throws IOException { + if (header.universal() && (header.tagNumber() == 16 || header.tagNumber() == 17)) { + throw new IOException("Primitive container is not canonical DER"); + } + } + + private static void skip(CountedInput input, long length, CancellationSignal cancellation) throws IOException { + byte[] buffer = new byte[BUFFER_BYTES]; + long remaining = length; + while (remaining != 0L) { + cancellation.throwIfCancelled(); + int read = input.read(buffer, 0, (int) Math.min(buffer.length, remaining)); + if (read < 0) { + throw new IOException("Truncated DER value"); + } + if (read == 0) { + input.readRequired(); + remaining--; + } else { + remaining -= read; + } + } + } + + /** + * Supported signed-object grammar. + */ + public enum SignedObjectKind { + /** X.509 Certificate. */ + CERTIFICATE, + /** X.509 CertificateList (CRL). */ + CRL + } + + /** + * Exact offsets into the validated original DER. + * + * @param tbsOffset complete TBS TLV offset + * @param tbsLength complete TBS TLV length + * @param tbsAlgorithmOffset TBS AlgorithmIdentifier TLV offset + * @param tbsAlgorithmLength TBS AlgorithmIdentifier TLV length + * @param outerAlgorithmOffset outer AlgorithmIdentifier TLV offset + * @param outerAlgorithmLength outer AlgorithmIdentifier TLV length + * @param subjectPublicKeyInfoOffset SPKI TLV offset, or {@code -1} for CRLs + * @param subjectPublicKeyInfoLength SPKI TLV length, or zero for CRLs + * @param signatureOffset signature octets offset + * @param signatureLength signature octets length + */ + public record SignedObjectLayout(long tbsOffset, long tbsLength, long tbsAlgorithmOffset, + long tbsAlgorithmLength, long outerAlgorithmOffset, long outerAlgorithmLength, + long subjectPublicKeyInfoOffset, long subjectPublicKeyInfoLength, long signatureOffset, + long signatureLength) { + } + + private record AlgorithmAndKey(long algorithmOffset, long algorithmLength, long spkiOffset, long spkiLength) { + } + + private record Header(int firstTag, int tagNumber, long length, long start, long valueOffset) { + private boolean constructed() { + return (firstTag & 0x20) != 0; + } + + private boolean universal() { + return (firstTag & 0xc0) == 0; + } + + private boolean contextSpecific(int expectedTag) { + return (firstTag & 0xc0) == 0x80 && tagNumber == expectedTag; + } + + private long encodedLength() { + return Math.addExact(valueOffset - start, length); + } + } + + /** Canonical DER tag and length decoder. */ + private static final class HeaderReader { + private static Header read(CountedInput input) throws IOException { + long start = input.position(); + int firstTag = input.readRequired(); + if (firstTag == 0) { + throw new IOException("DER end-of-contents is forbidden"); + } + int tagNumber = readTagNumber(input, firstTag); + long length = readLength(input); + return new Header(firstTag, tagNumber, length, start, input.position()); + } + + private static int readTagNumber(CountedInput input, int firstTag) throws IOException { + int tagNumber = firstTag & HIGH_TAG_NUMBER; + if (tagNumber == HIGH_TAG_NUMBER) { + int octet = input.readRequired(); + if ((octet & 0x7f) == 0) { + throw new IOException("Non-minimal DER high tag"); + } + while ((octet & CONTINUATION_BIT) != 0) { + octet = input.readRequired(); + } + tagNumber = -1; + } + return tagNumber; + } + + private static long readLength(CountedInput input) throws IOException { + int firstLength = input.readRequired(); + if (firstLength < CONTINUATION_BIT) { + return firstLength; + } + int octets = firstLength & 0x7f; + if (octets == 0) { + throw new IOException("Indefinite DER length: code=NON_CANONICAL_DER"); + } + if (octets > Long.BYTES) { + throw new IOException("DER length is not representable"); + } + int first = input.readRequired(); + if (first == 0) { + throw new IOException("Non-minimal DER length: code=NON_CANONICAL_DER"); + } + long length = first; + for (int index = 1; index < octets; index++) { + if (length > (Long.MAX_VALUE >>> Byte.SIZE)) { + throw new IOException("DER length overflow: code=CONTENT_LENGTH_OVERFLOW"); + } + length = (length << Byte.SIZE) | input.readRequired(); + } + if (length < SHORT_LENGTH_LIMIT) { + throw new IOException("Non-minimal DER length: code=NON_CANONICAL_DER"); + } + return length; + } + } + + /** Fixed-depth owner of monotonic comparison streams for DER SET values. */ + private static final class OrderingContext implements AutoCloseable { + private final RepeatableContent content; + private final SetOrdering[] levels = new SetOrdering[MAXIMUM_X509_STRUCTURE_DEPTH]; + + private OrderingContext(RepeatableContent content) { + this.content = content; + } + + private SetOrdering begin(int depth, long valueOffset, CancellationSignal cancellation) throws IOException { + SetOrdering ordering = levels[depth]; + if (ordering == null) { + ordering = new SetOrdering(content); + levels[depth] = ordering; + } + ordering.begin(valueOffset, cancellation); + return ordering; + } + + @Override + public void close() throws IOException { + IOException failure = null; + for (SetOrdering ordering : levels) { + if (ordering == null) { + continue; + } + try { + ordering.closeStreams(); + } catch (IOException exception) { + if (failure == null) { + failure = exception; + } else { + failure.addSuppressed(exception); + } + } + } + if (failure != null) { + throw failure; + } + } + } + + /** Adjacent-child comparator whose two source passes only move forward. */ + private static final class SetOrdering { + private static final long NO_PREVIOUS_VALUE = -1L; + + private final CountedInput left; + private final CountedInput right; + private final byte[] leftBuffer = new byte[BUFFER_BYTES]; + private final byte[] rightBuffer = new byte[BUFFER_BYTES]; + private long previousLength = NO_PREVIOUS_VALUE; + + private SetOrdering(RepeatableContent content) throws IOException { + left = new CountedInput(content.openStream()); + try { + right = new CountedInput(content.openStream()); + } catch (IOException exception) { + left.close(); + throw exception; + } + } + + private void begin(long valueOffset, CancellationSignal cancellation) throws IOException { + advanceTo(left, valueOffset, leftBuffer, cancellation); + advanceTo(right, valueOffset, rightBuffer, cancellation); + previousLength = NO_PREVIOUS_VALUE; + } + + private int accept(long currentLength, CancellationSignal cancellation) throws IOException { + if (previousLength == NO_PREVIOUS_VALUE) { + skipWithBuffer(right, currentLength, rightBuffer, cancellation); + previousLength = currentLength; + return 0; + } + int comparison = compare(previousLength, currentLength, cancellation); + previousLength = currentLength; + return comparison; + } + + private void finish(CancellationSignal cancellation) throws IOException { + if (previousLength != NO_PREVIOUS_VALUE) { + skipWithBuffer(left, previousLength, leftBuffer, cancellation); + } + } + + private int compare(long leftLength, long rightLength, CancellationSignal cancellation) throws IOException { + long common = Math.min(leftLength, rightLength); + int comparison = 0; + long remaining = common; + while (remaining != 0L) { + cancellation.throwIfCancelled(); + int count = (int) Math.min(BUFFER_BYTES, remaining); + readExactly(left, leftBuffer, count); + readExactly(right, rightBuffer, count); + if (comparison == 0) { + comparison = compareBuffers(leftBuffer, rightBuffer, count); + } + remaining -= count; + } + skipWithBuffer(left, leftLength - common, leftBuffer, cancellation); + skipWithBuffer(right, rightLength - common, rightBuffer, cancellation); + return comparison == 0 ? Long.compare(leftLength, rightLength) : comparison; + } + + private void closeStreams() throws IOException { + IOException failure = null; + try { + left.close(); + } catch (IOException exception) { + failure = exception; + } + try { + right.close(); + } catch (IOException exception) { + if (failure == null) { + failure = exception; + } else { + failure.addSuppressed(exception); + } + } + if (failure != null) { + throw failure; + } + } + + private static void advanceTo(CountedInput input, long position, byte[] buffer, + CancellationSignal cancellation) throws IOException { + if (input.position() > position) { + throw new IOException("DER SET comparison stream moved backward"); + } + skipWithBuffer(input, position - input.position(), buffer, cancellation); + } + + private static void skipWithBuffer(InputStream input, long length, byte[] buffer, + CancellationSignal cancellation) throws IOException { + long remaining = length; + while (remaining != 0L) { + cancellation.throwIfCancelled(); + int count = (int) Math.min(buffer.length, remaining); + readExactly(input, buffer, count); + remaining -= count; + } + } + + private static void readExactly(InputStream input, byte[] buffer, int length) throws IOException { + int offset = 0; + while (offset != length) { + int count = input.read(buffer, offset, length - offset); + if (count < 0) { + throw new IOException("Truncated DER value"); + } + if (count == 0) { + int value = input.read(); + if (value < 0) { + throw new IOException("Truncated DER value"); + } + buffer[offset] = (byte) value; + offset++; + } else { + offset += count; + } + } + } + + private static int compareBuffers(byte[] left, byte[] right, int length) { + for (int index = 0; index < length; index++) { + int comparison = Integer.compare(Byte.toUnsignedInt(left[index]), Byte.toUnsignedInt(right[index])); + if (comparison != 0) { + return comparison; + } + } + return 0; + } + } + + /** + * Input wrapper retaining an overflow-checked long byte position. + */ + private static final class CountedInput extends InputStream { + private final InputStream delegate; + private long position; + + private CountedInput(InputStream delegate) { + super(); + this.delegate = delegate; + } + + @Override + public int read() throws IOException { + int value = delegate.read(); + if (value >= 0) { + position = Math.addExact(position, 1L); + } + return value; + } + + @Override + public int read(byte[] bytes, int offset, int length) throws IOException { + int count = delegate.read(bytes, offset, length); + if (count > 0) { + position = Math.addExact(position, count); + } + return count; + } + + @Override + public void close() throws IOException { + delegate.close(); + } + + private int readRequired() throws IOException { + int value = read(); + if (value < 0) { + throw new IOException("Truncated DER object"); + } + return value; + } + + private long position() { + return position; + } + } + + /** + * Canonical validation for primitive DER values. + */ + private static final class PrimitiveReader { + private static final long EMPTY_LENGTH = 0L; + private static final long SINGLE_OCTET_LENGTH = 1L; + + private static void read(CountedInput input, Header header, CancellationSignal cancellation) + throws IOException { + if (!header.universal()) { + skip(input, header.length(), cancellation); + return; + } + switch (header.tagNumber()) { + case 1 -> readBoolean(input, header.length()); + case 2 -> readInteger(input, header.length(), cancellation); + case 3 -> readBitString(input, header.length(), cancellation); + case 5 -> readNull(header.length()); + case 6 -> readOid(input, header.length(), cancellation); + default -> skip(input, header.length(), cancellation); + } + } + + private static void readBoolean(CountedInput input, long length) throws IOException { + if (length != SINGLE_OCTET_LENGTH) { + throw new IOException("Malformed DER BOOLEAN"); + } + int value = input.readRequired(); + if (value != 0 && value != 0xff) { + throw new IOException("Non-canonical DER BOOLEAN"); + } + } + + private static void readInteger(CountedInput input, long length, CancellationSignal cancellation) + throws IOException { + if (length == EMPTY_LENGTH) { + throw new IOException("Malformed DER INTEGER"); + } + int first = input.readRequired(); + if (length > SINGLE_OCTET_LENGTH) { + int second = input.readRequired(); + if ((first == 0 && (second & CONTINUATION_BIT) == 0) + || (first == 0xff && (second & CONTINUATION_BIT) != 0)) { + throw new IOException("Non-canonical DER INTEGER"); + } + skip(input, length - 2L, cancellation); + } + } + + private static void readBitString(CountedInput input, long length, CancellationSignal cancellation) + throws IOException { + if (length == EMPTY_LENGTH) { + throw new IOException("Malformed DER BIT STRING"); + } + int unused = input.readRequired(); + if (unused > 7 || (length == SINGLE_OCTET_LENGTH && unused != 0)) { + throw new IOException("Malformed DER BIT STRING"); + } + long octets = length - SINGLE_OCTET_LENGTH; + if (octets == EMPTY_LENGTH) { + return; + } + int last = 0; + for (long index = 0L; index < octets; index++) { + cancellation.throwIfCancelled(); + last = input.readRequired(); + } + if (unused != 0 && (last & ((1 << unused) - 1)) != 0) { + throw new IOException("Non-canonical DER BIT STRING"); + } + } + + private static void readNull(long length) throws IOException { + if (length != EMPTY_LENGTH) { + throw new IOException("Malformed DER NULL"); + } + } + + private static void readOid(CountedInput input, long length, CancellationSignal cancellation) + throws IOException { + if (length == EMPTY_LENGTH) { + throw new IOException("Malformed DER OID"); + } + boolean atComponentStart = true; + for (long index = 0L; index < length; index++) { + cancellation.throwIfCancelled(); + int octet = input.readRequired(); + if (atComponentStart && octet == CONTINUATION_BIT) { + throw new IOException("Non-canonical DER OID"); + } + atComponentStart = (octet & CONTINUATION_BIT) == 0; + } + if (!atComponentStart) { + throw new IOException("Truncated DER OID"); + } + } + } +} diff --git a/pki/src/main/java/zeroecho/pki/impl/framework/x509/StreamingDerWriter.java b/pki/src/main/java/zeroecho/pki/impl/framework/x509/StreamingDerWriter.java new file mode 100644 index 0000000..692ed05 --- /dev/null +++ b/pki/src/main/java/zeroecho/pki/impl/framework/x509/StreamingDerWriter.java @@ -0,0 +1,155 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.pki.impl.framework.x509; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import zeroecho.core.io.CancellationSignal; + +/** + * Focused canonical DER streaming primitives used by signed-object adapters. + * + *+ * This is deliberately not a general ASN.1 framework. It writes already + * validated current X.509 child encodings into definite-length containers while + * accounting in {@code long}. ZeroEcho core imposes no arbitrary product-wide + * aggregate CRL-size or revocation-entry limit; completion remains subject to + * storage, I/O, technical representability, cancellation, and deployment + * policy. + *
+ */ +public final class StreamingDerWriter { + + /** DER universal SEQUENCE tag. */ + public static final int SEQUENCE_TAG = 0x30; + /** DER universal BIT STRING tag. */ + public static final int BIT_STRING_TAG = 0x03; + + private static final int BUFFER_SIZE = 16 * 1024; + private static final long EMPTY_LENGTH = 0L; + private static final long SHORT_FORM_LIMIT = 128L; + + private StreamingDerWriter() { + } + + /** + * Returns the encoded size of one tag-length-value object. + * + * @param valueLength non-negative value length + * @return complete encoded length + * @throws IllegalArgumentException if the length is negative + * @throws ArithmeticException if the result overflows {@code long} + */ + public static long encodedLength(long valueLength) { + if (valueLength < EMPTY_LENGTH) { + throw new IllegalArgumentException("DER value length must not be negative"); + } + return Math.addExact(Math.addExact(1L, lengthOctets(valueLength)), valueLength); + } + + /** + * Writes one canonical DER tag and definite length. + * + * @param output target stream + * @param tag one-octet tag + * @param valueLength non-negative value length + * @throws IOException if writing fails + * @throws IllegalArgumentException if the tag or length is invalid + */ + public static void writeTagAndLength(OutputStream output, int tag, long valueLength) throws IOException { + if (output == null) { + throw new IllegalArgumentException("output must not be null"); + } + if (tag < 0 || tag > 0xff) { + throw new IllegalArgumentException("DER tag must fit one octet"); + } + if (valueLength < EMPTY_LENGTH) { + throw new IllegalArgumentException("DER value length must not be negative"); + } + output.write(tag); + if (valueLength < SHORT_FORM_LIMIT) { + output.write((int) valueLength); + return; + } + int octets = significantOctets(valueLength); + output.write(0x80 | octets); + for (int shift = (octets - 1) * Byte.SIZE; shift >= 0; shift -= Byte.SIZE) { + output.write((int) (valueLength >>> shift) & 0xff); + } + } + + /** + * Copies content incrementally and returns the exact byte count. + * + * @param input source + * @param output destination + * @param cancellation cancellation signal + * @return copied byte count + * @throws IOException if reading or writing fails + * @throws ArithmeticException if the count overflows + */ + public static long copy(InputStream input, OutputStream output, CancellationSignal cancellation) + throws IOException { + if (input == null || output == null || cancellation == null) { + throw new IllegalArgumentException("DER copy arguments must not be null"); + } + byte[] buffer = new byte[BUFFER_SIZE]; + long count = 0L; + int read; + while ((read = input.read(buffer)) >= 0) { + cancellation.throwIfCancelled(); + if (read != 0) { + output.write(buffer, 0, read); + count = Math.addExact(count, read); + } + } + return count; + } + + private static long lengthOctets(long valueLength) { + return valueLength < 128L ? 1L : Math.addExact(1L, significantOctets(valueLength)); + } + + private static int significantOctets(long valueLength) { + int octets = 0; + long remaining = valueLength; + while (remaining != 0L) { + octets++; + remaining >>>= Byte.SIZE; + } + return octets; + } +} diff --git a/pki/src/main/java/zeroecho/pki/impl/framework/x509/X509AlgorithmIdentifier.java b/pki/src/main/java/zeroecho/pki/impl/framework/x509/X509AlgorithmIdentifier.java new file mode 100644 index 0000000..70268a5 --- /dev/null +++ b/pki/src/main/java/zeroecho/pki/impl/framework/x509/X509AlgorithmIdentifier.java @@ -0,0 +1,167 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.pki.impl.framework.x509; + +import java.util.Arrays; +import java.util.HexFormat; +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * Provider-neutral canonical X.509 {@code AlgorithmIdentifier} representation. + * + *+ * Parameters distinguish absent, DER NULL, and exact canonical structured DER. + * Parameter bytes are defensively copied. This value contains no Bouncy Castle + * object and is immutable. + *
+ */ +public final class X509AlgorithmIdentifier { + + private static final Pattern OID = Pattern.compile("[0-2](?:\\.[0-9]+)+"); + private static final byte[] DER_NULL = { 0x05, 0x00 }; + + /** + * Exact parameter representation. + */ + public enum ParameterForm { + /** Parameters are omitted. */ + ABSENT, + /** Parameters are canonical DER NULL. */ + DER_NULL, + /** Parameters are exact canonical structured DER. */ + EXACT_DER + } + + private final String oid; + private final ParameterForm parameterForm; + private final byte[] parameters; + + private X509AlgorithmIdentifier(String oid, ParameterForm parameterForm, byte[] parameters) { + Objects.requireNonNull(oid, "oid"); + if (!OID.matcher(oid).matches()) { + throw new IllegalArgumentException("Invalid dotted-decimal OID"); + } + this.oid = oid; + this.parameterForm = Objects.requireNonNull(parameterForm, "parameterForm"); + this.parameters = Objects.requireNonNull(parameters, "parameters").clone(); + if (parameterForm == ParameterForm.ABSENT && parameters.length != 0) { + throw new IllegalArgumentException("Absent parameters must have no DER"); + } + if (parameterForm == ParameterForm.DER_NULL && !Arrays.equals(DER_NULL, parameters)) { + throw new IllegalArgumentException("DER NULL parameters must be canonical"); + } + if (parameterForm == ParameterForm.EXACT_DER && parameters.length == 0) { + throw new IllegalArgumentException("Exact parameters must not be empty"); + } + } + + /** + * Creates an identifier with absent parameters. + * + * @param oid dotted-decimal OID + * @return immutable identifier + */ + public static X509AlgorithmIdentifier absent(String oid) { + return new X509AlgorithmIdentifier(oid, ParameterForm.ABSENT, new byte[0]); + } + + /** + * Creates an identifier with canonical DER NULL parameters. + * + * @param oid dotted-decimal OID + * @return immutable identifier + */ + public static X509AlgorithmIdentifier derNull(String oid) { + return new X509AlgorithmIdentifier(oid, ParameterForm.DER_NULL, DER_NULL); + } + + /** + * Creates an identifier with exact canonical structured parameters. + * + * @param oid dotted-decimal OID + * @param parameters complete DER parameter value + * @return immutable identifier + */ + public static X509AlgorithmIdentifier exact(String oid, byte[] parameters) { + return new X509AlgorithmIdentifier(oid, ParameterForm.EXACT_DER, parameters); + } + + /** + * Returns the OID. + * + * @return dotted-decimal OID + */ + public String oid() { + return oid; + } + + /** + * Returns the exact parameter form. + * + * @return parameter form + */ + public ParameterForm parameterForm() { + return parameterForm; + } + + /** + * Returns independently owned parameter DER. + * + * @return defensive copy, empty for absent parameters + */ + public byte[] parameters() { + return parameters.clone(); + } + + /** + * Returns the deterministic reverse-lookup representation. + * + * @return OID, form, and exact DER + */ + public String canonicalForm() { + return oid + "|" + parameterForm + "|" + HexFormat.of().formatHex(parameters); + } + + @Override + public boolean equals(Object other) { + return other instanceof X509AlgorithmIdentifier identifier && oid.equals(identifier.oid) + && parameterForm == identifier.parameterForm && Arrays.equals(parameters, identifier.parameters); + } + + @Override + public int hashCode() { + return 31 * (31 * oid.hashCode() + parameterForm.hashCode()) + Arrays.hashCode(parameters); + } +} diff --git a/pki/src/main/java/zeroecho/pki/impl/framework/x509/X509AlgorithmResolver.java b/pki/src/main/java/zeroecho/pki/impl/framework/x509/X509AlgorithmResolver.java new file mode 100644 index 0000000..12d87f1 --- /dev/null +++ b/pki/src/main/java/zeroecho/pki/impl/framework/x509/X509AlgorithmResolver.java @@ -0,0 +1,243 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.pki.impl.framework.x509; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import zeroecho.core.spec.AlgorithmIdentity; +import zeroecho.core.spec.AlgorithmSuite; +import zeroecho.core.spi.AlgorithmExecutionCapabilities; +import zeroecho.core.spi.AlgorithmExecutionCapability; + +/** + * Provider-independent intersection of exact identity, binding, installed + * execution capability, immutable security floor, configured policy, and key + * compatibility. + * + *+ * The resolver returns an immutable effective selection and never falls back to + * a default or provider alias. It contains no key material and performs no + * cryptographic execution. + *
+ */ +public final class X509AlgorithmResolver { + + private static final int UNIQUE_MATCH_COUNT = 1; + + private final X509BindingCatalog bindings; + private final AlgorithmExecutionCapabilities capabilities; + private final Policy policy; + + /** + * Stable resolution failure categories. + */ + public enum Failure { + /** No authoritative X.509 binding exists. */ + NO_BINDING, + /** No installed implementation supports the tuple. */ + NO_CAPABILITY, + /** More than one implementation remains without explicit selection. */ + AMBIGUOUS_IMPLEMENTATION, + /** Explicit implementation is unavailable for the tuple. */ + UNKNOWN_IMPLEMENTATION, + /** Semantic capability has no process-local executor binding. */ + NO_EXECUTOR, + /** Non-overridable security floor rejected the identity. */ + SECURITY_FLOOR, + /** Configured policy rejected the suite. */ + POLICY, + /** Signature and key identities are incompatible. */ + INCOMPATIBLE_KEY + } + + /** + * Policy decision over exact identity data. + */ + @FunctionalInterface + public interface Policy { + + /** + * Tests whether configured policy permits the exact operation. + * + * @param suite complete suite + * @param direction execution direction + * @return {@code true} when permitted + */ + boolean permits(AlgorithmSuite suite, AlgorithmExecutionCapability.Direction direction); + + /** + * Returns stable non-secret policy semantics for snapshot provenance. + * + * @return deterministic policy fingerprint + */ + default String semanticFingerprint() { + return getClass().getName(); + } + } + + /** + * Immutable effective selection. + * + * @param requested exact requested identity + * @param suite compatible exact suite + * @param binding exact X.509 representation + * @param implementation selected execution metadata + * @param direction authorized execution direction + * @param provenance stable default identifier or {@code explicit} + * @param authorityFingerprint authority snapshot fingerprint + */ + public record Selection(AlgorithmIdentity requested, AlgorithmSuite suite, X509AlgorithmIdentifier binding, + AlgorithmExecutionCapability implementation, AlgorithmExecutionCapability.Direction direction, + String provenance, String authorityFingerprint) { + + /** + * Creates an immutable selection. + * + * @throws NullPointerException if an argument is {@code null} + */ + public Selection { + Objects.requireNonNull(requested, "requested"); + Objects.requireNonNull(suite, "suite"); + Objects.requireNonNull(binding, "binding"); + Objects.requireNonNull(implementation, "implementation"); + Objects.requireNonNull(provenance, "provenance"); + Objects.requireNonNull(authorityFingerprint, "authorityFingerprint"); + } + } + + /** + * Resolution exception with a stable non-sensitive category. + */ + public static final class ResolutionException extends IllegalArgumentException { + + private static final long serialVersionUID = 1L; + private final Failure failure; + + /* default */ ResolutionException(Failure failure) { + super("X.509 algorithm resolution failed: " + failure); + this.failure = failure; + } + + private ResolutionException(Failure failure, IllegalArgumentException cause) { + super("X.509 algorithm resolution failed: " + failure, cause); + this.failure = failure; + } + + /** + * Returns the stable failure category. + * + * @return resolution failure + */ + public Failure failure() { + return failure; + } + } + + /** + * Creates an immutable resolver snapshot. + * + * @param bindings authoritative binding snapshot + * @param capabilities installed execution snapshot + * @param policy configured restrictive policy + */ + public X509AlgorithmResolver(X509BindingCatalog bindings, AlgorithmExecutionCapabilities capabilities, + Policy policy) { + this.bindings = Objects.requireNonNull(bindings, "bindings"); + this.capabilities = Objects.requireNonNull(capabilities, "capabilities"); + this.policy = Objects.requireNonNull(policy, "policy"); + } + + /** + * Resolves an exact explicit selection. + * + * @param requested exact signature identity + * @param key exact public-key identity + * @param direction execution direction + * @param implementation optional explicit implementation identifier + * @param provenance stable default identifier or {@code explicit} + * @return immutable effective selection + * @throws ResolutionException for a precise fail-closed category + */ + public Selection resolve(AlgorithmIdentity requested, AlgorithmIdentity key, + AlgorithmExecutionCapability.Direction direction, Optional+ * The snapshot composes identities, X.509 components, binding rules, installed + * execution capabilities, aliases, immutable defaults, security floor, and + * configured policy once. It owns the effective resolver and a stable semantic + * fingerprint. It contains no key material and is not persisted in Phase A. + *
+ */ +public final class X509AuthoritySnapshot { + + private final AlgorithmIdentityCatalog identities; + private final X509ComponentCatalog components; + private final X509BindingCatalog bindings; + private final AlgorithmExecutionCapabilities capabilities; + private final List