pub fn fors_sign_into_redundant<P: Params>(
md: &[u8],
sk_seed: &[u8],
pk_seed: &[u8],
adrs_template: &Adrs,
out: &mut [u8],
) -> Result<Vec<u8>, SlhDsaError>Expand description
Recompute-and-compare FORS signing — T1-C redundancy against single-fault grafting-tree forgery.
Signs the FORS component twice into independent scratch buffers,
derives the FORS public key from each signature via the
constant-time fors_pk_from_sig, then compares both
signatures and both derived public keys under
silentops::ct_eq. On any mismatch the function returns
SlhDsaError::FaultDetected without writing anything into
out — the faulted signature never leaves the device. On a clean
run, the validated signature is copied into the caller’s out
buffer and the FORS public key (reusable by the hypertree
signing step) is returned.
§Threat addressed
Castelnovi-Martinelli-Prest Grafting Trees (PQCrypto 2018, ePrint 2018/102) showed that a single random fault during any FORS hash chain produces a valid-looking signature under a different FORS root; after collecting a handful of such signatures an attacker forges arbitrary messages. Genêt On Protecting SPHINCS+ Against Fault Attacks (TCHES 2023(3), ePrint 2023/042) recommends recompute-and-compare at signing time as the canonical defence. The threat is no longer hypothetical-physical: Adiletta et al. SLasH-DSA (arXiv 2509.13048, Aug 2025) realised it on stock OpenSSL with software-only Rowhammer in 1–8 h.
§Design rationale
Comparing both the signature bytes and the derived public keys
is intentional defence-in-depth. A fault that corrupts auth-path
bytes might round-trip to the same FORS root under the verifier
path; the byte-level ct_eq catches that case. Symmetrically, a
fault that lands inside the second fors_pk_from_sig
derivation would not be caught by a signature-bytes-only check;
the pk ct_eq catches that case. Both checks together cost a
single extra ct_eq and are paid only on the slow path that
already runs the FORS signer twice.
§Abort posture (vs ML-KEM’s CT-substitute)
Unlike ML-KEM’s double-decaps + branchless fault-fallback
(quantica::ml_kem::kem::decaps), this routine aborts
rather than substituting a fault-derived key. The asymmetry is
deliberate: a KEM must always return a shared secret (or the
caller cannot proceed at all), while a signer that detects a
fault is allowed — required, per Genêt 2023 — to refuse to
emit, so the faulted signature does not propagate.
§Memory
One SecretBytes scratch of length `fors_sig_len = K * (1 + A)
- N` (≈ 10 KiB for SLH-DSA-SHAKE-256s). Heap-allocated so the stack budget on the M0 baseline stays honest. The scratch is drop-zeroized on both the success and the abort path.