Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion src/inc_encoding/target_sum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,32 @@ impl<MH: MessageHash, const TARGET_SUM: usize> IncomparableEncoding
randomness: &Self::Randomness,
epoch: u32,
) -> Result<Vec<u8>, Self::Error> {
// Compile-time parameter validation for Target Sum Encoding
//
// This encoding implements Construction 6 (IE for Target Sum Winternitz)
// from DKKW25. It maps a message to a codeword x ∈ C ⊆ Z_w^v, where:
//
// C = { (x_1, ..., x_v) ∈ {0, ..., w-1}^v | Σ x_i = T }
//
// The code C enforces the *incomparability* property (Definition 13):
// no two distinct codewords x, x' satisfy x_i ≥ x'_i for all i.
// This is critical for the security of the XMSS signature scheme.
//
// DKKW25: https://eprint.iacr.org/2025/055
// HHKTW26: https://eprint.iacr.org/2026/016
const {
// base and dimension must not be too large
// Representation constraints
//
// In the Generalized XMSS construction (DKKW25),
// each chain position and chain index is encoded as a single byte
// in the tweak function:
//
// tweak(ep, i, k) = (0x00 || ep || i || k)
// 8b ⌈log L⌉ ⌈log v⌉ w bits
//
// - Since chain_index `i` is stored as u8, we need v ≤ 256.
// - Since pos_in_chain `k` is stored as u8, we need w ≤ 256.
// - Codeword entries (chunks) are also stored as u8 in signatures.
assert!(
MH::BASE <= 1 << 8,
"Target Sum Encoding: Base must be at most 2^8"
Expand All @@ -71,6 +95,36 @@ impl<MH: MessageHash, const TARGET_SUM: usize> IncomparableEncoding
MH::DIMENSION <= 1 << 8,
"Target Sum Encoding: Dimension must be at most 2^8"
);

// Encoding well-formedness
//
// Definition 13 (DKKW25): an incomparable encoding maps messages
// to codewords in {0, ..., w-1}^v. For the incomparability
// property to be meaningful, we need w ≥ 2 (otherwise every
// codeword is the zero vector, and distinct codewords cannot
// exist).
assert!(
MH::BASE >= 2,
"Target Sum Encoding: Base must be at least 2"
);

// Target sum range
//
// Construction 6 (DKKW25) defines the code:
//
// C = { x ∈ {0,...,w-1}^v | Σ x_i = T }
//
// For C to be non-empty, T must be achievable: each x_i can
// contribute at most w-1 to the sum, so T ≤ v*(w-1). The lower
// bound T ≥ 0 is guaranteed by the usize type.
//
// Choosing T close to v*(w-1)/2 (the expected sum of a uniform
// hash) maximizes |C| and minimizes the signing retry rate
// (Lemma 7, DKKW25).
assert!(
TARGET_SUM <= MH::DIMENSION * (MH::BASE - 1),
"Target Sum Encoding: TARGET_SUM must be at most DIMENSION * (BASE - 1)"
);
}

// apply the message hash first to get chunks
Expand Down
68 changes: 54 additions & 14 deletions src/signature/generalized_xmss.rs
Original file line number Diff line number Diff line change
Expand Up @@ -653,37 +653,77 @@ where

const LIFETIME: u64 = 1 << LOG_LIFETIME;

#[allow(clippy::too_many_lines)]
fn key_gen<R: RngExt>(
rng: &mut R,
activation_epoch: usize,
num_active_epochs: usize,
) -> (Self::PublicKey, Self::SecretKey) {
const {
// assert BASE and DIMENSION are small enough to make sure that we can fit
// pos_in_chain and chain_index in u8.
// Encoding well-formedness
//
// Definition 13 (DKKW25): the incomparable encoding maps
// messages to codewords x ∈ C ⊆ {0, ..., w-1}^v. For the
// incomparability property to hold, we need:
// - w >= 2: a single-element alphabet makes all codewords
// identical, so incomparability is vacuous.
// - v >= 1: codewords must have at least one coordinate.
assert!(
IE::BASE >= 2,
"Generalized XMSS: Encoding base (w) must be at least 2"
);
assert!(
IE::DIMENSION >= 1,
"Generalized XMSS: Encoding dimension (v) must be at least 1"
);

// Representation constraints
//
// The chain tweak function (DKKW25) encodes:
//
// tweak(ep, i, k) = (0x00 || ep || i || k)
// 8 bits ceil(log L) ceil(log v) w bits
//
// chain_index `i` and pos_in_chain `k` are stored as u8, and
// chunk values in signatures are also u8. Therefore:
// - BASE (= w) <= 256 (chunk fits in u8)
// - DIMENSION (= v) <= 256 (chain_index fits in u8)
assert!(
IE::BASE <= 1 << 8,
"Generalized XMSS: Encoding base too large, must be at most 2^8"
"Generalized XMSS: Encoding base (w) must fit in u8 (<= 256)"
);
assert!(
IE::DIMENSION <= 1 << 8,
"Generalized XMSS: Encoding dimension too large, must be at most 2^8"
"Generalized XMSS: Encoding dimension (v) must fit in u8 (<= 256)"
);

// LOG_LIFETIME needs to be even, so that we can use the top-bottom tree approach
// Merkle tree structure
//
// The key lifetime is L = 2^LOG_LIFETIME epochs. The Merkle tree
// has depth h = LOG_LIFETIME, with L leaves (one per epoch).
//
// The top-bottom optimization splits the tree at depth h/2,
// creating one top tree of depth h/2 and sqrt(L) bottom trees of
// depth h/2. This requires h to be even.
assert!(
LOG_LIFETIME.is_multiple_of(2),
"Generalized XMSS: LOG_LIFETIME must be multiple of two"
"Generalized XMSS: LOG_LIFETIME must be even (top-bottom tree split)"
);

// sign() and verify() take epoch as u32, so LOG_LIFETIME > 32 would create
// epochs unreachable by the signing/verification API.
const {
assert!(
LOG_LIFETIME <= 32,
"Generalized XMSS: LOG_LIFETIME must be at most 32 (epoch type is u32)"
);
}
// The smallest valid even LOG_LIFETIME is 2, giving L = 4 epochs,
// a top tree of depth 1, and 2 bottom trees of depth 1.
// LOG_LIFETIME = 0 would mean L = 1 (no internal Merkle nodes).
assert!(
LOG_LIFETIME >= 2,
"Generalized XMSS: LOG_LIFETIME must be at least 2"
);

// The sign() and verify() APIs take the epoch as u32, so
// LOG_LIFETIME > 32 would create epochs that cannot be addressed.
assert!(
LOG_LIFETIME <= 32,
"Generalized XMSS: LOG_LIFETIME must be at most 32 (epoch is u32)"
);
}

// Overflow-safe validation of the requested activation interval.
Expand Down
98 changes: 78 additions & 20 deletions src/symmetric/message_hash/aborting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,50 +77,108 @@ where
randomness: &Self::Randomness,
message: &[u8; MESSAGE_LENGTH],
) -> Result<Vec<u8>, HypercubeHashError> {
// Compile-time parameter validation for AbortingHypercubeMessageHash
//
// This hash implements H^hc_{w,v,z,Q} from §6.1 of HHKTW26. It uses
// rejection sampling to uniformly map Poseidon field elements into
// the hypercube Z_w^v, avoiding big-integer arithmetic entirely:
//
// 1. Compute (A_1, ..., A_ℓ) := Poseidon(R || P || T || M)
// 2. For each A_i: reject if A_i ≥ Q·w^z (ensures uniformity)
// 3. Decompose d_i = ⌊A_i / Q⌋ into z base-w digits
// 4. Collect the first v digits as the output
//
// The field prime decomposes as p = Q·w^z + α (α ≥ 0).
// Rejection happens with per-element probability α/p, and the
// overall abort probability is θ = 1 - ((Q·w^z)/p)^ℓ (Lemma 8).
//
// By Theorem 4 of HHKTW26, this construction is indifferentiable
// from a θ-aborting random oracle when Poseidon is modeled as a
// standard random oracle.
//
// DKKW25: https://eprint.iacr.org/2025/055
// HHKTW26: https://eprint.iacr.org/2026/016
const {
// Check that Poseidon of width 24 is enough
// Poseidon capacity constraints
//
// We use Poseidon in compression mode with a width-24 permutation.
// All inputs must fit in one call, and the output is extracted
// from the same state.
assert!(
PARAMETER_LEN + RAND_LEN_FE + TWEAK_LEN_FE + MSG_LEN_FE <= 24,
"Poseidon of width 24 is not enough"
"Poseidon of width 24 is not enough for the input"
);
assert!(HASH_LEN_FE <= 24, "Poseidon of width 24 is not enough");

// Check that we have enough hash output field elements
assert!(
HASH_LEN_FE >= DIMENSION.div_ceil(Z),
"Not enough hash output field elements for the requested dimension"
HASH_LEN_FE <= 24,
"Poseidon of width 24 is not enough for the output"
);

// Poseidon compression mode can only produce as many output
// field elements as there are input elements.
assert!(
PARAMETER_LEN + RAND_LEN_FE + TWEAK_LEN_FE + MSG_LEN_FE >= HASH_LEN_FE,
"Input shorter than requested output"
);

// Base check
// Hypercube decomposition parameters
//
// Each good field element A_i < Q·w^z is decomposed into z
// base-w digits, so we need ℓ = ⌈v/z⌉ field elements to get
// at least v digits. HASH_LEN_FE must supply enough elements.
assert!(
BASE <= 1 << 8,
"Aborting Hypercube Message Hash: Base must be at most 2^8"
DIMENSION >= 1,
"AbortingHypercubeMessageHash: DIMENSION (v) must be at least 1"
);
assert!(
Z >= 1,
"AbortingHypercubeMessageHash: Z (digits per field element) must be at least 1"
);
assert!(
HASH_LEN_FE >= DIMENSION.div_ceil(Z),
"Not enough hash output field elements: need ceil(v/z)"
);

// Check that Q * w^z fits within the field
// Q is the quotient in the decomposition A_i = Q·d_i + c_i,
// where c_i ∈ {0, ..., Q-1} is discarded and d_i ∈ {0, ..., w^z-1}
// carries the uniform digits. Q must be positive for a valid range.
assert!(Q >= 1, "AbortingHypercubeMessageHash: Q must be at least 1");

// The rejection threshold Q·w^z must not exceed the field order p,
// since field elements A_i live in {0, ..., p-1}. The remainder
// α = p - Q·w^z determines the per-element abort probability α/p.
//
// Example (KoalaBear): p = 2^31 - 2^24 + 1 = 127·8^8 + 1
// ⟹ Q=127, w=8, z=8, α=1, abort prob ≈ 4.7e-10 per element.
assert!(
Q as u64 * (BASE as u64).pow(Z as u32) <= F::ORDER_U64,
"Q * w^z exceeds field order"
"Q * w^z exceeds field order p"
);

// floor(log2(ORDER))
let bits_per_fe = F::ORDER_U64.ilog2() as usize;
// Representation constraints
//
// Same as the Poseidon message hash: chunks and chain indices
// are stored as u8 in signatures and tweak encodings.
assert!(
BASE >= 2,
"AbortingHypercubeMessageHash: BASE (w) must be at least 2 (Definition 13, DKKW25)"
);
assert!(
BASE <= 1 << 8,
"AbortingHypercubeMessageHash: BASE (w) must fit in u8"
);

// Check that we have enough bits to encode message
// Injective encoding of inputs
//
// Same requirements as the standard Poseidon message hash:
// message and epoch must be losslessly encodable as field elements.
let bits_per_fe = F::ORDER_U64.ilog2() as usize;
assert!(
bits_per_fe * MSG_LEN_FE >= 8 * MESSAGE_LENGTH,
"Aborting Hypercube Message Hash: not enough field elements to encode the message"
"AbortingHypercubeMessageHash: not enough field elements to encode the message"
);

// Check that we have enough bits to encode tweak
// Epoch is a u32, and we have one domain separator byte
assert!(
bits_per_fe * TWEAK_LEN_FE >= 40,
"Aborting Hypercube Message Hash: not enough field elements to encode the epoch tweak"
"AbortingHypercubeMessageHash: not enough field elements to encode the epoch tweak"
);
}

Expand Down
Loading
Loading