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:
@@ -89,6 +89,14 @@ import zeroecho.core.spec.VoidSpec;
|
|||||||
* @since 1.0
|
* @since 1.0
|
||||||
*/
|
*/
|
||||||
public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
|
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 final Supplier<TagEngine<T>> factory;
|
||||||
|
|
||||||
private TagEngineBuilder(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}
|
* @throws NullPointerException if {@code privateKey} is {@code null}
|
||||||
*/
|
*/
|
||||||
public static TagEngineBuilder<Signature> ed25519Sign(final PrivateKey privateKey) {
|
public static TagEngineBuilder<Signature> ed25519Sign(final PrivateKey privateKey) {
|
||||||
Objects.requireNonNull(privateKey, "privateKey");
|
Objects.requireNonNull(privateKey, PRIVATE_KEY);
|
||||||
return signature("Ed25519", privateKey, VoidSpec.INSTANCE);
|
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}
|
* @throws NullPointerException if {@code publicKey} is {@code null}
|
||||||
*/
|
*/
|
||||||
public static TagEngineBuilder<Signature> ed25519Verify(final PublicKey publicKey) {
|
public static TagEngineBuilder<Signature> ed25519Verify(final PublicKey publicKey) {
|
||||||
Objects.requireNonNull(publicKey, "publicKey");
|
Objects.requireNonNull(publicKey, PUBLIC_KEY);
|
||||||
return signature("Ed25519", publicKey, VoidSpec.INSTANCE);
|
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}
|
* @throws NullPointerException if {@code privateKey} is {@code null}
|
||||||
*/
|
*/
|
||||||
public static TagEngineBuilder<Signature> rsaSign(final PrivateKey privateKey, final RsaSigSpec spec) {
|
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);
|
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}
|
* @throws NullPointerException if {@code publicKey} is {@code null}
|
||||||
*/
|
*/
|
||||||
public static TagEngineBuilder<Signature> rsaVerify(final PublicKey publicKey, final RsaSigSpec spec) {
|
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);
|
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}
|
* @throws NullPointerException if {@code privateKey} is {@code null}
|
||||||
*/
|
*/
|
||||||
public static TagEngineBuilder<Signature> ecdsaSign(final PrivateKey privateKey, final EcdsaCurveSpec spec) {
|
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;
|
final EcdsaCurveSpec s = spec == null ? EcdsaCurveSpec.P256 : spec;
|
||||||
return signature("ECDSA", privateKey, s);
|
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}
|
* @throws NullPointerException if {@code publicKey} is {@code null}
|
||||||
*/
|
*/
|
||||||
public static TagEngineBuilder<Signature> ecdsaVerify(final PublicKey publicKey, final EcdsaCurveSpec spec) {
|
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;
|
final EcdsaCurveSpec s = spec == null ? EcdsaCurveSpec.P256 : spec;
|
||||||
return signature("ECDSA", publicKey, s);
|
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}
|
* @throws NullPointerException if {@code privateKey} is {@code null}
|
||||||
*/
|
*/
|
||||||
public static TagEngineBuilder<Signature> ecdsaP256Sign(final PrivateKey privateKey) {
|
public static TagEngineBuilder<Signature> ecdsaP256Sign(final PrivateKey privateKey) {
|
||||||
Objects.requireNonNull(privateKey, "privateKey");
|
Objects.requireNonNull(privateKey, PRIVATE_KEY);
|
||||||
return signature("ECDSA", privateKey, EcdsaCurveSpec.P256);
|
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}
|
* @throws NullPointerException if {@code publicKey} is {@code null}
|
||||||
*/
|
*/
|
||||||
public static TagEngineBuilder<Signature> ecdsaP256Verify(final PublicKey publicKey) {
|
public static TagEngineBuilder<Signature> ecdsaP256Verify(final PublicKey publicKey) {
|
||||||
Objects.requireNonNull(publicKey, "publicKey");
|
Objects.requireNonNull(publicKey, PUBLIC_KEY);
|
||||||
return signature("ECDSA", publicKey, EcdsaCurveSpec.P256);
|
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}
|
* @throws NullPointerException if {@code privateKey} is {@code null}
|
||||||
*/
|
*/
|
||||||
public static TagEngineBuilder<Signature> sphincsPlusSign(final PrivateKey privateKey) {
|
public static TagEngineBuilder<Signature> sphincsPlusSign(final PrivateKey privateKey) {
|
||||||
Objects.requireNonNull(privateKey, "privateKey");
|
Objects.requireNonNull(privateKey, PRIVATE_KEY);
|
||||||
return signature("SPHINCS+", privateKey, VoidSpec.INSTANCE);
|
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}
|
* @throws NullPointerException if {@code publicKey} is {@code null}
|
||||||
*/
|
*/
|
||||||
public static TagEngineBuilder<Signature> sphincsPlusVerify(final PublicKey publicKey) {
|
public static TagEngineBuilder<Signature> sphincsPlusVerify(final PublicKey publicKey) {
|
||||||
Objects.requireNonNull(publicKey, "publicKey");
|
Objects.requireNonNull(publicKey, PUBLIC_KEY);
|
||||||
return signature("SPHINCS+", publicKey, VoidSpec.INSTANCE);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<Signature>(engine).bufferSize(8192)
|
||||||
|
* new TagTrailerDataContentBuilder<Signature>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -73,7 +73,9 @@
|
|||||||
* {@link zeroecho.sdk.builders.alg.EcdsaDataContentBuilder},
|
* {@link zeroecho.sdk.builders.alg.EcdsaDataContentBuilder},
|
||||||
* {@link zeroecho.sdk.builders.alg.Ed25519DataContentBuilder},
|
* {@link zeroecho.sdk.builders.alg.Ed25519DataContentBuilder},
|
||||||
* {@link zeroecho.sdk.builders.alg.Ed448DataContentBuilder},
|
* {@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},
|
* <li>MAC and digest: {@link zeroecho.sdk.builders.alg.HmacDataContentBuilder},
|
||||||
* {@link zeroecho.sdk.builders.alg.DigestDataContentBuilder}.</li>
|
* {@link zeroecho.sdk.builders.alg.DigestDataContentBuilder}.</li>
|
||||||
* <li>KEM envelopes: {@link zeroecho.sdk.builders.alg.KemDataContentBuilder}
|
* <li>KEM envelopes: {@link zeroecho.sdk.builders.alg.KemDataContentBuilder}
|
||||||
@@ -86,6 +88,11 @@
|
|||||||
* <li>{@link TagTrailerDataContentBuilder} - appends or verifies an
|
* <li>{@link TagTrailerDataContentBuilder} - appends or verifies an
|
||||||
* authentication tag carried as an input trailer using a
|
* authentication tag carried as an input trailer using a
|
||||||
* {@link zeroecho.core.tag.TagEngine}.</li>
|
* {@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>
|
* </ul>
|
||||||
* </li>
|
* </li>
|
||||||
* </ul>
|
* </ul>
|
||||||
@@ -107,6 +114,9 @@
|
|||||||
* signatures or tags.</li>
|
* signatures or tags.</li>
|
||||||
* <li>{@link TagTrailerDataContentBuilder} focuses on trailer-style tags with
|
* <li>{@link TagTrailerDataContentBuilder} focuses on trailer-style tags with
|
||||||
* explicit verify policies.</li>
|
* explicit verify policies.</li>
|
||||||
|
* <li>{@link SignatureTrailerDataContentBuilder} provides the corresponding
|
||||||
|
* trailer functionality specialized for digital signatures, including hybrid
|
||||||
|
* signature construction.</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* <h2>Typical usage</h2> <pre>{@code
|
* <h2>Typical usage</h2> <pre>{@code
|
||||||
@@ -136,4 +146,4 @@
|
|||||||
*
|
*
|
||||||
* @since 1.0
|
* @since 1.0
|
||||||
*/
|
*/
|
||||||
package zeroecho.sdk.builders;
|
package zeroecho.sdk.builders;
|
||||||
|
|||||||
63
lib/src/main/java/zeroecho/sdk/hybrid/HybridException.java
Normal file
63
lib/src/main/java/zeroecho/sdk/hybrid/HybridException.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 0–255}, or {@code -1} if the stream is exhausted (including
|
||||||
|
* any trailer)
|
||||||
|
* @throws IOException if an I/O error occurs or EOF finalization fails
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException {
|
||||||
|
byte[] one = new byte[1];
|
||||||
|
int r = read(one, 0, 1);
|
||||||
|
if (r == -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return one[0] & 0xff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads up to {@code len} bytes of data into {@code b}, starting at
|
||||||
|
* {@code off}.
|
||||||
|
*
|
||||||
|
* <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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -3,6 +3,7 @@ handlers = java.util.logging.ConsoleHandler
|
|||||||
|
|
||||||
zeroecho.core.tag.ByteVerificationStrategy.level = FINE
|
zeroecho.core.tag.ByteVerificationStrategy.level = FINE
|
||||||
zeroecho.core.tag.SignatureVerificationStrategy.level = FINE
|
zeroecho.core.tag.SignatureVerificationStrategy.level = FINE
|
||||||
|
zeroecho.sdk.hybrid.signature.level = FINE
|
||||||
|
|
||||||
# Console handler uses our one-line formatter
|
# Console handler uses our one-line formatter
|
||||||
java.util.logging.ConsoleHandler.level = ALL
|
java.util.logging.ConsoleHandler.level = ALL
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
240
samples/src/test/java/demo/HybridSigningAesTest.java
Normal file
240
samples/src/test/java/demo/HybridSigningAesTest.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,9 +48,7 @@ import javax.crypto.SecretKey;
|
|||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import zeroecho.core.CryptoAlgorithm;
|
|
||||||
import zeroecho.core.CryptoAlgorithms;
|
import zeroecho.core.CryptoAlgorithms;
|
||||||
import zeroecho.core.alg.aes.AesKeyGenSpec;
|
|
||||||
import zeroecho.core.alg.rsa.RsaKeyGenSpec;
|
import zeroecho.core.alg.rsa.RsaKeyGenSpec;
|
||||||
import zeroecho.core.alg.rsa.RsaSigSpec;
|
import zeroecho.core.alg.rsa.RsaSigSpec;
|
||||||
import zeroecho.core.tag.TagEngine;
|
import zeroecho.core.tag.TagEngine;
|
||||||
@@ -65,23 +63,6 @@ import zeroecho.sdk.content.api.DataContent;
|
|||||||
class SigningAesTest {
|
class SigningAesTest {
|
||||||
private static final Logger LOG = Logger.getLogger(SigningAesTest.class.getName());
|
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 generateRsaKeys() throws GeneralSecurityException {
|
||||||
KeyPair kp = CryptoAlgorithms.generateKeyPair("RSA", RsaKeyGenSpec.rsa4096());
|
KeyPair kp = CryptoAlgorithms.generateKeyPair("RSA", RsaKeyGenSpec.rsa4096());
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user