falcon-rs 0.2.0

Native Rust implementation of FN-DSA (FIPS 206), the NIST post-quantum digital signature standard (formerly Falcon)
Documentation

falcon-rust

Crates.io Docs.rs CI MSRV License

Native Rust implementation of FN-DSA (FIPS 206), the NIST post-quantum digital signature standard formerly known as Falcon. Ported from the C reference implementation by Thomas Pornin.

Status

Production-ready — 81 tests passing. Passes all NIST Known Answer Tests (FN-DSA-512 & FN-DSA-1024), full FIPS 206 domain-separation KAT vectors, and FIPS 180-4 SHA-2 vectors.

Features

  • NIST FIPS 206 standard — FN-DSA (FFT over NTRU-Lattice-Based Digital Signature Algorithm)
  • Pure FN-DSADomainSeparation::None / Context (ph_flag = 0x00)
  • HashFN-DSADomainSeparation::Prehashed with SHA-256 or SHA-512 (ph_flag = 0x01)
  • Context validation — context > 255 bytes returns Err(BadArgument), never truncates
  • no_std support — works in embedded and WASM environments
  • WASM ready — compiles to wasm32-unknown-unknown out of the box
  • Security hardening — PRNG state is zeroized on drop via write_volatile
  • Pure Rust — no C dependencies, no assembly, pure-Rust SHA-256/SHA-512
  • Full SDK — high-level API with key/signature serialization
  • Serde support — optional Serialize/Deserialize for keys and signatures
  • Performance optimized — bounds-check-free NTT, FFT, and ChaCha20 hot paths
  • Fuzz tested — 3 cargo-fuzz targets exercising all domain-separation modes

Quick Start

use falcon::prelude::*;  // FnDsaKeyPair, FnDsaSignature, DomainSeparation, …

// Generate an FN-DSA-512 key pair
let kp = FnDsaKeyPair::generate(9).unwrap();

// Sign a message
let sig = kp.sign(b"Hello, post-quantum world!", &DomainSeparation::None).unwrap();

// Verify the signature
FnDsaSignature::verify(sig.to_bytes(), kp.public_key(), b"Hello, post-quantum world!", &DomainSeparation::None).unwrap();

Domain Separation (FIPS 206)

FN-DSA mandates domain separation to prevent cross-protocol signature reuse. Use DomainSeparation::Context(b"...") to bind signatures to a specific protocol:

use falcon::prelude::*;

let kp = FnDsaKeyPair::generate(9).unwrap();

// Sign with a protocol-specific context
let ctx = DomainSeparation::Context(b"my-protocol-v1");
let sig = kp.sign(b"msg", &ctx).unwrap();

// Verification requires the same context
FnDsaSignature::verify(sig.to_bytes(), kp.public_key(), b"msg", &ctx).unwrap();

Key Serialization

use falcon::prelude::*;

let kp = FnDsaKeyPair::generate(9).unwrap();

// Export keys to bytes (for storage, transmission, etc.)
let private_key: Vec<u8> = kp.private_key().to_vec();  // 1281 bytes
let public_key: Vec<u8> = kp.public_key().to_vec();     // 897 bytes

// Import from both keys
let restored = FnDsaKeyPair::from_keys(&private_key, &public_key).unwrap();

// Import from private key only (recomputes public key)
let restored2 = FnDsaKeyPair::from_private_key(&private_key).unwrap();
assert_eq!(public_key, restored2.public_key());

// Extract public key without creating a full key pair
let pk = FnDsaKeyPair::public_key_from_private(&private_key).unwrap();

Signature Serialization

use falcon::prelude::*;

let kp = FnDsaKeyPair::generate(9).unwrap();
let sig = kp.sign(b"msg", &DomainSeparation::None).unwrap();

// Export
let sig_bytes: Vec<u8> = sig.into_bytes();

// Import
let sig2 = FnDsaSignature::from_bytes(sig_bytes);

Serde Support

Enable the serde feature for JSON/bincode/etc. serialization:

[dependencies]
falcon-rust = { version = "0.2", features = ["serde"] }

FnDsaKeyPair, FnDsaSignature, FalconError, DomainSeparation, and PreHashAlgorithm all implement Serialize/Deserialize when enabled. FalconError also implements std::error::Error (std builds only).

Security Levels

Variant logn NIST Level Private Key Public Key Signature
FN-DSA-512 9 I 1281 B 897 B 666 B
FN-DSA-1024 10 V 2305 B 1793 B 1280 B

Benchmarks — C vs Rust

Measured on Apple M-series (ARM64), single-threaded, release builds. C compiled with clang -O3, Rust with cargo --release (opt-level 3).

FN-DSA-512

Operation C (ref) Rust Ratio
keygen 5.55 ms 4.23 ms 0.76×
sign 213 µs 279 µs 1.31×
verify 14.3 µs 26.6 µs 1.86×

FN-DSA-1024

Operation C (ref) Rust Ratio
keygen 18.6 ms 15.2 ms 0.82×
sign 434 µs 569 µs 1.31×
verify 27.8 µs 54.5 µs 1.96×

Notes: Keygen is faster than C. Sign is ~1.3× slower (C reference uses AVX2/NEON ChaCha20 PRNG and hand-tuned NTT). Verify overhead is in the constant-time hash-to-point path; switching to FALCON_SIG_COMPRESSED format with hash_to_point_vartime closes this gap at the cost of timing-side-channel resistance.

Run benchmarks yourself:

# Criterion (statistical, recommended)
cargo bench

# Quick ad-hoc benchmarks
cargo test --release --test bench_falcon -- --ignored --nocapture

API Overview

High-Level SDK (safe_api)

Type Description
FnDsaKeyPair Key generation, signing, import/export
FnDsaSignature Verification, serialization
DomainSeparation::None Pure FN-DSA, no context
DomainSeparation::Context Pure FN-DSA with protocol context string
DomainSeparation::Prehashed HashFN-DSA — SHA-256/SHA-512 pre-hash
PreHashAlgorithm Sha256 / Sha512 selector for HashFN-DSA
FalconError Error codes (RandomError, FormatError, etc.)

HashFN-DSA (FIPS 206 §6)

FIPS 206 defines two operation modes. Use Prehashed when the message is large or must be committed to before signing:

use falcon::prelude::*;

let kp = FnDsaKeyPair::generate(9).unwrap();

// HashFN-DSA — message is pre-hashed with SHA-256 inside sign/verify
let domain = DomainSeparation::Prehashed {
    alg: PreHashAlgorithm::Sha256,
    context: b"my-protocol-v2",   // optional, max 255 bytes
};
let sig = kp.sign(b"large document bytes...", &domain).unwrap();
FnDsaSignature::verify(sig.to_bytes(), kp.public_key(), b"large document bytes...", &domain).unwrap();

Security note: The context string (0–255 bytes) must match exactly between sign and verify. Passing > 255 bytes returns Err(BadArgument). Signatures created under one DomainSeparation variant will never verify under a different variant.

Backward Compatibility

The type aliases FalconKeyPair and FalconSignature are provided for backward compatibility and map to FnDsaKeyPair and FnDsaSignature.

Low-Level (falcon)

For advanced use cases — streamed signing, expanded keys, custom signature formats:

use falcon::falcon as falcon_api;
use falcon::shake::InnerShake256Context;

// Streamed signing (hash-then-sign for large messages)
let mut hash = InnerShake256Context::new();
falcon_api::falcon_sign_start(&mut rng, &mut nonce, &mut hash);
falcon_api::shake256_inject(&mut hash, &chunk1);
falcon_api::shake256_inject(&mut hash, &chunk2);
falcon_api::falcon_sign_dyn_finish(&mut rng, &mut sig, ...);

// Expanded key (amortized cost for multiple signatures)
falcon_api::falcon_expand_privkey(&mut expanded, &privkey, &mut tmp);
falcon_api::falcon_sign_tree(&mut rng, &mut sig, ..., &expanded, ...);

Examples

cargo run --release --example keygen       # Generate key pair, inspect sizes
cargo run --release --example sign_verify  # Pure FN-DSA + HashFN-DSA demos
cargo run --release --example serialize    # Full serialization round-trip
cargo run --release --example expand_key   # Expanded-key amortized signing

Expanded Key API

For workloads that sign many messages with the same key, expand once and reuse:

use falcon::prelude::*;

let kp = FnDsaKeyPair::generate(9).unwrap();
let ek = kp.expand().unwrap();   // one-time cost: ~2.5× a single sign()
drop(kp);                         // private key zeroized here

// Each sign() is now ~1.5× faster than FnDsaKeyPair::sign()
let sig = ek.sign(b"hello", &DomainSeparation::None).unwrap();
FnDsaSignature::verify(sig.to_bytes(), ek.public_key(), b"hello",
    &DomainSeparation::None).unwrap();

Security Properties

Property Implementation
Private key zeroize-on-drop Zeroizing<Vec<u8>> from the zeroize crate
Expanded key zeroize-on-drop Same — Zeroizing<Vec<u8>> for the LDL tree
Constant-time verify falcon_verify inherits constant-time from C reference
Seed material zeroized write_volatile on the 48-byte OS-entropy seed in sign()
Context length bounded Context strings > 255 bytes return Err(BadArgument) per FIPS 206
Cross-domain isolation Signatures under one DomainSeparation variant never verify under another

Building

cargo build --release
cargo test --release

Testing

# Full suite — 81 tests across 5 test files
cargo test --release

# NIST Falcon KAT (FN-DSA-512 & FN-DSA-1024 algorithm core)
cargo test --release --test nist_kat

# FIPS 206 domain-separation KAT (pure + HashFN-DSA, all domain modes)
cargo test --release --test fips206_kat

# FIPS 180-4 SHA-256 / SHA-512 NIST vectors
cargo test --release --test full_coverage -- test_sha

# Benchmarks (low-level + safe_api + HashFN-DSA)
cargo test --release --test bench_falcon -- --ignored --nocapture

Test matrix

Suite Count Covers
full_coverage 47 safe_api, domain sep, HashFN-DSA, SHA-2 vectors, codec
fips206_kat 6 Deterministic KAT vectors for all FIPS 206 domain modes
prop_tests 7 Property-based tests (sign→verify, cross-domain, wrong-msg)
kat_test 16 Low-level API, NTT/FFT, codec, keygen/sign/verify
nist_kat 2 NIST SHA-1 KAT hashes for FN-DSA-512 and FN-DSA-1024
doc-tests 7 Crate-level and module doc examples
Total 92

Fuzz Testing

Three cargo-fuzz targets are included:

# Install cargo-fuzz (one-time)
cargo install cargo-fuzz

# Fuzz verify rejection (random data should never verify)
cargo fuzz run fuzz_verify_reject -- -max_total_time=60

# Fuzz sign+verify roundtrip (must always succeed)
cargo fuzz run fuzz_sign_verify -- -max_total_time=60

# Fuzz codec encode/decode roundtrip
cargo fuzz run fuzz_codec_roundtrip -- -max_total_time=60

WASM

FN-DSA compiles to WebAssembly out of the box:

# Install the WASM target (one-time)
rustup target add wasm32-unknown-unknown

# Build for WASM (no_std, no OS entropy)
cargo build --target wasm32-unknown-unknown --no-default-features --release

In no_std / WASM environments, use deterministic key generation with your own entropy:

use falcon::prelude::*;

let seed: [u8; 48] = /* your entropy source */;
let kp = FnDsaKeyPair::generate_deterministic(&seed, 9).unwrap();

Documentation

cargo doc --no-deps --open

License

MIT — matching the C reference implementation.