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}