/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.sdk.builders;
import java.io.IOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.util.Objects;
import java.util.function.Supplier;
import conflux.CtxInterface;
import conflux.Key;
import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.KeyUsage;
import zeroecho.core.spec.ContextSpec;
import zeroecho.core.tag.TagEngine;
import zeroecho.sdk.builders.core.DataContentBuilder;
import zeroecho.sdk.content.api.DataContent;
import zeroecho.sdk.hybrid.signature.HybridSignatureContexts;
import zeroecho.sdk.hybrid.signature.HybridSignatureProfile;
/**
* Signature-specific trailer builder for {@link DataContent} pipelines.
*
*
* This class is intended as a convenient, signature-specialized replacement
* for:
*
*
*
* new TagTrailerDataContentBuilder<Signature>(engine).bufferSize(8192)
* new TagTrailerDataContentBuilder<Signature>(engine).bufferSize(8192).throwOnMismatch()
*
*
*
* It keeps {@link TagTrailerDataContentBuilder} as the generic implementation
* while providing a compact API for {@code Signature} usage, including
* construction of {@code SignatureContext} for both single-algorithm and hybrid
* signatures.
*
*
* Mode selection
*
* - {@link #core(TagEngine)} / {@link #core(Supplier)}: wraps a ready engine
* (same parameters as {@link TagTrailerDataContentBuilder}).
* - {@link #single()}: constructs a non-hybrid {@code SignatureContext} via
* {@link CryptoAlgorithms}.
* - {@link #hybrid()}: constructs a hybrid {@code SignatureContext} via
* {@link HybridSignatureContexts}.
*
*
* Checked exceptions
*
* Context construction may involve I/O (e.g., catalog/provider loading) and
* therefore throw {@link IOException}. This builder converts such failures to
* {@link IllegalStateException} because fluent builder APIs are expected to be
* used in configuration code without mandatory checked-exception plumbing.
*
*
* @since 1.0
*/
public final class SignatureTrailerDataContentBuilder implements DataContentBuilder {
private final TagTrailerDataContentBuilder delegate;
private SignatureTrailerDataContentBuilder(TagEngine engine) {
this.delegate = new TagTrailerDataContentBuilder<>(engine);
}
private SignatureTrailerDataContentBuilder(Supplier extends TagEngine> engineFactory) {
this.delegate = new TagTrailerDataContentBuilder<>(engineFactory);
}
/**
* Core mode: wraps a fixed engine instance.
*
*
* This is the direct signature-specialized equivalent of:
* {@code new TagTrailerDataContentBuilder(engine)}.
*
*
* @param engine signature engine (typically a {@code SignatureContext}); must
* not be {@code null}
* @return builder instance
* @throws NullPointerException if {@code engine} is {@code null}
* @since 1.0
*/
public static SignatureTrailerDataContentBuilder core(TagEngine engine) {
Objects.requireNonNull(engine, "engine");
return new SignatureTrailerDataContentBuilder(engine);
}
/**
* Core mode: wraps an engine factory.
*
*
* This is the direct signature-specialized equivalent of:
* {@code new TagTrailerDataContentBuilder(engineFactory)}.
*
*
* @param engineFactory engine factory; must not be {@code null}
* @return builder instance
* @throws NullPointerException if {@code engineFactory} is {@code null}
* @since 1.0
*/
public static SignatureTrailerDataContentBuilder core(Supplier extends TagEngine> engineFactory) {
Objects.requireNonNull(engineFactory, "engineFactory");
return new SignatureTrailerDataContentBuilder(engineFactory);
}
/**
* Enters single-algorithm (non-hybrid) construction helpers.
*
* @return selector for creating signing/verifying builders
* @since 1.0
*/
public static SingleSelector single() {
return new SingleSelector();
}
/**
* Enters hybrid signature construction helpers.
*
* @return selector for creating signing/verifying builders
* @since 1.0
*/
public static HybridSelector hybrid() {
return new HybridSelector();
}
/**
* Sets the internal I/O buffer size used when stripping the trailer.
*
* @param bytes buffer size in bytes; must be at least 1
* @return this builder for chaining
* @throws IllegalArgumentException if {@code bytes < 1}
* @since 1.0
*/
public SignatureTrailerDataContentBuilder bufferSize(int bytes) {
delegate.bufferSize(bytes);
return this;
}
/**
* Configures verification to throw on mismatch (default verification behavior).
*
* @return this builder for chaining
* @since 1.0
*/
public SignatureTrailerDataContentBuilder throwOnMismatch() {
delegate.throwOnMismatch();
return this;
}
/**
* Instead of throwing on mismatch, records the verification outcome into a
* context flag.
*
* @param ctx context instance to receive the flag; must not be
* {@code null}
* @param verifyKey key under which the {@code Boolean} result is recorded; must
* not be {@code null}
* @return this builder for chaining
* @throws NullPointerException if {@code ctx} or {@code verifyKey} is
* {@code null}
* @since 1.0
*/
public SignatureTrailerDataContentBuilder flagInContext(CtxInterface ctx, Key verifyKey) {
delegate.flagInContext(Objects.requireNonNull(ctx, "ctx"), Objects.requireNonNull(verifyKey, "verifyKey"));
return this;
}
@Override
public DataContent build(boolean encrypt) {
return delegate.build(encrypt);
}
// ======================================================================
// Single-algorithm helpers
// ======================================================================
/**
* Helper entry for single-algorithm signature construction.
*
* @since 1.0
*/
public static final class SingleSelector {
private SingleSelector() {
}
/**
* Creates a signing trailer for a single signature algorithm (no spec).
*
* @param algorithmId signature algorithm id
* @param privateKey private key for signing
* @return builder ready to be added to a pipeline
* @throws NullPointerException if {@code algorithmId} or {@code privateKey} is
* {@code null}
* @throws IllegalStateException if the signature context cannot be created
* (e.g., I/O/provider issues)
* @since 1.0
*/
public SignatureTrailerDataContentBuilder sign(String algorithmId, PrivateKey privateKey) {
return sign(algorithmId, privateKey, null);
}
/**
* Creates a signing trailer for a single signature algorithm with an optional
* spec.
*
* @param algorithmId signature algorithm id
* @param privateKey private key for signing
* @param spec optional context spec (may be {@code null})
* @return builder ready to be added to a pipeline
* @throws NullPointerException if {@code algorithmId} or {@code privateKey} is
* {@code null}
* @throws IllegalStateException if the signature context cannot be created
* (e.g., I/O/provider issues)
* @since 1.0
*/
public SignatureTrailerDataContentBuilder sign(String algorithmId, PrivateKey privateKey, ContextSpec spec) {
Objects.requireNonNull(algorithmId, "algorithmId");
Objects.requireNonNull(privateKey, "privateKey");
Supplier> factory = () -> {
try {
return CryptoAlgorithms.create(algorithmId, KeyUsage.SIGN, privateKey, spec);
} catch (IOException e) {
throw new IllegalStateException("Failed to create SIGN SignatureContext for: " + algorithmId, e);
}
};
return core(factory);
}
/**
* Creates a verification trailer for a single signature algorithm (no spec).
*
* @param algorithmId signature algorithm id
* @param publicKey public key for verification
* @return builder ready to be added to a pipeline
* @throws NullPointerException if {@code algorithmId} or {@code publicKey} is
* {@code null}
* @throws IllegalStateException if the signature context cannot be created
* (e.g., I/O/provider issues)
* @since 1.0
*/
public SignatureTrailerDataContentBuilder verify(String algorithmId, PublicKey publicKey) {
return verify(algorithmId, publicKey, null);
}
/**
* Creates a verification trailer for a single signature algorithm with an
* optional spec.
*
* @param algorithmId signature algorithm id
* @param publicKey public key for verification
* @param spec optional context spec (may be {@code null})
* @return builder ready to be added to a pipeline
* @throws NullPointerException if {@code algorithmId} or {@code publicKey} is
* {@code null}
* @throws IllegalStateException if the signature context cannot be created
* (e.g., I/O/provider issues)
* @since 1.0
*/
public SignatureTrailerDataContentBuilder verify(String algorithmId, PublicKey publicKey, ContextSpec spec) {
Objects.requireNonNull(algorithmId, "algorithmId");
Objects.requireNonNull(publicKey, "publicKey");
Supplier> factory = () -> {
try {
return CryptoAlgorithms.create(algorithmId, KeyUsage.VERIFY, publicKey, spec);
} catch (IOException e) {
throw new IllegalStateException("Failed to create VERIFY SignatureContext for: " + algorithmId, e);
}
};
return core(factory);
}
}
// ======================================================================
// Hybrid helpers
// ======================================================================
/**
* Helper entry for hybrid signature construction.
*
* @since 1.0
*/
public static final class HybridSelector {
private static final int DEFAULT_MAX_BODY_BYTES = 2 * 1024 * 1024;
private HybridSelector() {
}
/**
* Creates a hybrid signing trailer for the common case where both specs are
* {@code null}.
*
* @param classicSigId classic signature algorithm id
* @param pqcSigId post-quantum signature algorithm id
* @param rule aggregation rule
* @param classicPrivate classic private key
* @param pqcPrivate PQC private key
* @return builder ready to be added to a pipeline
* @throws NullPointerException if any required argument is {@code null}
* @throws IllegalStateException if the hybrid context cannot be created
* @since 1.0
*/
public SignatureTrailerDataContentBuilder sign(String classicSigId, String pqcSigId,
HybridSignatureProfile.VerifyRule rule, PrivateKey classicPrivate, PrivateKey pqcPrivate) {
return sign(classicSigId, pqcSigId, null, null, rule, classicPrivate, pqcPrivate, DEFAULT_MAX_BODY_BYTES);
}
/**
* Creates a hybrid verification trailer for the common case where both specs
* are {@code null}.
*
* @param classicSigId classic signature algorithm id
* @param pqcSigId post-quantum signature algorithm id
* @param rule aggregation rule
* @param classicPublic classic public key
* @param pqcPublic PQC public key
* @return builder ready to be added to a pipeline
* @throws NullPointerException if any required argument is {@code null}
* @throws IllegalStateException if the hybrid context cannot be created
* @since 1.0
*/
public SignatureTrailerDataContentBuilder verify(String classicSigId, String pqcSigId,
HybridSignatureProfile.VerifyRule rule, PublicKey classicPublic, PublicKey pqcPublic) {
return verify(classicSigId, pqcSigId, null, null, rule, classicPublic, pqcPublic, DEFAULT_MAX_BODY_BYTES);
}
/**
* Creates a hybrid signing trailer with optional specs and explicit
* {@code maxBodyBytes}.
*
* @param classicSigId classic signature algorithm id
* @param pqcSigId post-quantum signature algorithm id
* @param classicSpec optional classic spec (may be {@code null})
* @param pqcSpec optional PQC spec (may be {@code null})
* @param rule aggregation rule
* @param classicPrivate classic private key
* @param pqcPrivate PQC private key
* @param maxBodyBytes maximum body size accepted by the hybrid context; must
* be at least 1
* @return builder ready to be added to a pipeline
* @throws NullPointerException if any required argument is {@code null}
* @throws IllegalArgumentException if {@code maxBodyBytes < 1}
* @throws IllegalStateException if the hybrid context cannot be created
* @since 1.0
*/
public SignatureTrailerDataContentBuilder sign(String classicSigId, String pqcSigId, ContextSpec classicSpec,
ContextSpec pqcSpec, HybridSignatureProfile.VerifyRule rule, PrivateKey classicPrivate,
PrivateKey pqcPrivate, int maxBodyBytes) {
Objects.requireNonNull(classicSigId, "classicSigId");
Objects.requireNonNull(pqcSigId, "pqcSigId");
Objects.requireNonNull(rule, "rule");
Objects.requireNonNull(classicPrivate, "classicPrivate");
Objects.requireNonNull(pqcPrivate, "pqcPrivate");
if (maxBodyBytes < 1) { // NOPMD
throw new IllegalArgumentException("maxBodyBytes must be >= 1");
}
HybridSignatureProfile profile = new HybridSignatureProfile(classicSigId, pqcSigId, classicSpec, pqcSpec,
rule);
Supplier> factory = () -> {
try {
return HybridSignatureContexts.sign(profile, classicPrivate, pqcPrivate, maxBodyBytes);
} catch (RuntimeException e) { // NOPMD
throw e;
} catch (Exception e) {
throw new IllegalStateException("Failed to create hybrid SIGN SignatureContext", e);
}
};
return core(factory);
}
/**
* Creates a hybrid verification trailer with optional specs and explicit
* {@code maxBodyBytes}.
*
* @param classicSigId classic signature algorithm id
* @param pqcSigId post-quantum signature algorithm id
* @param classicSpec optional classic spec (may be {@code null})
* @param pqcSpec optional PQC spec (may be {@code null})
* @param rule aggregation rule
* @param classicPublic classic public key
* @param pqcPublic PQC public key
* @param maxBodyBytes maximum body size accepted by the hybrid context; must
* be at least 1
* @return builder ready to be added to a pipeline
* @throws NullPointerException if any required argument is {@code null}
* @throws IllegalArgumentException if {@code maxBodyBytes < 1}
* @throws IllegalStateException if the hybrid context cannot be created
* @since 1.0
*/
public SignatureTrailerDataContentBuilder verify(String classicSigId, String pqcSigId, ContextSpec classicSpec,
ContextSpec pqcSpec, HybridSignatureProfile.VerifyRule rule, PublicKey classicPublic,
PublicKey pqcPublic, int maxBodyBytes) {
Objects.requireNonNull(classicSigId, "classicSigId");
Objects.requireNonNull(pqcSigId, "pqcSigId");
Objects.requireNonNull(rule, "rule");
Objects.requireNonNull(classicPublic, "classicPublic");
Objects.requireNonNull(pqcPublic, "pqcPublic");
if (maxBodyBytes < 1) { // NOPMD
throw new IllegalArgumentException("maxBodyBytes must be >= 1");
}
HybridSignatureProfile profile = new HybridSignatureProfile(classicSigId, pqcSigId, classicSpec, pqcSpec,
rule);
Supplier> factory = () -> {
try {
return HybridSignatureContexts.verify(profile, classicPublic, pqcPublic, maxBodyBytes);
} catch (RuntimeException e) { // NOPMD
throw e;
} catch (Exception e) {
throw new IllegalStateException("Failed to create hybrid VERIFY SignatureContext", e);
}
};
return core(factory);
}
}
}