Skip to main content

quantica/slh_dsa/
mod.rs

1//! SLH-DSA: Stateless Hash-Based Digital Signature Standard (FIPS 205).
2//!
3//! This crate provides a pure-Rust implementation of SLH-DSA (formerly known as SPHINCS+),
4//! a post-quantum digital signature scheme standardized in [FIPS 205]. SLH-DSA is purely
5//! hash-based: its security relies only on the properties of cryptographic hash functions,
6//! with no algebraic structure (lattices, codes, etc.) that could be exploited by quantum
7//! or classical algorithms beyond generic attacks.
8//!
9//! # Architecture
10//!
11//! SLH-DSA is built from a hierarchy of hash-based primitives:
12//!
13//! 1. **WOTS+** -- A one-time signature scheme that signs a single n-byte message using
14//!    hash chains (see [`wots`]).
15//! 2. **XMSS** -- An eXtended Merkle Signature Scheme that authenticates 2^h' WOTS+ keys
16//!    via a binary Merkle tree, producing a few-time signature (see [`xmss`]).
17//! 3. **Hypertree** -- A tree of XMSS trees stacked in `d` layers, giving a many-time
18//!    signature scheme with a total tree height of `h = d * h'` (see [`hypertree`]).
19//! 4. **FORS** -- A Forest of Random Subsets, a few-time signature used to sign the
20//!    message digest before passing it to the hypertree (see [`fors`]).
21//! 5. **SLH-DSA** -- The top-level scheme that combines FORS + Hypertree to produce a
22//!    stateless, many-time signature (see [`slh`]).
23//!
24//! # Supported parameter sets
25//!
26//! This crate implements all six SHAKE-based parameter sets defined in FIPS 205 Section 11:
27//!
28//! | Type | 128-bit | 192-bit | 256-bit |
29//! |------|---------|---------|---------|
30//! | Small (s) | [`Shake128s`] | [`Shake192s`] | [`Shake256s`] |
31//! | Fast (f)  | [`Shake128f`] | [`Shake192f`] | [`Shake256f`] |
32//!
33//! The "s" variants produce smaller signatures; the "f" variants are faster to sign and verify.
34//!
35//! # Examples
36//!
37//! ```rust
38//! use quantica::slh_dsa::{SlhDsa, Shake128f, OsRng};
39//!
40//! // Generate a key pair
41//! let mut rng = OsRng;
42//! let (secret_key, public_key) = SlhDsa::<Shake128f>::keygen(&mut rng).unwrap();
43//!
44//! // Sign a message
45//! let message = b"hello, post-quantum world!";
46//! let signature = SlhDsa::<Shake128f>::sign(message, &secret_key, &mut rng).unwrap();
47//!
48//! // Verify the signature
49//! let valid = SlhDsa::<Shake128f>::verify(message, &signature, &public_key).unwrap();
50//! assert!(valid);
51//! ```
52//!
53//! [FIPS 205]: https://doi.org/10.6028/NIST.FIPS.205
54
55/// Address structure used to domain-separate hash calls throughout SLH-DSA.
56pub mod address;
57/// FORS: Forest of Random Subsets few-time signature scheme.
58pub mod fors;
59/// SHAKE-based tweakable hash function wrappers (H_msg, PRF, PRF_msg, T_l, H, F).
60pub mod hash;
61/// Hypertree: a d-layer tree of XMSS trees for many-time signing.
62pub mod hypertree;
63/// SLH-DSA parameter set definitions and the [`Params`] trait.
64pub mod params;
65/// Minimal cryptographic RNG trait and OS-backed implementation.
66pub mod rng;
67/// `Keccak-f[1600]` based SHAKE256 implementation (FIPS 202).
68pub mod sha3;
69/// Top-level SLH-DSA key generation, signing, and verification algorithms.
70pub mod slh;
71/// WOTS+ one-time signature scheme based on hash chains.
72pub mod wots;
73/// XMSS: eXtended Merkle Signature Scheme combining WOTS+ with a Merkle tree.
74pub mod xmss;
75
76// Re-export parameter set types for convenience.
77pub use params::{Params, Shake128f, Shake128s, Shake192f, Shake192s, Shake256f, Shake256s};
78pub use rng::CryptoRng;
79#[cfg(feature = "std")]
80pub use rng::OsRng;
81
82/// Errors that can occur in SLH-DSA operations.
83///
84/// These errors cover failures in random number generation, malformed keys or signatures,
85/// and verification mismatches. All variants are non-recoverable in the sense that retrying
86/// with the same inputs will produce the same error.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum SlhDsaError {
89    /// The cryptographic random number generator failed to produce bytes.
90    ///
91    /// This typically indicates an OS-level failure (e.g., `/dev/urandom` unavailable).
92    RngFailure,
93    /// The provided key has an invalid format or unexpected length.
94    ///
95    /// Public keys must be `2*n` bytes and secret keys `4*n` bytes, where `n` is the
96    /// security parameter ([`Params::N`]).
97    InvalidKey,
98    /// The provided signature has an invalid format or unexpected length.
99    ///
100    /// Signatures must be exactly [`SlhDsa::signature_size`] bytes for the chosen parameter set.
101    InvalidSignature,
102    /// The signature did not verify against the given message and public key.
103    VerificationFailed,
104    /// Signing-side fault redundancy detected a mismatch between the two
105    /// independent signing runs (item `T1-C` of the SLH-DSA SCA roadmap).
106    ///
107    /// Gated by the `sca-fors-redundancy` cargo feature, the signer
108    /// produces the FORS signature twice and aborts before emission if
109    /// the two results disagree — single-fault grafting-tree forgeries
110    /// (Castelnovi 2018, Adiletta 2025) cannot then propagate out of
111    /// the device. The error is non-recoverable; a retry on the same
112    /// inputs will either succeed (the fault was transient) or surface
113    /// the same error again.
114    FaultDetected,
115}
116
117impl core::fmt::Display for SlhDsaError {
118    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
119        match self {
120            SlhDsaError::RngFailure => write!(f, "RNG failure"),
121            SlhDsaError::InvalidKey => write!(f, "Invalid key"),
122            SlhDsaError::InvalidSignature => write!(f, "Invalid signature"),
123            SlhDsaError::VerificationFailed => write!(f, "Verification failed"),
124            SlhDsaError::FaultDetected => write!(f, "Signing-side fault detected by FORS redundancy"),
125        }
126    }
127}
128
129#[cfg(feature = "std")]
130impl std::error::Error for SlhDsaError {}
131
132use crate::secret::SecretBytes;
133/// High-level SLH-DSA API parameterized by a parameter set.
134///
135/// This is the main entry point for using SLH-DSA. The type parameter `P` selects one of
136/// the six SHAKE-based parameter sets (e.g., [`Shake128f`], [`Shake256s`]).
137///
138/// `SlhDsa` is a zero-sized type that serves as a namespace for the static methods
139/// [`keygen`](Self::keygen), [`sign`](Self::sign), and [`verify`](Self::verify).
140///
141/// # Examples
142///
143/// ```rust
144/// use quantica::slh_dsa::{SlhDsa, Shake256s, OsRng};
145///
146/// let mut rng = OsRng;
147/// let (sk, pk) = SlhDsa::<Shake256s>::keygen(&mut rng).unwrap();
148/// let sig = SlhDsa::<Shake256s>::sign(b"data", &sk, &mut rng).unwrap();
149/// assert!(SlhDsa::<Shake256s>::verify(b"data", &sig, &pk).unwrap());
150/// ```
151use alloc::vec::Vec;
152use core::marker::PhantomData;
153
154// =====================================================================
155// Typed key / signature wrappers
156// =====================================================================
157
158/// SLH-DSA **verifying key** (public key, `2 * P::N` bytes).
159///
160/// Type-tagged with `P`. No zeroization.
161pub struct VerifyingKey<P: Params> {
162    bytes: Vec<u8>,
163    _marker: PhantomData<P>,
164}
165
166impl<P: Params> VerifyingKey<P> {
167    /// Wrap a raw byte slice. Length is validated against
168    /// [`Params::PK_LEN`].
169    pub fn from_bytes(bytes: &[u8]) -> Result<Self, SlhDsaError> {
170        if bytes.len() != P::PK_LEN {
171            return Err(SlhDsaError::InvalidKey);
172        }
173        Ok(Self {
174            bytes: bytes.to_vec(),
175            _marker: PhantomData,
176        })
177    }
178
179    /// Borrow the encoded verifying key as a byte slice.
180    pub fn as_bytes(&self) -> &[u8] {
181        &self.bytes
182    }
183
184    /// Length in bytes (always [`Params::PK_LEN`]).
185    pub fn len(&self) -> usize {
186        self.bytes.len()
187    }
188}
189
190impl<P: Params> AsRef<[u8]> for VerifyingKey<P> {
191    fn as_ref(&self) -> &[u8] {
192        &self.bytes
193    }
194}
195
196impl<P: Params> core::ops::Deref for VerifyingKey<P> {
197    type Target = [u8];
198    fn deref(&self) -> &[u8] {
199        &self.bytes
200    }
201}
202
203impl<P: Params> Clone for VerifyingKey<P> {
204    fn clone(&self) -> Self {
205        Self {
206            bytes: self.bytes.clone(),
207            _marker: PhantomData,
208        }
209    }
210}
211
212/// SLH-DSA **signing key** (secret key, `4 * P::N` bytes).
213///
214/// Backed by [`SecretBytes`] — wipes its memory on [`Drop`] via
215/// `silentops::ct_zeroize`. Type-tagged with `P`.
216pub struct SigningKey<P: Params> {
217    bytes: SecretBytes,
218    _marker: PhantomData<P>,
219}
220
221impl<P: Params> SigningKey<P> {
222    /// Wrap a raw byte slice. Length is validated against
223    /// [`Params::SK_LEN`].
224    pub fn from_bytes(bytes: &[u8]) -> Result<Self, SlhDsaError> {
225        if bytes.len() != P::SK_LEN {
226            return Err(SlhDsaError::InvalidKey);
227        }
228        Ok(Self {
229            bytes: SecretBytes::from_slice(bytes),
230            _marker: PhantomData,
231        })
232    }
233
234    /// Borrow the encoded signing key as a byte slice.
235    pub fn as_bytes(&self) -> &[u8] {
236        self.bytes.as_bytes()
237    }
238
239    /// Length in bytes (always [`Params::SK_LEN`]).
240    pub fn len(&self) -> usize {
241        self.bytes.len()
242    }
243}
244
245impl<P: Params> AsRef<[u8]> for SigningKey<P> {
246    fn as_ref(&self) -> &[u8] {
247        self.bytes.as_bytes()
248    }
249}
250
251impl<P: Params> core::ops::Deref for SigningKey<P> {
252    type Target = [u8];
253    fn deref(&self) -> &[u8] {
254        self.bytes.as_bytes()
255    }
256}
257
258/// SLH-DSA **signature**. Type-tagged with `P`.
259///
260/// Public material — not zeroized.
261pub struct Signature<P: Params> {
262    bytes: Vec<u8>,
263    _marker: PhantomData<P>,
264}
265
266impl<P: Params> Signature<P> {
267    /// Wrap a raw byte slice. Length is validated against
268    /// [`params::sig_len::<P>()`].
269    pub fn from_bytes(bytes: &[u8]) -> Result<Self, SlhDsaError> {
270        if bytes.len() != params::sig_len::<P>() {
271            return Err(SlhDsaError::InvalidSignature);
272        }
273        Ok(Self {
274            bytes: bytes.to_vec(),
275            _marker: PhantomData,
276        })
277    }
278
279    /// Borrow the encoded signature as a byte slice.
280    pub fn as_bytes(&self) -> &[u8] {
281        &self.bytes
282    }
283
284    /// Length in bytes (always [`params::sig_len::<P>()`]).
285    pub fn len(&self) -> usize {
286        self.bytes.len()
287    }
288}
289
290impl<P: Params> AsRef<[u8]> for Signature<P> {
291    fn as_ref(&self) -> &[u8] {
292        &self.bytes
293    }
294}
295
296impl<P: Params> core::ops::Deref for Signature<P> {
297    type Target = [u8];
298    fn deref(&self) -> &[u8] {
299        &self.bytes
300    }
301}
302
303impl<P: Params> Clone for Signature<P> {
304    fn clone(&self) -> Self {
305        Self {
306            bytes: self.bytes.clone(),
307            _marker: PhantomData,
308        }
309    }
310}
311
312pub struct SlhDsa<P: Params> {
313    _marker: core::marker::PhantomData<P>,
314}
315
316impl<P: Params> SlhDsa<P> {
317    /// Generate a new SLH-DSA key pair using the provided RNG.
318    ///
319    /// Returns `(secret_key, public_key)` as byte vectors. The secret key is `4*n` bytes
320    /// and the public key is `2*n` bytes, where `n = P::N`.
321    ///
322    /// Implements Algorithm 21 of FIPS 205.
323    pub fn keygen(rng: &mut dyn CryptoRng) -> Result<(SigningKey<P>, VerifyingKey<P>), SlhDsaError> {
324        let (sk_v, pk_v) = slh::slh_keygen::<P>(rng)?;
325        Ok((
326            SigningKey {
327                bytes: SecretBytes::from_vec(sk_v),
328                _marker: PhantomData,
329            },
330            VerifyingKey {
331                bytes: pk_v,
332                _marker: PhantomData,
333            },
334        ))
335    }
336
337    /// Generate a key pair deterministically from explicit seed material.
338    ///
339    /// This is the internal/deterministic variant (Algorithm 18 of FIPS 205). Each of
340    /// `sk_seed`, `sk_prf`, and `pk_seed` must be exactly `P::N` bytes.
341    ///
342    /// Returns `(secret_key, public_key)`.
343    pub fn keygen_internal(sk_seed: &[u8], sk_prf: &[u8], pk_seed: &[u8]) -> (Vec<u8>, Vec<u8>) {
344        slh::slh_keygen_internal::<P>(sk_seed, sk_prf, pk_seed)
345    }
346
347    /// Sign a message using randomized (hedged) signing.
348    ///
349    /// Produces a signature over `message` using the given `secret_key` and fresh randomness
350    /// from `rng`. The randomness provides hedged signing: even if the RNG is weak, security
351    /// degrades gracefully.
352    ///
353    /// Implements Algorithm 22 of FIPS 205.
354    pub fn sign(
355        message: &[u8],
356        secret_key: &SigningKey<P>,
357        rng: &mut dyn CryptoRng,
358    ) -> Result<Signature<P>, SlhDsaError> {
359        let sig_v = slh::slh_sign::<P>(message, secret_key.as_bytes(), rng)?;
360        Ok(Signature {
361            bytes: sig_v,
362            _marker: PhantomData,
363        })
364    }
365
366    /// Sign a message with explicit additional randomness (internal variant).
367    ///
368    /// This is Algorithm 19 of FIPS 205. The `addrnd` parameter is `P::N` bytes of
369    /// optional randomness; passing `pk_seed` here yields deterministic signing.
370    ///
371    /// Without the `sca-fors-redundancy` feature this is always [`Ok`]. With it,
372    /// the T1-C FORS recompute-and-compare check can return
373    /// [`Err(SlhDsaError::FaultDetected)`][SlhDsaError].
374    pub fn sign_internal(message: &[u8], secret_key: &[u8], addrnd: &[u8]) -> Result<Vec<u8>, SlhDsaError> {
375        slh::slh_sign_internal::<P>(message, secret_key, addrnd)
376    }
377
378    /// Verify a signature on a message against a public key.
379    ///
380    /// Returns `Ok(true)` if the signature is valid, `Ok(false)` if the signature is
381    /// well-formed but does not verify, or an `Err` if the key or signature has an
382    /// invalid length.
383    ///
384    /// Implements Algorithm 24 of FIPS 205.
385    pub fn verify(message: &[u8], signature: &Signature<P>, public_key: &VerifyingKey<P>) -> Result<bool, SlhDsaError> {
386        slh::slh_verify::<P>(message, signature.as_bytes(), public_key.as_bytes())
387    }
388
389    /// Returns the expected signature size in bytes for this parameter set.
390    ///
391    /// The signature consists of a randomizer `R` (n bytes), a FORS signature, and a
392    /// hypertree signature: `n + k*(1+a)*n + (h + d*len)*n`.
393    pub fn signature_size() -> usize {
394        params::sig_len::<P>()
395    }
396
397    /// Returns the expected public key size in bytes (`2*n`).
398    pub fn public_key_size() -> usize {
399        P::PK_LEN
400    }
401
402    /// Returns the expected secret key size in bytes (`4*n`).
403    pub fn secret_key_size() -> usize {
404        P::SK_LEN
405    }
406}