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}