Skip to main content

quantica/slh_dsa/
fors.rs

1//! FORS: Forest of Random Subsets (FIPS 205, Algorithms 14-17).
2//!
3//! FORS is a few-time signature scheme that signs the message digest by selecting one
4//! leaf from each of `k` independent binary trees of height `a`. Each tree has `2^a`
5//! leaves; the message digest is split into `k` indices of `a` bits each, and the
6//! signature reveals the secret leaf value plus an authentication path for each tree.
7//!
8//! In the SLH-DSA hierarchy, FORS sits between the message hash and the hypertree:
9//! the FORS public key (a compression of the `k` tree roots) becomes the message
10//! that the hypertree signs. This two-level structure is what makes SLH-DSA stateless
11//! -- FORS absorbs the message entropy, and the hypertree provides many-time security.
12
13use super::SlhDsaError;
14use super::address::{Adrs, FORS_PRF, FORS_ROOTS, FORS_TREE};
15use super::hash;
16use super::params::Params;
17use crate::secret::SecretBytes;
18use alloc::vec::Vec;
19
20/// Generate a single FORS secret key value at leaf index `idx`.
21///
22/// Implements Algorithm 14 of FIPS 205. Derives the secret value deterministically
23/// from `SK.seed` using the PRF, domain-separated by a FORS_PRF address.
24fn fors_sk_gen<P: Params>(sk_seed: &[u8], pk_seed: &[u8], adrs: &mut Adrs, idx: u32) -> Vec<u8> {
25    let mut sk_adrs = adrs.clone();
26    sk_adrs.set_type_and_clear(FORS_PRF);
27    sk_adrs.set_key_pair_address(adrs.get_key_pair_address());
28    sk_adrs.set_tree_index(idx);
29    hash::prf::<P>(pk_seed, sk_seed, &sk_adrs)
30}
31
32/// Compute the root of a FORS subtree of height `z` at leaf index `i`.
33///
34/// Implements Algorithm 15 of FIPS 205 using an iterative **treehash**
35/// (BDS-style) stack traversal: leaves are produced in order, and any
36/// time the two nodes on top of the stack have the same height they
37/// are combined with `H` and pushed back as a single higher-up node.
38/// Peak live memory is `O(z)` nodes (at most `z + 1` stack entries),
39/// instead of the `2^z` materialized leaves of the naïve approach.
40///
41/// For `z = 0` this degenerates to a single `F` evaluation of the
42/// secret leaf value. Used during signing to compute authentication
43/// path nodes (sibling subtree roots).
44///
45/// # Side-channel / memory posture
46///
47/// The iterative treehash is also the SPA-hardening posture for FORS.
48/// A recursive implementation would expose a memory-access envelope
49/// that matches the tree geometry, indirectly leaking the FORS digit
50/// encoded into `i`. The iterative variant instead keeps a BDS stack
51/// of `z + 1` nodes and walks the tree with a loop counter that is
52/// independent of the secret. See
53/// `doc/sca/countermeasures/slh_dsa.rst`, section *Memory / stack-
54/// timing — iterative treehash + streaming signature*, for the full
55/// threat analysis.
56///
57/// RAM impact (SLH-DSA-SHAKE-256s, `z = a - 1 = 13`, `n = 32`):
58///
59/// * naïve: `2^13 * 32 = 256 KiB` leaf buffer, peaking higher with
60///   the transient `new_nodes` vector during the climb;
61/// * treehash: `14 * 32 ≈ 448 B` on the stack of node bytes.
62pub fn fors_node<P: Params>(sk_seed: &[u8], pk_seed: &[u8], i: u32, z: u32, adrs: &mut Adrs) -> Vec<u8> {
63    if z == 0 {
64        // Leaf: hash the secret key value
65        let sk = fors_sk_gen::<P>(sk_seed, pk_seed, adrs, i);
66        let kp = adrs.get_key_pair_address();
67        adrs.set_type_and_clear(FORS_TREE);
68        adrs.set_key_pair_address(kp);
69        adrs.set_tree_height(0);
70        adrs.set_tree_index(i);
71        return hash::f_hash::<P>(pk_seed, adrs, &sk);
72    }
73
74    // Iterative treehash: stream the `2^z` leaves, merge equal-height
75    // tops into the parent node immediately. Peak depth ≤ z + 1.
76    let num_leaves = 1u32 << z;
77    let base = i;
78    let kp = adrs.get_key_pair_address();
79
80    // (node_bytes, height) — nodes at the "left edge" of what still
81    // needs to be merged. Height 0 = leaf.
82    let mut stack: Vec<(Vec<u8>, u32)> = Vec::with_capacity((z + 1) as usize);
83
84    for j in 0..num_leaves {
85        let leaf_idx = base + j;
86
87        // Produce leaf node.
88        let sk = fors_sk_gen::<P>(sk_seed, pk_seed, adrs, leaf_idx);
89        adrs.set_type_and_clear(FORS_TREE);
90        adrs.set_key_pair_address(kp);
91        adrs.set_tree_height(0);
92        adrs.set_tree_index(leaf_idx);
93        let mut node = hash::f_hash::<P>(pk_seed, adrs, &sk);
94        let mut height = 0u32;
95        let mut tree_idx = leaf_idx;
96
97        // Merge while the top of stack is at the same height as `node`.
98        // Each merge pops the left sibling and produces the parent.
99        while let Some(&(_, top_h)) = stack.last() {
100            if top_h != height {
101                break;
102            }
103            let (left, _) = stack.pop().expect("stack non-empty in merge loop");
104            tree_idx >>= 1;
105            height += 1;
106            adrs.set_type_and_clear(FORS_TREE);
107            adrs.set_key_pair_address(kp);
108            adrs.set_tree_height(height);
109            adrs.set_tree_index(tree_idx);
110            node = hash::hash_h::<P>(pk_seed, adrs, &left, &node);
111        }
112        stack.push((node, height));
113    }
114
115    // With `2^z` leaves processed, the stack holds exactly one entry
116    // at height `z`: the subtree root.
117    stack.pop().expect("treehash produces exactly one root").0
118}
119
120/// Sign a message digest using FORS.
121///
122/// Implements Algorithm 16 of FIPS 205. The message digest `md` contains `k * a` bits,
123/// which are split into `k` indices of `a` bits each. For each of the `k` FORS trees,
124/// the signature includes:
125/// - The secret leaf value at the selected index (n bytes)
126/// - An authentication path of `a` sibling nodes (a * n bytes)
127///
128/// The total FORS signature is `k * (1 + a) * n` bytes.
129///
130/// `adrs_template` is read-only — a local clone is taken internally for the
131/// scratch state mutated during signing. Returns
132/// `Err(SlhDsaError::FaultDetected)` only when `sca-fors-indices-check`
133/// (T1-E) is enabled and the integrity check on the FORS index vector
134/// fails; otherwise always `Ok`.
135pub fn fors_sign<P: Params>(
136    md: &[u8],
137    sk_seed: &[u8],
138    pk_seed: &[u8],
139    adrs_template: &Adrs,
140) -> Result<Vec<u8>, SlhDsaError> {
141    let mut sig_fors = alloc::vec![0u8; P::K * (1 + P::A) * P::N];
142    fors_sign_into::<P>(md, sk_seed, pk_seed, adrs_template, &mut sig_fors)?;
143    Ok(sig_fors)
144}
145
146/// Streaming variant of [`fors_sign`] — writes the `K * (1 + A) * N`
147/// byte FORS signature directly into the start of `out` (which must
148/// be at least that size).
149///
150/// Each per-tree block is written as `sk (N) || auth path (A * N)`
151/// directly into its slot inside `out`, avoiding the transient
152/// per-call `Vec<u8>` heap buffer.
153///
154/// `adrs_template` is read-only — a local clone is taken internally so the
155/// caller's address state is not mutated. The save/restore of
156/// `key_pair_address` inside the per-tree loop already guarantees that the
157/// function does not depend on its own residual mutation across iterations,
158/// so the local clone is semantically equivalent to the previous in-place
159/// mutation pattern.
160///
161/// # Feature: `sca-fors-dummy-siblings` (T1-D)
162///
163/// When enabled, the inner per-FORS-tree loop is replaced by a single
164/// **full-tree BDS streaming traversal**: for each FORS tree `i`, all
165/// `2^A` leaves at positions `[base, base + 2^A)` are produced in
166/// fixed order (`base = i * 2^A`), regardless of the secret index
167/// `idx`. The leaf secret and the `A` auth-path sibling nodes are
168/// extracted **branchlessly** during the climb, via `silentops::ct_copy`
169/// guarded by `silentops::ct_eq_u32`. The address sequence absorbed by
170/// Keccak (`set_tree_index(base + 0..base + 2^A - 1)` and the matching
171/// internal-merge addresses) is then a deterministic function of `i`
172/// only — the per-bit template oracle of Kannwischer COSADE 2018
173/// (ePrint 2018/673) collapses, since no
174/// `idx`-dependent address ever reaches the PRF. Cost: roughly 2×
175/// the FORS hash count vs the FIPS-205 default path (one full
176/// tree vs the auth-path subtrees that sum to ~`2^A - 1` leaves).
177/// Output bytes are byte-identical to the default path on every
178/// input.
179///
180/// # Historical correction
181///
182/// The earlier SCA-annex draft described T1-D as "compute both
183/// possible siblings (s=0 and s=1) at fixed positions and select
184/// branchlessly". That framing is wrong: FIPS-205 Algorithm 16 has
185/// `s = floor(idx / 2^j) XOR 1` (multi-bit, taking values in
186/// `[0, 2^(A-j))` at level `j`), so the sibling sits at one of
187/// `2^(A-j)` `idx`-dependent positions, not at a fixed pair. The
188/// only mechanism that produces an `idx`-independent address
189/// sequence at the same asymptotic cost is the full-tree streaming
190/// traversal implemented here.
191pub fn fors_sign_into<P: Params>(
192    md: &[u8],
193    sk_seed: &[u8],
194    pk_seed: &[u8],
195    adrs_template: &Adrs,
196    out: &mut [u8],
197) -> Result<(), SlhDsaError> {
198    let entry_size = (1 + P::A) * P::N;
199    debug_assert!(out.len() >= P::K * entry_size);
200
201    let indices = message_to_indices::<P>(md);
202    let mut adrs = adrs_template.clone();
203    let kp = adrs.get_key_pair_address();
204
205    for i in 0..P::K {
206        let idx = indices[i];
207        let slot = &mut out[i * entry_size..(i + 1) * entry_size];
208        let (leaf_slot, auth_slot) = slot.split_at_mut(P::N);
209        let base = (i as u32) * (1u32 << P::A);
210
211        #[cfg(feature = "sca-fors-dummy-siblings")]
212        {
213            // T1-D — full-tree BDS streaming. Absorbed Keccak address
214            // sequence is `set_tree_index(base + k)` for `k ∈ [0, 2^A)`,
215            // plus the matching internal-merge addresses, all
216            // idx-independent. Leaf secret and auth-path siblings are
217            // extracted branchlessly via `ct_copy` + `ct_eq_u32`.
218            let mut stack: Vec<(Vec<u8>, u32)> = Vec::with_capacity(P::A + 1);
219            let num_leaves = 1u32 << P::A;
220
221            for k in 0..num_leaves {
222                let leaf_idx = base + k;
223
224                // Leaf secret value (absorbs FORS_PRF address with tree_index = leaf_idx).
225                let sk = fors_sk_gen::<P>(sk_seed, pk_seed, &mut adrs, leaf_idx);
226                // Branchlessly save sk to leaf_slot if k == idx.
227                silentops::ct_copy(leaf_slot, &sk, silentops::ct_eq_u32(k, idx));
228
229                // f_hash of the leaf at height 0 (absorbs FORS_TREE address).
230                adrs.set_type_and_clear(FORS_TREE);
231                adrs.set_key_pair_address(kp);
232                adrs.set_tree_height(0);
233                adrs.set_tree_index(leaf_idx);
234                let mut node = hash::f_hash::<P>(pk_seed, &mut adrs, &sk);
235                let mut height: u32 = 0;
236                let mut local_pos: u32 = k;
237
238                // Save height-0 node to auth_slot[0..N] if it is the
239                // sibling-at-level-0 of idx (i.e. local_pos == idx XOR 1).
240                silentops::ct_copy(&mut auth_slot[0..P::N], &node, silentops::ct_eq_u32(local_pos, idx ^ 1));
241
242                // BDS merge — combine same-height tops.
243                while let Some(&(_, top_h)) = stack.last() {
244                    if top_h != height {
245                        break;
246                    }
247                    let (left, _) = stack.pop().expect("non-empty");
248                    local_pos >>= 1;
249                    height += 1;
250
251                    // Absolute tree_index for the merged node at height h
252                    // is `(base + leftmost_leaf_offset) >> h`
253                    // = i * 2^(A - h) + local_pos.
254                    let absolute_pos = (i as u32) * (1u32 << (P::A - height as usize)) + local_pos;
255                    adrs.set_type_and_clear(FORS_TREE);
256                    adrs.set_key_pair_address(kp);
257                    adrs.set_tree_height(height);
258                    adrs.set_tree_index(absolute_pos);
259                    node = hash::hash_h::<P>(pk_seed, &mut adrs, &left, &node);
260
261                    // Save to auth_slot[h] if this merged node is the
262                    // sibling-at-level-h of idx. The final merge produces
263                    // the FORS root at height A — skipped (h == A => no
264                    // auth-path slot).
265                    if (height as usize) < P::A {
266                        let sibling_pos = (idx >> height) ^ 1;
267                        let slot_off = (height as usize) * P::N;
268                        silentops::ct_copy(
269                            &mut auth_slot[slot_off..slot_off + P::N],
270                            &node,
271                            silentops::ct_eq_u32(local_pos, sibling_pos),
272                        );
273                    }
274                }
275                stack.push((node, height));
276            }
277            // The stack now holds exactly one element — the FORS root at
278            // height A. The caller will re-derive the root from the
279            // produced signature via `fors_pk_from_sig`, so we drop it.
280            drop(stack);
281        }
282
283        #[cfg(not(feature = "sca-fors-dummy-siblings"))]
284        {
285            // FIPS-205 Algorithm 16 — single sibling per level.
286            // Leak surface: `set_tree_index((base + ((idx >> j) XOR 1) * 2^j) + k)` for
287            // each level j and each leaf k of the sibling subtree, idx-dependent.
288            let sk = fors_sk_gen::<P>(sk_seed, pk_seed, &mut adrs, base + idx);
289            leaf_slot.copy_from_slice(&sk);
290
291            for j in 0..P::A {
292                let s = (idx >> j) ^ 1;
293                let node = fors_node::<P>(sk_seed, pk_seed, base + s * (1 << j), j as u32, &mut adrs);
294                adrs.set_type_and_clear(FORS_TREE);
295                adrs.set_key_pair_address(kp);
296                auth_slot[j * P::N..(j + 1) * P::N].copy_from_slice(&node);
297            }
298        }
299    }
300
301    // T1-E — recompute the FORS-index vector from `md` and CT-compare to
302    // the one we just consumed. Catches a fault that flips a bit during
303    // the original `message_to_indices` call (or anywhere upstream in
304    // the digest extraction). Cost: one extra `base_2b` (~K * A / 8
305    // bytes of bit-extraction, no hashing).
306    #[cfg(feature = "sca-fors-indices-check")]
307    fors_indices_consistency_check::<P>(md, &indices)?;
308
309    Ok(())
310}
311
312/// T1-E — verify the FORS-index vector used during signing matches the
313/// one derivable from the digest. Re-derives `message_to_indices(md)`
314/// and compares to `used` under `silentops::ct_eq` on the little-endian
315/// serialisation. Returns `Err(SlhDsaError::FaultDetected)` on any
316/// mismatch (same error envelope as T1-C's recompute-and-compare).
317///
318/// `pub(crate)` so the `#[cfg(test)]` module can drive it with
319/// synthetic divergent buffers without needing real fault injection.
320#[cfg(feature = "sca-fors-indices-check")]
321pub(crate) fn fors_indices_consistency_check<P: Params>(md: &[u8], used: &[u32]) -> Result<(), SlhDsaError> {
322    let recomputed = message_to_indices::<P>(md);
323    if recomputed.len() != used.len() {
324        return Err(SlhDsaError::FaultDetected);
325    }
326    let used_bytes: Vec<u8> = used.iter().flat_map(|x| x.to_le_bytes()).collect();
327    let recomputed_bytes: Vec<u8> = recomputed.iter().flat_map(|x| x.to_le_bytes()).collect();
328    if silentops::ct_eq(&used_bytes, &recomputed_bytes) != 1 {
329        return Err(SlhDsaError::FaultDetected);
330    }
331    Ok(())
332}
333
334/// Compute a FORS public key from a FORS signature — constant-time.
335///
336/// Implements Algorithm 17 of FIPS 205. For each of the `k` FORS trees,
337/// hashes the revealed secret leaf, then walks up the authentication path
338/// to recover the tree root. The `k` roots are compressed into a single
339/// `n`-byte public key via `T_k`. If the signature is valid, the returned
340/// public key matches the one produced by the signer.
341///
342/// # Side-channel posture
343///
344/// The secret-dependent argument ordering of `hash_h` along each
345/// authentication-path level is resolved by a branchless byte-wise
346/// `silentops::ct_select_u8` cswap rather than a Rust `if`. Address bytes
347/// and the `tree_index` written into `adrs` are identical in both original
348/// branches, so they need no extra masking. The per-iteration scratch is
349/// drop-zeroized via `silentops::ct_zeroize` before return.
350///
351/// In the *verifier* path the FORS-tree digit `idx` is reconstructed from
352/// the publicly transmitted message digest, so a timing leak on the digit
353/// would be harmless. But the same routine is reused inside the *signer*
354/// post-signing redundancy check (item ``T1-C`` of the SLH-DSA roadmap —
355/// see [`fors_sign_into_redundant`]). At that point of the signing flow,
356/// `idx` is derived from values (the freshly sampled randomizer `R`, the
357/// secret `SK.prf`) that have **not yet** been published, so a control-flow
358/// leak on the digit would become a secret-key leak. A single CT
359/// implementation across both paths removes the foot-gun of a future call
360/// site picking a variable-time variant by autocomplete.
361///
362/// # References
363///
364/// * Castelnovi-Martinelli-Prest, *Grafting Trees: a Fault Attack against
365///   the SPHINCS framework*, ePrint 2018/102 — motivates T1-C, which in
366///   turn motivates this CT routine.
367/// * Adiletta et al., *SLasH-DSA: Breaking SLH-DSA Using an Extensible
368///   End-to-End Rowhammer Framework*, arXiv 2509.13048 (Aug 2025) —
369///   software realisation of the same attack class.
370/// * Genêt, *On Protecting SPHINCS+ Against Fault Attacks*, TCHES 2023(3)
371///   (ePrint 2023/042) — recommends the recompute-and-compare redundancy
372///   that consumes this routine.
373pub fn fors_pk_from_sig<P: Params>(sig_fors: &[u8], md: &[u8], pk_seed: &[u8], adrs_template: &Adrs) -> Vec<u8> {
374    let indices = message_to_indices::<P>(md);
375    let mut adrs = adrs_template.clone();
376    let kp = adrs.get_key_pair_address();
377
378    let entry_size = (1 + P::A) * P::N;
379    let mut roots = Vec::with_capacity(P::K * P::N);
380
381    // Per-iteration scratch for the CT argument-swap.
382    let mut left = alloc::vec![0u8; P::N];
383    let mut right = alloc::vec![0u8; P::N];
384
385    for i in 0..P::K {
386        let idx = indices[i];
387        let offset = i * entry_size;
388        let sk = &sig_fors[offset..offset + P::N];
389        let auth = &sig_fors[offset + P::N..offset + entry_size];
390
391        // Leaf from sk — same as the non-CT path.
392        adrs.set_type_and_clear(FORS_TREE);
393        adrs.set_key_pair_address(kp);
394        adrs.set_tree_height(0);
395        adrs.set_tree_index((i as u32) * (1u32 << P::A) + idx);
396        let mut node = hash::f_hash::<P>(pk_seed, &mut adrs, sk);
397
398        // Climb the authentication path — argument order to `hash_h` is
399        // selected by a branchless `ct_select_u8` cswap.
400        for j in 0..P::A {
401            let auth_j = &auth[j * P::N..(j + 1) * P::N];
402
403            // bit = 0 → `node` is the left child  → hash_h(node, auth_j)
404            // bit = 1 → `node` is the right child → hash_h(auth_j, node)
405            let bit = ((idx >> j) & 1) as u8;
406            for k in 0..P::N {
407                // ct_select_u8(a, b, cond) returns a if cond != 0 else b.
408                left[k] = silentops::ct_select_u8(auth_j[k], node[k], bit);
409                right[k] = silentops::ct_select_u8(node[k], auth_j[k], bit);
410            }
411
412            // tree_index is identical in both original branches.
413            adrs.set_type_and_clear(FORS_TREE);
414            adrs.set_key_pair_address(kp);
415            adrs.set_tree_height((j + 1) as u32);
416            adrs.set_tree_index(((i as u32) * (1u32 << P::A) + idx) >> (j + 1));
417
418            node = hash::hash_h::<P>(pk_seed, &mut adrs, &left, &right);
419        }
420
421        roots.extend_from_slice(&node);
422    }
423
424    // Per-loop scratch is wiped — the cswap copies are derived from
425    // publish-ready inputs once the signer commits to the signature, but
426    // the contract of this CT routine is that nothing of `idx`-shape
427    // leaks. Zeroizing keeps that contract strict.
428    silentops::ct_zeroize(&mut left);
429    silentops::ct_zeroize(&mut right);
430
431    // Compress k roots into one public key — the roots themselves are not
432    // secret-shaped at this stage.
433    let mut fors_pk_adrs = adrs.clone();
434    fors_pk_adrs.set_type_and_clear(FORS_ROOTS);
435    fors_pk_adrs.set_key_pair_address(kp);
436
437    hash::t_l::<P>(pk_seed, &fors_pk_adrs, &roots)
438}
439
440/// Recompute-and-compare FORS signing — T1-C redundancy against
441/// single-fault grafting-tree forgery.
442///
443/// Signs the FORS component twice into independent scratch buffers,
444/// derives the FORS public key from each signature via the
445/// constant-time [`fors_pk_from_sig`], then compares **both
446/// signatures and both derived public keys** under
447/// `silentops::ct_eq`. On any mismatch the function returns
448/// [`SlhDsaError::FaultDetected`] *without* writing anything into
449/// `out` — the faulted signature never leaves the device. On a clean
450/// run, the validated signature is copied into the caller's `out`
451/// buffer and the FORS public key (reusable by the hypertree
452/// signing step) is returned.
453///
454/// # Threat addressed
455///
456/// Castelnovi-Martinelli-Prest *Grafting Trees* (PQCrypto 2018,
457/// ePrint 2018/102) showed that a single random fault during any FORS
458/// hash chain produces a valid-looking signature under a different
459/// FORS root; after collecting a handful of such signatures an
460/// attacker forges arbitrary messages. Genêt *On Protecting SPHINCS+
461/// Against Fault Attacks* (TCHES 2023(3), ePrint 2023/042) recommends
462/// recompute-and-compare at signing time as the canonical defence.
463/// The threat is no longer hypothetical-physical: Adiletta et al.
464/// *SLasH-DSA* (arXiv 2509.13048, Aug 2025) realised it on stock
465/// OpenSSL with software-only Rowhammer in 1–8 h.
466///
467/// # Design rationale
468///
469/// Comparing *both* the signature bytes and the derived public keys
470/// is intentional defence-in-depth. A fault that corrupts auth-path
471/// bytes might round-trip to the same FORS root under the verifier
472/// path; the byte-level `ct_eq` catches that case. Symmetrically, a
473/// fault that lands inside the second [`fors_pk_from_sig`]
474/// derivation would not be caught by a signature-bytes-only check;
475/// the pk `ct_eq` catches that case. Both checks together cost a
476/// single extra `ct_eq` and are paid only on the slow path that
477/// already runs the FORS signer twice.
478///
479/// # Abort posture (vs ML-KEM's CT-substitute)
480///
481/// Unlike ML-KEM's double-decaps + branchless fault-fallback
482/// (`quantica::ml_kem::kem::decaps`), this routine **aborts**
483/// rather than substituting a fault-derived key. The asymmetry is
484/// deliberate: a KEM must always return a shared secret (or the
485/// caller cannot proceed at all), while a signer that detects a
486/// fault is allowed — required, per Genêt 2023 — to refuse to
487/// emit, so the faulted signature does not propagate.
488///
489/// # Memory
490///
491/// One [`SecretBytes`] scratch of length `fors_sig_len = K * (1 + A)
492/// * N` (≈ 10 KiB for SLH-DSA-SHAKE-256s). Heap-allocated so the
493/// stack budget on the M0 baseline stays honest. The scratch is
494/// drop-zeroized on both the success and the abort path.
495pub fn fors_sign_into_redundant<P: Params>(
496    md: &[u8],
497    sk_seed: &[u8],
498    pk_seed: &[u8],
499    adrs_template: &Adrs,
500    out: &mut [u8],
501) -> Result<Vec<u8>, SlhDsaError> {
502    let sig_len = P::K * (1 + P::A) * P::N;
503    debug_assert!(out.len() >= sig_len);
504
505    // Two independent signings. `SecretBytes` ensures Drop-zeroize
506    // even if we return early via the `?`-less Err path below.
507    let mut sig1 = SecretBytes::from_vec(alloc::vec![0u8; sig_len]);
508    let mut sig2 = SecretBytes::from_vec(alloc::vec![0u8; sig_len]);
509
510    // Both FORS routines take an immutable address template and clone
511    // internally for their own scratch, so `adrs_template` is reused as-is
512    // across all four calls — four independent address-state pipelines
513    // remain inside the four hashings, with zero caller-side cloning.
514    fors_sign_into::<P>(md, sk_seed, pk_seed, adrs_template, &mut sig1)?;
515    fors_sign_into::<P>(md, sk_seed, pk_seed, adrs_template, &mut sig2)?;
516
517    let pk1 = fors_pk_from_sig::<P>(sig1.as_bytes(), md, pk_seed, adrs_template);
518    let pk2 = fors_pk_from_sig::<P>(sig2.as_bytes(), md, pk_seed, adrs_template);
519
520    // CT-compare both surfaces. Mismatch on either aborts.
521    fors_redundancy_compare(sig1.as_bytes(), sig2.as_bytes(), &pk1, &pk2)?;
522
523    // Commit the validated signature.
524    out[..sig_len].copy_from_slice(sig1.as_bytes());
525
526    Ok(pk1)
527}
528
529/// Internal — apply the two `silentops::ct_eq` checks of the T1-C
530/// redundancy and produce the verdict.
531///
532/// Factored out so the test suite can exercise the comparison logic
533/// with synthetically divergent inputs without injecting a fault into
534/// the FORS signing primitive itself.
535#[inline]
536fn fors_redundancy_compare(sig1: &[u8], sig2: &[u8], pk1: &[u8], pk2: &[u8]) -> Result<(), SlhDsaError> {
537    // `silentops::ct_eq` returns 1 on equality, 0 otherwise.
538    let sigs_equal = silentops::ct_eq(sig1, sig2) == 1;
539    let pks_equal = silentops::ct_eq(pk1, pk2) == 1;
540    if !sigs_equal || !pks_equal {
541        return Err(SlhDsaError::FaultDetected);
542    }
543    Ok(())
544}
545
546/// Split a message digest `md` (`k * a` bits) into `k` unsigned integers of `a` bits each.
547///
548/// Uses [`base_2b`](super::wots::base_2b) to extract the indices. Each index selects
549/// one leaf in its corresponding FORS tree.
550fn message_to_indices<P: Params>(md: &[u8]) -> Vec<u32> {
551    // Use base_2b with b=a to extract k values
552    // md contains ceil(k*a / 8) bytes; we extract k values of a bits each.
553    super::wots::base_2b(md, P::A, P::K)
554}
555
556#[cfg(test)]
557mod tests {
558    use super::super::address::{Adrs, FORS_TREE};
559    use super::super::params::{Params, Shake128f, Shake128s};
560    use super::*;
561
562    /// Sign FORS with deterministic seeds, then re-derive the FORS public
563    /// key from the produced signature; assert that two back-to-back
564    /// derivations yield byte-identical output and that the output has the
565    /// expected `N`-byte width. Smoke test for the sign / pk-from-sig
566    /// round trip on every parameter set we exercise.
567    fn round_trip<P: Params>(seed_byte: u8, message_byte: u8) {
568        let sk_seed = alloc::vec![seed_byte; P::N];
569        let pk_seed = alloc::vec![seed_byte.wrapping_add(0x80); P::N];
570
571        // Synthesise a `digest` long enough for `message_to_indices` to
572        // extract K * A bits worth of FORS indices.
573        let md_bytes = (P::K * P::A + 7) / 8;
574        let md: Vec<u8> = (0..md_bytes).map(|i| message_byte.wrapping_add(i as u8)).collect();
575
576        let mut sig_fors = alloc::vec![0u8; P::K * (1 + P::A) * P::N];
577        let mut adrs = Adrs::new();
578        adrs.set_type_and_clear(FORS_TREE);
579        adrs.set_key_pair_address(0);
580        // FORS routines take an immutable template; one address suffices
581        // for the whole test.
582        super::fors_sign_into::<P>(&md, &sk_seed, &pk_seed, &adrs, &mut sig_fors)
583            .expect("round-trip — clean signing must succeed");
584
585        let pk_a = super::fors_pk_from_sig::<P>(&sig_fors, &md, &pk_seed, &adrs);
586        let pk_b = super::fors_pk_from_sig::<P>(&sig_fors, &md, &pk_seed, &adrs);
587
588        assert_eq!(pk_a.len(), P::N, "FORS pk must be N bytes wide");
589        assert_eq!(pk_a, pk_b, "fors_pk_from_sig must be deterministic");
590    }
591
592    #[test]
593    fn fors_pk_from_sig_round_trip_shake128s() {
594        // Several seed/message combinations to exercise the per-tree
595        // bit-by-bit cswap (idx bits vary across runs).
596        round_trip::<Shake128s>(0x00, 0x00);
597        round_trip::<Shake128s>(0x55, 0xAA);
598        round_trip::<Shake128s>(0xFF, 0xFF);
599        round_trip::<Shake128s>(0x42, 0x13);
600    }
601
602    #[test]
603    fn fors_pk_from_sig_round_trip_shake128f() {
604        round_trip::<Shake128f>(0x00, 0x00);
605        round_trip::<Shake128f>(0x55, 0xAA);
606        round_trip::<Shake128f>(0xFF, 0xFF);
607    }
608
609    // ===========================================================
610    // T1-C — fors_sign_into_redundant equivalence + negative tests
611    // ===========================================================
612
613    /// Drive `fors_sign_into_redundant` on the same inputs as the
614    /// classic `fors_sign_into`, and check that:
615    ///   1. the validated signature is byte-equal to the non-redundant
616    ///      signature (clean-run equivalence — no fault, no divergence);
617    ///   2. the returned FORS pk matches the standalone `fors_pk_from_sig`
618    ///      derivation from the produced signature.
619    fn redundancy_equivalence<P: Params>(seed_byte: u8, message_byte: u8) {
620        let sk_seed = alloc::vec![seed_byte; P::N];
621        let pk_seed = alloc::vec![seed_byte.wrapping_add(0x80); P::N];
622
623        let md_bytes = (P::K * P::A + 7) / 8;
624        let md: Vec<u8> = (0..md_bytes).map(|i| message_byte.wrapping_add(i as u8)).collect();
625
626        let sig_len = P::K * (1 + P::A) * P::N;
627
628        // Reference: classic non-redundant path.
629        let mut sig_ref = alloc::vec![0u8; sig_len];
630        // FORS routines take an immutable template; one address suffices
631        // for both the reference and the redundant paths plus the pk-check.
632        let mut adrs = Adrs::new();
633        adrs.set_type_and_clear(FORS_TREE);
634        adrs.set_key_pair_address(0);
635
636        super::fors_sign_into::<P>(&md, &sk_seed, &pk_seed, &adrs, &mut sig_ref)
637            .expect("redundancy_equivalence — clean reference sign must succeed");
638
639        // Redundant path — must succeed and produce the same bytes.
640        let mut sig_red = alloc::vec![0u8; sig_len];
641        let pk_red = super::fors_sign_into_redundant::<P>(&md, &sk_seed, &pk_seed, &adrs, &mut sig_red)
642            .expect("clean run must succeed");
643
644        assert_eq!(
645            sig_red, sig_ref,
646            "redundant signature must equal the non-redundant signature on a clean run"
647        );
648
649        // pk returned by the redundant path must equal the standalone
650        // derivation from the produced signature.
651        let pk_classic = super::fors_pk_from_sig::<P>(&sig_red, &md, &pk_seed, &adrs);
652
653        assert_eq!(pk_red, pk_classic, "redundant-returned pk must match fors_pk_from_sig");
654    }
655
656    #[test]
657    fn fors_sign_into_redundant_matches_reference_shake128s() {
658        redundancy_equivalence::<Shake128s>(0x00, 0x00);
659        redundancy_equivalence::<Shake128s>(0x55, 0xAA);
660        redundancy_equivalence::<Shake128s>(0xFF, 0xFF);
661    }
662
663    #[test]
664    fn fors_sign_into_redundant_matches_reference_shake128f() {
665        redundancy_equivalence::<Shake128f>(0x00, 0x00);
666        redundancy_equivalence::<Shake128f>(0x55, 0xAA);
667    }
668
669    /// Negative test — drive `fors_redundancy_compare` (the internal
670    /// helper that produces the abort verdict) with synthetically
671    /// divergent buffers and check that each kind of mismatch
672    /// (signature only, pk only, both) surfaces as `FaultDetected`.
673    /// We do not need to inject a real fault into the FORS signer
674    /// for this — exercising the comparison logic in isolation is
675    /// what catches a regression in the abort path.
676    #[test]
677    fn fors_redundancy_compare_detects_divergence() {
678        let sig_a = alloc::vec![0x42u8; 32];
679        let sig_b_eq = sig_a.clone();
680        let mut sig_b_diff = sig_a.clone();
681        sig_b_diff[5] ^= 0x01;
682
683        let pk_a = alloc::vec![0xAAu8; 16];
684        let pk_b_eq = pk_a.clone();
685        let mut pk_b_diff = pk_a.clone();
686        pk_b_diff[3] ^= 0x80;
687
688        // 1. all four buffers agree → Ok.
689        assert!(super::fors_redundancy_compare(&sig_a, &sig_b_eq, &pk_a, &pk_b_eq).is_ok());
690
691        // 2. signature bytes diverge → FaultDetected.
692        assert!(matches!(
693            super::fors_redundancy_compare(&sig_a, &sig_b_diff, &pk_a, &pk_b_eq),
694            Err(SlhDsaError::FaultDetected)
695        ));
696
697        // 3. pk diverges → FaultDetected.
698        assert!(matches!(
699            super::fors_redundancy_compare(&sig_a, &sig_b_eq, &pk_a, &pk_b_diff),
700            Err(SlhDsaError::FaultDetected)
701        ));
702
703        // 4. both diverge → FaultDetected (same observable, but the
704        //    short-circuit must not change the verdict).
705        assert!(matches!(
706            super::fors_redundancy_compare(&sig_a, &sig_b_diff, &pk_a, &pk_b_diff),
707            Err(SlhDsaError::FaultDetected)
708        ));
709    }
710
711    // ===========================================================
712    // T1-E — fors_indices_consistency_check (positive + negative)
713    // ===========================================================
714
715    /// Positive — feed the helper the indices that `message_to_indices`
716    /// produces from the same `md`, expect `Ok`. Exercised across the
717    /// `Shake128s` and `Shake128f` parameter sets so the K * A bit
718    /// extraction is non-trivial on both.
719    #[cfg(feature = "sca-fors-indices-check")]
720    fn indices_check_accepts<P: Params>(seed_byte: u8) {
721        let md_bytes = (P::K * P::A + 7) / 8;
722        let md: Vec<u8> = (0..md_bytes).map(|i| seed_byte.wrapping_add(i as u8)).collect();
723        let indices = super::message_to_indices::<P>(&md);
724        assert!(super::fors_indices_consistency_check::<P>(&md, &indices).is_ok());
725    }
726
727    #[test]
728    #[cfg(feature = "sca-fors-indices-check")]
729    fn fors_indices_check_accepts_correct_shake128s() {
730        indices_check_accepts::<Shake128s>(0x00);
731        indices_check_accepts::<Shake128s>(0x42);
732        indices_check_accepts::<Shake128s>(0xFF);
733    }
734
735    #[test]
736    #[cfg(feature = "sca-fors-indices-check")]
737    fn fors_indices_check_accepts_correct_shake128f() {
738        indices_check_accepts::<Shake128f>(0x00);
739        indices_check_accepts::<Shake128f>(0x55);
740    }
741
742    /// Negative — flip one bit of one index and expect `FaultDetected`.
743    /// We do not need to inject a fault into `message_to_indices`
744    /// itself; passing a deliberately corrupted `used` vector
745    /// exercises the same CT-compare path the integrity check runs.
746    #[test]
747    #[cfg(feature = "sca-fors-indices-check")]
748    fn fors_indices_check_rejects_flipped_index() {
749        type P = Shake128s;
750        let md_bytes = (P::K * P::A + 7) / 8;
751        let md: Vec<u8> = (0..md_bytes).map(|i| i as u8).collect();
752        let mut indices = super::message_to_indices::<P>(&md);
753
754        // Corrupt one index — flip its lowest bit.
755        indices[0] ^= 1;
756
757        assert!(matches!(
758            super::fors_indices_consistency_check::<P>(&md, &indices),
759            Err(SlhDsaError::FaultDetected)
760        ));
761
762        // Corrupt by changing length — expect FaultDetected too.
763        let mut short = super::message_to_indices::<P>(&md);
764        short.pop();
765        assert!(matches!(
766            super::fors_indices_consistency_check::<P>(&md, &short),
767            Err(SlhDsaError::FaultDetected)
768        ));
769    }
770}