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}