Skip to main content

quantica/slh_dsa/
slh.rs

1//! Top-level SLH-DSA algorithms (FIPS 205, Algorithms 18-22, 24).
2//!
3//! This module implements the complete SLH-DSA signature lifecycle:
4//!
5//! - **Key generation** (`slh_keygen`, `slh_keygen_internal`): generates the master
6//!   seeds and computes the hypertree root to form the public key.
7//! - **Signing** (`slh_sign`, `slh_sign_internal`): hashes the message, signs it
8//!   with FORS, then signs the FORS public key with the hypertree.
9//! - **Verification** (`slh_verify`, `slh_verify_internal`): recomputes the FORS
10//!   public key from the FORS signature, then verifies the hypertree signature against
11//!   the public key root.
12//!
13//! The signature format is: `R || SIG_FORS || SIG_HT`, where `R` is the `n`-byte
14//! randomizer, `SIG_FORS` is the FORS signature, and `SIG_HT` is the hypertree signature.
15
16use super::SlhDsaError;
17use super::address::Adrs;
18use super::fors;
19use super::hash;
20use super::hypertree;
21use super::params::{self, Params};
22use super::rng::CryptoRng;
23use super::xmss;
24use alloc::vec::Vec;
25
26/// Generate an SLH-DSA key pair from explicit seed material (deterministic).
27///
28/// Implements Algorithm 18 of FIPS 205. Computes the top-level XMSS tree root
29/// from the provided seeds, then assembles the secret and public keys.
30///
31/// - `sk_seed`: the master secret seed (`n` bytes), used to derive all WOTS+ and FORS secrets
32/// - `sk_prf`: the PRF key (`n` bytes), used to generate per-signature randomizers
33/// - `pk_seed`: the public seed (`n` bytes), included in every hash call for domain separation
34///
35/// Returns `(sk, pk)` where:
36/// - `sk = SK.seed || SK.prf || PK.seed || PK.root` (4n bytes)
37/// - `pk = PK.seed || PK.root` (2n bytes)
38pub fn slh_keygen_internal<P: Params>(sk_seed: &[u8], sk_prf: &[u8], pk_seed: &[u8]) -> (Vec<u8>, Vec<u8>) {
39    let mut adrs = Adrs::new();
40    adrs.set_layer_address((P::D - 1) as u32);
41
42    // Compute the top-level XMSS tree root
43    let pk_root = xmss::xmss_node::<P>(sk_seed, pk_seed, 0, P::H_PRIME as u32, &mut adrs);
44
45    // sk = SK.seed || SK.prf || PK.seed || PK.root
46    let mut sk = Vec::with_capacity(P::SK_LEN);
47    sk.extend_from_slice(sk_seed);
48    sk.extend_from_slice(sk_prf);
49    sk.extend_from_slice(pk_seed);
50    sk.extend_from_slice(&pk_root);
51
52    // pk = PK.seed || PK.root
53    let mut pk = Vec::with_capacity(P::PK_LEN);
54    pk.extend_from_slice(pk_seed);
55    pk.extend_from_slice(&pk_root);
56
57    (sk, pk)
58}
59
60/// Internal SLH-DSA signing function.
61///
62/// Implements Algorithm 19 of FIPS 205. Performs the full signing pipeline:
63///
64/// 1. Generates the per-signature randomizer `R` via `PRF_msg(SK.prf, addrnd, M)`.
65/// 2. Hashes the message with `H_msg` to produce a digest, then splits it into
66///    FORS indices (`md`), a tree index (`idx_tree`), and a leaf index (`idx_leaf`).
67/// 3. Signs `md` with FORS to obtain `SIG_FORS`, then computes the FORS public key.
68/// 4. Signs the FORS public key with the hypertree to obtain `SIG_HT`.
69///
70/// The `addrnd` parameter is `n` bytes of additional randomness for hedged signing.
71/// Passing `PK.seed` instead yields deterministic signing.
72///
73/// Returns the full signature: `R || SIG_FORS || SIG_HT`.
74///
75/// # Feature: `sca-fors-redundancy`
76///
77/// When this cargo feature is enabled, the FORS sub-signature is produced
78/// by [`fors::fors_sign_into_redundant`] (T1-C — recompute-and-compare
79/// against single-fault grafting-tree forgery, per
80/// :cite:`genet2023_protecting_sphincs_faults`,
81/// :cite:`castelnovi2018_grafting_trees`,
82/// :cite:`adiletta2025_slashdsa_rowhammer`). On a detected divergence the
83/// function returns [`Err(SlhDsaError::FaultDetected)`][SlhDsaError] —
84/// the faulted signature never leaves the device. The validated FORS
85/// public key returned by the redundant routine is fed straight into the
86/// hypertree, saving a third FORS-root derivation.
87///
88/// Without the feature, the result is always [`Ok`] — the unified `Result`
89/// envelope is paid only to keep one signature across the two
90/// configurations. CAVP / KAT output bytes are unchanged.
91pub fn slh_sign_internal<P: Params>(m: &[u8], sk: &[u8], addrnd: &[u8]) -> Result<Vec<u8>, SlhDsaError> {
92    let n = P::N;
93    // Parse SK
94    let sk_seed = &sk[..n];
95    let sk_prf = &sk[n..2 * n];
96    let pk_seed = &sk[2 * n..3 * n];
97    let pk_root = &sk[3 * n..4 * n];
98
99    // Generate randomizer R
100    let r = hash::prf_msg::<P>(sk_prf, addrnd, m);
101
102    // One allocation for the full signature. All sub-components
103    // (R || SIG_FORS || SIG_HT) are written directly into slices of
104    // this buffer — no transient per-component `Vec<u8>` is held.
105    let sig_len = params::sig_len::<P>();
106    let fors_sig_len = P::K * (1 + P::A) * n;
107    let mut sig = alloc::vec![0u8; sig_len];
108    sig[..n].copy_from_slice(&r);
109
110    // Compute message digest
111    let digest = hash::h_msg::<P>(&r, pk_seed, pk_root, m);
112
113    // Split digest into: md (k*a/8 bytes), idx_tree, idx_leaf
114    let md_len = (P::K * P::A + 7) / 8;
115    let tree_bits = P::H - P::H_PRIME; // bits for idx_tree
116    let tree_bytes = (tree_bits + 7) / 8;
117    let leaf_bytes = (P::H_PRIME + 7) / 8;
118
119    let md = &digest[..md_len];
120    let tree_part = &digest[md_len..md_len + tree_bytes];
121    let leaf_part = &digest[md_len + tree_bytes..md_len + tree_bytes + leaf_bytes];
122
123    // Extract idx_tree and idx_leaf
124    let idx_tree = bytes_to_u64(tree_part) & ((1u64 << tree_bits) - 1);
125    let idx_leaf = bytes_to_u32(leaf_part) & ((1u32 << P::H_PRIME) - 1);
126
127    // FORS signature — streamed into sig[n..n + fors_sig_len].
128    // `adrs` is the immutable template passed to all FORS routines; each
129    // callee clones internally for its own scratch state.
130    let mut adrs = Adrs::new();
131    adrs.set_tree_address(idx_tree);
132    adrs.set_type_and_clear(super::address::FORS_TREE);
133    adrs.set_key_pair_address(idx_leaf);
134    let adrs = adrs;
135
136    let pk_fors = {
137        let fors_slot = &mut sig[n..n + fors_sig_len];
138        #[cfg(feature = "sca-fors-redundancy")]
139        {
140            // T1-C — sign FORS twice, CT-compare both signatures and both
141            // derived pks, abort with `FaultDetected` on any mismatch. The
142            // returned pk is reused for the hypertree signing below.
143            fors::fors_sign_into_redundant::<P>(md, sk_seed, pk_seed, &adrs, fors_slot)?
144        }
145        #[cfg(not(feature = "sca-fors-redundancy"))]
146        {
147            fors::fors_sign_into::<P>(md, sk_seed, pk_seed, &adrs, fors_slot)?;
148            // Re-read SIG_FORS from the slice we just filled to compute
149            // the FORS public key — no extra buffer needed.
150            fors::fors_pk_from_sig::<P>(&sig[n..n + fors_sig_len], md, pk_seed, &adrs)
151        }
152    };
153
154    // Hypertree signature — streamed into sig[n + fors_sig_len..].
155    {
156        let ht_slot = &mut sig[n + fors_sig_len..];
157        hypertree::ht_sign_into::<P>(&pk_fors, sk_seed, pk_seed, idx_tree, idx_leaf, ht_slot);
158    }
159
160    Ok(sig)
161}
162
163/// Internal SLH-DSA verification function.
164///
165/// Implements Algorithm 20 of FIPS 205. Reverses the signing pipeline:
166///
167/// 1. Parses `R`, `SIG_FORS`, and `SIG_HT` from the signature.
168/// 2. Recomputes the message digest using `H_msg(R, PK.seed, PK.root, M)`.
169/// 3. Recovers the FORS public key from `SIG_FORS` and the digest indices.
170/// 4. Verifies the hypertree signature `SIG_HT` over the recovered FORS public key.
171///
172/// Returns `true` if the hypertree root matches `PK.root`, `false` otherwise.
173pub fn slh_verify_internal<P: Params>(m: &[u8], sig: &[u8], pk: &[u8]) -> bool {
174    let n = P::N;
175    let expected_sig_len = params::sig_len::<P>();
176    if sig.len() != expected_sig_len {
177        return false;
178    }
179    if pk.len() != P::PK_LEN {
180        return false;
181    }
182
183    // Parse PK
184    let pk_seed = &pk[..n];
185    let pk_root = &pk[n..2 * n];
186
187    // Parse signature: R || SIG_FORS || SIG_HT
188    let r = &sig[..n];
189    let fors_sig_len = P::K * (1 + P::A) * n;
190    let sig_fors = &sig[n..n + fors_sig_len];
191    let sig_ht = &sig[n + fors_sig_len..];
192
193    // Compute message digest
194    let digest = hash::h_msg::<P>(r, pk_seed, pk_root, m);
195
196    let md_len = (P::K * P::A + 7) / 8;
197    let tree_bits = P::H - P::H_PRIME;
198    let tree_bytes = (tree_bits + 7) / 8;
199    let leaf_bytes = (P::H_PRIME + 7) / 8;
200
201    let md = &digest[..md_len];
202    let tree_part = &digest[md_len..md_len + tree_bytes];
203    let leaf_part = &digest[md_len + tree_bytes..md_len + tree_bytes + leaf_bytes];
204
205    let idx_tree = bytes_to_u64(tree_part) & ((1u64 << tree_bits) - 1);
206    let idx_leaf = bytes_to_u32(leaf_part) & ((1u32 << P::H_PRIME) - 1);
207
208    // Compute FORS public key from signature
209    let mut adrs = Adrs::new();
210    adrs.set_tree_address(idx_tree);
211    adrs.set_type_and_clear(super::address::FORS_TREE);
212    adrs.set_key_pair_address(idx_leaf);
213    let adrs = adrs;
214
215    let pk_fors = fors::fors_pk_from_sig::<P>(sig_fors, md, pk_seed, &adrs);
216
217    // Verify hypertree signature
218    hypertree::ht_verify::<P>(&pk_fors, sig_ht, pk_seed, idx_tree, idx_leaf, pk_root)
219}
220
221/// Generate an SLH-DSA key pair using random seeds from the provided RNG.
222///
223/// Implements Algorithm 21 of FIPS 205. Draws three independent `n`-byte random
224/// values (`SK.seed`, `SK.prf`, `PK.seed`) from `rng`, then delegates to
225/// `slh_keygen_internal` to compute the hypertree root and assemble the keys.
226///
227/// Returns `(secret_key, public_key)` or an error if the RNG fails.
228pub fn slh_keygen<P: Params>(rng: &mut dyn CryptoRng) -> Result<(Vec<u8>, Vec<u8>), SlhDsaError> {
229    let n = P::N;
230    let mut sk_seed = vec![0u8; n];
231    let mut sk_prf = vec![0u8; n];
232    let mut pk_seed = vec![0u8; n];
233
234    rng.fill_bytes(&mut sk_seed)?;
235    rng.fill_bytes(&mut sk_prf)?;
236    rng.fill_bytes(&mut pk_seed)?;
237
238    Ok(slh_keygen_internal::<P>(&sk_seed, &sk_prf, &pk_seed))
239}
240
241/// Sign a message with randomized (hedged) signing.
242///
243/// Implements Algorithm 22 of FIPS 205. Draws `n` bytes of randomness from `rng` to
244/// use as the `addrnd` parameter, providing hedged signing that remains secure even
245/// if the RNG is somewhat predictable.
246///
247/// Returns the full SLH-DSA signature or an error if the key is invalid, the RNG
248/// fails, or — when `sca-fors-redundancy` is enabled — the T1-C FORS
249/// recompute-and-compare check detects a fault.
250pub fn slh_sign<P: Params>(m: &[u8], sk: &[u8], rng: &mut dyn CryptoRng) -> Result<Vec<u8>, SlhDsaError> {
251    let n = P::N;
252    if sk.len() != P::SK_LEN {
253        return Err(SlhDsaError::InvalidKey);
254    }
255
256    let mut addrnd = vec![0u8; n];
257    rng.fill_bytes(&mut addrnd)?;
258
259    slh_sign_internal::<P>(m, sk, &addrnd)
260}
261
262/// Verify an SLH-DSA signature against a message and public key.
263///
264/// Implements Algorithm 24 of FIPS 205. Validates the key and signature lengths,
265/// then delegates to `slh_verify_internal`.
266///
267/// Returns `Ok(true)` if valid, `Ok(false)` if the signature does not verify,
268/// or `Err` if the key or signature has an invalid length.
269pub fn slh_verify<P: Params>(m: &[u8], sig: &[u8], pk: &[u8]) -> Result<bool, SlhDsaError> {
270    if pk.len() != P::PK_LEN {
271        return Err(SlhDsaError::InvalidKey);
272    }
273    let expected_sig_len = params::sig_len::<P>();
274    if sig.len() != expected_sig_len {
275        return Err(SlhDsaError::InvalidSignature);
276    }
277    Ok(slh_verify_internal::<P>(m, sig, pk))
278}
279
280// ---------- Helpers ----------
281
282/// Convert a big-endian byte slice to u64.
283fn bytes_to_u64(b: &[u8]) -> u64 {
284    let mut val = 0u64;
285    for &byte in b {
286        val = (val << 8) | byte as u64;
287    }
288    val
289}
290
291/// Convert a big-endian byte slice to u32.
292fn bytes_to_u32(b: &[u8]) -> u32 {
293    let mut val = 0u32;
294    for &byte in b {
295        val = (val << 8) | byte as u32;
296    }
297    val
298}