falconed 0.1.0

hybrid post-quantum signatures: ed25519 + falcon-512
Documentation

falconed

hybrid post-quantum signatures for people who ship things.

what

this crate combines ed25519 with falcon-512. both signatures are computed over the message. both must verify. if either is broken, you still have the other.

this is the conservative choice for systems that need to last.

why

  • 48% of nist pqc round-1 submissions are broken
  • falcon might join them someday
  • ed25519 will fall to cryptographically relevant quantum computers
  • hedge your bets

usage

use falconed::{SigningKey, VerifyingKey, Signature};
use rand_core::OsRng;

// generate a keypair
let sk = SigningKey::generate(&mut OsRng);
let pk = sk.verifying_key().unwrap();

// sign a message
let msg = b"the quick brown fox";
let sig = sk.sign(msg).unwrap();

// verify
assert!(pk.verify(msg, &sig).is_ok());

signature crate interop

implements signature::Signer and signature::Verifier:

use signature::{Signer, Verifier};
use falconed::SigningKey;
use rand_core::OsRng;

let sk = SigningKey::generate(&mut OsRng);
let pk = sk.verifying_key().unwrap();

let sig = sk.try_sign(b"message").unwrap();
assert!(pk.verify(b"message", &sig).is_ok());

serialization

all types are fixed-size. concatenation, no length prefixes, no asn.1.

SigningKey   = ed25519_seed (32) || falcon_sk (1281) = 1313 bytes
VerifyingKey = ed25519_pk (32) || falcon_pk (897) = 929 bytes
Signature    = ed25519_sig (64) || falcon_sig (666) = 730 bytes

to_bytes() and from_bytes() on everything. TryFrom<&[u8]> does what you expect.

verification semantics

verify() checks ed25519 first. if it fails, returns early without checking falcon. this is faster and leaks nothing—verification is a public operation on public inputs.

verify_all() always checks both signatures regardless of the first result.

// fast path (default)
pk.verify(msg, &sig)?;

// both always checked
pk.verify_all(msg, &sig)?;

features

[features]
default = ["std"]
std = []
serde = ["dep:serde"]
zeroize = ["dep:zeroize"]
hazmat = []  # expose internal key components
simd = []    # avx2 acceleration on x86_64

no_std

disable default features. requires alloc — a #[global_allocator] must be available. the allocator is used for domain-tagged message construction and by fn-dsa internally.

[dependencies]
falconed = { version = "0.1", default-features = false }

security considerations

hybrid construction: this crate concatenates independent ed25519 and falcon-512 signatures. an attacker must break both schemes to forge a signature. this is a standard construction but has not been formally analyzed for this specific pairing.

timing: signing operations aim to be constant-time, but this depends on the underlying implementations (ed25519-dalek, fn-dsa). verification is explicitly not constant-time—it operates on public inputs only.

zeroization: secret key material is zeroized on drop. decoded falcon signing keys are zeroized after each operation.

not audited: this implementation has not been professionally audited. use at your own risk for anything important.

benchmarks

measured on amd ryzen 9 7950x:

operation time
keygen ~2.0 ms
sign ~162 µs
verify ~34 µs

falcon backend

uses fn-dsa by thomas pornin, the original author of falcon. pure rust, no C bindings.

msrv

1.82 (required by fn-dsa)

license

mit or apache-2.0, your choice.

status

this is alpha software. the api may change. don't use it for anything important until it's been audited.

acknowledgments

built on: