Skip to content

Commit 86b2213

Browse files
authored
Use simple vrf (#236)
* [vrf] Encourage hashing the output Because security proofs require this. * [❄] improve saftey and ergonomics of certpedpop 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. * [❄] PartialEq for Certifier * [❄] s/compute_randomness_beacon/vrf_security_check compute_randomness_beacon was too abstract.
1 parent 5ae2b35 commit 86b2213

File tree

10 files changed

+835
-414
lines changed

10 files changed

+835
-414
lines changed

schnorr_fun/src/frost/chilldkg/certpedpop.rs

Lines changed: 199 additions & 373 deletions
Large diffs are not rendered by default.

schnorr_fun/src/frost/chilldkg/certpedpop/certificate.rs

Lines changed: 423 additions & 0 deletions
Large diffs are not rendered by default.

schnorr_fun/src/frost/chilldkg/encpedpop.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ impl Contributor {
4242
/// has nothing to do with the "receiver" index (the `ShareIndex` of share receivers). If
4343
/// there are `n` `KeyGenInputParty`s then each party must be assigned an index from `0` to `n-1`.
4444
///
45-
/// This method return `Self` to retain the state of the protocol which is needded to verify
45+
/// This method returns `Self` to retain the state of the protocol which is needed to verify
4646
/// the aggregated input later on.
4747
pub fn gen_keygen_input<H, NG>(
4848
schnorr: &Schnorr<H, NG>,

schnorr_fun/src/frost/chilldkg/simplepedpop.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ impl Contributor {
3535
/// has nothing to do with the "receiver" index (the `ShareIndex` of share receivers). If
3636
/// there are `n` `KeyGenInputParty`s then each party must be assigned an index from `0` to `n-1`.
3737
///
38-
/// This method return `Self` to retain the state of the protocol which is needded to verify
38+
/// This method returns `Self` to retain the state of the protocol which is needed to verify
3939
/// the aggregated input later on.
4040
pub fn gen_keygen_input<H, NG>(
4141
schnorr: &Schnorr<H, NG>,
@@ -50,7 +50,7 @@ impl Contributor {
5050
{
5151
let secret_poly = poly::scalar::generate(threshold as usize, rng);
5252
let pop_keypair = KeyPair::new_xonly(secret_poly[0]);
53-
// XXX The thing that's singed differs from the spec
53+
// XXX The thing that's signed differs from the spec
5454
let pop = schnorr.sign(&pop_keypair, Message::empty());
5555
let com = poly::scalar::to_point_poly(&secret_poly);
5656

secp256kfun/src/hash.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,35 @@ impl<T: HashInto + Ord> HashInto for alloc::collections::BTreeSet<T> {
163163
pub trait HashAdd {
164164
/// Converts something that implements [`HashInto`] to bytes and then incorporate the result into the digest (`self`).
165165
fn add<HI: HashInto>(self, data: HI) -> Self;
166+
167+
/// Adds a domain separator to the hash. This works to make sure the results
168+
/// of whatever you are hashing is different from other contexts. You should
169+
/// put this at the start of the hash.
170+
///
171+
/// ## Panics
172+
///
173+
/// If the length of `domain_separator` is greater than 255.
174+
fn ds(self, domain_separator: &'static str) -> Self
175+
where
176+
Self: Sized,
177+
{
178+
self.add(domain_separator.len() as u8).add(domain_separator)
179+
}
180+
181+
/// Adds a list of static domain separators. This works to make sure the
182+
/// results of whatever you are hashing is different from other contexts.
183+
/// You should put this at the start of the hash.
184+
///
185+
/// ## Panics
186+
///
187+
/// If the total byte length of the `separators` is greater than 255.
188+
fn ds_vectored(self, separators: &[&'static str]) -> Self
189+
where
190+
Self: Sized,
191+
{
192+
let total_len: usize = separators.iter().map(|sep| sep.len()).sum();
193+
self.add(total_len as u8).add(separators)
194+
}
166195
}
167196

168197
impl<D: digest::Update> HashAdd for D {

vrf_fun/src/lib.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,59 @@ use sigma_fun::{
2222
pub type VrfDleq<ChallengeLength> = Eq<DLG<ChallengeLength>, DL<ChallengeLength>>;
2323

2424
/// Simple VRF using HashTranscript with 32-byte challenges
25+
///
26+
/// This provides a straightforward VRF implementation using the standard
27+
/// HashTranscript from sigma_fun. It produces 32-byte proofs.
28+
///
29+
/// # Example
30+
///
31+
/// ```
32+
/// use secp256kfun::{KeyPair, Scalar, prelude::*};
33+
/// use secp256kfun::hash::HashAdd;
34+
/// use secp256kfun::digest::Digest;
35+
/// use vrf_fun::SimpleVrf;
36+
/// use sha2::Sha256;
37+
/// use rand::thread_rng;
38+
///
39+
/// // Generate a keypair
40+
/// let keypair = KeyPair::new(Scalar::random(&mut thread_rng()));
41+
///
42+
/// // Create the VRF instance
43+
/// let vrf = SimpleVrf::<Sha256>::default();
44+
///
45+
/// // Hash input data to a curve point
46+
/// let hasher = Sha256::default().add(b"my-input-data");
47+
/// let h = Point::hash_to_curve(hasher).normalize();
48+
///
49+
/// // Generate proof
50+
/// let proof = vrf.prove(&keypair, h);
51+
///
52+
/// // Verify proof
53+
/// let verified = vrf.verify(keypair.public_key(), h, &proof)
54+
/// .expect("proof should verify");
55+
///
56+
/// // The verified output contains a gamma point that can be hashed
57+
/// // to produce deterministic randomness
58+
/// let output_bytes = Sha256::default()
59+
/// .add(verified)
60+
/// .finalize();
61+
/// ```
62+
///
63+
/// # Domain Separation
64+
///
65+
/// You can set a custom name for domain separation using `with_name`:
66+
///
67+
/// ```
68+
/// # use secp256kfun::{KeyPair, Scalar, hash::{Hash32, HashAdd}, prelude::*};
69+
/// # use vrf_fun::SimpleVrf;
70+
/// # use sha2::Sha256;
71+
/// # use rand::thread_rng;
72+
/// # let keypair = KeyPair::new(Scalar::random(&mut thread_rng()));
73+
/// # let hasher = Sha256::default().add(b"my-input-data");
74+
/// # let h = Point::hash_to_curve(hasher).normalize();
75+
/// let vrf = SimpleVrf::<Sha256>::default().with_name("my-app-vrf");
76+
/// let proof = vrf.prove(&keypair, h);
77+
/// ```
2578
pub type SimpleVrf<H> = Vrf<HashTranscript<H, ChaCha20Rng>, U32>;
2679

2780
/// Re-export the [RFC 9381] type aliases

vrf_fun/src/rfc9381.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,11 @@ pub mod tai {
178178
/// Compute [RFC 9381] compliant output with TAI suite string (0xFE)
179179
///
180180
/// [RFC 9381]: https://datatracker.ietf.org/doc/html/rfc9381
181-
pub fn output<H: Hash32>(verified: &VerifiedRandomOutput) -> [u8; 32] {
181+
pub fn output<H: Hash32>(verified: VerifiedRandomOutput) -> [u8; 32] {
182182
H::default()
183183
.add([SUITE_STRING_TAI])
184184
.add(0x03u8) // Hash mode domain separator
185-
.add(verified.gamma.to_bytes())
185+
.add(verified)
186186
.add(0x00u8) // Hash mode trailer
187187
.finalize_fixed()
188188
.into()
@@ -281,11 +281,11 @@ pub mod sswu {
281281
///
282282
/// [RFC 9381]: https://datatracker.ietf.org/doc/html/rfc9381
283283
/// [RFC 9380]: https://datatracker.ietf.org/doc/html/rfc9380
284-
pub fn output<H: Hash32>(verified: &VerifiedRandomOutput) -> [u8; 32] {
284+
pub fn output<H: Hash32>(verified: VerifiedRandomOutput) -> [u8; 32] {
285285
H::default()
286286
.add([SUITE_STRING_RFC9380])
287287
.add(0x03u8) // Hash mode domain separator
288-
.add(verified.gamma.to_bytes())
288+
.add(verified)
289289
.add(0x00u8) // Hash mode trailer
290290
.finalize_fixed()
291291
.into()

vrf_fun/src/vrf.rs

Lines changed: 93 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Generic VRF implementation that can work with different transcript types
22
3-
use secp256kfun::{KeyPair, Scalar, prelude::*};
3+
use secp256kfun::{KeyPair, Scalar, hash::HashInto, prelude::*};
44
use sigma_fun::{
55
CompactProof, FiatShamir, ProverTranscript, Transcript,
66
generic_array::{
@@ -32,24 +32,96 @@ pub struct VrfProof<L = U16>
3232
where
3333
L: ArrayLength<u8>,
3434
{
35-
/// The VRF output point.
35+
/// The VRF output point (gamma).
3636
///
37-
/// Usually you don't use this directly but hash it.
38-
pub gamma: Point,
37+
/// **Security Warning**: According to VRF security proofs (see
38+
/// ["Making NSEC5 Practical for DNSSEC"](https://eprint.iacr.org/2017/099.pdf)),
39+
/// this point must be hashed before being used as randomness. Direct use of gamma
40+
/// may compromise the pseudorandomness properties of the VRF.
41+
///
42+
/// After verification, use the `HashInto` implementation on `VerifiedRandomOutput`
43+
/// to safely extract randomness.
44+
gamma: Point,
3945
/// The proof that `gamma` is correct.
40-
pub proof: CompactProof<Scalar<Public, Zero>, L>,
46+
proof: CompactProof<Scalar<Public, Zero>, L>,
47+
}
48+
49+
impl<L> VrfProof<L>
50+
where
51+
L: ArrayLength<u8>,
52+
{
53+
/// Create a new VrfProof from its components.
54+
///
55+
/// This is primarily for testing purposes. In production, proofs should
56+
/// be created through the VRF prove method.
57+
pub fn from_parts(gamma: Point, proof: CompactProof<Scalar<Public, Zero>, L>) -> Self {
58+
Self { gamma, proof }
59+
}
60+
61+
/// Access the gamma point without verifying the proof.
62+
///
63+
/// # Security Warning
64+
///
65+
/// This method accesses gamma WITHOUT verifying the proof is valid. You MUST
66+
/// have already verified this proof before using this method. Additionally,
67+
/// the gamma point should be hashed before being used.
68+
///
69+
/// This method exists for cases where the proof has already been verified
70+
/// and stored.
71+
///
72+
/// If you haven't verified the proof, use the `Vrf::verify` method instead.
73+
pub fn dangerously_access_gamma_without_verifying(&self) -> Point {
74+
self.gamma
75+
}
4176
}
4277

4378
/// Verified random output that ensures gamma has been verified
44-
#[derive(Debug, Clone)]
79+
///
80+
/// The gamma point is kept private to enforce proper usage. VRF security proofs
81+
/// require hashing gamma before use as randomness. Use the `HashInto` implementation
82+
/// to safely extract randomness from this output.
83+
#[derive(Debug, Clone, Copy)]
4584
pub struct VerifiedRandomOutput {
46-
pub gamma: Point,
85+
gamma: Point,
86+
}
87+
88+
impl VerifiedRandomOutput {
89+
/// Access the raw gamma point directly.
90+
///
91+
/// # Security Warning
92+
///
93+
/// The VRF security proofs require that gamma be hashed before being used as randomness.
94+
/// Using the gamma point directly without hashing may compromise the pseudorandomness
95+
/// properties of the VRF.
96+
///
97+
/// According to ["Making NSEC5 Practical for DNSSEC"](https://eprint.iacr.org/2017/099.pdf),
98+
/// the VRF output must be the hash of gamma, not gamma itself, to maintain security
99+
/// properties. The paper notes that "the VRF output is the hash of the unique point
100+
/// on the curve" to ensure proper domain separation and pseudorandomness.
101+
///
102+
/// **You should use the `HashInto` implementation instead** which allows
103+
/// you to put it into a hash. e.g.
104+
///
105+
/// ```ignore
106+
/// # use sha2::Sha256;
107+
/// let randomness = Sha256::default().add(&verified_output).finalize_fixed();
108+
/// ```
109+
pub fn dangerously_access_gamma(&self) -> Point {
110+
self.gamma
111+
}
112+
}
113+
114+
impl HashInto for VerifiedRandomOutput {
115+
fn hash_into(self, hash: &mut impl secp256kfun::digest::Update) {
116+
self.gamma.hash_into(hash)
117+
}
47118
}
48119

49120
/// Generic VRF implementation
50121
pub struct Vrf<T, ChallengeLength = U16> {
51122
dleq: crate::VrfDleq<ChallengeLength>,
52123
pub transcript: T,
124+
name: Option<&'static str>,
53125
}
54126

55127
impl<T: Clone, ChallengeLength> Vrf<T, ChallengeLength>
@@ -64,8 +136,20 @@ where
64136
Self {
65137
dleq: Eq::new(DLG::default(), DL::default()),
66138
transcript,
139+
name: None,
67140
}
68141
}
142+
143+
/// Set a custom name for domain separation
144+
///
145+
/// The name is used in the Fiat-Shamir transform to provide domain separation.
146+
///
147+
/// Note: For RFC 9381 VRFs, setting a name has no effect as they use their own
148+
/// transcript mechanism that doesn't support custom names.
149+
pub fn with_name(mut self, name: &'static str) -> Self {
150+
self.name = Some(name);
151+
self
152+
}
69153
}
70154

71155
impl<T: Clone + Default, ChallengeLength> Default for Vrf<T, ChallengeLength>
@@ -92,7 +176,7 @@ where
92176
{
93177
let (secret_key, public_key) = keypair.as_tuple();
94178
let gamma = g!(secret_key * h).normalize();
95-
let fs = FiatShamir::new(self.dleq.clone(), self.transcript.clone(), None);
179+
let fs = FiatShamir::new(self.dleq.clone(), self.transcript.clone(), self.name);
96180
let witness = secret_key;
97181
let statement = (public_key, (h, gamma));
98182
let proof = fs.prove::<R>(&witness, &statement, None);
@@ -106,7 +190,7 @@ where
106190
h: Point,
107191
proof: &VrfProof<ChallengeLength>,
108192
) -> Option<VerifiedRandomOutput> {
109-
let fs = FiatShamir::new(self.dleq.clone(), self.transcript.clone(), None);
193+
let fs = FiatShamir::new(self.dleq.clone(), self.transcript.clone(), self.name);
110194
let statement = (public_key.normalize(), (h, proof.gamma));
111195

112196
if !fs.verify(&statement, &proof.proof) {

vrf_fun/tests/proptest_tests.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ proptest! {
2323

2424
// Test deterministic output
2525
let proof1_again = rfc9381::tai::prove::<sha2::Sha256>(&keypair, &alpha1);
26-
assert_eq!(proof1.gamma, proof1_again.gamma, "Gamma should be deterministic");
26+
assert_eq!(proof1.dangerously_access_gamma_without_verifying(), proof1_again.dangerously_access_gamma_without_verifying(), "Gamma should be deterministic");
2727
let verified1_again = rfc9381::tai::verify::<sha2::Sha256>(keypair.public_key(), &alpha1, &proof1_again)
2828
.expect("Proof should verify again");
2929
assert_eq!(
30-
rfc9381::tai::output::<sha2::Sha256>(&verified1),
31-
rfc9381::tai::output::<sha2::Sha256>(&verified1_again),
30+
rfc9381::tai::output::<sha2::Sha256>(verified1),
31+
rfc9381::tai::output::<sha2::Sha256>(verified1_again),
3232
"VRF output should be deterministic"
3333
);
3434

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

4848
assert_ne!(
49-
rfc9381::tai::output::<sha2::Sha256>(&verified1),
50-
rfc9381::tai::output::<sha2::Sha256>(&verified2),
49+
rfc9381::tai::output::<sha2::Sha256>(verified1),
50+
rfc9381::tai::output::<sha2::Sha256>(verified2),
5151
"Different inputs should produce different outputs"
5252
);
5353

@@ -83,12 +83,12 @@ proptest! {
8383

8484
// Test deterministic output
8585
let proof1_again = rfc9381::sswu::prove::<sha2::Sha256>(&keypair, &alpha1);
86-
assert_eq!(proof1.gamma, proof1_again.gamma, "Gamma should be deterministic");
86+
assert_eq!(proof1.dangerously_access_gamma_without_verifying(), proof1_again.dangerously_access_gamma_without_verifying(), "Gamma should be deterministic");
8787
let verified1_again = rfc9381::sswu::verify::<sha2::Sha256>(keypair.public_key(), &alpha1, &proof1_again)
8888
.expect("Proof should verify again");
8989
assert_eq!(
90-
rfc9381::sswu::output::<sha2::Sha256>(&verified1),
91-
rfc9381::sswu::output::<sha2::Sha256>(&verified1_again),
90+
rfc9381::sswu::output::<sha2::Sha256>(verified1),
91+
rfc9381::sswu::output::<sha2::Sha256>(verified1_again),
9292
"VRF output should be deterministic"
9393
);
9494

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

108108
assert_ne!(
109-
rfc9381::sswu::output::<sha2::Sha256>(&verified1),
110-
rfc9381::sswu::output::<sha2::Sha256>(&verified2),
109+
rfc9381::sswu::output::<sha2::Sha256>(verified1),
110+
rfc9381::sswu::output::<sha2::Sha256>(verified2),
111111
"Different inputs should produce different outputs"
112112
);
113113

@@ -143,11 +143,11 @@ proptest! {
143143
// Test basic verify
144144
let verified1 = vrf.verify(keypair.public_key(), h1, &proof1)
145145
.expect("Proof should verify with correct public key");
146-
assert_eq!(proof1.gamma, verified1.gamma);
146+
assert_eq!(proof1.dangerously_access_gamma_without_verifying(), verified1.dangerously_access_gamma());
147147

148148
// Test deterministic output
149149
let proof1_again = vrf.prove(&keypair, h1);
150-
assert_eq!(proof1.gamma, proof1_again.gamma, "Gamma should be deterministic");
150+
assert_eq!(proof1.dangerously_access_gamma_without_verifying(), proof1_again.dangerously_access_gamma_without_verifying(), "Gamma should be deterministic");
151151

152152
// Test wrong public key
153153
let wrong_keypair = KeyPair::new(Scalar::random(&mut rand::thread_rng()));
@@ -170,7 +170,7 @@ proptest! {
170170
let verified2 = vrf.verify(keypair.public_key(), h2, &proof2)
171171
.expect("Second proof should verify");
172172

173-
assert_ne!(verified1.gamma, verified2.gamma, "Different inputs should produce different gamma");
173+
assert_ne!(verified1.dangerously_access_gamma(), verified2.dangerously_access_gamma(), "Different inputs should produce different gamma");
174174

175175
// Cross-verification should fail
176176
assert!(

0 commit comments

Comments
 (0)