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
|
||||
*/
|
||||
public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final String PUBLIC_KEY = "publicKey";
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final String PRIVATE_KEY = "privateKey";
|
||||
private final Supplier<TagEngine<T>> factory;
|
||||
|
||||
private TagEngineBuilder(Supplier<TagEngine<T>> factory) {
|
||||
@@ -205,7 +213,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
|
||||
* @throws NullPointerException if {@code privateKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> ed25519Sign(final PrivateKey privateKey) {
|
||||
Objects.requireNonNull(privateKey, "privateKey");
|
||||
Objects.requireNonNull(privateKey, PRIVATE_KEY);
|
||||
return signature("Ed25519", privateKey, VoidSpec.INSTANCE);
|
||||
}
|
||||
|
||||
@@ -217,7 +225,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
|
||||
* @throws NullPointerException if {@code publicKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> ed25519Verify(final PublicKey publicKey) {
|
||||
Objects.requireNonNull(publicKey, "publicKey");
|
||||
Objects.requireNonNull(publicKey, PUBLIC_KEY);
|
||||
return signature("Ed25519", publicKey, VoidSpec.INSTANCE);
|
||||
}
|
||||
|
||||
@@ -236,7 +244,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
|
||||
* @throws NullPointerException if {@code privateKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> rsaSign(final PrivateKey privateKey, final RsaSigSpec spec) {
|
||||
Objects.requireNonNull(privateKey, "privateKey");
|
||||
Objects.requireNonNull(privateKey, PRIVATE_KEY);
|
||||
return signature("RSA", privateKey, spec == null ? RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32) : spec);
|
||||
}
|
||||
|
||||
@@ -255,7 +263,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
|
||||
* @throws NullPointerException if {@code publicKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> rsaVerify(final PublicKey publicKey, final RsaSigSpec spec) {
|
||||
Objects.requireNonNull(publicKey, "publicKey");
|
||||
Objects.requireNonNull(publicKey, PUBLIC_KEY);
|
||||
return signature("RSA", publicKey, spec == null ? RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32) : spec);
|
||||
}
|
||||
|
||||
@@ -273,7 +281,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
|
||||
* @throws NullPointerException if {@code privateKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> ecdsaSign(final PrivateKey privateKey, final EcdsaCurveSpec spec) {
|
||||
Objects.requireNonNull(privateKey, "privateKey");
|
||||
Objects.requireNonNull(privateKey, PRIVATE_KEY);
|
||||
final EcdsaCurveSpec s = spec == null ? EcdsaCurveSpec.P256 : spec;
|
||||
return signature("ECDSA", privateKey, s);
|
||||
}
|
||||
@@ -292,7 +300,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
|
||||
* @throws NullPointerException if {@code publicKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> ecdsaVerify(final PublicKey publicKey, final EcdsaCurveSpec spec) {
|
||||
Objects.requireNonNull(publicKey, "publicKey");
|
||||
Objects.requireNonNull(publicKey, PUBLIC_KEY);
|
||||
final EcdsaCurveSpec s = spec == null ? EcdsaCurveSpec.P256 : spec;
|
||||
return signature("ECDSA", publicKey, s);
|
||||
}
|
||||
@@ -305,7 +313,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
|
||||
* @throws NullPointerException if {@code privateKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> ecdsaP256Sign(final PrivateKey privateKey) {
|
||||
Objects.requireNonNull(privateKey, "privateKey");
|
||||
Objects.requireNonNull(privateKey, PRIVATE_KEY);
|
||||
return signature("ECDSA", privateKey, EcdsaCurveSpec.P256);
|
||||
}
|
||||
|
||||
@@ -317,7 +325,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
|
||||
* @throws NullPointerException if {@code publicKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> ecdsaP256Verify(final PublicKey publicKey) {
|
||||
Objects.requireNonNull(publicKey, "publicKey");
|
||||
Objects.requireNonNull(publicKey, PUBLIC_KEY);
|
||||
return signature("ECDSA", publicKey, EcdsaCurveSpec.P256);
|
||||
}
|
||||
|
||||
@@ -334,7 +342,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
|
||||
* @throws NullPointerException if {@code privateKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> sphincsPlusSign(final PrivateKey privateKey) {
|
||||
Objects.requireNonNull(privateKey, "privateKey");
|
||||
Objects.requireNonNull(privateKey, PRIVATE_KEY);
|
||||
return signature("SPHINCS+", privateKey, VoidSpec.INSTANCE);
|
||||
}
|
||||
|
||||
@@ -351,7 +359,81 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
|
||||
* @throws NullPointerException if {@code publicKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> sphincsPlusVerify(final PublicKey publicKey) {
|
||||
Objects.requireNonNull(publicKey, "publicKey");
|
||||
Objects.requireNonNull(publicKey, PUBLIC_KEY);
|
||||
return signature("SPHINCS+", publicKey, VoidSpec.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a builder for an SLH-DSA signing engine.
|
||||
*
|
||||
* <p>
|
||||
* SLH-DSA is the NIST-standardized hash-based signature scheme (FIPS 205). The
|
||||
* concrete parameter set is encoded in the key material and interpreted by the
|
||||
* underlying {@link CryptoAlgorithms} implementation.
|
||||
* </p>
|
||||
*
|
||||
* @param privateKey private signing key; must not be {@code null}
|
||||
* @return a builder that produces SLH-DSA signature engines in SIGN mode
|
||||
* @throws NullPointerException if {@code privateKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> slhDsaSign(final PrivateKey privateKey) {
|
||||
Objects.requireNonNull(privateKey, PRIVATE_KEY);
|
||||
return signature("SLH-DSA", privateKey, VoidSpec.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a builder for an SLH-DSA verification engine.
|
||||
*
|
||||
* <p>
|
||||
* SLH-DSA is the NIST-standardized hash-based signature scheme (FIPS 205). The
|
||||
* concrete parameter set is encoded in the key material and interpreted by the
|
||||
* underlying {@link CryptoAlgorithms} implementation.
|
||||
* </p>
|
||||
*
|
||||
* @param publicKey public verification key; must not be {@code null}
|
||||
* @return a builder that produces SLH-DSA signature engines in VERIFY mode
|
||||
* @throws NullPointerException if {@code publicKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> slhDsaVerify(final PublicKey publicKey) {
|
||||
Objects.requireNonNull(publicKey, PUBLIC_KEY);
|
||||
return signature("SLH-DSA", publicKey, VoidSpec.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a builder for an ML-DSA signing engine.
|
||||
*
|
||||
* <p>
|
||||
* ML-DSA is the NIST-standardized module-lattice signature scheme (FIPS 204).
|
||||
* The concrete parameter set and any pre-hash variant is encoded in the key
|
||||
* material and interpreted by the underlying {@link CryptoAlgorithms}
|
||||
* implementation.
|
||||
* </p>
|
||||
*
|
||||
* @param privateKey private signing key; must not be {@code null}
|
||||
* @return a builder that produces ML-DSA signature engines in SIGN mode
|
||||
* @throws NullPointerException if {@code privateKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> mldsaSign(final PrivateKey privateKey) {
|
||||
Objects.requireNonNull(privateKey, PRIVATE_KEY);
|
||||
return signature("ML-DSA", privateKey, VoidSpec.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a builder for an ML-DSA verification engine.
|
||||
*
|
||||
* <p>
|
||||
* ML-DSA is the NIST-standardized module-lattice signature scheme (FIPS 204).
|
||||
* The concrete parameter set and any pre-hash variant is encoded in the key
|
||||
* material and interpreted by the underlying {@link CryptoAlgorithms}
|
||||
* implementation.
|
||||
* </p>
|
||||
*
|
||||
* @param publicKey public verification key; must not be {@code null}
|
||||
* @return a builder that produces ML-DSA signature engines in VERIFY mode
|
||||
* @throws NullPointerException if {@code publicKey} is {@code null}
|
||||
*/
|
||||
public static TagEngineBuilder<Signature> mldsaVerify(final PublicKey publicKey) {
|
||||
Objects.requireNonNull(publicKey, PUBLIC_KEY);
|
||||
return signature("ML-DSA", publicKey, VoidSpec.INSTANCE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.Ed25519DataContentBuilder},
|
||||
* {@link zeroecho.sdk.builders.alg.Ed448DataContentBuilder},
|
||||
* {@link zeroecho.sdk.builders.alg.SphincsPlusDataContentBuilder}.</li>
|
||||
* {@link zeroecho.sdk.builders.alg.SphincsPlusDataContentBuilder},
|
||||
* {@link zeroecho.sdk.builders.alg.MldsaDataContentBuilder},
|
||||
* {@link zeroecho.sdk.builders.alg.SlhDsaDataContentBuilder}.</li>
|
||||
* <li>MAC and digest: {@link zeroecho.sdk.builders.alg.HmacDataContentBuilder},
|
||||
* {@link zeroecho.sdk.builders.alg.DigestDataContentBuilder}.</li>
|
||||
* <li>KEM envelopes: {@link zeroecho.sdk.builders.alg.KemDataContentBuilder}
|
||||
@@ -86,6 +88,11 @@
|
||||
* <li>{@link TagTrailerDataContentBuilder} - appends or verifies an
|
||||
* authentication tag carried as an input trailer using a
|
||||
* {@link zeroecho.core.tag.TagEngine}.</li>
|
||||
* <li>{@link SignatureTrailerDataContentBuilder} - signature-specialized
|
||||
* trailer builder intended to replace
|
||||
* {@code TagTrailerDataContentBuilder<Signature>} in most signature use cases.
|
||||
* It can wrap existing signature engines and construct single-algorithm and
|
||||
* hybrid signature contexts.</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
@@ -107,6 +114,9 @@
|
||||
* signatures or tags.</li>
|
||||
* <li>{@link TagTrailerDataContentBuilder} focuses on trailer-style tags with
|
||||
* explicit verify policies.</li>
|
||||
* <li>{@link SignatureTrailerDataContentBuilder} provides the corresponding
|
||||
* trailer functionality specialized for digital signatures, including hybrid
|
||||
* signature construction.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Typical usage</h2> <pre>{@code
|
||||
@@ -136,4 +146,4 @@
|
||||
*
|
||||
* @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.SignatureVerificationStrategy.level = FINE
|
||||
zeroecho.sdk.hybrid.signature.level = FINE
|
||||
|
||||
# Console handler uses our one-line formatter
|
||||
java.util.logging.ConsoleHandler.level = ALL
|
||||
|
||||
Reference in New Issue
Block a user