diff --git a/lib/src/main/java/zeroecho/core/tag/TagEngineBuilder.java b/lib/src/main/java/zeroecho/core/tag/TagEngineBuilder.java
index 57aa534..abd2dcf 100644
--- a/lib/src/main/java/zeroecho/core/tag/TagEngineBuilder.java
+++ b/lib/src/main/java/zeroecho/core/tag/TagEngineBuilder.java
@@ -89,6 +89,14 @@ import zeroecho.core.spec.VoidSpec;
* @since 1.0
*/
public final class TagEngineBuilder implements Supplier> {
+ /**
+ *
+ */
+ private static final String PUBLIC_KEY = "publicKey";
+ /**
+ *
+ */
+ private static final String PRIVATE_KEY = "privateKey";
private final Supplier> factory;
private TagEngineBuilder(Supplier> factory) {
@@ -205,7 +213,7 @@ public final class TagEngineBuilder implements Supplier> {
* @throws NullPointerException if {@code privateKey} is {@code null}
*/
public static TagEngineBuilder ed25519Sign(final PrivateKey privateKey) {
- Objects.requireNonNull(privateKey, "privateKey");
+ Objects.requireNonNull(privateKey, PRIVATE_KEY);
return signature("Ed25519", privateKey, VoidSpec.INSTANCE);
}
@@ -217,7 +225,7 @@ public final class TagEngineBuilder implements Supplier> {
* @throws NullPointerException if {@code publicKey} is {@code null}
*/
public static TagEngineBuilder ed25519Verify(final PublicKey publicKey) {
- Objects.requireNonNull(publicKey, "publicKey");
+ Objects.requireNonNull(publicKey, PUBLIC_KEY);
return signature("Ed25519", publicKey, VoidSpec.INSTANCE);
}
@@ -236,7 +244,7 @@ public final class TagEngineBuilder implements Supplier> {
* @throws NullPointerException if {@code privateKey} is {@code null}
*/
public static TagEngineBuilder rsaSign(final PrivateKey privateKey, final RsaSigSpec spec) {
- Objects.requireNonNull(privateKey, "privateKey");
+ Objects.requireNonNull(privateKey, PRIVATE_KEY);
return signature("RSA", privateKey, spec == null ? RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32) : spec);
}
@@ -255,7 +263,7 @@ public final class TagEngineBuilder implements Supplier> {
* @throws NullPointerException if {@code publicKey} is {@code null}
*/
public static TagEngineBuilder rsaVerify(final PublicKey publicKey, final RsaSigSpec spec) {
- Objects.requireNonNull(publicKey, "publicKey");
+ Objects.requireNonNull(publicKey, PUBLIC_KEY);
return signature("RSA", publicKey, spec == null ? RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32) : spec);
}
@@ -273,7 +281,7 @@ public final class TagEngineBuilder implements Supplier> {
* @throws NullPointerException if {@code privateKey} is {@code null}
*/
public static TagEngineBuilder ecdsaSign(final PrivateKey privateKey, final EcdsaCurveSpec spec) {
- Objects.requireNonNull(privateKey, "privateKey");
+ Objects.requireNonNull(privateKey, PRIVATE_KEY);
final EcdsaCurveSpec s = spec == null ? EcdsaCurveSpec.P256 : spec;
return signature("ECDSA", privateKey, s);
}
@@ -292,7 +300,7 @@ public final class TagEngineBuilder implements Supplier> {
* @throws NullPointerException if {@code publicKey} is {@code null}
*/
public static TagEngineBuilder ecdsaVerify(final PublicKey publicKey, final EcdsaCurveSpec spec) {
- Objects.requireNonNull(publicKey, "publicKey");
+ Objects.requireNonNull(publicKey, PUBLIC_KEY);
final EcdsaCurveSpec s = spec == null ? EcdsaCurveSpec.P256 : spec;
return signature("ECDSA", publicKey, s);
}
@@ -305,7 +313,7 @@ public final class TagEngineBuilder implements Supplier> {
* @throws NullPointerException if {@code privateKey} is {@code null}
*/
public static TagEngineBuilder ecdsaP256Sign(final PrivateKey privateKey) {
- Objects.requireNonNull(privateKey, "privateKey");
+ Objects.requireNonNull(privateKey, PRIVATE_KEY);
return signature("ECDSA", privateKey, EcdsaCurveSpec.P256);
}
@@ -317,7 +325,7 @@ public final class TagEngineBuilder implements Supplier> {
* @throws NullPointerException if {@code publicKey} is {@code null}
*/
public static TagEngineBuilder ecdsaP256Verify(final PublicKey publicKey) {
- Objects.requireNonNull(publicKey, "publicKey");
+ Objects.requireNonNull(publicKey, PUBLIC_KEY);
return signature("ECDSA", publicKey, EcdsaCurveSpec.P256);
}
@@ -334,7 +342,7 @@ public final class TagEngineBuilder implements Supplier> {
* @throws NullPointerException if {@code privateKey} is {@code null}
*/
public static TagEngineBuilder sphincsPlusSign(final PrivateKey privateKey) {
- Objects.requireNonNull(privateKey, "privateKey");
+ Objects.requireNonNull(privateKey, PRIVATE_KEY);
return signature("SPHINCS+", privateKey, VoidSpec.INSTANCE);
}
@@ -351,7 +359,81 @@ public final class TagEngineBuilder implements Supplier> {
* @throws NullPointerException if {@code publicKey} is {@code null}
*/
public static TagEngineBuilder sphincsPlusVerify(final PublicKey publicKey) {
- Objects.requireNonNull(publicKey, "publicKey");
+ Objects.requireNonNull(publicKey, PUBLIC_KEY);
return signature("SPHINCS+", publicKey, VoidSpec.INSTANCE);
}
+
+ /**
+ * Creates a builder for an SLH-DSA signing engine.
+ *
+ *
+ * SLH-DSA is the NIST-standardized hash-based signature scheme (FIPS 205). The
+ * concrete parameter set is encoded in the key material and interpreted by the
+ * underlying {@link CryptoAlgorithms} implementation.
+ *
+ *
+ * @param privateKey private signing key; must not be {@code null}
+ * @return a builder that produces SLH-DSA signature engines in SIGN mode
+ * @throws NullPointerException if {@code privateKey} is {@code null}
+ */
+ public static TagEngineBuilder slhDsaSign(final PrivateKey privateKey) {
+ Objects.requireNonNull(privateKey, PRIVATE_KEY);
+ return signature("SLH-DSA", privateKey, VoidSpec.INSTANCE);
+ }
+
+ /**
+ * Creates a builder for an SLH-DSA verification engine.
+ *
+ *
+ * SLH-DSA is the NIST-standardized hash-based signature scheme (FIPS 205). The
+ * concrete parameter set is encoded in the key material and interpreted by the
+ * underlying {@link CryptoAlgorithms} implementation.
+ *
+ *
+ * @param publicKey public verification key; must not be {@code null}
+ * @return a builder that produces SLH-DSA signature engines in VERIFY mode
+ * @throws NullPointerException if {@code publicKey} is {@code null}
+ */
+ public static TagEngineBuilder slhDsaVerify(final PublicKey publicKey) {
+ Objects.requireNonNull(publicKey, PUBLIC_KEY);
+ return signature("SLH-DSA", publicKey, VoidSpec.INSTANCE);
+ }
+
+ /**
+ * Creates a builder for an ML-DSA signing engine.
+ *
+ *
+ * ML-DSA is the NIST-standardized module-lattice signature scheme (FIPS 204).
+ * The concrete parameter set and any pre-hash variant is encoded in the key
+ * material and interpreted by the underlying {@link CryptoAlgorithms}
+ * implementation.
+ *
+ *
+ * @param privateKey private signing key; must not be {@code null}
+ * @return a builder that produces ML-DSA signature engines in SIGN mode
+ * @throws NullPointerException if {@code privateKey} is {@code null}
+ */
+ public static TagEngineBuilder mldsaSign(final PrivateKey privateKey) {
+ Objects.requireNonNull(privateKey, PRIVATE_KEY);
+ return signature("ML-DSA", privateKey, VoidSpec.INSTANCE);
+ }
+
+ /**
+ * Creates a builder for an ML-DSA verification engine.
+ *
+ *
+ * ML-DSA is the NIST-standardized module-lattice signature scheme (FIPS 204).
+ * The concrete parameter set and any pre-hash variant is encoded in the key
+ * material and interpreted by the underlying {@link CryptoAlgorithms}
+ * implementation.
+ *
+ *
+ * @param publicKey public verification key; must not be {@code null}
+ * @return a builder that produces ML-DSA signature engines in VERIFY mode
+ * @throws NullPointerException if {@code publicKey} is {@code null}
+ */
+ public static TagEngineBuilder mldsaVerify(final PublicKey publicKey) {
+ Objects.requireNonNull(publicKey, PUBLIC_KEY);
+ return signature("ML-DSA", publicKey, VoidSpec.INSTANCE);
+ }
}
diff --git a/lib/src/main/java/zeroecho/sdk/builders/SignatureTrailerDataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/SignatureTrailerDataContentBuilder.java
new file mode 100644
index 0000000..4e46265
--- /dev/null
+++ b/lib/src/main/java/zeroecho/sdk/builders/SignatureTrailerDataContentBuilder.java
@@ -0,0 +1,464 @@
+/*******************************************************************************
+ * Copyright (C) 2025, 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.sdk.builders;
+
+import java.io.IOException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+import conflux.CtxInterface;
+import conflux.Key;
+import zeroecho.core.CryptoAlgorithms;
+import zeroecho.core.KeyUsage;
+import zeroecho.core.spec.ContextSpec;
+import zeroecho.core.tag.TagEngine;
+import zeroecho.sdk.builders.core.DataContentBuilder;
+import zeroecho.sdk.content.api.DataContent;
+import zeroecho.sdk.hybrid.signature.HybridSignatureContexts;
+import zeroecho.sdk.hybrid.signature.HybridSignatureProfile;
+
+/**
+ * Signature-specific trailer builder for {@link DataContent} pipelines.
+ *
+ *
+ * This class is intended as a convenient, signature-specialized replacement
+ * for:
+ *
+ *
+ *
+ * new TagTrailerDataContentBuilder<Signature>(engine).bufferSize(8192)
+ * new TagTrailerDataContentBuilder<Signature>(engine).bufferSize(8192).throwOnMismatch()
+ *
+ *
+ *
+ * It keeps {@link TagTrailerDataContentBuilder} as the generic implementation
+ * while providing a compact API for {@code Signature} usage, including
+ * construction of {@code SignatureContext} for both single-algorithm and hybrid
+ * signatures.
+ *
+ *
+ * Mode selection
+ *
+ * - {@link #core(TagEngine)} / {@link #core(Supplier)}: wraps a ready engine
+ * (same parameters as {@link TagTrailerDataContentBuilder}).
+ * - {@link #single()}: constructs a non-hybrid {@code SignatureContext} via
+ * {@link CryptoAlgorithms}.
+ * - {@link #hybrid()}: constructs a hybrid {@code SignatureContext} via
+ * {@link HybridSignatureContexts}.
+ *
+ *
+ * Checked exceptions
+ *
+ * Context construction may involve I/O (e.g., catalog/provider loading) and
+ * therefore throw {@link IOException}. This builder converts such failures to
+ * {@link IllegalStateException} because fluent builder APIs are expected to be
+ * used in configuration code without mandatory checked-exception plumbing.
+ *
+ *
+ * @since 1.0
+ */
+public final class SignatureTrailerDataContentBuilder implements DataContentBuilder {
+
+ private final TagTrailerDataContentBuilder delegate;
+
+ private SignatureTrailerDataContentBuilder(TagEngine engine) {
+ this.delegate = new TagTrailerDataContentBuilder<>(engine);
+ }
+
+ private SignatureTrailerDataContentBuilder(Supplier extends TagEngine> engineFactory) {
+ this.delegate = new TagTrailerDataContentBuilder<>(engineFactory);
+ }
+
+ /**
+ * Core mode: wraps a fixed engine instance.
+ *
+ *
+ * This is the direct signature-specialized equivalent of:
+ * {@code new TagTrailerDataContentBuilder(engine)}.
+ *
+ *
+ * @param engine signature engine (typically a {@code SignatureContext}); must
+ * not be {@code null}
+ * @return builder instance
+ * @throws NullPointerException if {@code engine} is {@code null}
+ * @since 1.0
+ */
+ public static SignatureTrailerDataContentBuilder core(TagEngine engine) {
+ Objects.requireNonNull(engine, "engine");
+ return new SignatureTrailerDataContentBuilder(engine);
+ }
+
+ /**
+ * Core mode: wraps an engine factory.
+ *
+ *
+ * This is the direct signature-specialized equivalent of:
+ * {@code new TagTrailerDataContentBuilder(engineFactory)}.
+ *
+ *
+ * @param engineFactory engine factory; must not be {@code null}
+ * @return builder instance
+ * @throws NullPointerException if {@code engineFactory} is {@code null}
+ * @since 1.0
+ */
+ public static SignatureTrailerDataContentBuilder core(Supplier extends TagEngine> engineFactory) {
+ Objects.requireNonNull(engineFactory, "engineFactory");
+ return new SignatureTrailerDataContentBuilder(engineFactory);
+ }
+
+ /**
+ * Enters single-algorithm (non-hybrid) construction helpers.
+ *
+ * @return selector for creating signing/verifying builders
+ * @since 1.0
+ */
+ public static SingleSelector single() {
+ return new SingleSelector();
+ }
+
+ /**
+ * Enters hybrid signature construction helpers.
+ *
+ * @return selector for creating signing/verifying builders
+ * @since 1.0
+ */
+ public static HybridSelector hybrid() {
+ return new HybridSelector();
+ }
+
+ /**
+ * Sets the internal I/O buffer size used when stripping the trailer.
+ *
+ * @param bytes buffer size in bytes; must be at least 1
+ * @return this builder for chaining
+ * @throws IllegalArgumentException if {@code bytes < 1}
+ * @since 1.0
+ */
+ public SignatureTrailerDataContentBuilder bufferSize(int bytes) {
+ delegate.bufferSize(bytes);
+ return this;
+ }
+
+ /**
+ * Configures verification to throw on mismatch (default verification behavior).
+ *
+ * @return this builder for chaining
+ * @since 1.0
+ */
+ public SignatureTrailerDataContentBuilder throwOnMismatch() {
+ delegate.throwOnMismatch();
+ return this;
+ }
+
+ /**
+ * Instead of throwing on mismatch, records the verification outcome into a
+ * context flag.
+ *
+ * @param ctx context instance to receive the flag; must not be
+ * {@code null}
+ * @param verifyKey key under which the {@code Boolean} result is recorded; must
+ * not be {@code null}
+ * @return this builder for chaining
+ * @throws NullPointerException if {@code ctx} or {@code verifyKey} is
+ * {@code null}
+ * @since 1.0
+ */
+ public SignatureTrailerDataContentBuilder flagInContext(CtxInterface ctx, Key verifyKey) {
+ delegate.flagInContext(Objects.requireNonNull(ctx, "ctx"), Objects.requireNonNull(verifyKey, "verifyKey"));
+ return this;
+ }
+
+ @Override
+ public DataContent build(boolean encrypt) {
+ return delegate.build(encrypt);
+ }
+
+ // ======================================================================
+ // Single-algorithm helpers
+ // ======================================================================
+
+ /**
+ * Helper entry for single-algorithm signature construction.
+ *
+ * @since 1.0
+ */
+ public static final class SingleSelector {
+
+ private SingleSelector() {
+ }
+
+ /**
+ * Creates a signing trailer for a single signature algorithm (no spec).
+ *
+ * @param algorithmId signature algorithm id
+ * @param privateKey private key for signing
+ * @return builder ready to be added to a pipeline
+ * @throws NullPointerException if {@code algorithmId} or {@code privateKey} is
+ * {@code null}
+ * @throws IllegalStateException if the signature context cannot be created
+ * (e.g., I/O/provider issues)
+ * @since 1.0
+ */
+ public SignatureTrailerDataContentBuilder sign(String algorithmId, PrivateKey privateKey) {
+ return sign(algorithmId, privateKey, null);
+ }
+
+ /**
+ * Creates a signing trailer for a single signature algorithm with an optional
+ * spec.
+ *
+ * @param algorithmId signature algorithm id
+ * @param privateKey private key for signing
+ * @param spec optional context spec (may be {@code null})
+ * @return builder ready to be added to a pipeline
+ * @throws NullPointerException if {@code algorithmId} or {@code privateKey} is
+ * {@code null}
+ * @throws IllegalStateException if the signature context cannot be created
+ * (e.g., I/O/provider issues)
+ * @since 1.0
+ */
+ public SignatureTrailerDataContentBuilder sign(String algorithmId, PrivateKey privateKey, ContextSpec spec) {
+ Objects.requireNonNull(algorithmId, "algorithmId");
+ Objects.requireNonNull(privateKey, "privateKey");
+
+ Supplier> factory = () -> {
+ try {
+ return CryptoAlgorithms.create(algorithmId, KeyUsage.SIGN, privateKey, spec);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to create SIGN SignatureContext for: " + algorithmId, e);
+ }
+ };
+
+ return core(factory);
+ }
+
+ /**
+ * Creates a verification trailer for a single signature algorithm (no spec).
+ *
+ * @param algorithmId signature algorithm id
+ * @param publicKey public key for verification
+ * @return builder ready to be added to a pipeline
+ * @throws NullPointerException if {@code algorithmId} or {@code publicKey} is
+ * {@code null}
+ * @throws IllegalStateException if the signature context cannot be created
+ * (e.g., I/O/provider issues)
+ * @since 1.0
+ */
+ public SignatureTrailerDataContentBuilder verify(String algorithmId, PublicKey publicKey) {
+ return verify(algorithmId, publicKey, null);
+ }
+
+ /**
+ * Creates a verification trailer for a single signature algorithm with an
+ * optional spec.
+ *
+ * @param algorithmId signature algorithm id
+ * @param publicKey public key for verification
+ * @param spec optional context spec (may be {@code null})
+ * @return builder ready to be added to a pipeline
+ * @throws NullPointerException if {@code algorithmId} or {@code publicKey} is
+ * {@code null}
+ * @throws IllegalStateException if the signature context cannot be created
+ * (e.g., I/O/provider issues)
+ * @since 1.0
+ */
+ public SignatureTrailerDataContentBuilder verify(String algorithmId, PublicKey publicKey, ContextSpec spec) {
+ Objects.requireNonNull(algorithmId, "algorithmId");
+ Objects.requireNonNull(publicKey, "publicKey");
+
+ Supplier> factory = () -> {
+ try {
+ return CryptoAlgorithms.create(algorithmId, KeyUsage.VERIFY, publicKey, spec);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to create VERIFY SignatureContext for: " + algorithmId, e);
+ }
+ };
+
+ return core(factory);
+ }
+ }
+
+ // ======================================================================
+ // Hybrid helpers
+ // ======================================================================
+
+ /**
+ * Helper entry for hybrid signature construction.
+ *
+ * @since 1.0
+ */
+ public static final class HybridSelector {
+
+ private static final int DEFAULT_MAX_BODY_BYTES = 2 * 1024 * 1024;
+
+ private HybridSelector() {
+ }
+
+ /**
+ * Creates a hybrid signing trailer for the common case where both specs are
+ * {@code null}.
+ *
+ * @param classicSigId classic signature algorithm id
+ * @param pqcSigId post-quantum signature algorithm id
+ * @param rule aggregation rule
+ * @param classicPrivate classic private key
+ * @param pqcPrivate PQC private key
+ * @return builder ready to be added to a pipeline
+ * @throws NullPointerException if any required argument is {@code null}
+ * @throws IllegalStateException if the hybrid context cannot be created
+ * @since 1.0
+ */
+ public SignatureTrailerDataContentBuilder sign(String classicSigId, String pqcSigId,
+ HybridSignatureProfile.VerifyRule rule, PrivateKey classicPrivate, PrivateKey pqcPrivate) {
+ return sign(classicSigId, pqcSigId, null, null, rule, classicPrivate, pqcPrivate, DEFAULT_MAX_BODY_BYTES);
+ }
+
+ /**
+ * Creates a hybrid verification trailer for the common case where both specs
+ * are {@code null}.
+ *
+ * @param classicSigId classic signature algorithm id
+ * @param pqcSigId post-quantum signature algorithm id
+ * @param rule aggregation rule
+ * @param classicPublic classic public key
+ * @param pqcPublic PQC public key
+ * @return builder ready to be added to a pipeline
+ * @throws NullPointerException if any required argument is {@code null}
+ * @throws IllegalStateException if the hybrid context cannot be created
+ * @since 1.0
+ */
+ public SignatureTrailerDataContentBuilder verify(String classicSigId, String pqcSigId,
+ HybridSignatureProfile.VerifyRule rule, PublicKey classicPublic, PublicKey pqcPublic) {
+ return verify(classicSigId, pqcSigId, null, null, rule, classicPublic, pqcPublic, DEFAULT_MAX_BODY_BYTES);
+ }
+
+ /**
+ * Creates a hybrid signing trailer with optional specs and explicit
+ * {@code maxBodyBytes}.
+ *
+ * @param classicSigId classic signature algorithm id
+ * @param pqcSigId post-quantum signature algorithm id
+ * @param classicSpec optional classic spec (may be {@code null})
+ * @param pqcSpec optional PQC spec (may be {@code null})
+ * @param rule aggregation rule
+ * @param classicPrivate classic private key
+ * @param pqcPrivate PQC private key
+ * @param maxBodyBytes maximum body size accepted by the hybrid context; must
+ * be at least 1
+ * @return builder ready to be added to a pipeline
+ * @throws NullPointerException if any required argument is {@code null}
+ * @throws IllegalArgumentException if {@code maxBodyBytes < 1}
+ * @throws IllegalStateException if the hybrid context cannot be created
+ * @since 1.0
+ */
+ public SignatureTrailerDataContentBuilder sign(String classicSigId, String pqcSigId, ContextSpec classicSpec,
+ ContextSpec pqcSpec, HybridSignatureProfile.VerifyRule rule, PrivateKey classicPrivate,
+ PrivateKey pqcPrivate, int maxBodyBytes) {
+ Objects.requireNonNull(classicSigId, "classicSigId");
+ Objects.requireNonNull(pqcSigId, "pqcSigId");
+ Objects.requireNonNull(rule, "rule");
+ Objects.requireNonNull(classicPrivate, "classicPrivate");
+ Objects.requireNonNull(pqcPrivate, "pqcPrivate");
+ if (maxBodyBytes < 1) { // NOPMD
+ throw new IllegalArgumentException("maxBodyBytes must be >= 1");
+ }
+
+ HybridSignatureProfile profile = new HybridSignatureProfile(classicSigId, pqcSigId, classicSpec, pqcSpec,
+ rule);
+
+ Supplier> factory = () -> {
+ try {
+ return HybridSignatureContexts.sign(profile, classicPrivate, pqcPrivate, maxBodyBytes);
+ } catch (RuntimeException e) { // NOPMD
+ throw e;
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to create hybrid SIGN SignatureContext", e);
+ }
+ };
+
+ return core(factory);
+ }
+
+ /**
+ * Creates a hybrid verification trailer with optional specs and explicit
+ * {@code maxBodyBytes}.
+ *
+ * @param classicSigId classic signature algorithm id
+ * @param pqcSigId post-quantum signature algorithm id
+ * @param classicSpec optional classic spec (may be {@code null})
+ * @param pqcSpec optional PQC spec (may be {@code null})
+ * @param rule aggregation rule
+ * @param classicPublic classic public key
+ * @param pqcPublic PQC public key
+ * @param maxBodyBytes maximum body size accepted by the hybrid context; must
+ * be at least 1
+ * @return builder ready to be added to a pipeline
+ * @throws NullPointerException if any required argument is {@code null}
+ * @throws IllegalArgumentException if {@code maxBodyBytes < 1}
+ * @throws IllegalStateException if the hybrid context cannot be created
+ * @since 1.0
+ */
+ public SignatureTrailerDataContentBuilder verify(String classicSigId, String pqcSigId, ContextSpec classicSpec,
+ ContextSpec pqcSpec, HybridSignatureProfile.VerifyRule rule, PublicKey classicPublic,
+ PublicKey pqcPublic, int maxBodyBytes) {
+ Objects.requireNonNull(classicSigId, "classicSigId");
+ Objects.requireNonNull(pqcSigId, "pqcSigId");
+ Objects.requireNonNull(rule, "rule");
+ Objects.requireNonNull(classicPublic, "classicPublic");
+ Objects.requireNonNull(pqcPublic, "pqcPublic");
+ if (maxBodyBytes < 1) { // NOPMD
+ throw new IllegalArgumentException("maxBodyBytes must be >= 1");
+ }
+
+ HybridSignatureProfile profile = new HybridSignatureProfile(classicSigId, pqcSigId, classicSpec, pqcSpec,
+ rule);
+
+ Supplier> factory = () -> {
+ try {
+ return HybridSignatureContexts.verify(profile, classicPublic, pqcPublic, maxBodyBytes);
+ } catch (RuntimeException e) { // NOPMD
+ throw e;
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to create hybrid VERIFY SignatureContext", e);
+ }
+ };
+
+ return core(factory);
+ }
+ }
+}
diff --git a/lib/src/main/java/zeroecho/sdk/builders/package-info.java b/lib/src/main/java/zeroecho/sdk/builders/package-info.java
index 8ccb9fb..6430dd3 100644
--- a/lib/src/main/java/zeroecho/sdk/builders/package-info.java
+++ b/lib/src/main/java/zeroecho/sdk/builders/package-info.java
@@ -73,7 +73,9 @@
* {@link zeroecho.sdk.builders.alg.EcdsaDataContentBuilder},
* {@link zeroecho.sdk.builders.alg.Ed25519DataContentBuilder},
* {@link zeroecho.sdk.builders.alg.Ed448DataContentBuilder},
- * {@link zeroecho.sdk.builders.alg.SphincsPlusDataContentBuilder}.
+ * {@link zeroecho.sdk.builders.alg.SphincsPlusDataContentBuilder},
+ * {@link zeroecho.sdk.builders.alg.MldsaDataContentBuilder},
+ * {@link zeroecho.sdk.builders.alg.SlhDsaDataContentBuilder}.
* MAC and digest: {@link zeroecho.sdk.builders.alg.HmacDataContentBuilder},
* {@link zeroecho.sdk.builders.alg.DigestDataContentBuilder}.
* KEM envelopes: {@link zeroecho.sdk.builders.alg.KemDataContentBuilder}
@@ -86,6 +88,11 @@
* {@link TagTrailerDataContentBuilder} - appends or verifies an
* authentication tag carried as an input trailer using a
* {@link zeroecho.core.tag.TagEngine}.
+ * {@link SignatureTrailerDataContentBuilder} - signature-specialized
+ * trailer builder intended to replace
+ * {@code TagTrailerDataContentBuilder} in most signature use cases.
+ * It can wrap existing signature engines and construct single-algorithm and
+ * hybrid signature contexts.
*
*
*
@@ -107,6 +114,9 @@
* signatures or tags.
* {@link TagTrailerDataContentBuilder} focuses on trailer-style tags with
* explicit verify policies.
+ * {@link SignatureTrailerDataContentBuilder} provides the corresponding
+ * trailer functionality specialized for digital signatures, including hybrid
+ * signature construction.
*
*
* Typical usage
{@code
@@ -136,4 +146,4 @@
*
* @since 1.0
*/
-package zeroecho.sdk.builders;
\ No newline at end of file
+package zeroecho.sdk.builders;
diff --git a/lib/src/main/java/zeroecho/sdk/hybrid/HybridException.java b/lib/src/main/java/zeroecho/sdk/hybrid/HybridException.java
new file mode 100644
index 0000000..5c4c346
--- /dev/null
+++ b/lib/src/main/java/zeroecho/sdk/hybrid/HybridException.java
@@ -0,0 +1,63 @@
+/*******************************************************************************
+ * Copyright (C) 2025, 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.sdk.hybrid;
+
+/**
+ * Base exception type for hybrid framework failures.
+ *
+ * @since 1.0
+ */
+public class HybridException extends Exception {
+ private static final long serialVersionUID = -3704484377176409054L;
+
+ /**
+ * Creates a new exception with a message.
+ *
+ * @param message error description
+ */
+ public HybridException(String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new exception with a message and a cause.
+ *
+ * @param message error description
+ * @param cause underlying cause
+ */
+ public HybridException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/lib/src/main/java/zeroecho/sdk/hybrid/signature/CapturePredicate.java b/lib/src/main/java/zeroecho/sdk/hybrid/signature/CapturePredicate.java
new file mode 100644
index 0000000..b693d7c
--- /dev/null
+++ b/lib/src/main/java/zeroecho/sdk/hybrid/signature/CapturePredicate.java
@@ -0,0 +1,212 @@
+/*******************************************************************************
+ * Copyright (C) 2025, 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.sdk.hybrid.signature;
+
+import java.security.Signature;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import zeroecho.core.err.VerificationException;
+import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate;
+
+/**
+ * Verification predicate wrapper that captures the boolean verification result
+ * and never propagates {@link VerificationException} to its caller.
+ *
+ *
+ * This wrapper delegates verification to a supplied
+ * {@link VerificationBiPredicate} and records the boolean outcome into a shared
+ * {@link AtomicBoolean}. If the delegate throws {@link VerificationException},
+ * the exception is suppressed and the verification is treated as a failure
+ * (returns {@code false}).
+ *
+ *
+ *
+ * This behavior is required for robust hybrid verification semantics, most
+ * notably for logical OR composition: individual component verifiers may throw
+ * on malformed or structurally invalid signatures (for example, certain Ed25519
+ * invalid point encodings). Translating such exceptions into a boolean failure
+ * allows the hybrid engine to continue evaluating alternative verification
+ * paths and to aggregate the final decision deterministically.
+ *
+ *
+ * Logging
+ *
+ * For diagnostic purposes, a {@link Level#FINE} log entry is emitted on each
+ * invocation containing:
+ *
+ *
+ * - a short hexadecimal prefix of {@code expectedTag} (bounded and
+ * truncated), and
+ * - the resulting verification decision ({@code true}/{@code false}).
+ *
+ *
+ *
+ * The logged tag prefix is intentionally truncated to reduce the risk of
+ * leaking sensitive material. The log message is produced using a JUL
+ * formatting string (no string concatenation in the hot path) and is only
+ * constructed when {@code FINE} is enabled.
+ *
+ *
+ * Threading and side effects
+ *
+ * - This class is immutable with respect to its own fields.
+ * - The {@code ok} flag is updated exactly once per invocation with the
+ * result returned by this method (including failures caused by suppressed
+ * exceptions).
+ * - Correctness depends on coordinated usage of the shared
+ * {@link AtomicBoolean} when used concurrently.
+ *
+ *
+ *
+ * Security note: this class must not log keys, plaintext, shared secrets, full
+ * tags, or other sensitive material. Only a bounded prefix is logged at
+ * {@code FINE} level.
+ *
+ *
+ * @since 1.0
+ */
+final class CapturePredicate extends VerificationBiPredicate {
+
+ private static final Logger LOGGER = Logger.getLogger(CapturePredicate.class.getName());
+
+ /**
+ * Maximum number of bytes from {@code expectedTag} included in log output.
+ */
+ private static final int TAG_LOG_PREFIX_BYTES = 8;
+
+ /**
+ * Underlying verification predicate performing the actual cryptographic check.
+ */
+ private final VerificationBiPredicate delegate;
+
+ /**
+ * Shared atomic flag capturing the result of the most recent verification.
+ */
+ private final AtomicBoolean ok;
+
+ /**
+ * Creates a new capturing predicate.
+ *
+ * @param delegate the underlying verification predicate to invoke
+ * @param ok shared atomic flag receiving the verification outcome
+ * @throws NullPointerException if {@code delegate} or {@code ok} is
+ * {@code null}
+ */
+ protected CapturePredicate(VerificationBiPredicate delegate, AtomicBoolean ok) {
+ super();
+ this.delegate = Objects.requireNonNull(delegate, "delegate");
+ this.ok = Objects.requireNonNull(ok, "ok");
+ }
+
+ /**
+ * Invokes the delegate predicate, captures its boolean result, and never
+ * propagates {@link VerificationException}.
+ *
+ *
+ * If the delegate completes normally, its return value is recorded into
+ * {@link #ok} and returned. If the delegate throws
+ * {@link VerificationException}, the exception is suppressed, {@code false} is
+ * recorded into {@link #ok}, and {@code false} is returned.
+ *
+ *
+ *
+ * A {@link Level#FINE} log entry is emitted with a truncated hexadecimal prefix
+ * of {@code expectedTag} and the resulting decision.
+ *
+ *
+ * @param signature the signature object to verify
+ * @param expectedTag the expected signature tag (may be {@code null}, in which
+ * case {@code ""} is logged)
+ * @return {@code true} if verification succeeded, {@code false} otherwise
+ * @throws VerificationException never thrown by this implementation, but
+ * declared to satisfy the overridden contract
+ */
+ @Override
+ public boolean verify(Signature signature, byte[] expectedTag) throws VerificationException {
+ boolean result;
+ try {
+ result = delegate.verify(signature, expectedTag);
+ } catch (VerificationException ex) {
+ result = false;
+ }
+
+ ok.set(result);
+
+ if (LOGGER.isLoggable(Level.FINE)) {
+ LOGGER.log(Level.FINE, "Hybrid verify: expectedTagPrefix={0}, result={1}",
+ new Object[] { formatTagPrefix(expectedTag), result });
+ }
+
+ return result;
+ }
+
+ /**
+ * Formats a short hexadecimal prefix of the provided tag for logging.
+ *
+ *
+ * At most {@link #TAG_LOG_PREFIX_BYTES} bytes are included. If the tag is
+ * longer, the output is suffixed with {@code "..."}.
+ *
+ *
+ *
+ * The output format uses lowercase hexadecimal digits without separators.
+ *
+ *
+ * @param tag tag bytes to format (may be {@code null})
+ * @return a bounded, log-safe hexadecimal prefix, or {@code ""} if
+ * {@code tag} is {@code null}
+ */
+ private static String formatTagPrefix(byte[] tag) {
+ if (tag == null) {
+ return "";
+ }
+
+ int n = Math.min(tag.length, TAG_LOG_PREFIX_BYTES);
+ StringBuilder sb = new StringBuilder(n * 2 + 3);
+
+ for (int i = 0; i < n; i++) {
+ sb.append(Character.forDigit((tag[i] >>> 4) & 0x0f, 16)).append(Character.forDigit(tag[i] & 0x0f, 16));
+ }
+
+ if (tag.length > n) {
+ sb.append("...");
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/lib/src/main/java/zeroecho/sdk/hybrid/signature/HybridCorePredicate.java b/lib/src/main/java/zeroecho/sdk/hybrid/signature/HybridCorePredicate.java
new file mode 100644
index 0000000..20e2a5f
--- /dev/null
+++ b/lib/src/main/java/zeroecho/sdk/hybrid/signature/HybridCorePredicate.java
@@ -0,0 +1,131 @@
+/*******************************************************************************
+ * Copyright (C) 2025, 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.sdk.hybrid.signature;
+
+import java.security.Signature;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import zeroecho.core.err.VerificationException;
+import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate;
+
+/**
+ * Hybrid verification core predicate delegating the final decision to the
+ * hybrid engine's end-of-stream (EOF) aggregation.
+ *
+ *
+ * This predicate does not perform any cryptographic verification on its own.
+ * Instead, it acts as a bridge between the generic verification pipeline and
+ * the hybrid streaming engine, where the actual verification of classic and PQC
+ * signatures is performed once the complete payload has been consumed.
+ *
+ *
+ *
+ * The boolean result returned by {@link #verify(Signature, byte[])} reflects
+ * the outcome computed during EOF processing and stored in the shared
+ * {@link AtomicBoolean} instance. This allows the verification decision to be
+ * made exactly once, based on the fully buffered message body and the hybrid
+ * verification rule.
+ *
+ *
+ * Design notes
+ *
+ * - The {@code signature} and {@code expectedTag} parameters are
+ * intentionally ignored, as the hybrid model defers verification until all data
+ * has been read from the stream.
+ * - This class is immutable and thread-safe with respect to its own state;
+ * however, correctness depends on coordinated usage with the associated hybrid
+ * stream.
+ * - The predicate must be used only in conjunction with a hybrid signature
+ * stream that updates the shared {@code lastOk} flag.
+ *
+ *
+ *
+ * Security note: this predicate must not leak any sensitive material. It merely
+ * returns a boolean decision computed elsewhere and does not inspect keys,
+ * signatures, or plaintext data.
+ *
+ *
+ * @since 1.0
+ */
+final class HybridCorePredicate extends VerificationBiPredicate {
+ private static final Logger LOGGER = Logger.getLogger(HybridCorePredicate.class.getName());
+
+ /**
+ * Holds the final hybrid verification result computed at end-of-stream.
+ */
+ private final AtomicBoolean lastOk;
+
+ /**
+ * Creates a new hybrid core predicate bound to the given verification result
+ * flag.
+ *
+ * @param lastOk shared atomic flag holding the final hybrid verification result
+ * @throws NullPointerException if {@code lastOk} is {@code null}
+ */
+ protected HybridCorePredicate(AtomicBoolean lastOk) {
+ super();
+ this.lastOk = Objects.requireNonNull(lastOk, "lastOk");
+ }
+
+ /**
+ * Returns the hybrid verification result computed during EOF processing.
+ *
+ *
+ * This method performs no validation of the provided {@code signature} or
+ * {@code expectedTag}. All cryptographic checks have already been executed by
+ * the hybrid stream once the complete payload was available.
+ *
+ *
+ * @param signature ignored; present to satisfy the predicate contract
+ * @param expectedTag ignored; present to satisfy the predicate contract
+ * @return {@code true} if the hybrid verification succeeded according to the
+ * configured hybrid verification rule, {@code false} otherwise
+ * @throws VerificationException never thrown by this implementation, but
+ * declared to satisfy the overridden contract
+ */
+ @Override
+ public boolean verify(Signature signature, byte[] expectedTag) throws VerificationException {
+ boolean result = lastOk.get();
+
+ if (LOGGER.isLoggable(Level.FINE)) {
+ LOGGER.log(Level.FINE, "Hybrid core verification result={0}", result);
+ }
+
+ return result;
+ }
+}
diff --git a/lib/src/main/java/zeroecho/sdk/hybrid/signature/HybridSignatureContext.java b/lib/src/main/java/zeroecho/sdk/hybrid/signature/HybridSignatureContext.java
new file mode 100644
index 0000000..2f089b6
--- /dev/null
+++ b/lib/src/main/java/zeroecho/sdk/hybrid/signature/HybridSignatureContext.java
@@ -0,0 +1,611 @@
+/*******************************************************************************
+ * Copyright (C) 2025, 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.sdk.hybrid.signature;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.Key;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import zeroecho.core.CryptoAlgorithm;
+import zeroecho.core.CryptoAlgorithms;
+import zeroecho.core.KeyUsage;
+import zeroecho.core.context.SignatureContext;
+import zeroecho.core.err.VerificationException;
+import zeroecho.core.io.TailStrippingInputStream;
+import zeroecho.core.spec.ContextSpec;
+import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate;
+
+/**
+ * Package-private {@link SignatureContext} that composes two signature engines.
+ *
+ *
+ * The signature trailer is {@code sigClassic || sigPqc} in the fixed order
+ * defined by the {@link HybridSignatureProfile}. The trailer carries no
+ * algorithm identifiers; the profile is the source of truth.
+ *
+ *
+ * Streaming semantics
+ *
+ * This context is compatible with the existing ZeroEcho signing/verification
+ * pipelines: callers wrap an {@link InputStream} and read until EOF. At EOF,
+ * the underlying engines produce/verify the trailer.
+ *
+ *
+ *
+ * Implementation note: the wrapper buffers the message body and runs the
+ * component engines at EOF. This avoids any need for changes in core contracts
+ * while keeping the same external streaming behavior.
+ *
+ *
+ * Verification aggregation
+ *
+ * Verification is performed for both component signatures and aggregated by the
+ * profile rule (AND / OR). The final decision is applied via this context's
+ * {@link #setVerificationApproach(VerificationBiPredicate)} predicate, enabling
+ * the standard ZeroEcho behavior (throw-on-mismatch, flagging, etc.).
+ *
+ *
+ * @since 1.0
+ */
+final class HybridSignatureContext implements SignatureContext {
+
+ private final HybridSignatureProfile profile;
+
+ private final boolean produceMode;
+ private final PrivateKey classicPrivate;
+ private final PrivateKey pqcPrivate;
+ private final PublicKey classicPublic;
+ private final PublicKey pqcPublic;
+
+ private final int maxBufferedBytes;
+
+ private final AtomicBoolean lastOk;
+ private final VerificationBiPredicate hybridCore;
+
+ private VerificationBiPredicate verificationApproach;
+ private byte[] expectedTag;
+
+ /**
+ * Creates a signing hybrid signature context.
+ *
+ * @param profile hybrid signature profile
+ * @param classicPrivate private key for classical engine
+ * @param pqcPrivate private key for PQC engine
+ * @param maxBufferedBytes maximum buffered bytes (DoS guard)
+ * @throws NullPointerException if {@code profile}, {@code classicPrivate},
+ * or {@code pqcPrivate} is {@code null}
+ * @throws IllegalArgumentException if {@code maxBufferedBytes <= 0}
+ */
+ protected HybridSignatureContext(HybridSignatureProfile profile, PrivateKey classicPrivate, PrivateKey pqcPrivate,
+ int maxBufferedBytes) {
+ this.profile = Objects.requireNonNull(profile, "profile");
+ this.classicPrivate = Objects.requireNonNull(classicPrivate, "classicPrivate");
+ this.pqcPrivate = Objects.requireNonNull(pqcPrivate, "pqcPrivate");
+ this.classicPublic = null;
+ this.pqcPublic = null;
+ this.produceMode = true;
+
+ if (maxBufferedBytes <= 0) {
+ throw new IllegalArgumentException("maxBufferedBytes must be positive");
+ }
+ this.maxBufferedBytes = maxBufferedBytes;
+
+ this.lastOk = new AtomicBoolean(false);
+ this.hybridCore = new HybridCorePredicate(this.lastOk);
+ this.verificationApproach = this.hybridCore;
+ this.expectedTag = null;
+ }
+
+ /**
+ * Creates a verifying hybrid signature context.
+ *
+ * @param profile hybrid signature profile
+ * @param classicPublic public key for classical engine
+ * @param pqcPublic public key for PQC engine
+ * @param maxBufferedBytes maximum buffered bytes (DoS guard)
+ * @throws NullPointerException if {@code profile}, {@code classicPublic},
+ * or {@code pqcPublic} is {@code null}
+ * @throws IllegalArgumentException if {@code maxBufferedBytes <= 0}
+ */
+ protected HybridSignatureContext(HybridSignatureProfile profile, PublicKey classicPublic, PublicKey pqcPublic,
+ int maxBufferedBytes) {
+ this.profile = Objects.requireNonNull(profile, "profile");
+ this.classicPublic = Objects.requireNonNull(classicPublic, "classicPublic");
+ this.pqcPublic = Objects.requireNonNull(pqcPublic, "pqcPublic");
+ this.classicPrivate = null;
+ this.pqcPrivate = null;
+ this.produceMode = false;
+
+ if (maxBufferedBytes <= 0) {
+ throw new IllegalArgumentException("maxBufferedBytes must be positive");
+ }
+ this.maxBufferedBytes = maxBufferedBytes;
+
+ this.lastOk = new AtomicBoolean(false);
+ this.hybridCore = new HybridCorePredicate(this.lastOk);
+ this.verificationApproach = this.hybridCore;
+ this.expectedTag = null;
+ }
+
+ @Override
+ public InputStream wrap(InputStream upstream) throws IOException {
+ Objects.requireNonNull(upstream, "upstream");
+ return new HybridStream(upstream);
+ }
+
+ @Override
+ public int tagLength() {
+ try (SignatureContext classic = createClassic(produceMode ? KeyUsage.SIGN : KeyUsage.VERIFY);
+ SignatureContext pqc = createPqc(produceMode ? KeyUsage.SIGN : KeyUsage.VERIFY)) {
+ return classic.tagLength() + pqc.tagLength();
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to determine hybrid tag length", e);
+ }
+ }
+
+ @Override
+ public void setExpectedTag(byte[] expected) {
+ if (produceMode) {
+ throw new UnsupportedOperationException("Expected tag is not used in sign mode");
+ }
+ Objects.requireNonNull(expected, "expected");
+ this.expectedTag = expected.clone();
+ }
+
+ @Override
+ public void setVerificationApproach(VerificationBiPredicate approach) {
+ this.verificationApproach = Objects.requireNonNull(approach, "approach");
+ }
+
+ @Override
+ public VerificationBiPredicate getVerificationCore() {
+ return hybridCore;
+ }
+
+ @Override
+ public CryptoAlgorithm algorithm() {
+ return CryptoAlgorithms.require(profile.classicSigId());
+ }
+
+ @Override
+ public Key key() {
+ return produceMode ? classicPrivate : classicPublic;
+ }
+
+ @Override
+ public void close() {
+ // Stateless at context level; per-stream resources are closed by the stream.
+ }
+
+ private SignatureContext createClassic(KeyUsage usage) throws IOException {
+ ContextSpec spec = profile.classicSpec();
+ if (usage == KeyUsage.SIGN) {
+ return CryptoAlgorithms.create(profile.classicSigId(), usage, classicPrivate, spec);
+ }
+ return CryptoAlgorithms.create(profile.classicSigId(), usage, classicPublic, spec);
+ }
+
+ private SignatureContext createPqc(KeyUsage usage) throws IOException {
+ ContextSpec spec = profile.pqcSpec();
+ if (usage == KeyUsage.SIGN) {
+ return CryptoAlgorithms.create(profile.pqcSigId(), usage, pqcPrivate, spec);
+ }
+ return CryptoAlgorithms.create(profile.pqcSigId(), usage, pqcPublic, spec);
+ }
+
+ /**
+ * Internal {@link InputStream} wrapper that transparently appends or validates
+ * a hybrid-signature trailer at end-of-stream.
+ *
+ *
+ * The stream proxies reads from an underlying upstream {@link InputStream}.
+ * While reading, it buffers the payload body (bounded by
+ * {@code maxBufferedBytes}) to enable hybrid signature processing at EOF:
+ *
+ *
+ *
+ * - Produce mode ({@code produceMode == true}): on upstream EOF, two
+ * signatures are computed (classic + PQC) over the buffered body and then
+ * emitted as a trailer appended to the stream.
+ * - Verify mode ({@code produceMode == false}): on upstream EOF, the
+ * expected tag is split into classic/PQC components based on tag lengths, both
+ * signatures are verified against the buffered body, and the final verification
+ * result is stored into {@code lastOk}. No trailer bytes are emitted in verify
+ * mode.
+ *
+ *
+ * Lifecycle and invariants
+ *
+ * - EOF finalization ({@link #finishAtEof()}) is executed at most once,
+ * guarded by {@link #eofSeen}.
+ * - In produce mode, the trailer is emitted strictly after all upstream
+ * bytes.
+ * - In verify mode, the stream ends immediately after the upstream ends.
+ * - This class is not thread-safe and assumes sequential consumption.
+ *
+ *
+ *
+ * Security note: this implementation must not expose sensitive materials (keys,
+ * seeds, plaintext, signatures, or intermediate state) via logging or exception
+ * messages. Exceptions raised by this stream are limited to generic error
+ * descriptions and non-sensitive metadata (e.g., length and limits).
+ *
+ */
+ private final class HybridStream extends InputStream {
+ /** The wrapped upstream stream providing the primary data. */
+ private final InputStream upstream;
+
+ /**
+ * Buffer accumulating upstream bytes required for hybrid signature processing.
+ * The buffer is bounded to mitigate unbounded memory growth.
+ */
+ private final ByteArrayOutputStream buffer;
+
+ /**
+ * Indicates whether end-of-stream has already been observed on the upstream.
+ * Ensures EOF finalization logic executes exactly once.
+ */
+ private boolean eofSeen;
+
+ /**
+ * Trailer bytes to be emitted after upstream exhaustion in produce mode, or
+ * {@code null} when no trailer is present (e.g., verify mode).
+ */
+ private byte[] trailer;
+
+ /**
+ * Current emission position within {@link #trailer}.
+ */
+ private int trailerPos;
+
+ /**
+ * Creates a new hybrid stream wrapping the provided upstream.
+ *
+ * @param upstream the underlying input stream supplying the primary data; must
+ * not be {@code null}
+ */
+ private HybridStream(InputStream upstream) {
+ super();
+ this.upstream = upstream;
+ this.buffer = new ByteArrayOutputStream(Math.min(8192, maxBufferedBytes));
+ this.eofSeen = false;
+ this.trailer = null;
+ this.trailerPos = 0;
+ }
+
+ /**
+ * Reads a single byte from this stream.
+ *
+ *
+ * Delegates to {@link #read(byte[], int, int)} and follows the standard
+ * {@link InputStream} contract.
+ *
+ *
+ * @return the next byte of data as an unsigned value in the range
+ * {@code 0–255}, or {@code -1} if the stream is exhausted (including
+ * any trailer)
+ * @throws IOException if an I/O error occurs or EOF finalization fails
+ */
+ @Override
+ public int read() throws IOException {
+ byte[] one = new byte[1];
+ int r = read(one, 0, 1);
+ if (r == -1) {
+ return -1;
+ }
+ return one[0] & 0xff;
+ }
+
+ /**
+ * Reads up to {@code len} bytes of data into {@code b}, starting at
+ * {@code off}.
+ *
+ *
+ * The method first serves any pending trailer bytes (produce mode). Otherwise
+ * it reads from the upstream stream, buffering all successfully read bytes for
+ * later hybrid signature processing at EOF. When the upstream stream returns
+ * {@code -1} for the first time, {@link #finishAtEof()} is invoked and may
+ * either prepare a trailer for emission (produce mode) or perform verification
+ * (verify mode).
+ *
+ *
+ * @param b destination buffer
+ * @param off offset at which to start storing bytes
+ * @param len maximum number of bytes to read
+ * @return the number of bytes read, or {@code -1} if the stream is exhausted
+ * @throws IOException if an I/O error occurs, the internal buffer
+ * limit is exceeded, or EOF finalization
+ * (signature creation/verification) fails
+ * @throws IndexOutOfBoundsException if {@code off} or {@code len} are invalid
+ */
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ Objects.checkFromIndexSize(off, len, b.length);
+
+ if (trailer != null) {
+ if (trailerPos >= trailer.length) {
+ return -1;
+ }
+ int n = Math.min(len, trailer.length - trailerPos);
+ System.arraycopy(trailer, trailerPos, b, off, n);
+ trailerPos += n;
+ return n;
+ }
+
+ int r = upstream.read(b, off, len);
+ if (r == -1) {
+ if (!eofSeen) {
+ eofSeen = true;
+ finishAtEof();
+ }
+ if (trailer != null && trailer.length > 0) {
+ return read(b, off, len);
+ }
+ return -1;
+ }
+
+ if (r > 0) {
+ writeToBuffer(b, off, r);
+ }
+ return r;
+ }
+
+ /**
+ * Closes this stream and releases the underlying upstream resource.
+ *
+ *
+ * Note: closing this stream closes the wrapped {@code upstream} stream.
+ *
+ *
+ * @throws IOException if closing the upstream fails
+ */
+ @Override
+ public void close() throws IOException {
+ upstream.close();
+ }
+
+ /**
+ * Appends the specified slice of bytes into the internal body buffer.
+ *
+ *
+ * The buffer is bounded by {@code maxBufferedBytes}. If appending would exceed
+ * the configured limit, the method fails fast with an {@link IOException}.
+ *
+ *
+ * @param b source buffer
+ * @param off offset within {@code b}
+ * @param len number of bytes to append
+ * @throws IOException if the buffer limit would be exceeded
+ */
+ private void writeToBuffer(byte[] b, int off, int len) throws IOException {
+ if (buffer.size() + len > maxBufferedBytes) {
+ throw new IOException("Hybrid signature buffer limit exceeded: " + maxBufferedBytes);
+ }
+ buffer.write(b, off, len);
+ }
+
+ /**
+ * Finalizes processing when upstream EOF is reached.
+ *
+ *
+ * This method is invoked exactly once upon the first observation of upstream
+ * EOF. It uses the buffered body to either:
+ *
+ *
+ *
+ * - Produce mode: compute the classic and PQC signatures over the
+ * body, concatenate them, and expose them as a trailer to be emitted by
+ * subsequent {@code read(...)} calls.
+ * - Verify mode: split the expected tag into classic/PQC signature
+ * components, verify each against the body, combine results using the profile
+ * verify rule, update {@code lastOk}, and delegate to
+ * {@code verificationApproach} for any additional policy enforcement. No
+ * trailer is produced.
+ *
+ *
+ * @throws IOException if required inputs are missing, if signature
+ * creation/verification fails, if the expected tag has an
+ * invalid length, or if the verification approach rejects
+ * the tag
+ */
+ private void finishAtEof() throws IOException {
+ byte[] body = buffer.toByteArray();
+
+ if (produceMode) {
+ byte[] sigClassic = signOne(profile.classicSigId(), classicPrivate, profile.classicSpec(), body);
+ byte[] sigPqc = signOne(profile.pqcSigId(), pqcPrivate, profile.pqcSpec(), body);
+ trailer = concat(sigClassic, sigPqc);
+ trailerPos = 0;
+ return;
+ }
+
+ byte[] exp = expectedTag;
+ if (exp == null) {
+ throw new IOException("Expected tag not set");
+ }
+
+ VerificationSplit split = splitExpected(exp);
+
+ boolean okClassic = verifyOne(profile.classicSigId(), classicPublic, profile.classicSpec(), body,
+ split.expectedClassic);
+ boolean okPqc = verifyOne(profile.pqcSigId(), pqcPublic, profile.pqcSpec(), body, split.expectedPqc);
+
+ boolean finalOk = (profile.verifyRule() == HybridSignatureProfile.VerifyRule.OR) ? (okClassic || okPqc)
+ : (okClassic && okPqc);
+
+ lastOk.set(finalOk);
+
+ try {
+ verificationApproach.verify(null, exp);
+ } catch (VerificationException e) {
+ throw new IOException("Hybrid signature verification failed", e);
+ }
+
+ trailer = null;
+ trailerPos = 0;
+ }
+
+ /**
+ * Splits the expected hybrid tag into classic and PQC signature components.
+ *
+ *
+ * The split is determined by querying tag lengths from freshly created
+ * verification contexts (classic and PQC). The expected tag must match the
+ * exact combined length {@code classicLen + pqcLen}; otherwise an
+ * {@link IOException} is thrown.
+ *
+ *
+ * @param exp expected hybrid tag bytes containing concatenated classic and PQC
+ * signatures
+ * @return a value object holding the classic and PQC expected signatures
+ * @throws IOException if context initialization fails or {@code exp} has an
+ * invalid length
+ */
+ private VerificationSplit splitExpected(byte[] exp) throws IOException {
+ int classicLen;
+ int pqcLen;
+
+ try (SignatureContext classic = createClassic(KeyUsage.VERIFY);
+ SignatureContext pqc = createPqc(KeyUsage.VERIFY)) {
+ classicLen = classic.tagLength();
+ pqcLen = pqc.tagLength();
+ }
+
+ int total = classicLen + pqcLen;
+ if (exp.length != total) {
+ throw new IOException("Invalid expected tag length: " + exp.length + ", expected " + total);
+ }
+
+ byte[] eClassic = new byte[classicLen];
+ byte[] ePqc = new byte[pqcLen];
+
+ System.arraycopy(exp, 0, eClassic, 0, classicLen);
+ System.arraycopy(exp, classicLen, ePqc, 0, pqcLen);
+
+ return new VerificationSplit(eClassic, ePqc);
+ }
+ }
+
+ /**
+ * Simple value object holding the classic and PQC portions of an expected
+ * hybrid signature tag.
+ *
+ *
+ * Instances are created only after the expected tag length is validated and
+ * then used to feed the per-algorithm verification routines.
+ *
+ *
+ *
+ * Security note: this structure stores signature bytes; it must not be logged
+ * or exposed outside the narrow verification flow.
+ *
+ */
+ private static final class VerificationSplit {
+ /** Expected signature bytes for the classic algorithm. */
+ private final byte[] expectedClassic;
+
+ /** Expected signature bytes for the PQC algorithm. */
+ private final byte[] expectedPqc;
+
+ /**
+ * Constructs the split expected-tag view.
+ *
+ * @param expectedClassic expected classic signature bytes; must not be
+ * {@code null}
+ * @param expectedPqc expected PQC signature bytes; must not be {@code null}
+ */
+ private VerificationSplit(byte[] expectedClassic, byte[] expectedPqc) {
+ this.expectedClassic = expectedClassic;
+ this.expectedPqc = expectedPqc;
+ }
+ }
+
+ private static byte[] signOne(String id, PrivateKey key, ContextSpec spec, byte[] body) throws IOException {
+ final byte[][] sigHolder = new byte[1][];
+
+ try (SignatureContext signer = CryptoAlgorithms.create(id, KeyUsage.SIGN, key, spec);
+ InputStream in = new TailStrippingInputStream(signer.wrap(new ByteArrayInputStream(body)),
+ signer.tagLength(), 8192) {
+ @Override
+ protected void processTail(byte[] tail) throws IOException {
+ if (tail == null || tail.length == 0) {
+ throw new IOException("Empty signature trailer for " + id);
+ }
+ sigHolder[0] = tail.clone();
+ }
+ }) {
+
+ in.transferTo(OutputStream.nullOutputStream());
+
+ byte[] sig = sigHolder[0];
+ if (sig == null) {
+ throw new IOException("Signature trailer missing for " + id);
+ }
+ return sig;
+ }
+ }
+
+ private static boolean verifyOne(String id, PublicKey key, ContextSpec spec, byte[] body, byte[] expected)
+ throws IOException {
+
+ AtomicBoolean ok = new AtomicBoolean(false);
+
+ try (SignatureContext verifier = CryptoAlgorithms.create(id, KeyUsage.VERIFY, key, spec)) {
+ verifier.setVerificationApproach(new CapturePredicate(verifier.getVerificationCore(), ok));
+ verifier.setExpectedTag(expected);
+
+ try (InputStream in = verifier.wrap(new ByteArrayInputStream(body))) {
+ in.transferTo(OutputStream.nullOutputStream());
+ }
+ }
+
+ return ok.get();
+ }
+
+ private static byte[] concat(byte[] a, byte[] b) {
+ byte[] out = new byte[a.length + b.length];
+ System.arraycopy(a, 0, out, 0, a.length);
+ System.arraycopy(b, 0, out, a.length, b.length);
+ return out;
+ }
+}
diff --git a/lib/src/main/java/zeroecho/sdk/hybrid/signature/HybridSignatureContexts.java b/lib/src/main/java/zeroecho/sdk/hybrid/signature/HybridSignatureContexts.java
new file mode 100644
index 0000000..99c879e
--- /dev/null
+++ b/lib/src/main/java/zeroecho/sdk/hybrid/signature/HybridSignatureContexts.java
@@ -0,0 +1,99 @@
+/*******************************************************************************
+ * Copyright (C) 2025, 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.sdk.hybrid.signature;
+
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.util.Objects;
+
+import zeroecho.core.context.SignatureContext;
+
+/**
+ * Factory for {@link SignatureContext}-compatible hybrid signature contexts.
+ *
+ *
+ * The returned contexts implement the standard ZeroEcho streaming contract:
+ * wrapping a stream produces/verifies a signature trailer at EOF.
+ *
+ *
+ * @since 1.0
+ */
+public final class HybridSignatureContexts {
+
+ private HybridSignatureContexts() {
+ }
+
+ /**
+ * Creates a signing hybrid signature context.
+ *
+ * @param profile hybrid signature profile
+ * @param classicPrivate private key for the classical signature engine
+ * @param pqcPrivate private key for the PQC signature engine
+ * @param maxBufferedBytes maximum number of bytes buffered from the wrapped
+ * stream (DoS guard)
+ * @return signing signature context
+ * @throws NullPointerException if any mandatory argument is {@code null}
+ * @throws IllegalArgumentException if {@code maxBufferedBytes <= 0}
+ * @since 1.0
+ */
+ public static SignatureContext sign(HybridSignatureProfile profile, PrivateKey classicPrivate,
+ PrivateKey pqcPrivate, int maxBufferedBytes) {
+ Objects.requireNonNull(profile, "profile");
+ Objects.requireNonNull(classicPrivate, "classicPrivate");
+ Objects.requireNonNull(pqcPrivate, "pqcPrivate");
+ return new HybridSignatureContext(profile, classicPrivate, pqcPrivate, maxBufferedBytes);
+ }
+
+ /**
+ * Creates a verification hybrid signature context.
+ *
+ * @param profile hybrid signature profile
+ * @param classicPublic public key for the classical signature engine
+ * @param pqcPublic public key for the PQC signature engine
+ * @param maxBufferedBytes maximum number of bytes buffered from the wrapped
+ * stream (DoS guard)
+ * @return verifying signature context
+ * @throws NullPointerException if any mandatory argument is {@code null}
+ * @throws IllegalArgumentException if {@code maxBufferedBytes <= 0}
+ * @since 1.0
+ */
+ public static SignatureContext verify(HybridSignatureProfile profile, PublicKey classicPublic, PublicKey pqcPublic,
+ int maxBufferedBytes) {
+ Objects.requireNonNull(profile, "profile");
+ Objects.requireNonNull(classicPublic, "classicPublic");
+ Objects.requireNonNull(pqcPublic, "pqcPublic");
+ return new HybridSignatureContext(profile, classicPublic, pqcPublic, maxBufferedBytes);
+ }
+}
diff --git a/lib/src/main/java/zeroecho/sdk/hybrid/signature/HybridSignatureProfile.java b/lib/src/main/java/zeroecho/sdk/hybrid/signature/HybridSignatureProfile.java
new file mode 100644
index 0000000..5ff9247
--- /dev/null
+++ b/lib/src/main/java/zeroecho/sdk/hybrid/signature/HybridSignatureProfile.java
@@ -0,0 +1,112 @@
+/*******************************************************************************
+ * Copyright (C) 2025, 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.sdk.hybrid.signature;
+
+import java.util.Objects;
+
+import zeroecho.core.spec.ContextSpec;
+
+/**
+ * Immutable definition of a hybrid signature composition.
+ *
+ * {@code HybridSignatureProfile} is a pure configuration object that defines:
+ *
+ * - which two signature algorithms participate in the hybrid
+ * composition,
+ * - their fixed ordering within the produced signature trailer,
+ * - the verification aggregation rule applied to their results.
+ *
+ *
+ *
+ * The profile is the single source of truth for hybrid signature
+ * processing. No algorithm identifiers or metadata are embedded in the
+ * signature trailer itself; both signing and verification sides must use the
+ * same profile.
+ *
+ *
+ *
+ * Instances of this record are immutable and thread-safe.
+ *
+ *
+ * @param classicSigId canonical identifier of the classical signature algorithm
+ * (for example {@code "Ed25519"})
+ * @param pqcSigId canonical identifier of the post-quantum signature
+ * algorithm (for example {@code "ML-DSA-65"})
+ * @param classicSpec optional {@link ContextSpec} for the classical signature
+ * engine; may be {@code null}
+ * @param pqcSpec optional {@link ContextSpec} for the post-quantum
+ * signature engine; may be {@code null}
+ * @param verifyRule aggregation rule applied during verification
+ *
+ * @since 1.0
+ */
+public record HybridSignatureProfile(String classicSigId, String pqcSigId, ContextSpec classicSpec, ContextSpec pqcSpec,
+ VerifyRule verifyRule) {
+
+ /**
+ * Verification aggregation rule for hybrid signatures.
+ *
+ * @since 1.0
+ */
+ public enum VerifyRule {
+ /**
+ * Both component signatures must verify successfully.
+ */
+ AND,
+
+ /**
+ * At least one component signature must verify successfully.
+ */
+ OR
+ }
+
+ /**
+ * Canonical constructor with invariant checks.
+ *
+ *
+ * Uses {@link Objects#requireNonNull(Object, String)} to enforce mandatory
+ * components while keeping validation idiomatic and consistent with the rest of
+ * the ZeroEcho codebase.
+ *
+ *
+ * @throws NullPointerException if {@code classicSigId}, {@code pqcSigId}, or
+ * {@code verifyRule} is {@code null}
+ */
+ public HybridSignatureProfile {
+ Objects.requireNonNull(classicSigId, "classicSigId");
+ Objects.requireNonNull(pqcSigId, "pqcSigId");
+ Objects.requireNonNull(verifyRule, "verifyRule");
+ }
+}
diff --git a/lib/src/main/java/zeroecho/sdk/hybrid/signature/package-info.java b/lib/src/main/java/zeroecho/sdk/hybrid/signature/package-info.java
new file mode 100644
index 0000000..d5a71a9
--- /dev/null
+++ b/lib/src/main/java/zeroecho/sdk/hybrid/signature/package-info.java
@@ -0,0 +1,118 @@
+/*******************************************************************************
+ * Copyright (C) 2025, 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.
+ ******************************************************************************/
+/**
+ * Hybrid signature composition for streaming pipelines.
+ *
+ *
+ * This package provides SDK-level hybrid signatures that combine two
+ * independent signature schemes (typically a classical and a post-quantum
+ * algorithm) and expose them as a single streaming signature engine suitable
+ * for {@link zeroecho.sdk.builders.TagTrailerDataContentBuilder} and related
+ * pipeline stages.
+ *
+ *
+ * Concept
+ *
+ * A hybrid signature computes two component signatures over the same message
+ * stream and then aggregates verification according to a configured rule:
+ *
+ *
+ * - AND - verification succeeds only if both component signatures
+ * verify.
+ * - OR - verification succeeds if at least one component signature
+ * verifies.
+ *
+ *
+ *
+ * The AND rule is intended for security-hardening scenarios (both schemes must
+ * hold). The OR rule is intended for migration/fallback scenarios (accept if at
+ * least one scheme verifies), and should be used with clear policy and
+ * operational intent.
+ *
+ *
+ * Main types
+ *
+ * - {@link zeroecho.sdk.hybrid.signature.HybridSignatureProfile} - immutable
+ * configuration describing the two component algorithms, optional per-algorithm
+ * specs, and the verification rule.
+ * - {@link zeroecho.sdk.hybrid.signature.HybridSignatureContexts} - factory
+ * methods for creating hybrid signing and verification contexts from keys and a
+ * {@link zeroecho.sdk.hybrid.signature.HybridSignatureProfile}.
+ * - {@link zeroecho.sdk.hybrid.signature.HybridSignatureContext} - the
+ * streaming hybrid {@link zeroecho.core.context.SignatureContext}
+ * implementation that computes/verifies two signatures in a single pass.
+ *
+ *
+ * Integration with pipeline builders
+ *
+ * The hybrid signature contexts created by this package are intended to be used
+ * as engines in trailer-oriented pipeline stages. In particular:
+ *
+ *
+ * - {@link zeroecho.sdk.builders.SignatureTrailerDataContentBuilder} can
+ * construct hybrid contexts directly and is the preferred signature-specialized
+ * builder API.
+ * - {@link zeroecho.sdk.builders.TagTrailerDataContentBuilder} can also be
+ * used with a hybrid {@link zeroecho.core.context.SignatureContext} when
+ * generic tag handling is desired.
+ *
+ *
+ * Streaming and resource management
+ *
+ * Hybrid signature computation and verification are streaming operations. The
+ * resulting contexts and streams must be closed to release resources and to
+ * finalize tag/signature production or verification.
+ *
+ *
+ * Security notes
+ *
+ * - Hybrid verification should be configured explicitly for mismatch handling
+ * (throw-on-mismatch vs. capture/flag) at the pipeline layer. This package
+ * focuses on computing/verifying the two component signatures and returning an
+ * aggregated result.
+ * - Implementations must not log or otherwise expose sensitive material
+ * (private keys, seeds, message contents, intermediate state).
+ *
+ *
+ * Thread safety
+ *
+ * Context instances are not thread-safe and are intended for single-use in a
+ * single pipeline execution. Create a new context instance for each independent
+ * signing or verification operation.
+ *
+ *
+ * @since 1.0
+ */
+package zeroecho.sdk.hybrid.signature;
\ No newline at end of file
diff --git a/lib/src/main/resources/jul.properties b/lib/src/main/resources/jul.properties
index 79d78cb..225ef76 100644
--- a/lib/src/main/resources/jul.properties
+++ b/lib/src/main/resources/jul.properties
@@ -3,6 +3,7 @@ handlers = java.util.logging.ConsoleHandler
zeroecho.core.tag.ByteVerificationStrategy.level = FINE
zeroecho.core.tag.SignatureVerificationStrategy.level = FINE
+zeroecho.sdk.hybrid.signature.level = FINE
# Console handler uses our one-line formatter
java.util.logging.ConsoleHandler.level = ALL
diff --git a/lib/src/test/java/zeroecho/sdk/hybrid/signature/HybridSignatureTest.java b/lib/src/test/java/zeroecho/sdk/hybrid/signature/HybridSignatureTest.java
new file mode 100644
index 0000000..c5efdc7
--- /dev/null
+++ b/lib/src/test/java/zeroecho/sdk/hybrid/signature/HybridSignatureTest.java
@@ -0,0 +1,601 @@
+/*******************************************************************************
+ * Copyright (C) 2025, 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.sdk.hybrid.signature;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.Key;
+import java.security.KeyPair;
+import java.security.Signature;
+import java.util.Arrays;
+import java.util.Random;
+
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import zeroecho.core.CryptoAlgorithms;
+import zeroecho.core.KeyUsage;
+import zeroecho.core.context.SignatureContext;
+import zeroecho.core.io.TailStrippingInputStream;
+import zeroecho.core.spec.ContextSpec;
+import zeroecho.sdk.builders.TagTrailerDataContentBuilder;
+import zeroecho.sdk.builders.core.DataContentBuilder;
+import zeroecho.sdk.builders.core.DataContentChainBuilder;
+import zeroecho.sdk.content.api.DataContent;
+import zeroecho.sdk.content.api.PlainContent;
+import zeroecho.sdk.util.BouncyCastleActivator;
+
+/**
+ * End-to-end tests for hybrid signatures (classic + PQC) across:
+ *
+ * - {@link HybridSignatureProfile.VerifyRule#AND} and
+ * {@link HybridSignatureProfile.VerifyRule#OR}
+ * - direct streaming use via {@link SignatureContext#wrap(InputStream)}
+ * - integration via {@link TagTrailerDataContentBuilder} and
+ * {@link DataContentChainBuilder}
+ *
+ *
+ *
+ * Tests focus on practical combinations: Ed25519 + SPHINCS+, and
+ * RSA-PSS(SHA-256) + SPHINCS+ (if registered).
+ *
+ */
+public class HybridSignatureTest {
+
+ @BeforeAll
+ static void setup() {
+ // Optional: enable BC if you use BC-only modes in KEM payloads (EAX/OCB/CCM,
+ // etc.)
+ try {
+ BouncyCastleActivator.init();
+ } catch (Throwable ignore) {
+ // keep tests runnable without BC if not present
+ }
+ }
+
+ // ---------- boilerplate logging ----------
+ private static void logBegin(Object... params) {
+ String thisClass = HybridSignatureTest.class.getName();
+ String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
+ .walk(frames -> frames
+ .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin"))
+ .findFirst().map(StackWalker.StackFrame::getMethodName).orElse(">"));
+ System.out.println(method + "(" + Arrays.deepToString(params) + ")");
+ }
+
+ private static void logEnd() {
+ String thisClass = HybridSignatureTest.class.getName();
+ String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
+ .walk(frames -> frames
+ .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd"))
+ .findFirst().map(StackWalker.StackFrame::getMethodName).orElse(">"));
+ System.out.println(method + "...ok");
+ }
+
+ private static void requireAlgOrSkip(String id) {
+ if (!CryptoAlgorithms.available().contains(id)) {
+ System.out.println("...*** SKIP *** " + id + " not registered");
+ Assumptions.assumeTrue(false);
+ }
+ }
+
+ private static byte[] randomBytes(int n) {
+ byte[] b = new byte[n];
+ Random r = new Random(123456789L); // deterministic
+ r.nextBytes(b);
+ return b;
+ }
+
+ private static byte[] readAll(InputStream in) throws Exception {
+ try (InputStream closeMe = in) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ closeMe.transferTo(out);
+ return out.toByteArray();
+ }
+ }
+
+ private static String hexShort(byte[] b) {
+ if (b == null) {
+ return "null";
+ }
+ int max = Math.min(b.length, 24);
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < max; i++) {
+ sb.append(String.format("%02x", Integer.valueOf(b[i] & 0xff)));
+ }
+ if (b.length > max) {
+ sb.append("...");
+ }
+ return sb.toString();
+ }
+
+ private static byte[] flipOneBit(byte[] in, int index) {
+ byte[] out = in.clone();
+ out[index] = (byte) (out[index] ^ 0x01);
+ return out;
+ }
+
+ private static byte[] sub(byte[] b, int off, int len) {
+ byte[] out = new byte[len];
+ System.arraycopy(b, off, out, 0, len);
+ return out;
+ }
+
+ private static byte[] concat(byte[] a, byte[] b) {
+ byte[] out = new byte[a.length + b.length];
+ System.arraycopy(a, 0, out, 0, a.length);
+ System.arraycopy(b, 0, out, a.length, b.length);
+ return out;
+ }
+
+ private static int tagLen(String algoId, KeyUsage role, Key key, ContextSpec specOrNull) throws Exception {
+ try (SignatureContext ctx = CryptoAlgorithms.create(algoId, role, key, specOrNull)) {
+ return ctx.tagLength();
+ }
+ }
+
+ private static byte[] signTrailer(SignatureContext signer, byte[] body) throws Exception {
+ int tagLen = signer.tagLength();
+ final byte[][] holder = new byte[1][];
+
+ try (InputStream in = new TailStrippingInputStream(signer.wrap(new ByteArrayInputStream(body)), tagLen, 8192) {
+ @Override
+ protected void processTail(byte[] tail) {
+ holder[0] = (tail == null ? null : tail.clone());
+ }
+ }) {
+ byte[] pt = readAll(in);
+ assertArrayEquals(body, pt, "sign passthrough mismatch");
+ }
+
+ if (holder[0] == null) {
+ throw new IllegalStateException("Signature trailer missing");
+ }
+ return holder[0];
+ }
+
+ /**
+ * Minimal source builder for DataContent chains (same shape as other tests).
+ */
+ private static final class BytesSourceBuilder implements DataContentBuilder {
+ private final byte[] data;
+
+ private BytesSourceBuilder(byte[] data) {
+ this.data = data.clone();
+ }
+
+ static BytesSourceBuilder of(byte[] data) {
+ return new BytesSourceBuilder(data);
+ }
+
+ @Override
+ public PlainContent build(boolean encrypt) {
+ return new PlainContent() {
+ @Override
+ public void setInput(DataContent input) {
+ /* no upstream for sources */
+ }
+
+ @Override
+ public InputStream getStream() {
+ return new ByteArrayInputStream(data);
+ }
+ };
+ }
+ }
+
+ // ======================================================================
+ // 1) DIRECT streaming tests (SignatureContext.wrap + transferTo)
+ // ======================================================================
+
+ @Test
+ void hybrid_ed25519_sphincsplus_direct_and_or_negative() throws Exception {
+ final int size = 64 * 1024 + 13;
+ logBegin("direct", "Ed25519+SPHINCS+", Integer.valueOf(size));
+
+ requireAlgOrSkip("Ed25519");
+ requireAlgOrSkip("SPHINCS+");
+
+ byte[] msg = randomBytes(size);
+ System.out.println("...msg=" + msg.length + " bytes");
+
+ KeyPair ed = CryptoAlgorithms.require("Ed25519").generateKeyPair();
+ KeyPair spx = CryptoAlgorithms.require("SPHINCS+").generateKeyPair();
+
+ int edLen = tagLen("Ed25519", KeyUsage.SIGN, ed.getPrivate(), null);
+ int spxLen = tagLen("SPHINCS+", KeyUsage.SIGN, spx.getPrivate(), null);
+ System.out.println("...classicTagLen=" + edLen + ", pqcTagLen=" + spxLen);
+
+ // ---- AND ----
+ HybridSignatureProfile andProfile = new HybridSignatureProfile("Ed25519", "SPHINCS+", null, null,
+ HybridSignatureProfile.VerifyRule.AND);
+
+ byte[] sigAnd;
+ try (SignatureContext signer = HybridSignatureContexts.sign(andProfile, ed.getPrivate(), spx.getPrivate(),
+ 2 * 1024 * 1024)) {
+ sigAnd = signTrailer(signer, msg);
+ }
+ System.out.println("...sig(AND).len=" + sigAnd.length + ", head=" + hexShort(sigAnd));
+
+ // verify OK
+ try (SignatureContext verifier = HybridSignatureContexts.verify(andProfile, ed.getPublic(), spx.getPublic(),
+ 2 * 1024 * 1024)) {
+ verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
+ verifier.setExpectedTag(sigAnd);
+ try (InputStream in = verifier.wrap(new ByteArrayInputStream(msg))) {
+ in.transferTo(OutputStream.nullOutputStream());
+ }
+ }
+ System.out.println("...verify(AND)=ok");
+
+ // corrupt classic => must fail
+ byte[] badClassic = concat(flipOneBit(sub(sigAnd, 0, edLen), 0), sub(sigAnd, edLen, spxLen));
+ try (SignatureContext verifier = HybridSignatureContexts.verify(andProfile, ed.getPublic(), spx.getPublic(),
+ 2 * 1024 * 1024)) {
+ verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
+ verifier.setExpectedTag(badClassic);
+ assertThrows(java.io.IOException.class, () -> {
+ try (InputStream in = verifier.wrap(new ByteArrayInputStream(msg))) {
+ in.transferTo(OutputStream.nullOutputStream());
+ }
+ });
+ }
+ System.out.println("...verify(AND) bad classic -> throws");
+
+ // corrupt pqc => must fail
+ byte[] badPqc = concat(sub(sigAnd, 0, edLen), flipOneBit(sub(sigAnd, edLen, spxLen), 0));
+ try (SignatureContext verifier = HybridSignatureContexts.verify(andProfile, ed.getPublic(), spx.getPublic(),
+ 2 * 1024 * 1024)) {
+ verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
+ verifier.setExpectedTag(badPqc);
+ assertThrows(java.io.IOException.class, () -> {
+ try (InputStream in = verifier.wrap(new ByteArrayInputStream(msg))) {
+ in.transferTo(OutputStream.nullOutputStream());
+ }
+ });
+ }
+ System.out.println("...verify(AND) bad pqc -> throws");
+
+ // ---- OR ----
+ HybridSignatureProfile orProfile = new HybridSignatureProfile("Ed25519", "SPHINCS+", null, null,
+ HybridSignatureProfile.VerifyRule.OR);
+
+ byte[] sigOr;
+ try (SignatureContext signer = HybridSignatureContexts.sign(orProfile, ed.getPrivate(), spx.getPrivate(),
+ 2 * 1024 * 1024)) {
+ sigOr = signTrailer(signer, msg);
+ }
+ System.out.println("...sig(OR).len=" + sigOr.length + ", head=" + hexShort(sigOr));
+
+ // corrupt classic => OR must pass
+ byte[] orBadClassic = concat(flipOneBit(sub(sigOr, 0, edLen), 0), sub(sigOr, edLen, spxLen));
+ try (SignatureContext verifier = HybridSignatureContexts.verify(orProfile, ed.getPublic(), spx.getPublic(),
+ 2 * 1024 * 1024)) {
+ verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
+ verifier.setExpectedTag(orBadClassic);
+ try (InputStream in = verifier.wrap(new ByteArrayInputStream(msg))) {
+ in.transferTo(OutputStream.nullOutputStream());
+ }
+ }
+ System.out.println("...verify(OR) bad classic -> ok");
+
+ // corrupt pqc => OR must pass
+ byte[] orBadPqc = concat(sub(sigOr, 0, edLen), flipOneBit(sub(sigOr, edLen, spxLen), 0));
+ try (SignatureContext verifier = HybridSignatureContexts.verify(orProfile, ed.getPublic(), spx.getPublic(),
+ 2 * 1024 * 1024)) {
+ verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
+ verifier.setExpectedTag(orBadPqc);
+ try (InputStream in = verifier.wrap(new ByteArrayInputStream(msg))) {
+ in.transferTo(OutputStream.nullOutputStream());
+ }
+ }
+ System.out.println("...verify(OR) bad pqc -> ok");
+
+ // corrupt both => OR must fail
+ byte[] orBadBoth = concat(flipOneBit(sub(sigOr, 0, edLen), 0), flipOneBit(sub(sigOr, edLen, spxLen), 0));
+ try (SignatureContext verifier = HybridSignatureContexts.verify(orProfile, ed.getPublic(), spx.getPublic(),
+ 2 * 1024 * 1024)) {
+ verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
+ verifier.setExpectedTag(orBadBoth);
+ assertThrows(java.io.IOException.class, () -> {
+ try (InputStream in = verifier.wrap(new ByteArrayInputStream(msg))) {
+ in.transferTo(OutputStream.nullOutputStream());
+ }
+ });
+ }
+ System.out.println("...verify(OR) bad both -> throws");
+
+ logEnd();
+ }
+
+ @Test
+ void hybrid_rsa_sphincsplus_direct_and_roundtrip() throws Exception {
+ final int size = 96 * 1024 + 3;
+ logBegin("direct", "RSA+SPHINCS+", Integer.valueOf(size));
+
+ requireAlgOrSkip("RSA");
+ requireAlgOrSkip("SPHINCS+");
+
+ byte[] msg = randomBytes(size);
+ System.out.println("...msg=" + msg.length + " bytes");
+
+ KeyPair rsa = CryptoAlgorithms.require("RSA").generateKeyPair();
+ KeyPair spx = CryptoAlgorithms.require("SPHINCS+").generateKeyPair();
+
+ int rsaLen = tagLen("RSA", KeyUsage.SIGN, rsa.getPrivate(), null);
+ int spxLen = tagLen("SPHINCS+", KeyUsage.SIGN, spx.getPrivate(), null);
+ System.out.println("...classicTagLen=" + rsaLen + ", pqcTagLen=" + spxLen);
+
+ HybridSignatureProfile profile = new HybridSignatureProfile("RSA", "SPHINCS+", null, null,
+ HybridSignatureProfile.VerifyRule.AND);
+
+ byte[] sig;
+ try (SignatureContext signer = HybridSignatureContexts.sign(profile, rsa.getPrivate(), spx.getPrivate(),
+ 2 * 1024 * 1024)) {
+ sig = signTrailer(signer, msg);
+ }
+ System.out.println("...sig.len=" + sig.length + ", head=" + hexShort(sig));
+
+ try (SignatureContext verifier = HybridSignatureContexts.verify(profile, rsa.getPublic(), spx.getPublic(),
+ 2 * 1024 * 1024)) {
+ verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
+ verifier.setExpectedTag(sig);
+ try (InputStream in = verifier.wrap(new ByteArrayInputStream(msg))) {
+ in.transferTo(OutputStream.nullOutputStream());
+ }
+ }
+ System.out.println("...verify(AND)=ok");
+
+ // negative sanity: corrupt classic => must fail (AND)
+ byte[] badClassic = concat(flipOneBit(sub(sig, 0, rsaLen), 0), sub(sig, rsaLen, spxLen));
+ try (SignatureContext verifier = HybridSignatureContexts.verify(profile, rsa.getPublic(), spx.getPublic(),
+ 2 * 1024 * 1024)) {
+ verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch());
+ verifier.setExpectedTag(badClassic);
+ assertThrows(java.io.IOException.class, () -> {
+ try (InputStream in = verifier.wrap(new ByteArrayInputStream(msg))) {
+ in.transferTo(OutputStream.nullOutputStream());
+ }
+ });
+ }
+ System.out.println("...verify(AND) bad classic -> throws");
+
+ logEnd();
+ }
+
+ // ======================================================================
+ // 2) TagTrailer integration tests (DataContentChainBuilder + TagTrailer)
+ // ======================================================================
+
+ @Test
+ void hybrid_ed25519_sphincsplus_via_tagtrailer_and_roundtrip() throws Exception {
+ final int size = 32 * 1024 + 7;
+ logBegin("TagTrailer", "AND", "Ed25519+SPHINCS+", Integer.valueOf(size));
+
+ requireAlgOrSkip("Ed25519");
+ requireAlgOrSkip("SPHINCS+");
+
+ byte[] msg = randomBytes(size);
+ System.out.println("...msg=" + msg.length + " bytes");
+
+ KeyPair ed = CryptoAlgorithms.require("Ed25519").generateKeyPair();
+ KeyPair spx = CryptoAlgorithms.require("SPHINCS+").generateKeyPair();
+
+ HybridSignatureProfile profile = new HybridSignatureProfile("Ed25519", "SPHINCS+", null, null,
+ HybridSignatureProfile.VerifyRule.AND);
+
+ byte[] out;
+ int tagLen;
+
+ try (SignatureContext tagEnc = HybridSignatureContexts.sign(profile, ed.getPrivate(), spx.getPrivate(),
+ 2 * 1024 * 1024)) {
+
+ DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg))
+ .add(new TagTrailerDataContentBuilder(tagEnc).bufferSize(8192)).build();
+
+ out = readAll(enc.getStream());
+ tagLen = tagEnc.tagLength();
+ }
+
+ System.out.println("...out=" + out.length + " bytes");
+
+ try (SignatureContext tagDec = HybridSignatureContexts.verify(profile, ed.getPublic(), spx.getPublic(),
+ 2 * 1024 * 1024)) {
+ tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch());
+
+ // IMPORTANT: TagTrailerDataContentBuilder supplies expectedTag internally
+ // during streaming.
+ // HybridSignatureContext.wrap must NOT require expectedTag pre-set.
+ DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(out))
+ .add(new TagTrailerDataContentBuilder(tagDec).bufferSize(8192).throwOnMismatch())
+ .build();
+
+ byte[] pt = readAll(dec.getStream());
+ assertArrayEquals(msg, pt, "hybrid TagTrailer AND roundtrip mismatch");
+ }
+
+ System.out.println("...tagLen=" + tagLen);
+ logEnd();
+ }
+
+ @Test
+ void hybrid_ed25519_sphincsplus_via_tagtrailer_or_negative() throws Exception {
+ final int size = 24 * 1024 + 9;
+ logBegin("TagTrailer", "OR", "Ed25519+SPHINCS+", Integer.valueOf(size));
+
+ requireAlgOrSkip("Ed25519");
+ requireAlgOrSkip("SPHINCS+");
+
+ byte[] msg = ("zeroecho-hybrid-or-negative-").getBytes(StandardCharsets.UTF_8);
+ msg = Arrays.copyOf(msg, size);
+ System.out.println("...msg=" + msg.length + " bytes");
+
+ KeyPair ed = CryptoAlgorithms.require("Ed25519").generateKeyPair();
+ KeyPair spx = CryptoAlgorithms.require("SPHINCS+").generateKeyPair();
+
+ int edLen = tagLen("Ed25519", KeyUsage.SIGN, ed.getPrivate(), null);
+ int spxLen = tagLen("SPHINCS+", KeyUsage.SIGN, spx.getPrivate(), null);
+ System.out.println("...classicTagLen=" + edLen + ", pqcTagLen=" + spxLen);
+
+ HybridSignatureProfile profile = new HybridSignatureProfile("Ed25519", "SPHINCS+", null, null,
+ HybridSignatureProfile.VerifyRule.OR);
+
+ byte[] out;
+ int tagLen;
+
+ try (SignatureContext tagEnc = HybridSignatureContexts.sign(profile, ed.getPrivate(), spx.getPrivate(),
+ 2 * 1024 * 1024)) {
+
+ DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg))
+ .add(new TagTrailerDataContentBuilder(tagEnc).bufferSize(8192)).build();
+
+ out = readAll(enc.getStream());
+ tagLen = tagEnc.tagLength();
+ }
+
+ byte[] body = sub(out, 0, out.length - tagLen);
+ byte[] tag = sub(out, out.length - tagLen, tagLen);
+
+ System.out.println("...tag.len=" + tag.length + ", head=" + hexShort(tag));
+
+ // Corrupt ONLY classic part => OR must still PASS
+ byte[] badClassic = concat(flipOneBit(sub(tag, 0, edLen), 0), sub(tag, edLen, spxLen));
+ byte[] outBadClassic = concat(body, badClassic);
+
+ try (SignatureContext tagDec = HybridSignatureContexts.verify(profile, ed.getPublic(), spx.getPublic(),
+ 2 * 1024 * 1024)) {
+ tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch());
+
+ DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(outBadClassic))
+ .add(new TagTrailerDataContentBuilder(tagDec).bufferSize(8192).throwOnMismatch())
+ .build();
+
+ byte[] pt = readAll(dec.getStream());
+ assertArrayEquals(msg, pt, "OR should accept when only classic signature is corrupted");
+ }
+
+ System.out.println("...OR verify with bad classic -> ok");
+
+ // Corrupt ONLY pqc part => OR must still PASS
+ byte[] badPqc = concat(sub(tag, 0, edLen), flipOneBit(sub(tag, edLen, spxLen), 0));
+ byte[] outBadPqc = concat(body, badPqc);
+
+ try (SignatureContext tagDec = HybridSignatureContexts.verify(profile, ed.getPublic(), spx.getPublic(),
+ 2 * 1024 * 1024)) {
+ tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch());
+
+ DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(outBadPqc))
+ .add(new TagTrailerDataContentBuilder(tagDec).bufferSize(8192).throwOnMismatch())
+ .build();
+
+ byte[] pt = readAll(dec.getStream());
+ assertArrayEquals(msg, pt, "OR should accept when only PQC signature is corrupted");
+ }
+
+ System.out.println("...OR verify with bad pqc -> ok");
+
+ // Corrupt BOTH => OR must FAIL
+ byte[] badBoth = concat(flipOneBit(sub(tag, 0, edLen), 0), flipOneBit(sub(tag, edLen, spxLen), 0));
+ byte[] outBadBoth = concat(body, badBoth);
+
+ try (SignatureContext tagDec = HybridSignatureContexts.verify(profile, ed.getPublic(), spx.getPublic(),
+ 2 * 1024 * 1024)) {
+ tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch());
+
+ DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(outBadBoth))
+ .add(new TagTrailerDataContentBuilder(tagDec).bufferSize(8192).throwOnMismatch())
+ .build();
+
+ assertThrows(java.io.IOException.class, () -> readAll(dec.getStream()));
+ }
+
+ System.out.println("...OR verify with bad both -> throws");
+ System.out.println("...tagLen=" + tagLen);
+
+ logEnd();
+ }
+
+ @Test
+ void hybrid_rsa_sphincsplus_via_tagtrailer_and_roundtrip() throws Exception {
+ final int size = 40 * 1024 + 1;
+ logBegin("TagTrailer", "AND", "RSA+SPHINCS+", Integer.valueOf(size));
+
+ requireAlgOrSkip("RSA");
+ requireAlgOrSkip("SPHINCS+");
+
+ byte[] msg = randomBytes(size);
+ System.out.println("...msg=" + msg.length + " bytes");
+
+ KeyPair rsa = CryptoAlgorithms.require("RSA").generateKeyPair();
+ KeyPair spx = CryptoAlgorithms.require("SPHINCS+").generateKeyPair();
+
+ HybridSignatureProfile profile = new HybridSignatureProfile("RSA", "SPHINCS+", null, null,
+ HybridSignatureProfile.VerifyRule.AND);
+
+ byte[] out;
+
+ try (SignatureContext tagEnc = HybridSignatureContexts.sign(profile, rsa.getPrivate(), spx.getPrivate(),
+ 2 * 1024 * 1024)) {
+
+ DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg))
+ .add(new TagTrailerDataContentBuilder(tagEnc).bufferSize(8192)).build();
+
+ out = readAll(enc.getStream());
+ }
+
+ System.out.println("...out=" + out.length + " bytes");
+
+ try (SignatureContext tagDec = HybridSignatureContexts.verify(profile, rsa.getPublic(), spx.getPublic(),
+ 2 * 1024 * 1024)) {
+ tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch());
+
+ DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(out))
+ .add(new TagTrailerDataContentBuilder(tagDec).bufferSize(8192).throwOnMismatch())
+ .build();
+
+ byte[] pt = readAll(dec.getStream());
+ assertArrayEquals(msg, pt, "hybrid TagTrailer AND (RSA+SPHINCS+) roundtrip mismatch");
+ }
+
+ logEnd();
+ }
+}
diff --git a/samples/src/test/java/demo/HybridSigningAesTest.java b/samples/src/test/java/demo/HybridSigningAesTest.java
new file mode 100644
index 0000000..f7d0a50
--- /dev/null
+++ b/samples/src/test/java/demo/HybridSigningAesTest.java
@@ -0,0 +1,240 @@
+/*******************************************************************************
+ * Copyright (C) 2025, 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 demo;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.SecureRandom;
+import java.security.Signature;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.crypto.SecretKey;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import zeroecho.core.CryptoAlgorithm;
+import zeroecho.core.CryptoAlgorithms;
+import zeroecho.core.context.SignatureContext;
+import zeroecho.sdk.builders.TagTrailerDataContentBuilder;
+import zeroecho.sdk.builders.alg.AesDataContentBuilder;
+import zeroecho.sdk.builders.core.DataContentChainBuilder;
+import zeroecho.sdk.builders.core.PlainBytesBuilder;
+import zeroecho.sdk.content.api.DataContent;
+import zeroecho.sdk.hybrid.signature.HybridSignatureContexts;
+import zeroecho.sdk.hybrid.signature.HybridSignatureProfile;
+import zeroecho.sdk.util.BouncyCastleActivator;
+
+/**
+ * Demonstration of hybrid signing combined with AES-GCM encryption.
+ *
+ *
+ * This sample shows both canonical compositions:
+ *
+ * - StE: Sign-then-Encrypt
+ * - EtS: Encrypt-then-Sign
+ *
+ *
+ *
+ *
+ * Hybrid signature used here (popular practical choice): Ed25519 + SPHINCS+
+ * with AND verification.
+ *
+ */
+class HybridSigningAesTest {
+
+ private static final Logger LOG = Logger.getLogger(HybridSigningAesTest.class.getName());
+
+ @BeforeAll
+ static void setup() {
+ // Optional: enable BC if you use BC-only modes in KEM payloads (EAX/OCB/CCM,
+ // etc.)
+ try {
+ BouncyCastleActivator.init();
+ } catch (Throwable ignore) {
+ // keep tests runnable without BC if not present
+ }
+ }
+
+ @Test
+ void aesRoundStE_withHybridSignature() throws GeneralSecurityException, IOException {
+ LOG.info("aesRoundStE_withHybridSignature - Sign then Encrypt (Hybrid signature)");
+
+ // Prepare plaintext
+ byte[] msg = randomBytes(100);
+
+ // AES-GCM with header, runtime params are stored in header
+ AesDataContentBuilder aesBuilder = AesDataContentBuilder.builder().generateKey(256).modeGcm(128).withHeader();
+
+ // Hybrid signature: Ed25519 + SPHINCS+ (AND)
+ HybridSignatureProfile profile = new HybridSignatureProfile("Ed25519", "SPHINCS+", null, null,
+ HybridSignatureProfile.VerifyRule.AND);
+
+ KeyPair ed = generateKeyPair("Ed25519");
+ KeyPair spx = generateKeyPair("SPHINCS+");
+
+ SignatureContext tagEnc = HybridSignatureContexts.sign(profile, ed.getPrivate(), spx.getPrivate(),
+ 2 * 1024 * 1024);
+ SignatureContext tagDec = HybridSignatureContexts.verify(profile, ed.getPublic(), spx.getPublic(),
+ 2 * 1024 * 1024);
+
+ // For verification, make mismatch behavior explicit (builder also supports
+ // throwOnMismatch()).
+ tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch());
+
+ // Build StE pipeline: PLAIN -> SIGN(trailer) -> ENCRYPT
+ DataContent dccb = DataContentChainBuilder.encrypt()
+ // plaintext source
+ .add(PlainBytesBuilder.builder().bytes(msg))
+ // hybrid signature trailer
+ .add(new TagTrailerDataContentBuilder(tagEnc).bufferSize(8192))
+ // AES-GCM encryption
+ .add(aesBuilder).build();
+
+ SecretKey aesKey = aesBuilder.generatedKey();
+ LOG.log(Level.INFO, "StE: produced ciphertext, aesKeySizeBits={0}",
+ Integer.valueOf(aesKey.getEncoded().length * 8));
+
+ byte[] ciphertext;
+ try (InputStream in = dccb.getStream()) {
+ ciphertext = readAll(in);
+ }
+ LOG.log(Level.INFO, "StE: ciphertextSize={0}", Integer.valueOf(ciphertext.length));
+
+ // Build decrypt pipeline: ENCRYPTED -> DECRYPT -> VERIFY(trailer)
+ dccb = DataContentChainBuilder.decrypt()
+ // encrypted input
+ .add(PlainBytesBuilder.builder().bytes(ciphertext))
+ // AES-GCM decryption
+ .add(AesDataContentBuilder.builder().importKeyRaw(aesKey.getEncoded()).modeGcm(128).withHeader())
+ // hybrid signature verification
+ .add(new TagTrailerDataContentBuilder(tagDec).bufferSize(8192).throwOnMismatch()).build();
+
+ byte[] decrypted;
+ try (InputStream in = dccb.getStream()) {
+ decrypted = readAll(in);
+ }
+
+ LOG.log(Level.INFO, "StE: roundtrip ok={0}", Boolean.valueOf(java.util.Arrays.equals(msg, decrypted)));
+ }
+
+ @Test
+ void aesRoundEtS_withHybridSignature() throws GeneralSecurityException, IOException {
+ LOG.info("aesRoundEtS_withHybridSignature - Encrypt then Sign (Hybrid signature)");
+
+ // Prepare plaintext
+ byte[] msg = randomBytes(100);
+
+ // AES-GCM with header, runtime params are stored in header
+ AesDataContentBuilder aesBuilder = AesDataContentBuilder.builder().generateKey(256).modeGcm(128).withHeader();
+
+ // Hybrid signature: Ed25519 + SPHINCS+ (AND)
+ HybridSignatureProfile profile = new HybridSignatureProfile("Ed25519", "SPHINCS+", null, null,
+ HybridSignatureProfile.VerifyRule.AND);
+
+ KeyPair ed = generateKeyPair("Ed25519");
+ KeyPair spx = generateKeyPair("SPHINCS+");
+
+ SignatureContext tagEnc = HybridSignatureContexts.sign(profile, ed.getPrivate(), spx.getPrivate(),
+ 2 * 1024 * 1024);
+ SignatureContext tagDec = HybridSignatureContexts.verify(profile, ed.getPublic(), spx.getPublic(),
+ 2 * 1024 * 1024);
+
+ tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch());
+
+ // Build EtS pipeline: PLAIN -> ENCRYPT -> SIGN(trailer)
+ DataContent dccb = DataContentChainBuilder.encrypt()
+ // plaintext source
+ .add(PlainBytesBuilder.builder().bytes(msg))
+ // AES-GCM encryption
+ .add(aesBuilder)
+ // hybrid signature trailer
+ .add(new TagTrailerDataContentBuilder(tagEnc).bufferSize(8192)).build();
+
+ SecretKey aesKey = aesBuilder.generatedKey();
+ LOG.log(Level.INFO, "EtS: produced ciphertext, aesKeySizeBits={0}",
+ Integer.valueOf(aesKey.getEncoded().length * 8));
+
+ byte[] ciphertext;
+ try (InputStream in = dccb.getStream()) {
+ ciphertext = readAll(in);
+ }
+ LOG.log(Level.INFO, "EtS: ciphertextSize={0}", Integer.valueOf(ciphertext.length));
+
+ // Build decrypt pipeline: ENCRYPTED -> VERIFY(trailer) -> DECRYPT
+ // Verification runs during streaming; consumer gets plaintext only if signature
+ // matches.
+ dccb = DataContentChainBuilder.decrypt()
+ // encrypted input
+ .add(PlainBytesBuilder.builder().bytes(ciphertext))
+ // hybrid signature verification
+ .add(new TagTrailerDataContentBuilder(tagDec).bufferSize(8192).throwOnMismatch())
+ // AES-GCM decryption
+ .add(AesDataContentBuilder.builder().importKeyRaw(aesKey.getEncoded()).modeGcm(128).withHeader())
+ .build();
+
+ byte[] decrypted;
+ try (InputStream in = dccb.getStream()) {
+ decrypted = readAll(in);
+ }
+
+ LOG.log(Level.INFO, "EtS: roundtrip ok={0}", Boolean.valueOf(java.util.Arrays.equals(msg, decrypted)));
+ }
+
+ // helpers
+
+ private static KeyPair generateKeyPair(String algId) throws GeneralSecurityException {
+ CryptoAlgorithm alg = CryptoAlgorithms.require(algId);
+ return alg.generateKeyPair();
+ }
+
+ private static byte[] randomBytes(int len) {
+ byte[] data = new byte[len];
+ new SecureRandom().nextBytes(data);
+ return data;
+ }
+
+ private static byte[] readAll(InputStream in) throws IOException {
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ in.transferTo(out);
+ out.flush();
+ return out.toByteArray();
+ }
+ }
+}
diff --git a/samples/src/test/java/demo/SigningAesTest.java b/samples/src/test/java/demo/SigningAesTest.java
index a289829..bdf880e 100644
--- a/samples/src/test/java/demo/SigningAesTest.java
+++ b/samples/src/test/java/demo/SigningAesTest.java
@@ -48,9 +48,7 @@ import javax.crypto.SecretKey;
import org.junit.jupiter.api.Test;
-import zeroecho.core.CryptoAlgorithm;
import zeroecho.core.CryptoAlgorithms;
-import zeroecho.core.alg.aes.AesKeyGenSpec;
import zeroecho.core.alg.rsa.RsaKeyGenSpec;
import zeroecho.core.alg.rsa.RsaSigSpec;
import zeroecho.core.tag.TagEngine;
@@ -65,23 +63,6 @@ import zeroecho.sdk.content.api.DataContent;
class SigningAesTest {
private static final Logger LOG = Logger.getLogger(SigningAesTest.class.getName());
- SecretKey generateAesKey() throws GeneralSecurityException {
- // Locate the AES algorithm in the catalog
- CryptoAlgorithm aes = CryptoAlgorithms.require("AES");
- SecretKey key = aes
- // Retrieve the builder that works with AesKeyGenSpec - the specification for
- // AES key generation
- .symmetricKeyBuilder(AesKeyGenSpec.class)
- // Generate a secret key according to the AES256 specification
- .generateSecret(AesKeyGenSpec.aes256());
- // Log the generated key (truncated to short hex for readability)
- LOG.log(Level.INFO, "AES256 key generated: {0}", Strings.toShortHexString(key.getEncoded()));
-
- // or just: CryptoAlgorithms.generateSecret("AES", AesKeyGenSpec.aes256())
-
- return key;
- }
-
KeyPair generateRsaKeys() throws GeneralSecurityException {
KeyPair kp = CryptoAlgorithms.generateKeyPair("RSA", RsaKeyGenSpec.rsa4096());