falcon-multisig 0.1.0

Production-grade post-quantum threshold multisignature library using Falcon-512 (NIST FIPS 206 / FN-DSA)
Documentation
//! Core verification primitive for Falcon-512 signatures.
//!
//! This module provides the lowest-level verification function used internally
//! throughout the library. Higher-level callers should use [`SigningSession::verify`]
//! or [`ThresholdConfig`] APIs instead.

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

use falcon_rust::falcon512::{self as fr, PublicKey as FrPublicKey, Signature as FrSignature};

use crate::{
    error::Error,
    keypair::domain_hash,
    PUBLIC_KEY_BYTES, SIGNATURE_MAX_BYTES, SIGNATURE_MIN_BYTES,
};

/// Verify a single Falcon-512 signature against a message and public key.
///
/// This is the fundamental verification primitive. It:
/// 1. Validates input lengths.
/// 2. Recomputes the domain-separated digest of `message`.
/// 3. Runs the Falcon-512 verification algorithm.
///
/// Returns `Ok(true)` on success, `Ok(false)` if the signature is
/// cryptographically invalid, and `Err(_)` if the inputs are malformed.
///
/// The `signer_index` parameter is used only for error context; it does not
/// affect cryptographic verification.
pub fn verify_raw(
    message: &[u8],
    signature: &[u8],
    public_key: &[u8],
    signer_index: usize,
) -> Result<bool, Error> {
    // Validate public key length.
    if public_key.len() != PUBLIC_KEY_BYTES {
        return Err(Error::InvalidPublicKeyLength {
            expected: PUBLIC_KEY_BYTES,
            actual: public_key.len(),
        });
    }

    // Validate signature length bounds.
    if signature.len() < SIGNATURE_MIN_BYTES || signature.len() > SIGNATURE_MAX_BYTES {
        return Err(Error::InvalidSignatureLength {
            actual: signature.len(),
            min: SIGNATURE_MIN_BYTES,
            max: SIGNATURE_MAX_BYTES,
        });
    }

    // Parse the public key.
    let pk = FrPublicKey::from_bytes(public_key).map_err(|_| Error::PublicKeyParseError {
        index: signer_index,
    })?;

    // Parse the signature.
    let sig = FrSignature::from_bytes(signature).map_err(|_| Error::SignatureParseError {
        index: signer_index,
    })?;

    // Recompute the domain-separated digest and verify.
    let digest = domain_hash(message);
    Ok(fr::verify(&digest, &sig, &pk))
}

/// Verify a single partial signature contributed to a threshold session.
///
/// This is a convenience wrapper around [`verify_raw`] that is semantically
/// named for the threshold context. It is re-exported at the crate root as
/// [`crate::verify_partial`].
///
/// # Errors
///
/// See [`verify_raw`] for error conditions.
pub fn verify_partial(
    message: &[u8],
    signature: &[u8],
    public_key: &[u8],
    signer_index: usize,
) -> Result<bool, Error> {
    verify_raw(message, signature, public_key, signer_index)
}

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

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

    fn make_signed(message: &[u8]) -> (KeyPair, Vec<u8>) {
        let kp = KeyPair::generate();
        let sig = kp.sign(message);
        (kp, sig)
    }

    #[test]
    fn valid_signature_verifies() {
        let msg = b"chain-agnostic test message";
        let (kp, sig) = make_signed(msg);
        let result = verify_raw(msg, &sig, kp.public_key().as_bytes(), 0).unwrap();
        assert!(result);
    }

    #[test]
    fn tampered_message_fails() {
        let (kp, sig) = make_signed(b"original");
        let result = verify_raw(b"tampered", &sig, kp.public_key().as_bytes(), 0).unwrap();
        assert!(!result);
    }

    #[test]
    fn wrong_public_key_fails() {
        let msg = b"some payload";
        let (kp1, sig) = make_signed(msg);
        let kp2 = KeyPair::generate();
        let result = verify_raw(msg, &sig, kp2.public_key().as_bytes(), 0).unwrap();
        assert!(!result);
    }

    #[test]
    fn bad_pubkey_length_returns_error() {
        let msg = b"payload";
        let (_, sig) = make_signed(msg);
        let bad_pk = vec![0u8; 32];
        let err = verify_raw(msg, &sig, &bad_pk, 0).unwrap_err();
        assert!(matches!(err, Error::InvalidPublicKeyLength { .. }));
    }

    #[test]
    fn bad_sig_length_returns_error() {
        let msg = b"payload";
        let kp = KeyPair::generate();
        let short_sig = vec![0u8; 1];
        let err = verify_raw(msg, &short_sig, kp.public_key().as_bytes(), 2).unwrap_err();
        assert!(matches!(err, Error::InvalidSignatureLength { .. }));
    }

    #[test]
    fn signer_index_appears_in_parse_error() {
        let msg = b"x";
        let kp = KeyPair::generate();
        // A syntactically correct-length but semantically garbage signature.
        let garbage_sig = vec![0u8; SIGNATURE_MAX_BYTES];
        let err = verify_raw(msg, &garbage_sig, kp.public_key().as_bytes(), 7).unwrap_err();
        // Either SignatureParseError or VerificationFailed — index 7 must appear.
        let err_str = format!("{err}");
        assert!(err_str.contains('7'), "signer index must appear in error: {err_str}");
    }
}