feat: introduce hybrid signature framework and signature trailer builder

Add a complete hybrid signature implementation combining two independent
signature algorithms with AND/OR verification semantics, designed for
streaming pipelines.

Key changes:
- Add zeroecho.sdk.hybrid.signature package with core hybrid signature
  abstractions (HybridSignatureContext, HybridSignatureProfile,
  factories, predicates, and package documentation).
- Introduce SignatureTrailerDataContentBuilder as a
  signature-specialized replacement for
  TagTrailerDataContentBuilder<Signature>, supporting
  core, single-algorithm, and hybrid signature construction.
- Extend sdk.builders package documentation to reference the new
  signature trailer builder and newly added PQC signature builders.
- Adjust TagEngineBuilder where required to support hybrid verification
  integration.
- Update JUL configuration to accommodate hybrid signature diagnostics
  without leaking sensitive material.

Tests and samples:
- Add comprehensive JUnit 5 tests covering hybrid signatures in all
  supported modes, including positive and negative cases.
- Add a dedicated sample demonstrating hybrid signing combined with AES
  encryption (StE and EtS).
- Update existing signing samples to reflect the new signature trailer
  builder.

The changes introduce a unified, extensible hybrid signature model
without breaking existing core APIs or pipeline composition patterns.

Signed-off-by: Leo Galambos <lg@hq.egothor.org>
This commit is contained in:
2025-12-26 02:01:29 +01:00
parent 174d63dff4
commit 7f79082adc
14 changed files with 2756 additions and 31 deletions

View File

@@ -89,6 +89,14 @@ import zeroecho.core.spec.VoidSpec;
* @since 1.0
*/
public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
/**
*
*/
private static final String PUBLIC_KEY = "publicKey";
/**
*
*/
private static final String PRIVATE_KEY = "privateKey";
private final Supplier<TagEngine<T>> factory;
private TagEngineBuilder(Supplier<TagEngine<T>> factory) {
@@ -205,7 +213,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
* @throws NullPointerException if {@code privateKey} is {@code null}
*/
public static TagEngineBuilder<Signature> 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<T> implements Supplier<TagEngine<T>> {
* @throws NullPointerException if {@code publicKey} is {@code null}
*/
public static TagEngineBuilder<Signature> 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<T> implements Supplier<TagEngine<T>> {
* @throws NullPointerException if {@code privateKey} is {@code null}
*/
public static TagEngineBuilder<Signature> 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<T> implements Supplier<TagEngine<T>> {
* @throws NullPointerException if {@code publicKey} is {@code null}
*/
public static TagEngineBuilder<Signature> 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<T> implements Supplier<TagEngine<T>> {
* @throws NullPointerException if {@code privateKey} is {@code null}
*/
public static TagEngineBuilder<Signature> 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<T> implements Supplier<TagEngine<T>> {
* @throws NullPointerException if {@code publicKey} is {@code null}
*/
public static TagEngineBuilder<Signature> 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<T> implements Supplier<TagEngine<T>> {
* @throws NullPointerException if {@code privateKey} is {@code null}
*/
public static TagEngineBuilder<Signature> 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<T> implements Supplier<TagEngine<T>> {
* @throws NullPointerException if {@code publicKey} is {@code null}
*/
public static TagEngineBuilder<Signature> 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<T> implements Supplier<TagEngine<T>> {
* @throws NullPointerException if {@code privateKey} is {@code null}
*/
public static TagEngineBuilder<Signature> 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<T> implements Supplier<TagEngine<T>> {
* @throws NullPointerException if {@code publicKey} is {@code null}
*/
public static TagEngineBuilder<Signature> 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.
*
* <p>
* 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.
* </p>
*
* @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<Signature> 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.
*
* <p>
* 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.
* </p>
*
* @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<Signature> 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.
*
* <p>
* 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.
* </p>
*
* @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<Signature> 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.
*
* <p>
* 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.
* </p>
*
* @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<Signature> mldsaVerify(final PublicKey publicKey) {
Objects.requireNonNull(publicKey, PUBLIC_KEY);
return signature("ML-DSA", publicKey, VoidSpec.INSTANCE);
}
}

View File

@@ -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.
*
* <p>
* This class is intended as a convenient, signature-specialized replacement
* for:
* </p>
*
* <pre>
* new TagTrailerDataContentBuilder&lt;Signature&gt;(engine).bufferSize(8192)
* new TagTrailerDataContentBuilder&lt;Signature&gt;(engine).bufferSize(8192).throwOnMismatch()
* </pre>
*
* <p>
* 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.
* </p>
*
* <h2>Mode selection</h2>
* <ul>
* <li>{@link #core(TagEngine)} / {@link #core(Supplier)}: wraps a ready engine
* (same parameters as {@link TagTrailerDataContentBuilder}).</li>
* <li>{@link #single()}: constructs a non-hybrid {@code SignatureContext} via
* {@link CryptoAlgorithms}.</li>
* <li>{@link #hybrid()}: constructs a hybrid {@code SignatureContext} via
* {@link HybridSignatureContexts}.</li>
* </ul>
*
* <h2>Checked exceptions</h2>
* <p>
* 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.
* </p>
*
* @since 1.0
*/
public final class SignatureTrailerDataContentBuilder implements DataContentBuilder<DataContent> {
private final TagTrailerDataContentBuilder<Signature> delegate;
private SignatureTrailerDataContentBuilder(TagEngine<Signature> engine) {
this.delegate = new TagTrailerDataContentBuilder<>(engine);
}
private SignatureTrailerDataContentBuilder(Supplier<? extends TagEngine<Signature>> engineFactory) {
this.delegate = new TagTrailerDataContentBuilder<>(engineFactory);
}
/**
* Core mode: wraps a fixed engine instance.
*
* <p>
* This is the direct signature-specialized equivalent of:
* {@code new TagTrailerDataContentBuilder<Signature>(engine)}.
* </p>
*
* @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<Signature> engine) {
Objects.requireNonNull(engine, "engine");
return new SignatureTrailerDataContentBuilder(engine);
}
/**
* Core mode: wraps an engine factory.
*
* <p>
* This is the direct signature-specialized equivalent of:
* {@code new TagTrailerDataContentBuilder<Signature>(engineFactory)}.
* </p>
*
* @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<Signature>> 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<Boolean> 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<TagEngine<Signature>> 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<TagEngine<Signature>> 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<TagEngine<Signature>> 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<TagEngine<Signature>> 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);
}
}
}

View File

@@ -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}.</li>
* {@link zeroecho.sdk.builders.alg.SphincsPlusDataContentBuilder},
* {@link zeroecho.sdk.builders.alg.MldsaDataContentBuilder},
* {@link zeroecho.sdk.builders.alg.SlhDsaDataContentBuilder}.</li>
* <li>MAC and digest: {@link zeroecho.sdk.builders.alg.HmacDataContentBuilder},
* {@link zeroecho.sdk.builders.alg.DigestDataContentBuilder}.</li>
* <li>KEM envelopes: {@link zeroecho.sdk.builders.alg.KemDataContentBuilder}
@@ -86,6 +88,11 @@
* <li>{@link TagTrailerDataContentBuilder} - appends or verifies an
* authentication tag carried as an input trailer using a
* {@link zeroecho.core.tag.TagEngine}.</li>
* <li>{@link SignatureTrailerDataContentBuilder} - signature-specialized
* trailer builder intended to replace
* {@code TagTrailerDataContentBuilder<Signature>} in most signature use cases.
* It can wrap existing signature engines and construct single-algorithm and
* hybrid signature contexts.</li>
* </ul>
* </li>
* </ul>
@@ -107,6 +114,9 @@
* signatures or tags.</li>
* <li>{@link TagTrailerDataContentBuilder} focuses on trailer-style tags with
* explicit verify policies.</li>
* <li>{@link SignatureTrailerDataContentBuilder} provides the corresponding
* trailer functionality specialized for digital signatures, including hybrid
* signature construction.</li>
* </ul>
*
* <h2>Typical usage</h2> <pre>{@code

View File

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

View File

@@ -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.
*
* <p>
* 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}).
* </p>
*
* <p>
* 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.
* </p>
*
* <h2>Logging</h2>
* <p>
* For diagnostic purposes, a {@link Level#FINE} log entry is emitted on each
* invocation containing:
* </p>
* <ul>
* <li>a short hexadecimal prefix of {@code expectedTag} (bounded and
* truncated), and</li>
* <li>the resulting verification decision ({@code true}/{@code false}).</li>
* </ul>
*
* <p>
* 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.
* </p>
*
* <h2>Threading and side effects</h2>
* <ul>
* <li>This class is immutable with respect to its own fields.</li>
* <li>The {@code ok} flag is updated exactly once per invocation with the
* result returned by this method (including failures caused by suppressed
* exceptions).</li>
* <li>Correctness depends on coordinated usage of the shared
* {@link AtomicBoolean} when used concurrently.</li>
* </ul>
*
* <p>
* 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.
* </p>
*
* @since 1.0
*/
final class CapturePredicate extends VerificationBiPredicate<Signature> {
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<Signature> 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<Signature> 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}.
*
* <p>
* 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.
* </p>
*
* <p>
* A {@link Level#FINE} log entry is emitted with a truncated hexadecimal prefix
* of {@code expectedTag} and the resulting decision.
* </p>
*
* @param signature the signature object to verify
* @param expectedTag the expected signature tag (may be {@code null}, in which
* case {@code "<null>"} 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.
*
* <p>
* At most {@link #TAG_LOG_PREFIX_BYTES} bytes are included. If the tag is
* longer, the output is suffixed with {@code "..."}.
* </p>
*
* <p>
* The output format uses lowercase hexadecimal digits without separators.
* </p>
*
* @param tag tag bytes to format (may be {@code null})
* @return a bounded, log-safe hexadecimal prefix, or {@code "<null>"} if
* {@code tag} is {@code null}
*/
private static String formatTagPrefix(byte[] tag) {
if (tag == null) {
return "<null>";
}
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();
}
}

View File

@@ -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.
*
* <p>
* 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.
* </p>
*
* <p>
* 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.
* </p>
*
* <h2>Design notes</h2>
* <ul>
* <li>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.</li>
* <li>This class is immutable and thread-safe with respect to its own state;
* however, correctness depends on coordinated usage with the associated hybrid
* stream.</li>
* <li>The predicate must be used only in conjunction with a hybrid signature
* stream that updates the shared {@code lastOk} flag.</li>
* </ul>
*
* <p>
* 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.
* </p>
*
* @since 1.0
*/
final class HybridCorePredicate extends VerificationBiPredicate<Signature> {
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.
*
* <p>
* 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.
* </p>
*
* @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;
}
}

View File

@@ -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.
*
* <p>
* 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.
* </p>
*
* <h3>Streaming semantics</h3>
* <p>
* 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.
* </p>
*
* <p>
* 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.
* </p>
*
* <h3>Verification aggregation</h3>
* <p>
* 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.).
* </p>
*
* @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<Signature> hybridCore;
private VerificationBiPredicate<Signature> 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<Signature> approach) {
this.verificationApproach = Objects.requireNonNull(approach, "approach");
}
@Override
public VerificationBiPredicate<Signature> 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.
*
* <p>
* 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:
* </p>
*
* <ul>
* <li><b>Produce mode</b> ({@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.</li>
* <li><b>Verify mode</b> ({@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.</li>
* </ul>
*
* <h2>Lifecycle and invariants</h2>
* <ul>
* <li>EOF finalization ({@link #finishAtEof()}) is executed at most once,
* guarded by {@link #eofSeen}.</li>
* <li>In produce mode, the trailer is emitted strictly after all upstream
* bytes.</li>
* <li>In verify mode, the stream ends immediately after the upstream ends.</li>
* <li>This class is not thread-safe and assumes sequential consumption.</li>
* </ul>
*
* <p>
* 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).
* </p>
*/
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.
*
* <p>
* Delegates to {@link #read(byte[], int, int)} and follows the standard
* {@link InputStream} contract.
* </p>
*
* @return the next byte of data as an unsigned value in the range
* {@code 0255}, 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}.
*
* <p>
* 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).
* </p>
*
* @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.
*
* <p>
* Note: closing this stream closes the wrapped {@code upstream} stream.
* </p>
*
* @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.
*
* <p>
* The buffer is bounded by {@code maxBufferedBytes}. If appending would exceed
* the configured limit, the method fails fast with an {@link IOException}.
* </p>
*
* @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.
*
* <p>
* This method is invoked exactly once upon the first observation of upstream
* EOF. It uses the buffered body to either:
* </p>
*
* <ul>
* <li><b>Produce mode</b>: 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.</li>
* <li><b>Verify mode</b>: 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.</li>
* </ul>
*
* @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.
*
* <p>
* 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.
* </p>
*
* @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.
*
* <p>
* Instances are created only after the expected tag length is validated and
* then used to feed the per-algorithm verification routines.
* </p>
*
* <p>
* Security note: this structure stores signature bytes; it must not be logged
* or exposed outside the narrow verification flow.
* </p>
*/
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;
}
}

View File

@@ -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.
*
* <p>
* The returned contexts implement the standard ZeroEcho streaming contract:
* wrapping a stream produces/verifies a signature trailer at EOF.
* </p>
*
* @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);
}
}

View File

@@ -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:
* <ul>
* <li>which two signature algorithms participate in the hybrid
* composition,</li>
* <li>their fixed ordering within the produced signature trailer,</li>
* <li>the verification aggregation rule applied to their results.</li>
* </ul>
*
* <p>
* The profile is the <em>single source of truth</em> 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.
* </p>
*
* <p>
* Instances of this record are immutable and thread-safe.
* </p>
*
* @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.
*
* <p>
* Uses {@link Objects#requireNonNull(Object, String)} to enforce mandatory
* components while keeping validation idiomatic and consistent with the rest of
* the ZeroEcho codebase.
* </p>
*
* @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");
}
}

View File

@@ -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.
*
* <p>
* 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.
* </p>
*
* <h2>Concept</h2>
* <p>
* A hybrid signature computes two component signatures over the same message
* stream and then aggregates verification according to a configured rule:
* </p>
* <ul>
* <li><b>AND</b> - verification succeeds only if both component signatures
* verify.</li>
* <li><b>OR</b> - verification succeeds if at least one component signature
* verifies.</li>
* </ul>
*
* <p>
* 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.
* </p>
*
* <h2>Main types</h2>
* <ul>
* <li>{@link zeroecho.sdk.hybrid.signature.HybridSignatureProfile} - immutable
* configuration describing the two component algorithms, optional per-algorithm
* specs, and the verification rule.</li>
* <li>{@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}.</li>
* <li>{@link zeroecho.sdk.hybrid.signature.HybridSignatureContext} - the
* streaming hybrid {@link zeroecho.core.context.SignatureContext}
* implementation that computes/verifies two signatures in a single pass.</li>
* </ul>
*
* <h2>Integration with pipeline builders</h2>
* <p>
* The hybrid signature contexts created by this package are intended to be used
* as engines in trailer-oriented pipeline stages. In particular:
* </p>
* <ul>
* <li>{@link zeroecho.sdk.builders.SignatureTrailerDataContentBuilder} can
* construct hybrid contexts directly and is the preferred signature-specialized
* builder API.</li>
* <li>{@link zeroecho.sdk.builders.TagTrailerDataContentBuilder} can also be
* used with a hybrid {@link zeroecho.core.context.SignatureContext} when
* generic tag handling is desired.</li>
* </ul>
*
* <h2>Streaming and resource management</h2>
* <p>
* 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.
* </p>
*
* <h2>Security notes</h2>
* <ul>
* <li>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.</li>
* <li>Implementations must not log or otherwise expose sensitive material
* (private keys, seeds, message contents, intermediate state).</li>
* </ul>
*
* <h2>Thread safety</h2>
* <p>
* 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.
* </p>
*
* @since 1.0
*/
package zeroecho.sdk.hybrid.signature;

View File

@@ -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

View File

@@ -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:
* <ul>
* <li>{@link HybridSignatureProfile.VerifyRule#AND} and
* {@link HybridSignatureProfile.VerifyRule#OR}</li>
* <li>direct streaming use via {@link SignatureContext#wrap(InputStream)}</li>
* <li>integration via {@link TagTrailerDataContentBuilder} and
* {@link DataContentChainBuilder}</li>
* </ul>
*
* <p>
* Tests focus on practical combinations: Ed25519 + SPHINCS+, and
* RSA-PSS(SHA-256) + SPHINCS+ (if registered).
* </p>
*/
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<PlainContent> {
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<Signature>(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<Signature>(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<Signature>(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<Signature>(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<Signature>(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<Signature>(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<Signature>(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<Signature>(tagDec).bufferSize(8192).throwOnMismatch())
.build();
byte[] pt = readAll(dec.getStream());
assertArrayEquals(msg, pt, "hybrid TagTrailer AND (RSA+SPHINCS+) roundtrip mismatch");
}
logEnd();
}
}

View File

@@ -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.
*
* <p>
* This sample shows both canonical compositions:
* <ul>
* <li>StE: Sign-then-Encrypt</li>
* <li>EtS: Encrypt-then-Sign</li>
* </ul>
* </p>
*
* <p>
* Hybrid signature used here (popular practical choice): Ed25519 + SPHINCS+
* with AND verification.
* </p>
*/
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<Signature>(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<Signature>(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<Signature>(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<Signature>(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();
}
}
}

View File

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