Skip to main content

pqc_sig/
lib.rs

1//! # pqc-sig — Post-Quantum Digital Signatures
2//!
3//! A standalone, WASM-compatible library implementing post-quantum signature algorithms:
4//!
5//! - **ML-DSA** (NIST FIPS 204) — Primary standard, pure Rust, WASM-native. `ml-dsa` feature (default)
6//!   - [`fips204::MlDsa44Keypair`] — Security Level 2
7//!   - [`fips204::MlDsa65Keypair`] — Security Level 3 (recommended)
8//!   - [`fips204::MlDsa87Keypair`] — Security Level 5
9//!
10//! - **SLH-DSA** (NIST FIPS 205) — Stateless hash-based, pure Rust, WASM-native. `slh-dsa` feature (default)
11//!   - SHA2 variants: 128s/128f, 192s/192f, 256s/256f
12//!   - SHAKE variants: 128s/128f, 192s/192f, 256s/256f
13//!
14//! - **FN-DSA** (NIST FIPS 206 / Falcon) — requires `fndsa` feature, pure Rust, WASM-compatible
15//!   - [`fips206::FnDsa512Keypair`] — Security Level 1
16//!   - [`fips206::FnDsa1024Keypair`] — Security Level 5
17//!   - Multikey/DID Document encoding works via a provisional `0x307`-reserved private-use
18//!     multicodec code (no upstream multiformats/multicodec registration exists yet) — see
19//!     [`types::FN_DSA_PRIVATE_USE_BASE`].
20//!
21//! - **Hybrid Ed25519 + ML-DSA-65** — requires `hybrid` feature; classical/PQC combiner for migration bridging
22//!   - [`hybrid::HybridSigner`]
23//!
24//! ## Quick Start
25//!
26//! ```rust,no_run
27//! use pqc_sig::fips204::MlDsa65Keypair;
28//! use rand::rngs::OsRng;
29//!
30//! // Generate a keypair
31//! let keypair = MlDsa65Keypair::generate(&mut OsRng).unwrap();
32//! let pk = keypair.public_key();
33//!
34//! // Sign a message
35//! let message = b"Hello, post-quantum world!";
36//! let signature = keypair.sign(&mut OsRng, message).unwrap();
37//!
38//! // Verify the signature
39//! MlDsa65Keypair::verify(&pk, message, &signature).unwrap();
40//! ```
41//!
42//! ## Domain separation
43//!
44//! Every algorithm family (ML-DSA, SLH-DSA, and — behind the `fndsa` feature — FN-DSA)
45//! also exposes a `sign_ctx`/`sign_ctx_deterministic`/`verify_ctx` trio that takes a FIPS
46//! 204/205/206 "context" byte string (≤ [`MAX_CONTEXT_LEN`] bytes) in addition to the
47//! message. Use this to bind a signature to a specific application or protocol so that a
48//! signature produced for one purpose cannot be replayed as valid for another:
49//!
50//! ```rust,no_run
51//! use pqc_sig::fips204::MlDsa65Keypair;
52//! use rand::rngs::OsRng;
53//!
54//! let keypair = MlDsa65Keypair::generate(&mut OsRng).unwrap();
55//! let pk = keypair.public_key();
56//! let msg = b"Hello, post-quantum world!";
57//!
58//! let sig = keypair.sign_ctx(&mut OsRng, b"8gentz-agent-v1", msg).unwrap();
59//! MlDsa65Keypair::verify_ctx(&pk, b"8gentz-agent-v1", msg, &sig).unwrap();
60//!
61//! // A signature made under one context MUST NOT verify under another.
62//! assert!(MlDsa65Keypair::verify_ctx(&pk, b"8gentz-fabric-v1", msg, &sig).is_err());
63//! ```
64//!
65//! An empty context (`&[]`) is byte-identical to the plain, non-`ctx` API: `sign_ctx(&[],
66//! m)`/`sign_ctx_deterministic(&[], m)` produce the same signature as `sign`/
67//! `sign_deterministic`, and `verify_ctx(pk, &[], m, sig)` accepts anything `verify` does
68//! (and vice versa). See [`MAX_CONTEXT_LEN`] for the 255-byte limit shared by all three
69//! FIPS standards, and `examples/domain_separation.rs` for a runnable walkthrough.
70//!
71//! ## Pre-hash signing (large payloads)
72//!
73//! Every algorithm family also exposes a `sign_prehash`/`sign_prehash_deterministic`/
74//! `verify_prehash` trio for signing very large payloads (multi-MB WASM modules, log
75//! bundles, ...) without holding the whole message in memory twice. The **caller**
76//! hashes the payload once with an approved [`prehash::PreHash`] function — this crate
77//! never hashes on the caller's behalf — and passes the resulting digest:
78//!
79//! ```rust,no_run
80//! use pqc_sig::fips204::MlDsa65Keypair;
81//! use pqc_sig::prehash::PreHash;
82//! use rand::rngs::OsRng;
83//!
84//! let keypair = MlDsa65Keypair::generate(&mut OsRng).unwrap();
85//! let pk = keypair.public_key();
86//!
87//! // The host already hashes large payloads ad hoc; feed that digest in directly.
88//! let digest = [0x11u8; 64]; // stand-in for e.g. Sha512::digest(&module_bytes)
89//!
90//! let sig = keypair
91//!     .sign_prehash_deterministic(b"8gentz-module-v1", PreHash::Sha512, &digest)
92//!     .unwrap();
93//! MlDsa65Keypair::verify_prehash(&pk, b"8gentz-module-v1", PreHash::Sha512, &digest, &sig)
94//!     .unwrap();
95//! ```
96//!
97//! For **ML-DSA** and **SLH-DSA**, this is the literal FIPS `HashML-DSA`/`HashSLH-DSA`
98//! construction (FIPS 204 §5.4 Algorithm 4/5, FIPS 205 §10.2.2 Algorithm 23/25): it
99//! builds `M' = 0x01 ‖ len(ctx) ‖ ctx ‖ OID(hash) ‖ digest` and signs/verifies `M'` via
100//! the upstream `ml-dsa`/`slh-dsa` crates' `sign_internal`/`verify_internal` and
101//! `slh_sign_internal`/`slh_verify_internal` primitives — both confirmed `pub` and
102//! reachable in the pinned versions. This is byte-for-byte interoperable with any other
103//! conformant `HashML-DSA`/`HashSLH-DSA` implementation. **FN-DSA**'s `sign_prehash`/
104//! `verify_prehash` use the upstream `fn-dsa` crate's native `HashIdentifier`
105//! pre-hash support directly (FIPS 206 draft framing, performed by `fn-dsa` itself).
106//!
107//! The pre-hash function's collision strength must be at least the parameter set's
108//! security strength (each keypair type exposes this as `SECURITY_STRENGTH_BITS`):
109//! 128 bits for ML-DSA-44/SLH-DSA-128\*/FN-DSA-512, 192 bits for ML-DSA-65/
110//! SLH-DSA-192\*, 256 bits for ML-DSA-87/SLH-DSA-256\*/FN-DSA-1024. Violating this
111//! returns [`SigError::PreHashTooWeak`]; a digest of the wrong length for the chosen
112//! [`prehash::PreHash`] returns [`SigError::InvalidDigestLength`]. A pre-hash signature
113//! does **not** verify via `verify`/`verify_ctx` (and vice versa) — the leading `0x01`
114//! domain byte structurally separates the two modes. See `examples/prehash_large_payload.rs`
115//! for a runnable walkthrough with a multi-MB payload.
116//!
117//! ## Hybrid bridge (migrating a classical identity)
118//!
119//! The `hybrid` feature's [`hybrid::HybridSigner`] also supports bridging an *existing*
120//! classical Ed25519 identity — rather than only generating a brand-new one — via
121//! [`hybrid::HybridSigner::from_ed25519_secret`], plus a `secret_key()`/
122//! `from_secret_key_bytes` pair for persisting both halves across restarts, and its own
123//! domain-separated `sign_ctx`/`verify_ctx` trio (S-1 parity, with a crate-defined
124//! Ed25519 context framing since RFC 8032 has none — see
125//! [`hybrid::HYBRID_CTX_FRAME_DOMAIN`]). See `examples/hybrid_bridge.rs` for a runnable
126//! walkthrough and `docs/SAGP_NOTES.md` for Wave-1 migration guidance.
127//!
128//! ## WASM Usage
129//!
130//! This crate is a pure library (rlib-only) and is never built as a standalone WASM
131//! artifact itself. For WASM/JS bindings, use the sibling `pqc-sig-wasm` crate.
132//!
133//! ## Encoding only
134//!
135//! With `default-features = false` and no algorithm feature, the crate is the encoding
136//! surface alone: [`SigAlgorithm`], [`SigPublicKey`], [`Signature`], [`SignedMessage`],
137//! multicodec codes, W3C Multikey, base64url and JSON. No signature implementation is
138//! compiled in, and CI checks the dependency graph to keep it that way. Use this when you
139//! only carry keys and signatures between systems, for example in a DID tool or an SDK
140//! whose signing happens elsewhere. The encodings are byte-identical to the full build's.
141//! ```toml
142//! pqc-sig = { version = "0.5", default-features = false, features = ["std"] }
143//! ```
144//!
145//! ## `no_std` Support
146//!
147//! This crate is `no_std`-compatible with `alloc`. Disable the `std` feature and keep the
148//! algorithms you use:
149//! ```toml
150//! pqc-sig = { version = "0.5", default-features = false, features = ["ml-dsa", "slh-dsa"] }
151//! ```
152
153#![cfg_attr(not(feature = "std"), no_std)]
154#![doc = include_str!("../README.md")]
155#![forbid(unsafe_code)]
156
157#[cfg(not(feature = "std"))]
158extern crate alloc;
159
160// ── Public Modules ────────────────────────────────────────────────────────────
161
162/// Error types for all signature operations.
163pub mod error;
164
165/// Wire types: public keys, secret keys, signatures, algorithm identifiers.
166pub mod types;
167
168/// Domain-separation context helpers shared by `sign_ctx`/`verify_ctx` across all
169/// algorithm families. See the crate-level "Domain separation" docs above.
170// Its check is only called by the algorithm modules; the encoding-only build keeps the
171// public `MAX_CONTEXT_LEN` constant.
172#[cfg_attr(not(any(feature = "ml-dsa", feature = "slh-dsa", feature = "fndsa")), allow(dead_code))]
173mod ctx;
174
175/// Pre-hash signing support (`HashML-DSA`/`HashSLH-DSA`/FN-DSA pre-hashed mode) shared
176/// by `sign_prehash`/`sign_prehash_deterministic`/`verify_prehash` across all algorithm
177/// families. See the crate-level "Pre-hash signing" docs above.
178pub mod prehash;
179
180/// ML-DSA (NIST FIPS 204) — pure Rust, WASM-native. Requires the `ml-dsa` feature (default).
181#[cfg(feature = "ml-dsa")]
182pub mod fips204;
183
184/// SLH-DSA (NIST FIPS 205) — pure Rust, WASM-native. Requires the `slh-dsa` feature (default).
185#[cfg(feature = "slh-dsa")]
186pub mod fips205;
187
188/// FN-DSA / Falcon (NIST FIPS 206) — requires `fndsa` feature, pure Rust, WASM-compatible.
189#[cfg(feature = "fndsa")]
190pub mod fips206;
191
192/// Hybrid classical + post-quantum signatures (Ed25519 + ML-DSA-65) — requires `hybrid` feature.
193#[cfg(feature = "hybrid")]
194pub mod hybrid;
195
196// ── Top-Level Re-exports ──────────────────────────────────────────────────────
197
198pub use error::{SigError, SigResult};
199pub use types::{SigAlgorithm, SigPublicKey, SigSecretKey, Signature, SignedMessage};
200pub use ctx::MAX_CONTEXT_LEN;
201pub use prehash::PreHash;
202
203// FIPS 204 — ML-DSA (feature-gated, default)
204#[cfg(feature = "ml-dsa")]
205pub use fips204::{MlDsa44Keypair, MlDsa65Keypair, MlDsa87Keypair};
206
207// FIPS 205 — SLH-DSA (feature-gated, default)
208#[cfg(feature = "slh-dsa")]
209pub use fips205::{
210    SlhDsaSha2_128sKeypair, SlhDsaSha2_128fKeypair,
211    SlhDsaSha2_192sKeypair, SlhDsaSha2_192fKeypair,
212    SlhDsaSha2_256sKeypair, SlhDsaSha2_256fKeypair,
213    SlhDsaShake128sKeypair, SlhDsaShake128fKeypair,
214    SlhDsaShake192sKeypair, SlhDsaShake192fKeypair,
215    SlhDsaShake256sKeypair, SlhDsaShake256fKeypair,
216};
217
218// FIPS 206 — FN-DSA (feature-gated)
219#[cfg(feature = "fndsa")]
220pub use fips206::{FnDsa512Keypair, FnDsa1024Keypair};
221
222// Hybrid Ed25519 + ML-DSA-65 (feature-gated)
223#[cfg(feature = "hybrid")]
224pub use hybrid::{HybridPublicKey, HybridSignature, HybridSigner};
225
226// ── Crate Metadata ────────────────────────────────────────────────────────────
227
228/// Crate version string.
229pub const VERSION: &str = env!("CARGO_PKG_VERSION");
230
231/// Primary signature algorithm (ML-DSA-65, FIPS 204 Level 3).
232pub const PRIMARY_ALGORITHM: &str = "ML-DSA-65";
233
234/// Audit signature algorithm (ML-DSA-87, FIPS 204 Level 5).
235pub const AUDIT_ALGORITHM: &str = "ML-DSA-87";
236
237/// WASM integrity algorithm (SLH-DSA-SHA2-128s, FIPS 205).
238pub const WASM_INTEGRITY_ALGORITHM: &str = "SLH-DSA-SHA2-128s";
239
240/// Compact signature algorithm (FN-DSA-512, FIPS 206).
241pub const COMPACT_ALGORITHM: &str = "FN-DSA-512";