Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
572 changes: 199 additions & 373 deletions schnorr_fun/src/frost/chilldkg/certpedpop.rs

Large diffs are not rendered by default.

423 changes: 423 additions & 0 deletions schnorr_fun/src/frost/chilldkg/certpedpop/certificate.rs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion schnorr_fun/src/frost/chilldkg/encpedpop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<H, NG>(
schnorr: &Schnorr<H, NG>,
Expand Down
4 changes: 2 additions & 2 deletions schnorr_fun/src/frost/chilldkg/simplepedpop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<H, NG>(
schnorr: &Schnorr<H, NG>,
Expand All @@ -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);

Expand Down
29 changes: 29 additions & 0 deletions secp256kfun/src/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,35 @@ impl<T: HashInto + Ord> HashInto for alloc::collections::BTreeSet<T> {
pub trait HashAdd {
/// Converts something that implements [`HashInto`] to bytes and then incorporate the result into the digest (`self`).
fn add<HI: HashInto>(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<D: digest::Update> HashAdd for D {
Expand Down
53 changes: 53 additions & 0 deletions vrf_fun/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,59 @@ use sigma_fun::{
pub type VrfDleq<ChallengeLength> = Eq<DLG<ChallengeLength>, DL<ChallengeLength>>;

/// 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::<Sha256>::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::<Sha256>::default().with_name("my-app-vrf");
/// let proof = vrf.prove(&keypair, h);
/// ```
pub type SimpleVrf<H> = Vrf<HashTranscript<H, ChaCha20Rng>, U32>;

/// Re-export the [RFC 9381] type aliases
Expand Down
8 changes: 4 additions & 4 deletions vrf_fun/src/rfc9381.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<H: Hash32>(verified: &VerifiedRandomOutput) -> [u8; 32] {
pub fn output<H: Hash32>(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()
Expand Down Expand Up @@ -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<H: Hash32>(verified: &VerifiedRandomOutput) -> [u8; 32] {
pub fn output<H: Hash32>(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()
Expand Down
102 changes: 93 additions & 9 deletions vrf_fun/src/vrf.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -32,24 +32,96 @@ pub struct VrfProof<L = U16>
where
L: ArrayLength<u8>,
{
/// The VRF output point.
/// The VRF output point (gamma).
///
/// Usually you don't use this directly but hash it.
pub gamma: Point,
/// **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.
gamma: Point,
/// The proof that `gamma` is correct.
pub proof: CompactProof<Scalar<Public, Zero>, L>,
proof: CompactProof<Scalar<Public, Zero>, L>,
}

impl<L> VrfProof<L>
where
L: ArrayLength<u8>,
{
/// 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<Scalar<Public, Zero>, 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
#[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 allows
/// you to put it into a hash. e.g.
///
/// ```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<T, ChallengeLength = U16> {
dleq: crate::VrfDleq<ChallengeLength>,
pub transcript: T,
name: Option<&'static str>,
}

impl<T: Clone, ChallengeLength> Vrf<T, ChallengeLength>
Expand All @@ -64,8 +136,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<T: Clone + Default, ChallengeLength> Default for Vrf<T, ChallengeLength>
Expand All @@ -92,7 +176,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::<R>(&witness, &statement, None);
Expand All @@ -106,7 +190,7 @@ where
h: Point,
proof: &VrfProof<ChallengeLength>,
) -> Option<VerifiedRandomOutput> {
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) {
Expand Down
26 changes: 13 additions & 13 deletions vrf_fun/tests/proptest_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ proptest! {

// Test deterministic output
let proof1_again = rfc9381::tai::prove::<sha2::Sha256>(&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::<sha2::Sha256>(keypair.public_key(), &alpha1, &proof1_again)
.expect("Proof should verify again");
assert_eq!(
rfc9381::tai::output::<sha2::Sha256>(&verified1),
rfc9381::tai::output::<sha2::Sha256>(&verified1_again),
rfc9381::tai::output::<sha2::Sha256>(verified1),
rfc9381::tai::output::<sha2::Sha256>(verified1_again),
"VRF output should be deterministic"
);

Expand All @@ -46,8 +46,8 @@ proptest! {
.expect("Second proof should verify");

assert_ne!(
rfc9381::tai::output::<sha2::Sha256>(&verified1),
rfc9381::tai::output::<sha2::Sha256>(&verified2),
rfc9381::tai::output::<sha2::Sha256>(verified1),
rfc9381::tai::output::<sha2::Sha256>(verified2),
"Different inputs should produce different outputs"
);

Expand Down Expand Up @@ -83,12 +83,12 @@ proptest! {

// Test deterministic output
let proof1_again = rfc9381::sswu::prove::<sha2::Sha256>(&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::<sha2::Sha256>(keypair.public_key(), &alpha1, &proof1_again)
.expect("Proof should verify again");
assert_eq!(
rfc9381::sswu::output::<sha2::Sha256>(&verified1),
rfc9381::sswu::output::<sha2::Sha256>(&verified1_again),
rfc9381::sswu::output::<sha2::Sha256>(verified1),
rfc9381::sswu::output::<sha2::Sha256>(verified1_again),
"VRF output should be deterministic"
);

Expand All @@ -106,8 +106,8 @@ proptest! {
.expect("Second proof should verify");

assert_ne!(
rfc9381::sswu::output::<sha2::Sha256>(&verified1),
rfc9381::sswu::output::<sha2::Sha256>(&verified2),
rfc9381::sswu::output::<sha2::Sha256>(verified1),
rfc9381::sswu::output::<sha2::Sha256>(verified2),
"Different inputs should produce different outputs"
);

Expand Down Expand Up @@ -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.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()));
Expand All @@ -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!(
Expand Down
Loading