From 00d53b892144bdb1f509fb23ab8721f55551f3af Mon Sep 17 00:00:00 2001 From: LLFourn Date: Wed, 3 Sep 2025 14:38:17 +1000 Subject: [PATCH 1/4] [vrf] Encourage hashing the output Because security proofs require this. --- schnorr_fun/src/frost/chilldkg/certpedpop.rs | 38 +++++++---- vrf_fun/src/lib.rs | 53 +++++++++++++++ vrf_fun/src/rfc9381.rs | 8 +-- vrf_fun/src/vrf.rs | 69 ++++++++++++++++++-- vrf_fun/tests/proptest_tests.rs | 20 +++--- vrf_fun/tests/rfc9381_test_vectors.rs | 8 +-- 6 files changed, 158 insertions(+), 38 deletions(-) diff --git a/schnorr_fun/src/frost/chilldkg/certpedpop.rs b/schnorr_fun/src/frost/chilldkg/certpedpop.rs index fd591a55..fd042114 100644 --- a/schnorr_fun/src/frost/chilldkg/certpedpop.rs +++ b/schnorr_fun/src/frost/chilldkg/certpedpop.rs @@ -71,13 +71,14 @@ impl CertificationScheme for Schnorr { #[cfg(feature = "vrf_cert_keygen")] pub mod vrf_cert { use super::*; - use secp256kfun::digest::core_api::BlockSizeUser; - use vrf_fun::VrfProof; + use secp256kfun::digest::typenum::U32; + use secp256kfun::{Tag, digest::core_api::BlockSizeUser, hash::HashAdd}; + use vrf_fun::{SimpleVrf, VrfProof}; - /// VRF certification scheme using SSWU VRF + /// VRF certification scheme using SimpleVrf #[derive(Clone, Copy, Debug, PartialEq, Default)] pub struct VrfCertifier { - hash: core::marker::PhantomData, + _hash: core::marker::PhantomData, } /// The output from VRF verification containing the gamma point @@ -88,8 +89,13 @@ pub mod vrf_cert { } /// Implement CertificationScheme for VrfCertifier - impl CertificationScheme for VrfCertifier { - type Signature = VrfProof; + impl CertificationScheme for VrfCertifier + where + H: Hash32 + BlockSizeUser + Clone + Tag, + // This constraint ensures the hash has a 512-bit block size (required for HashTranscript) + H: BlockSizeUser, + { + type Signature = VrfProof; type Output = VrfOutput; fn certify( @@ -99,7 +105,9 @@ pub mod vrf_cert { ) -> Self::Signature { // Use the certification bytes as the VRF input let cert_bytes = agg_input.cert_bytes(); - vrf_fun::rfc9381::sswu::prove::(keypair, &cert_bytes) + let h = Point::hash_to_curve(H::default().add(&cert_bytes[..])).normalize(); + let vrf = SimpleVrf::::default().with_name("chilldkg-vrf"); + vrf.prove(keypair, h) } fn verify_cert( @@ -110,10 +118,10 @@ pub mod vrf_cert { ) -> Option { // Use the certification bytes as the VRF input let cert_bytes = agg_input.cert_bytes(); - vrf_fun::rfc9381::sswu::verify::(cert_key, &cert_bytes, signature).map(|output| { - VrfOutput { - gamma: output.gamma, - } + let h = Point::hash_to_curve(H::default().add(&cert_bytes[..])).normalize(); + let vrf = SimpleVrf::::default().with_name("chilldkg-vrf"); + vrf.verify(cert_key, h, signature).map(|output| VrfOutput { + gamma: output.dangerously_access_gamma(), }) } } @@ -280,8 +288,12 @@ impl CertifiedKeygen { } #[cfg(feature = "vrf_cert_keygen")] -impl - CertifiedKeygen> +impl CertifiedKeygen> +where + H: Hash32 + secp256kfun::digest::crypto_common::BlockSizeUser + Clone + Tag, + H: secp256kfun::digest::crypto_common::BlockSizeUser< + BlockSize = secp256kfun::digest::typenum::U64, + >, { /// Compute a randomness beacon from the VRF outputs /// diff --git a/vrf_fun/src/lib.rs b/vrf_fun/src/lib.rs index 6d98d218..568f1dd2 100644 --- a/vrf_fun/src/lib.rs +++ b/vrf_fun/src/lib.rs @@ -22,6 +22,59 @@ use sigma_fun::{ pub type VrfDleq = Eq, DL>; /// Simple VRF using HashTranscript with 32-byte challenges +/// +/// This provides a straightforward VRF implementation using the standard +/// HashTranscript from sigma_fun. It produces 32-byte proofs. +/// +/// # Example +/// +/// ``` +/// use secp256kfun::{KeyPair, Scalar, prelude::*}; +/// use secp256kfun::hash::HashAdd; +/// use secp256kfun::digest::Digest; +/// use vrf_fun::SimpleVrf; +/// use sha2::Sha256; +/// use rand::thread_rng; +/// +/// // Generate a keypair +/// let keypair = KeyPair::new(Scalar::random(&mut thread_rng())); +/// +/// // Create the VRF instance +/// let vrf = SimpleVrf::::default(); +/// +/// // Hash input data to a curve point +/// let hasher = Sha256::default().add(b"my-input-data"); +/// let h = Point::hash_to_curve(hasher).normalize(); +/// +/// // Generate proof +/// let proof = vrf.prove(&keypair, h); +/// +/// // Verify proof +/// let verified = vrf.verify(keypair.public_key(), h, &proof) +/// .expect("proof should verify"); +/// +/// // The verified output contains a gamma point that can be hashed +/// // to produce deterministic randomness +/// let output_bytes = Sha256::default() +/// .add(verified) +/// .finalize(); +/// ``` +/// +/// # Domain Separation +/// +/// You can set a custom name for domain separation using `with_name`: +/// +/// ``` +/// # use secp256kfun::{KeyPair, Scalar, hash::{Hash32, HashAdd}, prelude::*}; +/// # use vrf_fun::SimpleVrf; +/// # use sha2::Sha256; +/// # use rand::thread_rng; +/// # let keypair = KeyPair::new(Scalar::random(&mut thread_rng())); +/// # let hasher = Sha256::default().add(b"my-input-data"); +/// # let h = Point::hash_to_curve(hasher).normalize(); +/// let vrf = SimpleVrf::::default().with_name("my-app-vrf"); +/// let proof = vrf.prove(&keypair, h); +/// ``` pub type SimpleVrf = Vrf, U32>; /// Re-export the [RFC 9381] type aliases diff --git a/vrf_fun/src/rfc9381.rs b/vrf_fun/src/rfc9381.rs index 070e6b60..4e8491d0 100644 --- a/vrf_fun/src/rfc9381.rs +++ b/vrf_fun/src/rfc9381.rs @@ -178,11 +178,11 @@ pub mod tai { /// Compute [RFC 9381] compliant output with TAI suite string (0xFE) /// /// [RFC 9381]: https://datatracker.ietf.org/doc/html/rfc9381 - pub fn output(verified: &VerifiedRandomOutput) -> [u8; 32] { + pub fn output(verified: VerifiedRandomOutput) -> [u8; 32] { H::default() .add([SUITE_STRING_TAI]) .add(0x03u8) // Hash mode domain separator - .add(verified.gamma.to_bytes()) + .add(verified) .add(0x00u8) // Hash mode trailer .finalize_fixed() .into() @@ -281,11 +281,11 @@ pub mod sswu { /// /// [RFC 9381]: https://datatracker.ietf.org/doc/html/rfc9381 /// [RFC 9380]: https://datatracker.ietf.org/doc/html/rfc9380 - pub fn output(verified: &VerifiedRandomOutput) -> [u8; 32] { + pub fn output(verified: VerifiedRandomOutput) -> [u8; 32] { H::default() .add([SUITE_STRING_RFC9380]) .add(0x03u8) // Hash mode domain separator - .add(verified.gamma.to_bytes()) + .add(verified) .add(0x00u8) // Hash mode trailer .finalize_fixed() .into() diff --git a/vrf_fun/src/vrf.rs b/vrf_fun/src/vrf.rs index bf9794c5..9331a411 100644 --- a/vrf_fun/src/vrf.rs +++ b/vrf_fun/src/vrf.rs @@ -1,6 +1,6 @@ //! Generic VRF implementation that can work with different transcript types -use secp256kfun::{KeyPair, Scalar, prelude::*}; +use secp256kfun::{KeyPair, Scalar, hash::HashInto, prelude::*}; use sigma_fun::{ CompactProof, FiatShamir, ProverTranscript, Transcript, generic_array::{ @@ -32,24 +32,67 @@ pub struct VrfProof where L: ArrayLength, { - /// The VRF output point. + /// The VRF output point (gamma). /// - /// Usually you don't use this directly but hash it. + /// **Security Warning**: According to VRF security proofs (see + /// ["Making NSEC5 Practical for DNSSEC"](https://eprint.iacr.org/2017/099.pdf)), + /// this point must be hashed before being used as randomness. Direct use of gamma + /// may compromise the pseudorandomness properties of the VRF. + /// + /// After verification, use the `HashInto` implementation on `VerifiedRandomOutput` + /// to safely extract randomness. pub gamma: Point, /// The proof that `gamma` is correct. pub proof: CompactProof, L>, } /// Verified random output that ensures gamma has been verified -#[derive(Debug, Clone)] +/// +/// The gamma point is kept private to enforce proper usage. VRF security proofs +/// require hashing gamma before use as randomness. Use the `HashInto` implementation +/// to safely extract randomness from this output. +#[derive(Debug, Clone, Copy)] pub struct VerifiedRandomOutput { - pub gamma: Point, + gamma: Point, +} + +impl VerifiedRandomOutput { + /// Access the raw gamma point directly. + /// + /// # Security Warning + /// + /// The VRF security proofs require that gamma be hashed before being used as randomness. + /// Using the gamma point directly without hashing may compromise the pseudorandomness + /// properties of the VRF. + /// + /// According to ["Making NSEC5 Practical for DNSSEC"](https://eprint.iacr.org/2017/099.pdf), + /// the VRF output must be the hash of gamma, not gamma itself, to maintain security + /// properties. The paper notes that "the VRF output is the hash of the unique point + /// on the curve" to ensure proper domain separation and pseudorandomness. + /// + /// **You should use the `HashInto` implementation instead**, which properly hashes + /// gamma to produce secure randomness: + /// + /// ```ignore + /// use sha2::Sha256; + /// let randomness = Sha256::default().add(&verified_output).finalize_fixed(); + /// ``` + pub fn dangerously_access_gamma(&self) -> Point { + self.gamma + } +} + +impl HashInto for VerifiedRandomOutput { + fn hash_into(self, hash: &mut impl secp256kfun::digest::Update) { + self.gamma.hash_into(hash) + } } /// Generic VRF implementation pub struct Vrf { dleq: crate::VrfDleq, pub transcript: T, + name: Option<&'static str>, } impl Vrf @@ -64,8 +107,20 @@ where Self { dleq: Eq::new(DLG::default(), DL::default()), transcript, + name: None, } } + + /// Set a custom name for domain separation + /// + /// The name is used in the Fiat-Shamir transform to provide domain separation. + /// + /// Note: For RFC 9381 VRFs, setting a name has no effect as they use their own + /// transcript mechanism that doesn't support custom names. + pub fn with_name(mut self, name: &'static str) -> Self { + self.name = Some(name); + self + } } impl Default for Vrf @@ -92,7 +147,7 @@ where { let (secret_key, public_key) = keypair.as_tuple(); let gamma = g!(secret_key * h).normalize(); - let fs = FiatShamir::new(self.dleq.clone(), self.transcript.clone(), None); + let fs = FiatShamir::new(self.dleq.clone(), self.transcript.clone(), self.name); let witness = secret_key; let statement = (public_key, (h, gamma)); let proof = fs.prove::(&witness, &statement, None); @@ -106,7 +161,7 @@ where h: Point, proof: &VrfProof, ) -> Option { - let fs = FiatShamir::new(self.dleq.clone(), self.transcript.clone(), None); + let fs = FiatShamir::new(self.dleq.clone(), self.transcript.clone(), self.name); let statement = (public_key.normalize(), (h, proof.gamma)); if !fs.verify(&statement, &proof.proof) { diff --git a/vrf_fun/tests/proptest_tests.rs b/vrf_fun/tests/proptest_tests.rs index 75ce3756..12e87ecf 100644 --- a/vrf_fun/tests/proptest_tests.rs +++ b/vrf_fun/tests/proptest_tests.rs @@ -27,8 +27,8 @@ proptest! { let verified1_again = rfc9381::tai::verify::(keypair.public_key(), &alpha1, &proof1_again) .expect("Proof should verify again"); assert_eq!( - rfc9381::tai::output::(&verified1), - rfc9381::tai::output::(&verified1_again), + rfc9381::tai::output::(verified1), + rfc9381::tai::output::(verified1_again), "VRF output should be deterministic" ); @@ -46,8 +46,8 @@ proptest! { .expect("Second proof should verify"); assert_ne!( - rfc9381::tai::output::(&verified1), - rfc9381::tai::output::(&verified2), + rfc9381::tai::output::(verified1), + rfc9381::tai::output::(verified2), "Different inputs should produce different outputs" ); @@ -87,8 +87,8 @@ proptest! { let verified1_again = rfc9381::sswu::verify::(keypair.public_key(), &alpha1, &proof1_again) .expect("Proof should verify again"); assert_eq!( - rfc9381::sswu::output::(&verified1), - rfc9381::sswu::output::(&verified1_again), + rfc9381::sswu::output::(verified1), + rfc9381::sswu::output::(verified1_again), "VRF output should be deterministic" ); @@ -106,8 +106,8 @@ proptest! { .expect("Second proof should verify"); assert_ne!( - rfc9381::sswu::output::(&verified1), - rfc9381::sswu::output::(&verified2), + rfc9381::sswu::output::(verified1), + rfc9381::sswu::output::(verified2), "Different inputs should produce different outputs" ); @@ -143,7 +143,7 @@ proptest! { // Test basic verify let verified1 = vrf.verify(keypair.public_key(), h1, &proof1) .expect("Proof should verify with correct public key"); - assert_eq!(proof1.gamma, verified1.gamma); + assert_eq!(proof1.gamma, verified1.dangerously_access_gamma()); // Test deterministic output let proof1_again = vrf.prove(&keypair, h1); @@ -170,7 +170,7 @@ proptest! { let verified2 = vrf.verify(keypair.public_key(), h2, &proof2) .expect("Second proof should verify"); - assert_ne!(verified1.gamma, verified2.gamma, "Different inputs should produce different gamma"); + assert_ne!(verified1.dangerously_access_gamma(), verified2.dangerously_access_gamma(), "Different inputs should produce different gamma"); // Cross-verification should fail assert!( diff --git a/vrf_fun/tests/rfc9381_test_vectors.rs b/vrf_fun/tests/rfc9381_test_vectors.rs index aae89103..cc9863c7 100644 --- a/vrf_fun/tests/rfc9381_test_vectors.rs +++ b/vrf_fun/tests/rfc9381_test_vectors.rs @@ -105,7 +105,7 @@ fn verify_tai_test_vector(tv: &TestVector) { .expect("Proof verification failed"); // Check VRF output matches - let output = rfc9381::tai::output::(&verified); + let output = rfc9381::tai::output::(verified); let expected_output = hex::decode_array::<32>(tv.beta).expect("Invalid beta hex"); assert_eq!(output, expected_output); @@ -119,7 +119,7 @@ fn verify_tai_test_vector(tv: &TestVector) { rfc9381::tai::verify::(keypair.public_key(), tv.alpha, &proof_generated) .expect("Generated proof verification failed"); assert_eq!( - rfc9381::tai::output::(&verified_generated), + rfc9381::tai::output::(verified_generated), expected_output ); } @@ -207,7 +207,7 @@ fn verify_sswu_test_vector(tv: &TestVector) { .expect("Proof verification failed"); // Check VRF output matches - let output = rfc9381::sswu::output::(&verified); + let output = rfc9381::sswu::output::(verified); let expected_output = hex::decode_array::<32>(tv.beta).expect("Invalid beta hex"); assert_eq!(output, expected_output); @@ -221,7 +221,7 @@ fn verify_sswu_test_vector(tv: &TestVector) { rfc9381::sswu::verify::(keypair.public_key(), tv.alpha, &proof_generated) .expect("Generated proof verification failed"); assert_eq!( - rfc9381::sswu::output::(&verified_generated), + rfc9381::sswu::output::(verified_generated), expected_output ); } From 13d2f12d1981f398f7243eeca03d52e083120f38 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Thu, 4 Sep 2025 10:59:31 +1000 Subject: [PATCH 2/4] =?UTF-8?q?[=E2=9D=84]=20improve=20saftey=20and=20ergo?= =?UTF-8?q?nomics=20of=20certpedpop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Add a `Certifier` where you can add certificates as you get them. Crucially this certifier also verifies them. 2. Add a concept of a Contributor that is also a share receiver. --- schnorr_fun/src/frost/chilldkg/certpedpop.rs | 582 ++++++------------ .../frost/chilldkg/certpedpop/certificate.rs | 395 ++++++++++++ secp256kfun/src/hash.rs | 29 + vrf_fun/src/vrf.rs | 39 +- vrf_fun/tests/proptest_tests.rs | 8 +- vrf_fun/tests/rfc9381_test_vectors.rs | 22 +- 6 files changed, 674 insertions(+), 401 deletions(-) create mode 100644 schnorr_fun/src/frost/chilldkg/certpedpop/certificate.rs diff --git a/schnorr_fun/src/frost/chilldkg/certpedpop.rs b/schnorr_fun/src/frost/chilldkg/certpedpop.rs index fd042114..c2504634 100644 --- a/schnorr_fun/src/frost/chilldkg/certpedpop.rs +++ b/schnorr_fun/src/frost/chilldkg/certpedpop.rs @@ -7,6 +7,14 @@ //! can finally output the key. //! //! Certificates are collected from other share receivers as well as `Contributor`s. + +pub mod certificate; +#[cfg(feature = "vrf_cert_keygen")] +pub use certificate::vrf_cert; +pub use certificate::{ + CertificateError, CertificationScheme, CertifiedKeygen, Certifier, CertifierError, +}; + use super::{encpedpop, simplepedpop}; use crate::{Schnorr, frost::*}; use alloc::{ @@ -15,132 +23,17 @@ use alloc::{ }; use secp256kfun::{KeyPair, hash::Hash32, nonce::NonceGen, prelude::*, rand_core}; -/// A trait for signature schemes that can be used to certify the DKG output. -/// -/// This allows applications to choose their preferred signature scheme for -/// certifying the aggregated keygen input in certpedpop. -pub trait CertificationScheme { - /// The signature type produced by this scheme - type Signature: Clone + core::fmt::Debug + PartialEq; - - /// The output produced by successful verification - type Output: Clone + core::fmt::Debug; - - /// Sign the AggKeygenInput with the given keypair - fn certify(&self, keypair: &KeyPair, agg_input: &encpedpop::AggKeygenInput) -> Self::Signature; - - /// Verify a certification signature and return the output - fn verify_cert( - &self, - cert_key: Point, - agg_input: &encpedpop::AggKeygenInput, - signature: &Self::Signature, - ) -> Option; -} - -/// Standard Schnorr (BIP340) implementation of the CertificationScheme trait -impl CertificationScheme for Schnorr { - type Signature = crate::Signature; - type Output = (); - - fn certify(&self, keypair: &KeyPair, agg_input: &encpedpop::AggKeygenInput) -> Self::Signature { - let cert_bytes = agg_input.cert_bytes(); - let message = crate::Message::new("BIP DKG/cert", cert_bytes.as_ref()); - let keypair_even_y = (*keypair).into(); - self.sign(&keypair_even_y, message) - } - - fn verify_cert( - &self, - cert_key: Point, - agg_input: &encpedpop::AggKeygenInput, - signature: &Self::Signature, - ) -> Option { - let cert_bytes = agg_input.cert_bytes(); - let message = crate::Message::new("BIP DKG/cert", cert_bytes.as_ref()); - let cert_key_even_y = cert_key.into_point_with_even_y().0; - if self.verify(&cert_key_even_y, message, signature) { - Some(()) - } else { - None - } - } -} - -/// VRF-based implementation of CertificationScheme -#[cfg(feature = "vrf_cert_keygen")] -pub mod vrf_cert { - use super::*; - use secp256kfun::digest::typenum::U32; - use secp256kfun::{Tag, digest::core_api::BlockSizeUser, hash::HashAdd}; - use vrf_fun::{SimpleVrf, VrfProof}; - - /// VRF certification scheme using SimpleVrf - #[derive(Clone, Copy, Debug, PartialEq, Default)] - pub struct VrfCertifier { - _hash: core::marker::PhantomData, - } - - /// The output from VRF verification containing the gamma point - #[derive(Clone, Debug, PartialEq)] - pub struct VrfOutput { - /// The VRF output point (gamma) - pub gamma: Point, - } - - /// Implement CertificationScheme for VrfCertifier - impl CertificationScheme for VrfCertifier - where - H: Hash32 + BlockSizeUser + Clone + Tag, - // This constraint ensures the hash has a 512-bit block size (required for HashTranscript) - H: BlockSizeUser, - { - type Signature = VrfProof; - type Output = VrfOutput; - - fn certify( - &self, - keypair: &KeyPair, - agg_input: &encpedpop::AggKeygenInput, - ) -> Self::Signature { - // Use the certification bytes as the VRF input - let cert_bytes = agg_input.cert_bytes(); - let h = Point::hash_to_curve(H::default().add(&cert_bytes[..])).normalize(); - let vrf = SimpleVrf::::default().with_name("chilldkg-vrf"); - vrf.prove(keypair, h) - } - - fn verify_cert( - &self, - cert_key: Point, - agg_input: &encpedpop::AggKeygenInput, - signature: &Self::Signature, - ) -> Option { - // Use the certification bytes as the VRF input - let cert_bytes = agg_input.cert_bytes(); - let h = Point::hash_to_curve(H::default().add(&cert_bytes[..])).normalize(); - let vrf = SimpleVrf::::default().with_name("chilldkg-vrf"); - vrf.verify(cert_key, h, signature).map(|output| VrfOutput { - gamma: output.dangerously_access_gamma(), - }) - } - } -} - -/// A party that generates secret input to the key generation. You need at least one of these -/// and if at least one of these parties is honest then the final secret key will not be known by an -/// attacker (unless they obtain `t` shares!). -#[derive(Clone, Debug, PartialEq)] -pub struct Contributor { - inner: encpedpop::Contributor, -} - /// Produced by [`Contributor::gen_keygen_input`]. This is sent from the each /// `Contributor` to the *coordinator*. pub type KeygenInput = encpedpop::KeygenInput; /// Key generation inputs after being aggregated by the coordinator pub type AggKeygenInput = encpedpop::AggKeygenInput; -/// A certificate containing signatures or proofs from certifying parties + +pub use encpedpop::Coordinator; + +/// A party that generates secret input to the key generation. You need at least one of these +/// and if at least one of these parties is honest then the final secret key will not be known by an +/// attacker (unless they obtain `t` shares!). #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))] #[cfg_attr( @@ -148,49 +41,8 @@ pub type AggKeygenInput = encpedpop::AggKeygenInput; derive(crate::fun::serde::Deserialize, crate::fun::serde::Serialize), serde(crate = "crate::fun::serde") )] -pub struct Certificate(BTreeMap); - -/// A certificate containing signatures or proofs from certifying parties -impl Default for Certificate { - fn default() -> Self { - Self(BTreeMap::new()) - } -} - -impl Certificate { - /// Create a new empty certificate - pub fn new() -> Self { - Self::default() - } - - /// Insert a signature/proof for a public key - pub fn insert(&mut self, key: Point, sig: Sig) { - if let Some(existing) = self.0.get(&key) { - assert_eq!(existing, &sig, "certification should not change"); - } - self.0.insert(key, sig); - } - - /// Get the signature/proof for a public key - pub fn get(&self, key: &Point) -> Option<&Sig> { - self.0.get(key) - } - - /// Iterate over all entries in the certificate - pub fn iter(&self) -> impl Iterator { - self.0.iter() - } - - /// The number of certificates stored - pub fn len(&self) -> usize { - self.0.len() - } - - /// Have any certificates been collected - pub fn is_empty(&self) -> bool { - // clippy::len_without_is_empty wanted this method - self.0.is_empty() - } +pub struct Contributor { + inner: encpedpop::Contributor, } impl Contributor { @@ -233,106 +85,38 @@ impl Contributor { let sig = cert_scheme.certify(cert_keypair, agg_keygen_input); Ok(sig) } -} - -/// A key generation session that has been certified by each certifying party (contributors and share receivers). -#[derive(Clone, Debug, PartialEq)] -pub struct CertifiedKeygen { - /// The aggregated inputs to keygen - pub input: AggKeygenInput, - /// The collected certificates from each party - pub certificate: Certificate, - /// The outputs from successful verification, indexed by certifying party's public key - pub outputs: BTreeMap, -} -impl CertifiedKeygen { - /// Recover a share from a certified key generation with the decryption key. + /// For parties that are both contributors and receivers: verify contribution, + /// receive secret share, and certify with a single keypair. /// - /// This checks that the `keypair` has signed the key generation first. - pub fn recover_share( - &self, + /// This method is used when a party acts as both a contributor (providing entropy) + /// and a receiver (getting a secret share), using the same keypair for both + /// encryption/decryption and certification. + pub fn verify_receive_share_and_certify( + self, + pop_schnorr: &Schnorr, cert_scheme: &S, share_index: ShareIndex, - keypair: KeyPair, - ) -> Result { - let cert_key = keypair.public_key(); - let my_cert = self - .certificate - .get(&cert_key) - .ok_or("I haven't certified this keygen")?; - // We may have gotten this certificate from *somewhere* so must verify we certified it - if cert_scheme - .verify_cert(cert_key, &self.input, my_cert) - .is_none() - { - return Err("my certification was invalid"); - } - self.input.recover_share::(share_index, &keypair) - } - - /// Gets the inner `encpedpop::AggKeygenInput`. - pub fn inner(&self) -> &AggKeygenInput { - &self.input - } - - /// Gets the certificate. - pub fn certificate(&self) -> &Certificate { - &self.certificate - } + keypair: &KeyPair, + agg_input: &AggKeygenInput, + ) -> Result<(PairedSecretShare, S::Signature), CombinedRoleError> { + // First verify my contribution was included + self.inner + .verify_agg_input(agg_input) + .map_err(|_| CombinedRoleError::ContributionDidntMatch)?; - /// Gets the verification outputs. - pub fn outputs(&self) -> &BTreeMap { - &self.outputs - } -} + // Then receive my secret share + let paired_secret_share = + encpedpop::receive_secret_share(pop_schnorr, share_index, keypair, agg_input) + .map_err(CombinedRoleError::ReceiveShareError)?; -#[cfg(feature = "vrf_cert_keygen")] -impl CertifiedKeygen> -where - H: Hash32 + secp256kfun::digest::crypto_common::BlockSizeUser + Clone + Tag, - H: secp256kfun::digest::crypto_common::BlockSizeUser< - BlockSize = secp256kfun::digest::typenum::U64, - >, -{ - /// Compute a randomness beacon from the VRF outputs - /// - /// This function hashes all the VRF gamma points together to produce - /// unpredictable randomness that no single party could have controlled - /// (as long as at least one party is honest). - /// - /// ## Use for Manual Verification - /// - /// In settings where participants must manually verify the keygen succeeded - /// and there's no trusted public key infrastructure, the randomness beacon - /// serves as a compact fingerprint of the entire protocol execution. - /// - /// Participants can verify they all have the same view of the protocol by - /// comparing just a few bytes of the beacon (e.g., the first 4 bytes shown - /// on device screens). This works because: - /// - /// 1. Each honest participant verifies their VRF contribution is included - /// 2. VRFs are deterministic - malicious parties cannot adapt their - /// contribution after seeing honest contributions - /// 3. No party can predict the final beacon value before the protocol runs - /// - /// This prevents malicious parties from giving different participants - /// different views of the keygen outcome without detection, achieving similar - /// security to comparing a full 32-byte hash but with better usability. - pub fn compute_randomness_beacon(&self) -> [u8; 32] { - let mut hasher = H::default(); - - // BTreeMap already maintains sorted order by key - for output in self.outputs.values() { - hasher.update(output.gamma.to_bytes().as_ref()); - } + // Finally certify the result + let sig = cert_scheme.certify(keypair, agg_input); - hasher.finalize_fixed().into() + Ok((paired_secret_share, sig)) } } -pub use encpedpop::Coordinator; - /// Stores the state of share recipient who first receives their share and then waits to get /// signatures from all the certifying parties on the keygeneration before accepting it. #[derive(Debug, Clone, PartialEq)] @@ -376,10 +160,9 @@ impl SecretShareReceiver { pub fn finalize( self, cert_scheme: &S, - certificate: Certificate, + certificate: BTreeMap, contributor_keys: &[Point], - ) -> Result<(CertifiedKeygen, PairedSecretShare), CertificateError> { - let mut outputs = BTreeMap::new(); + ) -> Result, CertificateError> { let cert_keys = self .agg_input .encryption_keys() @@ -389,45 +172,70 @@ impl SecretShareReceiver { for cert_key in cert_keys { match certificate.get(&cert_key) { - Some(sig) => match cert_scheme.verify_cert(cert_key, &self.agg_input, sig) { - Some(output) => { - outputs.insert(cert_key, output); + Some(sig) => { + if !cert_scheme.verify_cert(cert_key, &self.agg_input, sig) { + return Err(CertificateError::InvalidCert { key: cert_key }); } - None => return Err(CertificateError::InvalidCert { key: cert_key }), - }, + } None => return Err(CertificateError::Missing { key: cert_key }), } } - let certified_keygen = CertifiedKeygen { - input: self.agg_input, - certificate, - outputs, - }; + let certified_keygen = CertifiedKeygen::new(self.agg_input, certificate); - Ok((certified_keygen, self.paired_secret_share)) + Ok(CertifiedSecretShare { + certified_keygen, + paired_share: self.paired_secret_share, + }) + } +} + +/// Errors that can occur when a party acts as both contributor and receiver +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum CombinedRoleError { + /// The contribution we provided was not included correctly + ContributionDidntMatch, + /// Could not receive the secret share + ReceiveShareError(simplepedpop::ReceiveShareError), +} + +impl core::fmt::Display for CombinedRoleError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + CombinedRoleError::ContributionDidntMatch => { + write!(f, "contribution was not included correctly") + } + CombinedRoleError::ReceiveShareError(e) => write!(f, "failed to receive share: {}", e), + } } } +#[cfg(feature = "std")] +impl std::error::Error for CombinedRoleError {} + /// Simulate running a key generation with `certpedpop`. /// /// This calls all the other functions defined in this module to get the whole job done on a /// single computer by simulating all the other parties. /// /// A fingerprint can be provided to grind into the polynomial coefficients. -pub fn simulate_keygen( +/// +/// Returns: +/// - The `CertifiedKeygen` containing the aggregated keygen input and all certificates +/// - A vector of paired secret shares along with their corresponding keypairs. The keypairs +/// are returned so that callers can test the `recover_share` functionality, which allows +/// parties to recover their share from the certified keygen using their keypair (verifying +/// both that they certified the keygen and can decrypt their share). +pub fn simulate_keygen( schnorr: &Schnorr, - cert_scheme: &S, + cert_scheme: S, threshold: u32, n_receivers: u32, n_extra_generators: u32, fingerprint: Fingerprint, rng: &mut impl rand_core::RngCore, -) -> ( - CertifiedKeygen, - Vec<(PairedSecretShare, KeyPair)>, -) { - let mut receiver_enckeys = (1..=n_receivers) +) -> SimulatedKeygenOutput { + let receiver_enckeys = (1..=n_receivers) .map(|i| { let party_index = Scalar::from(i).non_zero().unwrap(); (party_index, KeyPair::new(Scalar::random(rng))) @@ -439,7 +247,6 @@ pub fn simulate_keygen( .map(|(party_index, enckeypair)| (*party_index, enckeypair.public_key())) .collect::>(); - // Total number of generators is receivers + extra generators let n_generators = n_receivers + n_extra_generators; // Generate keypairs for contributors - receivers will use their existing keypairs @@ -448,10 +255,10 @@ pub fn simulate_keygen( let party_index = Scalar::from(i).non_zero().unwrap(); receiver_enckeys[&party_index] }) - .chain(core::iter::repeat_n( - KeyPair::new(Scalar::random(rng)), - n_extra_generators as _, - )) + .chain( + core::iter::repeat_with(|| KeyPair::new(Scalar::random(rng))) + .take(n_extra_generators as _), + ) .collect(); let (contributors, to_coordinator_messages): (Vec, Vec) = (0 @@ -475,92 +282,83 @@ pub fn simulate_keygen( } let mut agg_input = aggregator.finish().unwrap(); - - // Apply fingerprint grinding agg_input.grind_fingerprint::(fingerprint); - let mut certificate = Certificate::new(); - for (contributor, keypair) in contributors.into_iter().zip(contributor_keys.iter()) { - let sig = contributor - .verify_agg_input(cert_scheme, &agg_input, keypair) - .unwrap(); - certificate.insert(keypair.public_key(), sig); - } + // Create a Certifier to validate certificates as they're received + let mut certifier = Certifier::new( + cert_scheme.clone(), + agg_input.clone(), + &contributor_public_keys, + ); let mut paired_secret_shares = vec![]; - let mut share_receivers = vec![]; - for (party_index, enckey) in &receiver_enckeys { - let (share_receiver, cert) = SecretShareReceiver::receive_secret_share( - schnorr, - cert_scheme, - *party_index, - enckey, - &agg_input, - ) - .unwrap(); - certificate.insert(enckey.public_key(), cert); - share_receivers.push(share_receiver); - } - // Collect outputs by verifying all certificates - let mut outputs = BTreeMap::new(); - for (key, sig) in certificate.iter() { - if let Some(output) = cert_scheme.verify_cert(*key, &agg_input, sig) { - outputs.insert(*key, output); - } - } + // Handle parties that are both contributors and receivers using the combined API + for (i, (party_index, enckey)) in receiver_enckeys + .iter() + .enumerate() + .take(n_receivers as usize) + { + // This party is both a contributor and receiver - use combined method + let (paired_secret_share, sig) = contributors[i] + .clone() + .verify_receive_share_and_certify( + schnorr, + &cert_scheme, + *party_index, + enckey, + &agg_input, + ) + .unwrap(); - let certified_keygen = CertifiedKeygen { - input: agg_input.clone(), - certificate: certificate.clone(), - outputs, - }; + // Only one certificate for this dual-role party + certifier + .receive_certificate(enckey.public_key(), sig) + .unwrap(); + + paired_secret_shares.push((paired_secret_share.non_zero().unwrap(), *enckey)); + } - for share_receiver in share_receivers { - let (_certified, paired_secret_share) = share_receiver - .finalize(cert_scheme, certificate.clone(), &contributor_public_keys) + // Handle extra contributors that are only contributors (not receivers) + for i in n_receivers as usize..n_generators as usize { + let sig = contributors[i] + .clone() + .verify_agg_input(&cert_scheme, &agg_input, &contributor_keys[i]) + .unwrap(); + certifier + .receive_certificate(contributor_keys[i].public_key(), sig) .unwrap(); - paired_secret_shares.push(( - paired_secret_share.non_zero().unwrap(), - receiver_enckeys - .remove(&paired_secret_share.index()) - .unwrap(), - )); } - (certified_keygen, paired_secret_shares) -} + // Finish certification and get the CertifiedKeygen + let certified_keygen = certifier + .finish() + .expect("Certifier should have all required certificates"); -/// There was a problem with the keygen certificate so the key generation can't be trusted. -#[derive(Clone, Debug, Copy, PartialEq)] -pub enum CertificateError { - /// A certificate was invalid - InvalidCert { - /// The key that had the invalid cert - key: Point, - }, - /// A certificate was missing - Missing { - /// They key whose cert was missing - key: Point, - }, + SimulatedKeygenOutput { + certified_keygen, + paired_shares_with_keys: paired_secret_shares, + contributor_public_keys, + } } -impl core::fmt::Display for CertificateError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - CertificateError::InvalidCert { key } => { - write!(f, "certificate for key {key} was invalid") - } - CertificateError::Missing { key } => { - write!(f, "certificate for key {key} was missing") - } - } - } +/// The result of finalizing a share receiver's key generation +pub struct CertifiedSecretShare { + /// The certified keygen containing the shared key and certificates + pub certified_keygen: CertifiedKeygen, + /// The secret share for this receiver + pub paired_share: PairedSecretShare, } -#[cfg(feature = "std")] -impl std::error::Error for CertificateError {} +/// The result of simulating a complete key generation ceremony +pub struct SimulatedKeygenOutput { + /// The certified keygen containing the shared key and certificates + pub certified_keygen: CertifiedKeygen, + /// All paired shares with their corresponding keypairs + pub paired_shares_with_keys: Vec<(PairedSecretShare, KeyPair)>, + /// The public keys of all contributors + pub contributor_public_keys: Vec, +} #[cfg(test)] mod test { @@ -582,9 +380,9 @@ mod test { let schnorr = crate::new_with_deterministic_nonces::(); let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); - let (certified_keygen, paired_secret_shares_and_keys) = certpedpop::simulate_keygen( - &schnorr, + let output = certpedpop::simulate_keygen( &schnorr, + schnorr.clone(), threshold, n_receivers, n_extra_generators, @@ -592,47 +390,63 @@ mod test { &mut rng ); - for (paired_secret_share, keypair) in paired_secret_shares_and_keys { - let recovered = certified_keygen.recover_share::(&schnorr, paired_secret_share.index(), keypair).unwrap(); + // Verify the certified keygen is valid + output.certified_keygen.verify(schnorr.clone(), &output.contributor_public_keys).expect("CertifiedKeygen should be valid"); + + for (paired_secret_share, keypair) in output.paired_shares_with_keys { + let recovered = output.certified_keygen.recover_share::(&schnorr, paired_secret_share.index(), keypair).unwrap(); assert_eq!(paired_secret_share, recovered); } + + // Verify we have the expected number of VRF certificates + assert_eq!( + output.certified_keygen.certificate().len(), + (n_receivers + n_extra_generators) as usize + ); + } } - #[test] - #[cfg(feature = "vrf_cert_keygen")] - fn vrf_certified_keygen_randomness_beacon() { - use proptest::test_runner::{RngAlgorithm, TestRng}; + proptest! { + #[test] + #[cfg(feature = "vrf_cert_keygen")] + fn vrf_certified_keygen_randomness_beacon( + (n_receivers, threshold) in (1u32..=4).prop_flat_map(|n| (Just(n), 1u32..=n)), + n_extra_generators in 0u32..=3, + ) { + use proptest::test_runner::{RngAlgorithm, TestRng}; - let schnorr = crate::new_with_deterministic_nonces::(); - let vrf_certifier = vrf_cert::VrfCertifier::::default(); - let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); + let schnorr = crate::new_with_deterministic_nonces::(); + let vrf_certifier = vrf_cert::VrfCertScheme::::new("chilldkg-vrf"); + let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); - let threshold = 2; - let n_receivers = 3; - let n_extra_generators = 0; // All receivers are also generators + let output = certpedpop::simulate_keygen( + &schnorr, + vrf_certifier.clone(), + threshold, + n_receivers, + n_extra_generators, + Fingerprint::NONE, + &mut rng, + ); - let (certified_keygen, _) = certpedpop::simulate_keygen( - &schnorr, - &vrf_certifier, - threshold, - n_receivers, - n_extra_generators, - Fingerprint::NONE, - &mut rng, - ); + // Verify the certified keygen is valid + output.certified_keygen + .verify(vrf_certifier, &output.contributor_public_keys) + .expect("CertifiedKeygen should be valid"); - // Compute randomness beacon from the VRF outputs - let randomness = certified_keygen.compute_randomness_beacon(); + // Compute randomness beacon from the VRF outputs + let randomness = output.certified_keygen.compute_randomness_beacon(sha2::Sha256::default()); - // Verify the randomness is deterministic - let randomness2 = certified_keygen.compute_randomness_beacon(); - assert_eq!(randomness, randomness2); + // Verify the randomness is deterministic + let randomness2 = output.certified_keygen.compute_randomness_beacon(sha2::Sha256::default()); + assert_eq!(randomness, randomness2); - // Verify we have the expected number of VRF outputs - assert_eq!( - certified_keygen.outputs().len(), - (n_receivers + n_extra_generators) as usize - ); + // Verify we have the expected number of VRF certificates + assert_eq!( + output.certified_keygen.certificate().len(), + (n_receivers + n_extra_generators) as usize + ); + } } } diff --git a/schnorr_fun/src/frost/chilldkg/certpedpop/certificate.rs b/schnorr_fun/src/frost/chilldkg/certpedpop/certificate.rs new file mode 100644 index 00000000..7e5a651d --- /dev/null +++ b/schnorr_fun/src/frost/chilldkg/certpedpop/certificate.rs @@ -0,0 +1,395 @@ +//! Certificate types and certification schemes for ChillDKG +//! +//! This module contains: +//! - Certificate: A collection of certification signatures +//! - CertifiedKeygen: The result of a successfully certified key generation +//! - CertificationScheme: Trait for certification methods +//! - Certifier: A stateful validator that checks certificates as they are received + +use super::{AggKeygenInput, encpedpop}; +use crate::{Schnorr, frost::*}; +use alloc::collections::{BTreeMap, BTreeSet}; +use secp256kfun::{hash::*, prelude::*}; + +/// A trait for different ways of certifying the aggregated keygen input in certpedpop. +pub trait CertificationScheme { + /// The signature type produced by this scheme + type Signature: Clone + core::fmt::Debug + PartialEq; + + /// Sign the AggKeygenInput with the given keypair + fn certify(&self, keypair: &KeyPair, agg_input: &encpedpop::AggKeygenInput) -> Self::Signature; + + /// Verify a certification signature + fn verify_cert( + &self, + cert_key: Point, + agg_input: &encpedpop::AggKeygenInput, + signature: &Self::Signature, + ) -> bool; +} + +/// Standard Schnorr (BIP340) implementation of the CertificationScheme trait +impl CertificationScheme for Schnorr { + type Signature = crate::Signature; + + fn certify(&self, keypair: &KeyPair, agg_input: &encpedpop::AggKeygenInput) -> Self::Signature { + let cert_bytes = agg_input.cert_bytes(); + let message = crate::Message::new("BIP DKG/cert", cert_bytes.as_ref()); + let keypair_even_y = (*keypair).into(); + self.sign(&keypair_even_y, message) + } + + fn verify_cert( + &self, + cert_key: Point, + agg_input: &encpedpop::AggKeygenInput, + signature: &Self::Signature, + ) -> bool { + let cert_bytes = agg_input.cert_bytes(); + let message = crate::Message::new("BIP DKG/cert", cert_bytes.as_ref()); + let cert_key_even_y = cert_key.into_point_with_even_y().0; + self.verify(&cert_key_even_y, message, signature) + } +} + +/// The result of a certified key generation +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))] +#[cfg_attr( + feature = "serde", + derive(crate::fun::serde::Deserialize, crate::fun::serde::Serialize), + serde(crate = "crate::fun::serde") +)] +pub struct CertifiedKeygen { + /// The aggregated inputs to keygen + input: AggKeygenInput, + /// The collected certificates from each party + certificate: BTreeMap, +} + +impl CertifiedKeygen { + /// Internal constructor for use within the crate. + pub(crate) fn new(input: AggKeygenInput, certificate: BTreeMap) -> Self { + Self { input, certificate } + } + + /// Verify that all certificates are valid for the given certification scheme and contributor keys. + /// + /// This type should normally be impossible to construct in an invalid state through the API, + /// but since it supports serialization, this method allows validating instances that have been + /// deserialized or received from untrusted sources. + pub fn verify( + &self, + cert_scheme: S, + contributor_keys: &[Point], + ) -> Result<(), CertifierError> + where + S: CertificationScheme, + Sig: Clone, + { + let mut certifier = Certifier::new(cert_scheme, self.input.clone(), contributor_keys); + + // Add all certificates to the certifier + for (key, sig) in &self.certificate { + certifier.receive_certificate(*key, sig.clone())?; + } + + // Check if all required certificates are present + if !certifier.is_finished() { + return Err(CertifierError::IncompleteCertificates); + } + + Ok(()) + } + + /// Recover a share from a certified key generation with the decryption key. + /// + /// This checks that the `keypair` has signed the key generation first. + pub fn recover_share>( + &self, + cert_scheme: &S, + share_index: ShareIndex, + keypair: KeyPair, + ) -> Result { + let cert_key = keypair.public_key(); + let my_cert = self + .certificate + .get(&cert_key) + .ok_or("I haven't certified this keygen")?; + // We may have gotten this certificate from *somewhere* so must verify we certified it + if !cert_scheme.verify_cert(cert_key, &self.input, my_cert) { + return Err("my certification was invalid"); + } + self.input.recover_share::(share_index, &keypair) + } + + /// Gets the aggregated keygen input. + pub fn agg_input(&self) -> &AggKeygenInput { + &self.input + } + + /// Gets the certificate. + pub fn certificate(&self) -> &BTreeMap { + &self.certificate + } +} + +/// There was a problem with the keygen certificate so the key generation can't be trusted. +#[derive(Clone, Debug, Copy, PartialEq)] +pub enum CertificateError { + /// A certificate was invalid + InvalidCert { + /// The key that had the invalid cert + key: Point, + }, + /// A certificate was missing + Missing { + /// They key whose cert was missing + key: Point, + }, +} + +impl core::fmt::Display for CertificateError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + CertificateError::InvalidCert { key } => { + write!(f, "certificate for key {key} was invalid") + } + CertificateError::Missing { key } => { + write!(f, "certificate for key {key} was missing") + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for CertificateError {} + +/// VRF-based implementation of CertificationScheme +#[cfg(feature = "vrf_cert_keygen")] +pub mod vrf_cert { + use super::*; + use secp256kfun::digest::typenum::U32; + use secp256kfun::{digest::core_api::BlockSizeUser, hash::HashAdd}; + use vrf_fun::{SimpleVrf, VrfProof}; + + /// Type alias for VRF proofs used in certification + pub type CertVrfProof = VrfProof; + + /// VRF certification scheme using SimpleVrf + #[derive(Clone, Debug, PartialEq)] + pub struct VrfCertScheme { + name: &'static str, + _hash: core::marker::PhantomData, + } + + impl VrfCertScheme { + /// Create a new VRF certification scheme with a domain separator name. + pub fn new(name: &'static str) -> Self { + Self { + name, + _hash: core::marker::PhantomData, + } + } + } + + /// Implement CertificationScheme for VrfCertScheme + impl CertificationScheme for VrfCertScheme + where + H: Hash32, // This constraint ensures the hash has a 512-bit block size (required for HashTranscript) + H: BlockSizeUser, + { + type Signature = CertVrfProof; + + fn certify( + &self, + keypair: &KeyPair, + agg_input: &encpedpop::AggKeygenInput, + ) -> Self::Signature { + // Use the certification bytes as the VRF input + let cert_bytes = agg_input.cert_bytes(); + let h = + Point::hash_to_curve(H::default().ds(self.name).add(&cert_bytes[..])).normalize(); + let vrf = SimpleVrf::::default().with_name(self.name); + vrf.prove(keypair, h) + } + + fn verify_cert( + &self, + cert_key: Point, + agg_input: &encpedpop::AggKeygenInput, + signature: &Self::Signature, + ) -> bool { + // Use the certification bytes as the VRF input + let cert_bytes = agg_input.cert_bytes(); + let h = + Point::hash_to_curve(H::default().ds(self.name).add(&cert_bytes[..])).normalize(); + let vrf = SimpleVrf::::default().with_name(self.name); + vrf.verify(cert_key, h, signature).is_some() + } + } + + impl super::CertifiedKeygen { + /// Compute a randomness beacon from the VRF outputs + /// + /// This function hashes all the VRF gamma points together to produce + /// unpredictable randomness that no single party could have controlled + /// (as long as at least one party is honest). + /// + /// ## Use for Manual Verification + /// + /// In settings where participants must manually verify the keygen succeeded + /// and there's no trusted public key infrastructure, the randomness beacon + /// serves as a compact fingerprint of the entire protocol execution. + /// + /// Participants can verify they all have the same view of the protocol by + /// comparing just a few bytes of the beacon (e.g., the first 4 bytes shown + /// on device screens). + /// + /// ## Security + /// + /// This is secure because from the view of the honest party the + /// certpedpop acts as a secure coin tossing protocol, where if no + /// parties controlled my the adversary **do not** abort the output will + /// be uniformly distributed. Observe that: + /// + /// 1. The malicious party must commit to all the VRF public keys up front. + /// 2. The honest party verifies its contribution to the keygen is included (which are always rampled randomly) + /// 3. The VRF is over the transcript and every transcript with the honest party can never happen twice (because of #2). + /// 4. The honest party's VRF output will be both hidden and uniformly distributed. + pub fn compute_randomness_beacon(&self, hasher: impl Hash32) -> [u8; 32] { + // BTreeMap already maintains sorted order by key + let mut hasher = hasher; + for vrf_proof in self.certificate.values() { + let gamma = vrf_proof.dangerously_access_gamma_without_verifying(); + hasher.update(gamma.to_bytes().as_ref()); + } + hasher.finalize_fixed().into() + } + } +} + +/// A certifier that validates certificates as they are received +pub struct Certifier { + cert_scheme: S, + agg_input: encpedpop::AggKeygenInput, + required_keys: BTreeSet, + certificates: BTreeMap, +} + +impl Certifier { + /// Create a new certifier that expects certificates from contributors and receivers + pub fn new( + cert_scheme: S, + agg_input: encpedpop::AggKeygenInput, + contributor_keys: &[Point], + ) -> Self { + // Collect all expected keys - deduplicate since some parties may be both contributors and receivers + let mut required_keys = BTreeSet::new(); + + // Add contributor certification keys + for key in contributor_keys { + required_keys.insert(*key); + } + + // Add receiver encryption keys from the agg_input + for (_, encryption_key) in agg_input.encryption_keys() { + required_keys.insert(encryption_key); + } + + Self { + cert_scheme, + agg_input, + required_keys, + certificates: BTreeMap::new(), + } + } + + /// Receive and validate a certificate from a party + pub fn receive_certificate( + &mut self, + from: Point, + signature: S::Signature, + ) -> Result<(), CertifierError> { + // Check if we're expecting this party + if !self.required_keys.contains(&from) { + return Err(CertifierError::UnknownParty); + } + + // Check for duplicate - if we already have a cert from this key, it must be identical + if let Some(existing_sig) = self.certificates.get(&from) { + debug_assert_eq!( + existing_sig, &signature, + "Conflicting certificates from same party" + ); + // Same signature, this is fine - party is certifying multiple times with same key + return Ok(()); + } + + // Verify the certificate + if !self + .cert_scheme + .verify_cert(from, &self.agg_input, &signature) + { + return Err(CertifierError::InvalidSignature); + } + + // Store the validated certificate + self.certificates.insert(from, signature); + + Ok(()) + } + + /// Check if all required certificates have been received + pub fn is_finished(&self) -> bool { + self.certificates.len() == self.required_keys.len() + } + + /// Get the number of certificates still needed + pub fn missing_count(&self) -> usize { + self.required_keys + .len() + .saturating_sub(self.certificates.len()) + } + + /// Get the number of required keys + pub fn required_count(&self) -> usize { + self.required_keys.len() + } + + /// Finish certification and return the certified keygen + pub fn finish(self) -> Result, CertifierError> { + if !self.is_finished() { + return Err(CertifierError::IncompleteCertificates); + } + + Ok(CertifiedKeygen::new(self.agg_input, self.certificates)) + } +} + +/// Errors that can occur during certificate validation +#[derive(Debug, Clone)] +pub enum CertifierError { + /// Party is not in the expected keyset + UnknownParty, + /// Certificate already received from this party + DuplicateCertificate, + /// Certificate signature is invalid + InvalidSignature, + /// Not all required certificates have been received + IncompleteCertificates, +} + +#[cfg(feature = "std")] +impl std::error::Error for CertifierError {} + +impl core::fmt::Display for CertifierError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + CertifierError::UnknownParty => write!(f, "Certificate from unknown party"), + CertifierError::DuplicateCertificate => write!(f, "Duplicate certificate"), + CertifierError::InvalidSignature => write!(f, "Invalid certificate signature"), + CertifierError::IncompleteCertificates => write!(f, "Not all certificates received"), + } + } +} diff --git a/secp256kfun/src/hash.rs b/secp256kfun/src/hash.rs index b564af1e..238790c5 100644 --- a/secp256kfun/src/hash.rs +++ b/secp256kfun/src/hash.rs @@ -163,6 +163,35 @@ impl HashInto for alloc::collections::BTreeSet { pub trait HashAdd { /// Converts something that implements [`HashInto`] to bytes and then incorporate the result into the digest (`self`). fn add(self, data: HI) -> Self; + + /// Adds a domain separator to the hash. This works to make sure the results + /// of whatever you are hashing is different from other contexts. You should + /// put this at the start of the hash. + /// + /// ## Panics + /// + /// If the length of `domain_separator` is greater than 255. + fn ds(self, domain_separator: &'static str) -> Self + where + Self: Sized, + { + self.add(domain_separator.len() as u8).add(domain_separator) + } + + /// Adds a list of static domain separators. This works to make sure the + /// results of whatever you are hashing is different from other contexts. + /// You should put this at the start of the hash. + /// + /// ## Panics + /// + /// If the total byte length of the `separators` is greater than 255. + fn ds_vectored(self, separators: &[&'static str]) -> Self + where + Self: Sized, + { + let total_len: usize = separators.iter().map(|sep| sep.len()).sum(); + self.add(total_len as u8).add(separators) + } } impl HashAdd for D { diff --git a/vrf_fun/src/vrf.rs b/vrf_fun/src/vrf.rs index 9331a411..60a68283 100644 --- a/vrf_fun/src/vrf.rs +++ b/vrf_fun/src/vrf.rs @@ -41,9 +41,38 @@ where /// /// After verification, use the `HashInto` implementation on `VerifiedRandomOutput` /// to safely extract randomness. - pub gamma: Point, + gamma: Point, /// The proof that `gamma` is correct. - pub proof: CompactProof, L>, + proof: CompactProof, L>, +} + +impl VrfProof +where + L: ArrayLength, +{ + /// Create a new VrfProof from its components. + /// + /// This is primarily for testing purposes. In production, proofs should + /// be created through the VRF prove method. + pub fn from_parts(gamma: Point, proof: CompactProof, L>) -> Self { + Self { gamma, proof } + } + + /// Access the gamma point without verifying the proof. + /// + /// # Security Warning + /// + /// This method accesses gamma WITHOUT verifying the proof is valid. You MUST + /// have already verified this proof before using this method. Additionally, + /// the gamma point should be hashed before being used. + /// + /// This method exists for cases where the proof has already been verified + /// and stored. + /// + /// If you haven't verified the proof, use the `Vrf::verify` method instead. + pub fn dangerously_access_gamma_without_verifying(&self) -> Point { + self.gamma + } } /// Verified random output that ensures gamma has been verified @@ -70,11 +99,11 @@ impl VerifiedRandomOutput { /// properties. The paper notes that "the VRF output is the hash of the unique point /// on the curve" to ensure proper domain separation and pseudorandomness. /// - /// **You should use the `HashInto` implementation instead**, which properly hashes - /// gamma to produce secure randomness: + /// **You should use the `HashInto` implementation instead** which allows + /// you to put it into a hash. e.g. /// /// ```ignore - /// use sha2::Sha256; + /// # use sha2::Sha256; /// let randomness = Sha256::default().add(&verified_output).finalize_fixed(); /// ``` pub fn dangerously_access_gamma(&self) -> Point { diff --git a/vrf_fun/tests/proptest_tests.rs b/vrf_fun/tests/proptest_tests.rs index 12e87ecf..f0ba86af 100644 --- a/vrf_fun/tests/proptest_tests.rs +++ b/vrf_fun/tests/proptest_tests.rs @@ -23,7 +23,7 @@ proptest! { // Test deterministic output let proof1_again = rfc9381::tai::prove::(&keypair, &alpha1); - assert_eq!(proof1.gamma, proof1_again.gamma, "Gamma should be deterministic"); + assert_eq!(proof1.dangerously_access_gamma_without_verifying(), proof1_again.dangerously_access_gamma_without_verifying(), "Gamma should be deterministic"); let verified1_again = rfc9381::tai::verify::(keypair.public_key(), &alpha1, &proof1_again) .expect("Proof should verify again"); assert_eq!( @@ -83,7 +83,7 @@ proptest! { // Test deterministic output let proof1_again = rfc9381::sswu::prove::(&keypair, &alpha1); - assert_eq!(proof1.gamma, proof1_again.gamma, "Gamma should be deterministic"); + assert_eq!(proof1.dangerously_access_gamma_without_verifying(), proof1_again.dangerously_access_gamma_without_verifying(), "Gamma should be deterministic"); let verified1_again = rfc9381::sswu::verify::(keypair.public_key(), &alpha1, &proof1_again) .expect("Proof should verify again"); assert_eq!( @@ -143,11 +143,11 @@ proptest! { // Test basic verify let verified1 = vrf.verify(keypair.public_key(), h1, &proof1) .expect("Proof should verify with correct public key"); - assert_eq!(proof1.gamma, verified1.dangerously_access_gamma()); + assert_eq!(proof1.dangerously_access_gamma_without_verifying(), verified1.dangerously_access_gamma()); // Test deterministic output let proof1_again = vrf.prove(&keypair, h1); - assert_eq!(proof1.gamma, proof1_again.gamma, "Gamma should be deterministic"); + assert_eq!(proof1.dangerously_access_gamma_without_verifying(), proof1_again.dangerously_access_gamma_without_verifying(), "Gamma should be deterministic"); // Test wrong public key let wrong_keypair = KeyPair::new(Scalar::random(&mut rand::thread_rng())); diff --git a/vrf_fun/tests/rfc9381_test_vectors.rs b/vrf_fun/tests/rfc9381_test_vectors.rs index cc9863c7..95a31762 100644 --- a/vrf_fun/tests/rfc9381_test_vectors.rs +++ b/vrf_fun/tests/rfc9381_test_vectors.rs @@ -92,13 +92,13 @@ fn verify_tai_test_vector(tv: &TestVector) { let response = Scalar::from_bytes_mod_order(response_bytes); // Construct proof - let proof = VrfProof { + let proof = VrfProof::from_parts( gamma, - proof: CompactProof { + CompactProof { challenge, response, }, - }; + ); // Verify proof using high-level API let verified = rfc9381::tai::verify::(keypair.public_key(), tv.alpha, &proof) @@ -111,7 +111,10 @@ fn verify_tai_test_vector(tv: &TestVector) { // Also test proving with the same inputs let proof_generated = rfc9381::tai::prove::(&keypair, tv.alpha); - assert_eq!(proof_generated.gamma, gamma); + assert_eq!( + proof_generated.dangerously_access_gamma_without_verifying(), + gamma + ); // The challenge and response will be different due to different nonce generation, // but the proof should still verify @@ -194,13 +197,13 @@ fn verify_sswu_test_vector(tv: &TestVector) { let response = Scalar::from_bytes_mod_order(response_bytes); // Construct proof - let proof = VrfProof { + let proof = VrfProof::from_parts( gamma, - proof: CompactProof { + CompactProof { challenge, response, }, - }; + ); // Verify proof using high-level API let verified = rfc9381::sswu::verify::(keypair.public_key(), tv.alpha, &proof) @@ -213,7 +216,10 @@ fn verify_sswu_test_vector(tv: &TestVector) { // Also test proving with the same inputs let proof_generated = rfc9381::sswu::prove::(&keypair, tv.alpha); - assert_eq!(proof_generated.gamma, gamma); + assert_eq!( + proof_generated.dangerously_access_gamma_without_verifying(), + gamma + ); // The challenge and response will be different due to different nonce generation, // but the proof should still verify From fefd9d09ea93b34b168de3d09328f8b13da4c027 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Fri, 5 Sep 2025 13:25:13 +1000 Subject: [PATCH 3/4] =?UTF-8?q?[=E2=9D=84]=20PartialEq=20for=20Certifier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../frost/chilldkg/certpedpop/certificate.rs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/schnorr_fun/src/frost/chilldkg/certpedpop/certificate.rs b/schnorr_fun/src/frost/chilldkg/certpedpop/certificate.rs index 7e5a651d..a9c02431 100644 --- a/schnorr_fun/src/frost/chilldkg/certpedpop/certificate.rs +++ b/schnorr_fun/src/frost/chilldkg/certpedpop/certificate.rs @@ -177,12 +177,18 @@ pub mod vrf_cert { pub type CertVrfProof = VrfProof; /// VRF certification scheme using SimpleVrf - #[derive(Clone, Debug, PartialEq)] + #[derive(Clone, Debug)] pub struct VrfCertScheme { name: &'static str, _hash: core::marker::PhantomData, } + impl PartialEq for VrfCertScheme { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } + } + impl VrfCertScheme { /// Create a new VRF certification scheme with a domain separator name. pub fn new(name: &'static str) -> Self { @@ -270,6 +276,7 @@ pub mod vrf_cert { } /// A certifier that validates certificates as they are received +#[derive(Clone, Debug, PartialEq)] pub struct Certifier { cert_scheme: S, agg_input: encpedpop::AggKeygenInput, @@ -393,3 +400,19 @@ impl core::fmt::Display for CertifierError { } } } + +#[cfg(test)] +mod test { + #[test] + #[cfg(feature = "vrf_cert_keygen")] + fn test_certifier_with_vrf_cert_scheme_is_partial_eq() { + use super::*; + use sha2::Sha256; + + // Function that requires T to implement PartialEq + fn assert_partial_eq() {} + + // This will only compile if Certifier> implements PartialEq + assert_partial_eq::>>(); + } +} From decebe3bf43fd40e4c0b8497f8ec3d46b23f100c Mon Sep 17 00:00:00 2001 From: LLFourn Date: Fri, 5 Sep 2025 17:55:25 +1000 Subject: [PATCH 4/4] =?UTF-8?q?[=E2=9D=84]=20s/compute=5Frandomness=5Fbeac?= =?UTF-8?q?on/vrf=5Fsecurity=5Fcheck?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit compute_randomness_beacon was too abstract. --- schnorr_fun/src/frost/chilldkg/certpedpop.rs | 6 ++--- .../frost/chilldkg/certpedpop/certificate.rs | 23 +++++++++++-------- schnorr_fun/src/frost/chilldkg/encpedpop.rs | 2 +- .../src/frost/chilldkg/simplepedpop.rs | 4 ++-- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/schnorr_fun/src/frost/chilldkg/certpedpop.rs b/schnorr_fun/src/frost/chilldkg/certpedpop.rs index c2504634..97316ef2 100644 --- a/schnorr_fun/src/frost/chilldkg/certpedpop.rs +++ b/schnorr_fun/src/frost/chilldkg/certpedpop.rs @@ -168,7 +168,7 @@ impl SecretShareReceiver { .encryption_keys() .map(|(_, encryption_key)| encryption_key) .chain(contributor_keys.iter().cloned()) - .collect::>(); // dedupe as some contributers may have also be receivers + .collect::>(); // dedupe as some contributors may also be receivers for cert_key in cert_keys { match certificate.get(&cert_key) { @@ -436,10 +436,10 @@ mod test { .expect("CertifiedKeygen should be valid"); // Compute randomness beacon from the VRF outputs - let randomness = output.certified_keygen.compute_randomness_beacon(sha2::Sha256::default()); + let randomness = output.certified_keygen.vrf_security_check(sha2::Sha256::default()); // Verify the randomness is deterministic - let randomness2 = output.certified_keygen.compute_randomness_beacon(sha2::Sha256::default()); + let randomness2 = output.certified_keygen.vrf_security_check(sha2::Sha256::default()); assert_eq!(randomness, randomness2); // Verify we have the expected number of VRF certificates diff --git a/schnorr_fun/src/frost/chilldkg/certpedpop/certificate.rs b/schnorr_fun/src/frost/chilldkg/certpedpop/certificate.rs index a9c02431..5354c625 100644 --- a/schnorr_fun/src/frost/chilldkg/certpedpop/certificate.rs +++ b/schnorr_fun/src/frost/chilldkg/certpedpop/certificate.rs @@ -254,18 +254,23 @@ pub mod vrf_cert { /// /// ## Security /// - /// This is secure because from the view of the honest party the - /// certpedpop acts as a secure coin tossing protocol, where if no - /// parties controlled my the adversary **do not** abort the output will - /// be uniformly distributed. Observe that: + /// If no parties controlled by the adversary abort, the output will + /// be uniformly distributed. The VRF outputs effectively act as a "randomness beacon" - + /// a source of verifiable randomness that all parties can compute deterministically + /// from the certificates. Observe that: /// /// 1. The malicious party must commit to all the VRF public keys up front. - /// 2. The honest party verifies its contribution to the keygen is included (which are always rampled randomly) - /// 3. The VRF is over the transcript and every transcript with the honest party can never happen twice (because of #2). + /// 2. The honest party verifies its contribution to the keygen is included (which are always sampled randomly) + /// 3. The VRF is over the transcript and every transcript with an honest party will always be unique (because of #2). /// 4. The honest party's VRF output will be both hidden and uniformly distributed. - pub fn compute_randomness_beacon(&self, hasher: impl Hash32) -> [u8; 32] { - // BTreeMap already maintains sorted order by key - let mut hasher = hasher; + /// 5. All honest parties with the same `AggKeygenInput::cert_bytes` will output the same check + /// 6. All honest parties with a different `AggKeygenInput::cert_bytes` are statistically likely to output different bytes. + /// + /// This check is *statistically* secure -- per keygen the attacker only + /// has 1/2ⁿ chance of succeeding to collide the checks where `n` is the + /// number of bits the honest parties check among each other. **It is up + /// to the application to limit the number of attempts the adversary can make.** + pub fn vrf_security_check(&self, mut hasher: impl Hash32) -> [u8; 32] { for vrf_proof in self.certificate.values() { let gamma = vrf_proof.dangerously_access_gamma_without_verifying(); hasher.update(gamma.to_bytes().as_ref()); diff --git a/schnorr_fun/src/frost/chilldkg/encpedpop.rs b/schnorr_fun/src/frost/chilldkg/encpedpop.rs index 6740008f..5b01b5ab 100644 --- a/schnorr_fun/src/frost/chilldkg/encpedpop.rs +++ b/schnorr_fun/src/frost/chilldkg/encpedpop.rs @@ -42,7 +42,7 @@ impl Contributor { /// has nothing to do with the "receiver" index (the `ShareIndex` of share receivers). If /// there are `n` `KeyGenInputParty`s then each party must be assigned an index from `0` to `n-1`. /// - /// This method return `Self` to retain the state of the protocol which is needded to verify + /// This method returns `Self` to retain the state of the protocol which is needed to verify /// the aggregated input later on. pub fn gen_keygen_input( schnorr: &Schnorr, diff --git a/schnorr_fun/src/frost/chilldkg/simplepedpop.rs b/schnorr_fun/src/frost/chilldkg/simplepedpop.rs index d28891bd..4537aefc 100644 --- a/schnorr_fun/src/frost/chilldkg/simplepedpop.rs +++ b/schnorr_fun/src/frost/chilldkg/simplepedpop.rs @@ -35,7 +35,7 @@ impl Contributor { /// has nothing to do with the "receiver" index (the `ShareIndex` of share receivers). If /// there are `n` `KeyGenInputParty`s then each party must be assigned an index from `0` to `n-1`. /// - /// This method return `Self` to retain the state of the protocol which is needded to verify + /// This method returns `Self` to retain the state of the protocol which is needed to verify /// the aggregated input later on. pub fn gen_keygen_input( schnorr: &Schnorr, @@ -50,7 +50,7 @@ impl Contributor { { let secret_poly = poly::scalar::generate(threshold as usize, rng); let pop_keypair = KeyPair::new_xonly(secret_poly[0]); - // XXX The thing that's singed differs from the spec + // XXX The thing that's signed differs from the spec let pop = schnorr.sign(&pop_keypair, Message::empty()); let com = poly::scalar::to_point_poly(&secret_poly);