Skip to main content

qssm_le/
lib.rs

1//! QSSM-LE: \(R_q = \mathbb{Z}_q[X]/(X^{256}+1)\) with NTT-backed multiply, MLWE commitment
2//! \(C = A r + \mu\), and Lyubashevsky-style Fiat–Shamir proofs (**witness-hiding** on the wire).
3//!
4//! ```
5//! use qssm_le::{
6//!     commit_mlwe, prove_arithmetic, verify_lattice, PublicInstance, VerifyingKey, Witness,
7//!     PUBLIC_DIGEST_COEFFS,
8//! };
9//! let vk = VerifyingKey::from_seed([9u8; 32]);
10//! let public = PublicInstance::digest_coeffs([0u32; PUBLIC_DIGEST_COEFFS]).unwrap();
11//! let witness = Witness::new([0i32; qssm_le::N]);
12//! let ctx = [7u8; 32];
13//! let rng_seed = [42u8; 32]; // deterministic masking seed (from entropy pipeline)
14//! let (commitment, proof) = prove_arithmetic(&vk, &public, &witness, &ctx, rng_seed).unwrap();
15//! assert!(verify_lattice(&vk, &public, &commitment, &proof, &ctx).unwrap());
16//! ```
17#![forbid(unsafe_code)]
18#![allow(dead_code, clippy::manual_is_multiple_of, clippy::needless_range_loop)]
19
20mod algebra;
21mod crs;
22mod error;
23mod protocol;
24
25pub use algebra::ring::{
26    encode_rq_coeffs_le, short_vec_to_rq, short_vec_to_rq_bound, RqPoly, ScrubbedPoly,
27};
28pub use crs::VerifyingKey;
29pub use error::LeError;
30pub use protocol::commit::{
31    commit_mlwe, verify_lattice_algebraic, Commitment, CommitmentRandomness, LatticeProof,
32    PublicBinding, PublicInstance, Witness,
33};
34// prove_with_witness is intentionally NOT re-exported. It accepts an arbitrary
35// RngCore, which would let external callers inject a weak/biased RNG and defeat
36// the rejection sampling security guarantee. Use prove_arithmetic instead.
37pub(crate) use protocol::commit::prove_with_witness;
38
39/// Minimal deterministic byte-stream trait.
40///
41/// This replaces `rand::RngCore` so the crate has **zero** dependency on any
42/// PRNG / CSPRNG / OS-RNG crate. Implementations must be purely deterministic
43/// (seeded by the sovereign entropy pipeline, never by OS randomness).
44pub(crate) trait DeterministicRng {
45    /// Return the next 4 bytes from the deterministic stream as a `u32`.
46    fn next_u32(&mut self) -> u32;
47    /// Fill `dest` from the deterministic stream.
48    fn fill_bytes(&mut self, dest: &mut [u8]);
49}
50
51pub use protocol::params::{
52    BETA, C_POLY_SIZE, C_POLY_SPAN, ETA, GAMMA, N, PUBLIC_DIGEST_COEFFS, PUBLIC_DIGEST_COEFF_MAX, Q,
53};
54pub use qssm_utils::LE_FS_PUBLIC_BINDING_LAYOUT_VERSION;
55
56/// Witness-free verifier (includes `binding_context` in FS challenge).
57pub fn verify_lattice(
58    vk: &VerifyingKey,
59    public: &PublicInstance,
60    commitment: &Commitment,
61    proof: &LatticeProof,
62    binding_context: &[u8; 32],
63) -> Result<bool, LeError> {
64    verify_lattice_algebraic(vk, public, commitment, proof, binding_context)
65}
66
67/// Prove: commit + deterministic Fiat–Shamir proof using a BLAKE3-seeded CSPRNG.
68///
69/// Given the same `(vk, public, witness, binding_context, rng_seed)`, this
70/// function always produces the same `(Commitment, LatticeProof)`.
71/// No OS entropy is consumed — the masking vector `y` and rejection loop
72/// are driven entirely by the BLAKE3-XOF keyed with `rng_seed`.
73///
74/// `rng_seed` must come from the sovereign entropy pipeline
75/// (`qssm_entropy::Heartbeat::to_seed()` → domain-separated derivation).
76pub fn prove_arithmetic(
77    vk: &VerifyingKey,
78    public: &PublicInstance,
79    witness: &Witness,
80    binding_context: &[u8; 32],
81    rng_seed: [u8; 32],
82) -> Result<(Commitment, LatticeProof), LeError> {
83    let commitment = commit_mlwe(vk, public, witness)?;
84    let mut rng = Blake3Rng::new(rng_seed);
85    let proof = prove_with_witness(vk, public, witness, &commitment, binding_context, &mut rng)?;
86    Ok((commitment, proof))
87}
88
89/// BLAKE3-keyed XOF as a deterministic CSPRNG (`RngCore`).
90///
91/// Construction: `BLAKE3-XOF(key = rng_seed)`, streaming output.
92/// No OS entropy, no hardware calls — purely deterministic.
93//
94// SECURITY-CONCESSION: `blake3::OutputReader` is opaque and cannot be
95// zeroized on drop. The XOF internal state (derived from rng_seed) may
96// persist on the stack after this struct is dropped. Acceptable because:
97// (1) rng_seed is a domain-separated derived value, not a master secret,
98// (2) Blake3Rng is short-lived (created and consumed within prove_arithmetic),
99// (3) the OutputReader holds streaming state, not the original key bytes.
100struct Blake3Rng {
101    reader: blake3::OutputReader,
102}
103
104impl Blake3Rng {
105    fn new(mut seed: [u8; 32]) -> Self {
106        let h = blake3::Hasher::new_keyed(&seed);
107        zeroize::Zeroize::zeroize(&mut seed);
108        Self {
109            reader: h.finalize_xof(),
110        }
111    }
112}
113
114impl DeterministicRng for Blake3Rng {
115    fn next_u32(&mut self) -> u32 {
116        let mut buf = [0u8; 4];
117        self.reader.fill(&mut buf);
118        u32::from_le_bytes(buf)
119    }
120
121    fn fill_bytes(&mut self, dest: &mut [u8]) {
122        self.reader.fill(dest);
123    }
124}