Skip to main content

quantica/slh_dsa/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Cédric Mesnil <cslashm@pm.me>
3
4// The key / signature / ciphertext types are fixed-size byte containers;
5// `len()` returns a compile-time constant (`Params::*_LEN`), so `is_empty()`
6// (always `false`) carries no meaning. Documented allow per CLAUDE.md §6.
7#![allow(clippy::len_without_is_empty)]
8
9//! SLH-DSA: Stateless Hash-Based Digital Signature Standard (FIPS 205).
10//!
11//! This crate provides a pure-Rust implementation of SLH-DSA (formerly known as SPHINCS+),
12//! a post-quantum digital signature scheme standardized in [FIPS 205]. SLH-DSA is purely
13//! hash-based: its security relies only on the properties of cryptographic hash functions,
14//! with no algebraic structure (lattices, codes, etc.) that could be exploited by quantum
15//! or classical algorithms beyond generic attacks.
16//!
17//! # Architecture
18//!
19//! SLH-DSA is built from a hierarchy of hash-based primitives:
20//!
21//! 1. **WOTS+** -- A one-time signature scheme that signs a single n-byte message using
22//!    hash chains (see `wots`, internal).
23//! 2. **XMSS** -- An eXtended Merkle Signature Scheme that authenticates 2^h' WOTS+ keys
24//!    via a binary Merkle tree, producing a few-time signature (see `xmss`, internal).
25//! 3. **Hypertree** -- A tree of XMSS trees stacked in `d` layers, giving a many-time
26//!    signature scheme with a total tree height of `h = d * h'` (see `hypertree`, internal).
27//! 4. **FORS** -- A Forest of Random Subsets, a few-time signature used to sign the
28//!    message digest before passing it to the hypertree (see `fors`, internal).
29//! 5. **SLH-DSA** -- The top-level scheme that combines FORS + Hypertree to produce a
30//!    stateless, many-time signature (see `slh`, internal).
31//!
32//! # Supported parameter sets
33//!
34//! This crate implements all six SHAKE-based parameter sets defined in FIPS 205 Section 11:
35//!
36//! | Type | 128-bit | 192-bit | 256-bit |
37//! |------|---------|---------|---------|
38//! | Small (s) | [`Shake128s`] | [`Shake192s`] | [`Shake256s`] |
39//! | Fast (f)  | [`Shake128f`] | [`Shake192f`] | [`Shake256f`] |
40//!
41//! The "s" variants produce smaller signatures; the "f" variants are faster to sign and verify.
42//!
43//! # Examples
44//!
45//! ```rust
46//! use quantica::slh_dsa::{SlhDsa, Shake128f, OsRng};
47//!
48//! // Generate a key pair
49//! let mut rng = OsRng;
50//! let (secret_key, public_key) = SlhDsa::<Shake128f>::keygen(&mut rng).unwrap();
51//!
52//! // Sign a message (empty context string)
53//! let message = b"hello, post-quantum world!";
54//! let signature = SlhDsa::<Shake128f>::sign(message, b"", &secret_key, &mut rng).unwrap();
55//!
56//! // Verify the signature
57//! let valid = SlhDsa::<Shake128f>::verify(message, b"", &signature, &public_key).unwrap();
58//! assert!(valid);
59//! ```
60//!
61//! [FIPS 205]: https://doi.org/10.6028/NIST.FIPS.205
62
63// API tiers: `params`/`rng` are facade; `address`/`hash`/`hash_sha2` are
64// hazmat expert bricks (no stability promise); the signature components
65// (WOTS+/XMSS/FORS/hypertree) are crate-internal — they are ONE-TIME /
66// few-time primitives whose standalone misuse (key reuse) enables forgery.
67
68/// Address structure used to domain-separate hash calls throughout SLH-DSA.
69#[cfg(feature = "hazmat")]
70pub mod address;
71#[cfg(not(feature = "hazmat"))]
72pub(crate) mod address;
73// FORS few-time signature component — crate-internal (misuse hazard).
74pub(crate) mod fors;
75/// Tweakable hash functions (H_msg, PRF, PRF_msg, T_l, H, F) and the
76/// SHAKE instantiation (FIPS 205 §11.1).
77#[cfg(feature = "hazmat")]
78pub mod hash;
79#[cfg(not(feature = "hazmat"))]
80pub(crate) mod hash;
81/// SHA-2 instantiation of the tweakable hash functions (FIPS 205 §11.2).
82#[cfg(feature = "hazmat")]
83pub mod hash_sha2;
84#[cfg(not(feature = "hazmat"))]
85pub(crate) mod hash_sha2;
86// Hypertree composition layer — crate-internal.
87pub(crate) mod hypertree;
88/// SLH-DSA parameter set definitions and the [`Params`] trait.
89pub mod params;
90/// Minimal cryptographic RNG trait and OS-backed implementation.
91pub mod rng;
92// SHAKE256 wrappers over the shared tessera core — crate-internal.
93pub(crate) mod sha3;
94/// Top-level SLH-DSA algorithms — the un-typed raw-slice facade (hazmat
95/// tier: no typed-wrapper validation; used by the C FFI and KAT tooling).
96#[cfg(feature = "hazmat")]
97pub mod slh;
98#[cfg(not(feature = "hazmat"))]
99pub(crate) mod slh;
100// WOTS+ ONE-TIME signature component — key reuse on two messages enables
101// forgery; crate-internal.
102pub(crate) mod wots;
103// XMSS few-time component — crate-internal.
104pub(crate) mod xmss;
105
106// In-crate KATs for the `pub(crate)` internal interface (sign/verify_internal).
107#[cfg(test)]
108mod acvp_internal;
109
110// Re-export parameter set types for convenience.
111pub use crate::prehash::PreHash;
112pub use params::{
113    Params, Sha2_128f, Sha2_128s, Sha2_192f, Sha2_192s, Sha2_256f, Sha2_256s, Shake128f, Shake128s, Shake192f,
114    Shake192s, Shake256f, Shake256s,
115};
116pub use rng::CryptoRng;
117#[cfg(feature = "std")]
118pub use rng::OsRng;
119
120/// Errors that can occur in SLH-DSA operations.
121///
122/// These errors cover failures in random number generation, malformed keys or signatures,
123/// and verification mismatches. All variants are non-recoverable in the sense that retrying
124/// with the same inputs will produce the same error.
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub enum SlhDsaError {
127    /// The cryptographic random number generator failed to produce bytes.
128    ///
129    /// This typically indicates an OS-level failure (e.g., `/dev/urandom` unavailable).
130    RngFailure,
131    /// The provided key has an invalid format or unexpected length.
132    ///
133    /// Public keys must be `2*n` bytes and secret keys `4*n` bytes, where `n` is the
134    /// security parameter ([`Params::N`]).
135    InvalidKey,
136    /// The provided signature has an invalid format or unexpected length.
137    ///
138    /// Signatures must be exactly [`SlhDsa::signature_size`] bytes for the chosen parameter set.
139    InvalidSignature,
140    /// The signature did not verify against the given message and public key.
141    VerificationFailed,
142    /// The context string exceeds the 255-byte maximum (FIPS 205 §10.2).
143    ContextTooLong,
144    /// Signing-side fault redundancy detected a mismatch between the two
145    /// independent signing runs (item `T1-C` of the SLH-DSA SCA roadmap).
146    ///
147    /// Gated by the `sca-fors-redundancy` cargo feature, the signer
148    /// produces the FORS signature twice and aborts before emission if
149    /// the two results disagree — single-fault grafting-tree forgeries
150    /// (Castelnovi 2018, Adiletta 2025) cannot then propagate out of
151    /// the device. The error is non-recoverable; a retry on the same
152    /// inputs will either succeed (the fault was transient) or surface
153    /// the same error again.
154    FaultDetected,
155}
156
157impl core::fmt::Display for SlhDsaError {
158    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
159        match self {
160            SlhDsaError::RngFailure => write!(f, "RNG failure"),
161            SlhDsaError::InvalidKey => write!(f, "Invalid key"),
162            SlhDsaError::InvalidSignature => write!(f, "Invalid signature"),
163            SlhDsaError::VerificationFailed => write!(f, "Verification failed"),
164            SlhDsaError::ContextTooLong => write!(f, "Context string exceeds 255 bytes"),
165            SlhDsaError::FaultDetected => write!(f, "Signing-side fault detected by FORS redundancy"),
166        }
167    }
168}
169
170#[cfg(feature = "std")]
171impl std::error::Error for SlhDsaError {}
172
173use crate::secret::SecretBytes;
174/// High-level SLH-DSA API parameterized by a parameter set.
175///
176/// This is the main entry point for using SLH-DSA. The type parameter `P` selects one of
177/// the six SHAKE-based parameter sets (e.g., [`Shake128f`], [`Shake256s`]).
178///
179/// `SlhDsa` is a zero-sized type that serves as a namespace for the static methods
180/// [`keygen`](Self::keygen), [`sign`](Self::sign), and [`verify`](Self::verify).
181///
182/// # Examples
183///
184/// ```rust
185/// use quantica::slh_dsa::{SlhDsa, Shake256s, OsRng};
186///
187/// let mut rng = OsRng;
188/// let (sk, pk) = SlhDsa::<Shake256s>::keygen(&mut rng).unwrap();
189/// let sig = SlhDsa::<Shake256s>::sign(b"data", b"", &sk, &mut rng).unwrap();
190/// assert!(SlhDsa::<Shake256s>::verify(b"data", b"", &sig, &pk).unwrap());
191/// ```
192use alloc::vec::Vec;
193use core::marker::PhantomData;
194
195// =====================================================================
196// Typed key / signature wrappers
197// =====================================================================
198
199/// SLH-DSA **verifying key** (public key, `2 * P::N` bytes).
200///
201/// Type-tagged with `P`. No zeroization.
202pub struct VerifyingKey<P: Params> {
203    bytes: Vec<u8>,
204    _marker: PhantomData<P>,
205}
206
207impl<P: Params> VerifyingKey<P> {
208    /// Wrap a raw byte slice. Length is validated against
209    /// [`Params::PK_LEN`].
210    pub fn from_bytes(bytes: &[u8]) -> Result<Self, SlhDsaError> {
211        if bytes.len() != P::PK_LEN {
212            return Err(SlhDsaError::InvalidKey);
213        }
214        Ok(Self {
215            bytes: bytes.to_vec(),
216            _marker: PhantomData,
217        })
218    }
219
220    /// Borrow the encoded verifying key as a byte slice.
221    pub fn as_bytes(&self) -> &[u8] {
222        &self.bytes
223    }
224
225    /// Length in bytes (always [`Params::PK_LEN`]).
226    pub fn len(&self) -> usize {
227        self.bytes.len()
228    }
229}
230
231impl<P: Params> AsRef<[u8]> for VerifyingKey<P> {
232    fn as_ref(&self) -> &[u8] {
233        &self.bytes
234    }
235}
236
237impl<P: Params> core::ops::Deref for VerifyingKey<P> {
238    type Target = [u8];
239    fn deref(&self) -> &[u8] {
240        &self.bytes
241    }
242}
243
244impl<P: Params> Clone for VerifyingKey<P> {
245    fn clone(&self) -> Self {
246        Self {
247            bytes: self.bytes.clone(),
248            _marker: PhantomData,
249        }
250    }
251}
252
253/// SLH-DSA **signing key** (secret key, `4 * P::N` bytes).
254///
255/// Backed by [`SecretBytes`] — wipes its memory on [`Drop`] via
256/// `silentops::ct_zeroize`. Type-tagged with `P`.
257pub struct SigningKey<P: Params> {
258    bytes: SecretBytes,
259    _marker: PhantomData<P>,
260}
261
262impl<P: Params> SigningKey<P> {
263    /// Wrap a raw byte slice. Length is validated against
264    /// [`Params::SK_LEN`].
265    pub fn from_bytes(bytes: &[u8]) -> Result<Self, SlhDsaError> {
266        if bytes.len() != P::SK_LEN {
267            return Err(SlhDsaError::InvalidKey);
268        }
269        Ok(Self {
270            bytes: SecretBytes::from_slice(bytes),
271            _marker: PhantomData,
272        })
273    }
274
275    /// Borrow the encoded signing key as a byte slice.
276    pub fn as_bytes(&self) -> &[u8] {
277        self.bytes.as_bytes()
278    }
279
280    /// Length in bytes (always [`Params::SK_LEN`]).
281    pub fn len(&self) -> usize {
282        self.bytes.len()
283    }
284}
285
286impl<P: Params> AsRef<[u8]> for SigningKey<P> {
287    fn as_ref(&self) -> &[u8] {
288        self.bytes.as_bytes()
289    }
290}
291
292impl<P: Params> core::ops::Deref for SigningKey<P> {
293    type Target = [u8];
294    fn deref(&self) -> &[u8] {
295        self.bytes.as_bytes()
296    }
297}
298
299/// SLH-DSA **signature**. Type-tagged with `P`.
300///
301/// Public material — not zeroized.
302pub struct Signature<P: Params> {
303    bytes: Vec<u8>,
304    _marker: PhantomData<P>,
305}
306
307impl<P: Params> Signature<P> {
308    /// Wrap a raw byte slice. Length is validated against
309    /// [`params::sig_len::<P>()`].
310    pub fn from_bytes(bytes: &[u8]) -> Result<Self, SlhDsaError> {
311        if bytes.len() != params::sig_len::<P>() {
312            return Err(SlhDsaError::InvalidSignature);
313        }
314        Ok(Self {
315            bytes: bytes.to_vec(),
316            _marker: PhantomData,
317        })
318    }
319
320    /// Borrow the encoded signature as a byte slice.
321    pub fn as_bytes(&self) -> &[u8] {
322        &self.bytes
323    }
324
325    /// Length in bytes (always [`params::sig_len::<P>()`]).
326    pub fn len(&self) -> usize {
327        self.bytes.len()
328    }
329}
330
331impl<P: Params> AsRef<[u8]> for Signature<P> {
332    fn as_ref(&self) -> &[u8] {
333        &self.bytes
334    }
335}
336
337impl<P: Params> core::ops::Deref for Signature<P> {
338    type Target = [u8];
339    fn deref(&self) -> &[u8] {
340        &self.bytes
341    }
342}
343
344impl<P: Params> Clone for Signature<P> {
345    fn clone(&self) -> Self {
346        Self {
347            bytes: self.bytes.clone(),
348            _marker: PhantomData,
349        }
350    }
351}
352
353/// Generic SLH-DSA interface parameterized by parameter set (FIPS 205).
354///
355/// This struct provides the high-level API for SLH-DSA key generation,
356/// signing, and verification. The type parameter `P` selects one of the
357/// twelve FIPS 205 parameter sets (`Sha2_128s` … `Shake256f`) defined in
358/// the `params` module.
359///
360/// All methods are stateless; `SlhDsa` carries no runtime data and exists
361/// only to bind the parameter set at the type level.
362pub struct SlhDsa<P: Params> {
363    _marker: core::marker::PhantomData<P>,
364}
365
366impl<P: Params> SlhDsa<P> {
367    /// Generate a new SLH-DSA key pair using the provided RNG.
368    ///
369    /// Implements Algorithm 21 of FIPS 205.
370    ///
371    /// # Arguments
372    ///
373    /// * `rng` - A cryptographic random number generator implementing [`CryptoRng`].
374    ///
375    /// # Returns
376    ///
377    /// A tuple `(secret_key, public_key)` of typed wrappers. The secret key is
378    /// `4*n` bytes and the public key is `2*n` bytes, where `n = P::N`.
379    ///
380    /// # Errors
381    ///
382    /// * [`SlhDsaError::RngFailure`] if the RNG cannot provide bytes.
383    pub fn keygen(rng: &mut dyn CryptoRng) -> Result<(SigningKey<P>, VerifyingKey<P>), SlhDsaError> {
384        let (sk_v, pk_v) = slh::slh_keygen::<P>(rng)?;
385        Ok((
386            SigningKey {
387                bytes: SecretBytes::from_vec(sk_v),
388                _marker: PhantomData,
389            },
390            VerifyingKey {
391                bytes: pk_v,
392                _marker: PhantomData,
393            },
394        ))
395    }
396
397    /// Sign a message with an optional context string (hedged mode).
398    ///
399    /// Implements Algorithm 22 of FIPS 205 (external SLH-DSA.Sign): wraps
400    /// the message as `M' = 0x00 || len(ctx) || ctx || message` for domain
401    /// separation, then hedged-signs it. Use an empty `ctx` (`b""`) when no
402    /// context is needed — the wrapping still applies, keeping signatures
403    /// interoperable with conformant verifiers.
404    ///
405    /// # Arguments
406    ///
407    /// * `message` - The message to sign (arbitrary length).
408    /// * `ctx` - Optional context string, at most 255 bytes (`b""` for none).
409    /// * `secret_key` - The signing key for this parameter set.
410    /// * `rng` - A cryptographic random number generator implementing [`CryptoRng`].
411    ///
412    /// # Returns
413    ///
414    /// A [`Signature<P>`] of length [`SlhDsa::signature_size`].
415    ///
416    /// # Errors
417    ///
418    /// * [`SlhDsaError::ContextTooLong`] if `ctx` exceeds 255 bytes.
419    /// * [`SlhDsaError::RngFailure`] if the RNG cannot provide bytes.
420    /// * [`SlhDsaError::FaultDetected`] if the `sca-fors-redundancy` feature is
421    ///   enabled and the two independent signing runs disagree.
422    pub fn sign(
423        message: &[u8],
424        ctx: &[u8],
425        secret_key: &SigningKey<P>,
426        rng: &mut dyn CryptoRng,
427    ) -> Result<Signature<P>, SlhDsaError> {
428        let sig_v = slh::slh_sign::<P>(message, ctx, secret_key.as_bytes(), rng)?;
429        Ok(Signature {
430            bytes: sig_v,
431            _marker: PhantomData,
432        })
433    }
434
435    /// Sign with pre-hashing — HashSLH-DSA (Algorithm 23 of FIPS 205).
436    ///
437    /// The message is replaced by its digest under `ph`; the library
438    /// computes `ph(message)` and embeds the matching OID, forming
439    /// `M' = 0x01 || len(ctx) || ctx || OID(ph) || ph(message)`.
440    ///
441    /// # Arguments
442    ///
443    /// * `message` - The message to pre-hash and sign (arbitrary length).
444    /// * `ctx` - Optional context string, at most 255 bytes (`b""` for none).
445    /// * `ph` - The pre-hash function [`PreHash`] applied to `message`.
446    /// * `secret_key` - The signing key for this parameter set.
447    /// * `rng` - A cryptographic random number generator implementing [`CryptoRng`].
448    ///
449    /// # Returns
450    ///
451    /// A [`Signature<P>`] of length [`SlhDsa::signature_size`].
452    ///
453    /// # Errors
454    ///
455    /// * [`SlhDsaError::ContextTooLong`] if `ctx` exceeds 255 bytes.
456    /// * [`SlhDsaError::RngFailure`] if the RNG cannot provide bytes.
457    /// * [`SlhDsaError::FaultDetected`] if the `sca-fors-redundancy` feature is
458    ///   enabled and the two independent signing runs disagree.
459    pub fn sign_prehash(
460        message: &[u8],
461        ctx: &[u8],
462        ph: PreHash,
463        secret_key: &SigningKey<P>,
464        rng: &mut dyn CryptoRng,
465    ) -> Result<Signature<P>, SlhDsaError> {
466        let sig_v = slh::slh_sign_prehash::<P>(message, ctx, ph, secret_key.as_bytes(), rng)?;
467        Ok(Signature {
468            bytes: sig_v,
469            _marker: PhantomData,
470        })
471    }
472
473    /// Verify a signature on a message with an optional context string.
474    ///
475    /// Implements Algorithm 24 of FIPS 205 (external SLH-DSA.Verify):
476    /// reconstructs `M' = 0x00 || len(ctx) || ctx || message` and checks it.
477    /// `ctx` must match the one used at signing time.
478    ///
479    /// # Arguments
480    ///
481    /// * `message` - The signed message.
482    /// * `ctx` - The context string used during signing, at most 255 bytes.
483    /// * `signature` - The signature to check.
484    /// * `public_key` - The verifying key for this parameter set.
485    ///
486    /// # Returns
487    ///
488    /// `Ok(true)` if the signature is valid, `Ok(false)` if it does not verify.
489    ///
490    /// # Errors
491    ///
492    /// * [`SlhDsaError::ContextTooLong`] if `ctx` exceeds 255 bytes.
493    /// * [`SlhDsaError::InvalidKey`] if `public_key` has the wrong length.
494    /// * [`SlhDsaError::InvalidSignature`] if `signature` has the wrong length.
495    pub fn verify(
496        message: &[u8],
497        ctx: &[u8],
498        signature: &Signature<P>,
499        public_key: &VerifyingKey<P>,
500    ) -> Result<bool, SlhDsaError> {
501        slh::slh_verify::<P>(message, signature.as_bytes(), ctx, public_key.as_bytes())
502    }
503
504    /// Verify a pre-hash HashSLH-DSA signature (Algorithm 25 of FIPS 205).
505    ///
506    /// `ph` must match the [`PreHash`] used at signing time.
507    ///
508    /// # Arguments
509    ///
510    /// * `message` - The signed message.
511    /// * `ctx` - The context string used during signing, at most 255 bytes.
512    /// * `ph` - The pre-hash function [`PreHash`], matching the one used at signing.
513    /// * `signature` - The signature to check.
514    /// * `public_key` - The verifying key for this parameter set.
515    ///
516    /// # Returns
517    ///
518    /// `Ok(true)` if the signature is valid, `Ok(false)` if it does not verify.
519    ///
520    /// # Errors
521    ///
522    /// * [`SlhDsaError::ContextTooLong`] if `ctx` exceeds 255 bytes.
523    /// * [`SlhDsaError::InvalidKey`] if `public_key` has the wrong length.
524    /// * [`SlhDsaError::InvalidSignature`] if `signature` has the wrong length.
525    pub fn verify_prehash(
526        message: &[u8],
527        ctx: &[u8],
528        ph: PreHash,
529        signature: &Signature<P>,
530        public_key: &VerifyingKey<P>,
531    ) -> Result<bool, SlhDsaError> {
532        slh::slh_verify_prehash::<P>(message, signature.as_bytes(), ctx, ph, public_key.as_bytes())
533    }
534
535    /// Returns the expected signature size in bytes for this parameter set.
536    ///
537    /// The signature consists of a randomizer `R` (n bytes), a FORS signature, and a
538    /// hypertree signature: `n + k*(1+a)*n + (h + d*len)*n`.
539    pub fn signature_size() -> usize {
540        params::sig_len::<P>()
541    }
542
543    /// Returns the expected public key size in bytes (`2*n`).
544    pub fn public_key_size() -> usize {
545        P::PK_LEN
546    }
547
548    /// Returns the expected secret key size in bytes (`4*n`).
549    pub fn secret_key_size() -> usize {
550        P::SK_LEN
551    }
552}