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.

100 changes: 64 additions & 36 deletions schnorr_fun/src/frost/chilldkg/encpedpop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
//!
//! [`AggKeygenInput`]: AggKeygenInput
use super::simplepedpop;
use crate::{Message, Schnorr, Signature, frost::*};
use crate::{Schnorr, frost::*};
use alloc::{
collections::{BTreeMap, BTreeSet},
vec::Vec,
Expand All @@ -35,14 +35,15 @@ use secp256kfun::{
)]
pub struct Contributor {
inner: simplepedpop::Contributor,
my_nonce: Point,
}

impl Contributor {
/// Generates the keygen input for a party at `my_index`. Note that `my_index`
/// 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 Expand Up @@ -83,7 +84,13 @@ impl Contributor {
encryption_nonce: multi_nonce_keypair.public_key(),
};

(Contributor { inner: inner_state }, keygen_input)
(
Contributor {
inner: inner_state,
my_nonce: multi_nonce_keypair.public_key(),
},
keygen_input,
)
}

/// Verifies that the coordinator has honestly included this party's input into the
Expand All @@ -96,6 +103,15 @@ impl Contributor {
self,
agg_keygen_input: &AggKeygenInput,
) -> Result<(), simplepedpop::ContributionDidntMatch> {
// check the encryption nonce we provided was still in the
// AggKeygenInput. This may not be necessary for security but we do it
// for completeness.
let my_index = self.inner.contributor_index();
let expected = self.my_nonce;
let got = agg_keygen_input.encryption_nonces[my_index as usize];
if got != expected {
return Err(simplepedpop::ContributionDidntMatch);
}
self.inner.verify_agg_input(&agg_keygen_input.inner)?;
Ok(())
}
Expand Down Expand Up @@ -154,34 +170,6 @@ impl AggKeygenInput {
.map(|(party_index, (ek, _))| (*party_index, *ek))
}

/// Certify the `AggKeygenInput`. If all parties certify this then the keygen was
/// successful.
pub fn certify<H, NG>(&self, schnorr: &Schnorr<H, NG>, keypair: &KeyPair<EvenY>) -> Signature
where
H: Hash32,
NG: NonceGen,
{
schnorr.sign(
keypair,
Message::new("BIP DKG/cert", self.cert_bytes().as_ref()),
)
}

/// Verify that another party has certified the keygen. If you collect certifications from
/// all parties then the keygen was successful
pub fn verify_cert<H: Hash32, NG>(
&self,
schnorr: &Schnorr<H, NG>,
cert_key: Point<EvenY>,
signature: Signature,
) -> bool {
schnorr.verify(
&cert_key,
Message::new("BIP DKG/cert", self.cert_bytes().as_ref()),
&signature,
)
}

/// Recover a share with the decryption key from the `AggKeygenInput`.
pub fn recover_share<H: Hash32>(
&self,
Expand Down Expand Up @@ -303,7 +291,7 @@ impl Coordinator {
Self {
inner: simplepedpop::Coordinator::new(threshold, n_contribtors),
agg_encrypted_shares,
encryption_nonces: Default::default(),
encryption_nonces: vec![Point::default(); n_contribtors as usize],
}
}

Expand Down Expand Up @@ -340,8 +328,7 @@ impl Coordinator {
*agg_encrypted_share += encrypted_share_contrib;
}

self.encryption_nonces.push(input.encryption_nonce);

self.encryption_nonces[from as usize] = input.encryption_nonce;
Ok(())
}

Expand Down Expand Up @@ -508,13 +495,17 @@ where

#[cfg(test)]
mod test {
use crate::frost::{Fingerprint, chilldkg::encpedpop};
use alloc::{collections::BTreeMap, vec::Vec};

use crate::frost::{Fingerprint, ShareIndex, chilldkg::encpedpop};

use proptest::{
prelude::*,
test_runner::{RngAlgorithm, TestRng},
};
use secp256kfun::proptest;
use secp256kfun::{KeyPair, Scalar, proptest};

use super::{Contributor, Coordinator};

proptest! {
#[test]
Expand Down Expand Up @@ -569,4 +560,41 @@ mod test {
assert!(shared_key.check_fingerprint::<sha2::Sha256>(&fingerprint), "fingerprint was grinded correctly");
}
}

#[test]
fn test_input_arrival_order() {
let schnorr = crate::new_with_deterministic_nonces::<sha2::Sha256>();
let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha);
let threshold = 2u32;

let receiver_enckeys = [(
ShareIndex::from(core::num::NonZeroU32::new(1).unwrap()),
KeyPair::new(Scalar::random(&mut rng)).public_key(),
)]
.into_iter()
.collect::<BTreeMap<_, _>>();

let mut coordinator = Coordinator::new(threshold, 3, &receiver_enckeys);

// Create contributors with indices 0, 1, 2
let contributors_and_inputs: Vec<_> = (0..3)
.map(|i| {
Contributor::gen_keygen_input(&schnorr, threshold, &receiver_enckeys, i, &mut rng)
})
.collect();

// Add them to coordinator in different order
let arrival_order = [2, 0, 1];
for &contributor_idx in arrival_order.iter() {
let (_, input) = &contributors_and_inputs[contributor_idx as usize];
coordinator
.add_input(&schnorr, contributor_idx, input.clone())
.unwrap();
}

let agg_input = coordinator.finish().unwrap();

let (contributor_1, _) = &contributors_and_inputs[1];
contributor_1.clone().verify_agg_input(&agg_input).unwrap(); // This should fail
}
}
23 changes: 15 additions & 8 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 Expand Up @@ -87,6 +87,11 @@ impl Contributor {

Ok(())
}

/// Get the index for the contributor
pub fn contributor_index(&self) -> u32 {
self.my_index
}
}

/// Produced by [`Contributor::gen_keygen_input`]. This is sent from the each
Expand Down Expand Up @@ -250,12 +255,14 @@ impl AggKeygenInput {
///
/// In `simplepedpop` this is just the coefficients of the polynomial.
pub fn cert_bytes(&self) -> Vec<u8> {
let mut cert_bytes = vec![];
cert_bytes.extend((self.agg_poly.len() as u32).to_be_bytes());
for coeff in self.shared_key().point_polynomial() {
cert_bytes.extend(coeff.to_bytes());
}
cert_bytes
let shared_key = self.shared_key();
let poly = shared_key.point_polynomial();
let cert_bytes = (poly.len() as u32)
.to_be_bytes()
.into_iter()
.chain(poly.iter().flat_map(|coeff| coeff.to_bytes()));

cert_bytes.collect()
}
}

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
Loading