falcon-multisig 0.1.0

Production-grade post-quantum threshold multisignature library using Falcon-512 (NIST FIPS 206 / FN-DSA)
Documentation
//! Signing session — stateful collection and verification of threshold signatures.
//!
//! A [`SigningSession`] is created from a [`ThresholdConfig`] and a message, and
//! accumulates partial signatures from committee members until the threshold is met.
//!
//! # Workflow
//!
//! ```rust
//! use falcon_multisig::{KeyPair, ThresholdConfig, SigningSession};
//!
//! // Setup: generate a 2-of-3 committee
//! let keypairs: Vec<KeyPair> = (0..3).map(|_| KeyPair::generate()).collect();
//! let public_keys: Vec<_> = keypairs.iter().map(|kp| kp.public_key().clone()).collect();
//! let config = ThresholdConfig::new(2, public_keys).unwrap();
//!
//! // Create a session for a specific message
//! let message = b"proposal:transfer:100";
//! let mut session = SigningSession::new(&config, message);
//!
//! // Signers contribute their partial signatures
//! let sig0 = keypairs[0].sign(message);
//! let sig2 = keypairs[2].sign(message);
//!
//! session.add_signature(0, sig0).unwrap();
//! session.add_signature(2, sig2).unwrap();
//!
//! // Verify once the threshold is met
//! assert!(session.is_complete());
//! assert!(session.verify().unwrap());
//! ```
//!
//! # Thread Safety
//!
//! `SigningSession` is not `Sync`. If multiple threads must add signatures
//! concurrently, wrap the session in a `Mutex`.

#[cfg(not(feature = "std"))]
use alloc::vec::Vec;

use crate::{
    error::Error,
    threshold::ThresholdConfig,
    verify::verify_partial,
};

/// The state of a single signer slot in the session.
#[derive(Clone, Debug, PartialEq, Eq)]
enum SlotState {
    /// No signature has been submitted for this index yet.
    Empty,
    /// A cryptographically valid signature has been recorded.
    Valid(Vec<u8>),
}

/// A stateful signing session for a single message under a [`ThresholdConfig`].
///
/// The session verifies each partial signature at the time it is added, so
/// [`SigningSession::verify`] is a pure threshold check — it does not
/// re-verify individual signatures.
///
/// Sessions are single-use: one session corresponds to exactly one message.
/// Create a new session for each message that requires threshold authorisation.
#[derive(Debug)]
pub struct SigningSession {
    config: ThresholdConfig,
    message: Vec<u8>,
    slots: Vec<SlotState>,
    valid_count: usize,
}

impl SigningSession {
    /// Create a new signing session.
    ///
    /// # Parameters
    ///
    /// - `config`: The threshold configuration for this session.
    /// - `message`: The raw message payload to be authorised. Domain separation
    ///   is applied internally; pass the plaintext message, not a pre-computed hash.
    pub fn new(config: &ThresholdConfig, message: &[u8]) -> Self {
        let n = config.total();
        Self {
            config: config.clone(),
            message: message.to_vec(),
            slots: vec![SlotState::Empty; n],
            valid_count: 0,
        }
    }

    /// Add a partial signature from committee member at `signer_index`.
    ///
    /// The signature is verified cryptographically before being recorded.
    /// Adding the same index twice returns [`Error::DuplicateSignature`].
    ///
    /// # Errors
    ///
    /// - [`Error::SignerIndexOutOfRange`] if `signer_index >= total`.
    /// - [`Error::DuplicateSignature`] if this index has already been signed.
    /// - [`Error::VerificationFailed`] if the Falcon-512 check fails.
    /// - Propagates length-validation errors from [`verify_partial`].
    pub fn add_signature(&mut self, signer_index: usize, signature: Vec<u8>) -> Result<(), Error> {
        let total = self.config.total();

        if signer_index >= total {
            return Err(Error::SignerIndexOutOfRange {
                index: signer_index,
                total,
            });
        }

        if self.slots[signer_index] != SlotState::Empty {
            return Err(Error::DuplicateSignature { index: signer_index });
        }

        let pk = self
            .config
            .get_public_key(signer_index)
            .expect("index is within bounds; already checked above");

        let valid = verify_partial(&self.message, &signature, pk.as_bytes(), signer_index)?;

        if !valid {
            return Err(Error::VerificationFailed { index: signer_index });
        }

        self.slots[signer_index] = SlotState::Valid(signature);
        self.valid_count += 1;
        Ok(())
    }

    /// Return `true` if the session holds at least M valid signatures.
    pub fn is_complete(&self) -> bool {
        self.valid_count >= self.config.required()
    }

    /// Verify that the threshold is met.
    ///
    /// Because individual signatures are verified at insertion time, this method
    /// only checks that `valid_count >= required`.
    ///
    /// # Errors
    ///
    /// Returns [`Error::ThresholdNotMet`] if the session does not yet hold
    /// enough valid signatures.
    pub fn verify(&self) -> Result<bool, Error> {
        if self.valid_count < self.config.required() {
            return Err(Error::ThresholdNotMet {
                have: self.valid_count,
                need: self.config.required(),
            });
        }
        Ok(true)
    }

    /// Return the number of valid signatures collected so far.
    pub fn valid_signature_count(&self) -> usize {
        self.valid_count
    }

    /// Return the threshold requirement.
    pub fn required(&self) -> usize {
        self.config.required()
    }

    /// Return progress as `(collected, required)`.
    pub fn progress(&self) -> (usize, usize) {
        (self.valid_count, self.config.required())
    }

    /// Return a reference to the message this session is authorising.
    pub fn message(&self) -> &[u8] {
        &self.message
    }

    /// Return a reference to the threshold configuration.
    pub fn config(&self) -> &ThresholdConfig {
        &self.config
    }

    /// Retrieve the recorded signature for `signer_index`, if present.
    ///
    /// Returns `None` if no valid signature has been submitted for that index.
    pub fn get_signature(&self, signer_index: usize) -> Option<&[u8]> {
        match self.slots.get(signer_index)? {
            SlotState::Valid(sig) => Some(sig.as_slice()),
            SlotState::Empty => None,
        }
    }

    /// Return the indices of all signers who have submitted valid signatures.
    pub fn signed_indices(&self) -> Vec<usize> {
        self.slots
            .iter()
            .enumerate()
            .filter_map(|(i, slot)| {
                if *slot != SlotState::Empty {
                    Some(i)
                } else {
                    None
                }
            })
            .collect()
    }
}

// ---------------------------------------------------------------------------
// Unit tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::keypair::KeyPair;

    fn setup(required: usize, total: usize) -> (Vec<KeyPair>, ThresholdConfig) {
        let keypairs: Vec<KeyPair> = (0..total).map(|_| KeyPair::generate()).collect();
        let pks = keypairs.iter().map(|kp| kp.public_key().clone()).collect();
        let config = ThresholdConfig::new(required, pks).unwrap();
        (keypairs, config)
    }

    #[test]
    fn session_2of3_complete_roundtrip() {
        let (kps, cfg) = setup(2, 3);
        let msg = b"payload";
        let mut session = SigningSession::new(&cfg, msg);

        session.add_signature(0, kps[0].sign(msg)).unwrap();
        assert!(!session.is_complete());

        session.add_signature(2, kps[2].sign(msg)).unwrap();
        assert!(session.is_complete());
        assert!(session.verify().unwrap());
    }

    #[test]
    fn session_3of5_any_three_suffice() {
        let (kps, cfg) = setup(3, 5);
        let msg = b"3-of-5 test";
        let mut session = SigningSession::new(&cfg, msg);

        session.add_signature(1, kps[1].sign(msg)).unwrap();
        session.add_signature(3, kps[3].sign(msg)).unwrap();
        session.add_signature(4, kps[4].sign(msg)).unwrap();

        assert!(session.is_complete());
        assert!(session.verify().unwrap());
    }

    #[test]
    fn verify_before_threshold_met_returns_error() {
        let (kps, cfg) = setup(2, 3);
        let msg = b"incomplete";
        let mut session = SigningSession::new(&cfg, msg);

        session.add_signature(0, kps[0].sign(msg)).unwrap();

        let err = session.verify().unwrap_err();
        assert!(matches!(err, Error::ThresholdNotMet { have: 1, need: 2 }));
    }

    #[test]
    fn duplicate_signature_rejected() {
        let (kps, cfg) = setup(2, 3);
        let msg = b"dup test";
        let mut session = SigningSession::new(&cfg, msg);

        session.add_signature(0, kps[0].sign(msg)).unwrap();
        let err = session.add_signature(0, kps[0].sign(msg)).unwrap_err();
        assert!(matches!(err, Error::DuplicateSignature { index: 0 }));
    }

    #[test]
    fn out_of_range_index_rejected() {
        let (kps, cfg) = setup(2, 3);
        let msg = b"oob test";
        let mut session = SigningSession::new(&cfg, msg);

        let err = session.add_signature(99, kps[0].sign(msg)).unwrap_err();
        assert!(matches!(err, Error::SignerIndexOutOfRange { index: 99, total: 3 }));
    }

    #[test]
    fn wrong_key_signature_rejected() {
        let (kps, cfg) = setup(2, 3);
        let attacker = KeyPair::generate();
        let msg = b"attack";
        let mut session = SigningSession::new(&cfg, msg);

        // Attacker produces a valid Falcon-512 signature but with their own key.
        let forged = attacker.sign(msg);
        let err = session.add_signature(0, forged).unwrap_err();
        assert!(matches!(err, Error::VerificationFailed { index: 0 }));
    }

    #[test]
    fn wrong_message_signature_rejected() {
        let (kps, cfg) = setup(2, 3);
        let msg = b"correct message";
        let mut session = SigningSession::new(&cfg, msg);

        // Signer signs a different message and submits it.
        let sig_for_wrong_msg = kps[0].sign(b"wrong message");
        let err = session.add_signature(0, sig_for_wrong_msg).unwrap_err();
        assert!(matches!(err, Error::VerificationFailed { index: 0 }));
    }

    #[test]
    fn progress_reports_correctly() {
        let (kps, cfg) = setup(3, 5);
        let msg = b"progress test";
        let mut session = SigningSession::new(&cfg, msg);

        assert_eq!(session.progress(), (0, 3));
        session.add_signature(0, kps[0].sign(msg)).unwrap();
        assert_eq!(session.progress(), (1, 3));
        session.add_signature(1, kps[1].sign(msg)).unwrap();
        assert_eq!(session.progress(), (2, 3));
    }

    #[test]
    fn signed_indices_tracks_contributors() {
        let (kps, cfg) = setup(2, 4);
        let msg = b"indices test";
        let mut session = SigningSession::new(&cfg, msg);

        session.add_signature(0, kps[0].sign(msg)).unwrap();
        session.add_signature(3, kps[3].sign(msg)).unwrap();

        let indices = session.signed_indices();
        assert_eq!(indices, vec![0, 3]);
    }

    #[test]
    fn get_signature_returns_correct_bytes() {
        let (kps, cfg) = setup(2, 3);
        let msg = b"get sig test";
        let mut session = SigningSession::new(&cfg, msg);

        let sig = kps[1].sign(msg);
        session.add_signature(1, sig.clone()).unwrap();

        assert_eq!(session.get_signature(1), Some(sig.as_slice()));
        assert!(session.get_signature(0).is_none());
    }

    #[test]
    fn n_of_n_requires_all_signers() {
        let (kps, cfg) = setup(4, 4);
        let msg = b"unanimous";
        let mut session = SigningSession::new(&cfg, msg);

        for i in 0..3 {
            session.add_signature(i, kps[i].sign(msg)).unwrap();
            assert!(!session.is_complete(), "should not be complete after {i} sigs");
        }

        session.add_signature(3, kps[3].sign(msg)).unwrap();
        assert!(session.is_complete());
        assert!(session.verify().unwrap());
    }
}