Skip to content

Commit

Permalink
Add PRF evaluation circuit (#833)
Browse files Browse the repository at this point in the history
Evaluating OPRF requires using a new field, so this change adds a bunch of integration with curve-25519

---------

Co-authored-by: Daniel Masny <[email protected]>
Co-authored-by: Alex Koshelev <[email protected]>
Co-authored-by: Alex Koshelev <[email protected]>
  • Loading branch information
4 people authored Nov 6, 2023
1 parent 57c3e2f commit c5b59f8
Show file tree
Hide file tree
Showing 20 changed files with 798 additions and 54 deletions.
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ default = [
"tracing/max_level_trace",
"tracing/release_max_level_info",
"descriptive-gate",
"aggregate-circuit"
"aggregate-circuit",
"ipa-prf"
]
cli = ["comfy-table", "clap"]
enable-serde = ["serde", "serde_json"]
Expand Down Expand Up @@ -41,6 +42,8 @@ compact-gate = ["ipa-macros/compact-gate"]
# Standalone aggregation protocol. We use IPA infra for communication
# but it has nothing to do with IPA.
aggregate-circuit = []
# IPA protocol based on OPRF
ipa-prf = []

[dependencies]
aes = "0.8.3"
Expand All @@ -54,6 +57,7 @@ clap = { version = "4.3.2", optional = true, features = ["derive"] }
comfy-table = { version = "7.0", optional = true }
config = "0.13.2"
criterion = { version = "0.5.1", optional = true, default-features = false, features = ["async_tokio", "plotters", "html_reports"] }
curve25519-dalek = "4.1.1"
dashmap = "5.4"
dhat = "0.3.2"
embed-doc-image = "0.1.4"
Expand Down
2 changes: 2 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ pub enum Error {
InvalidReport(#[from] InvalidReportError),
#[error("unsupported: {0}")]
Unsupported(String),
#[error("Decompressing invalid elliptic curve point: {0}")]
DecompressingInvalidCurvePoint(String),
}

impl Default for Error {
Expand Down
239 changes: 239 additions & 0 deletions src/ff/curve_points.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
use curve25519_dalek::{
ristretto::{CompressedRistretto, RistrettoPoint},
Scalar,
};
use generic_array::GenericArray;
use typenum::U32;

use crate::{
ff::{ec_prime_field::Fp25519, Serializable},
secret_sharing::{Block, WeakSharedValue},
};

impl Block for CompressedRistretto {
type Size = U32;
}

///ristretto point for curve 25519,
/// we store it in compressed format since it is 3 times smaller and we do a limited amount of
/// arithmetic operations on the curve points
///
/// We use ristretto points such that we have a prime order elliptic curve,
/// This is needed for the Dodis Yampolski PRF
///
/// decompressing invalid curve points will cause panics,
/// since we always generate curve points from scalars (elements in Fp25519) and
/// only deserialize previously serialized valid points, panics will not occur
/// However, we still added a debug assert to deserialize since values are sent by other servers
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct RP25519(CompressedRistretto);

/// Implementing trait for secret sharing
impl WeakSharedValue for RP25519 {
type Storage = CompressedRistretto;
const BITS: u32 = 256;
const ZERO: Self = Self(CompressedRistretto([0_u8; 32]));
}

impl Serializable for RP25519 {
type Size = <<RP25519 as WeakSharedValue>::Storage as Block>::Size;

fn serialize(&self, buf: &mut GenericArray<u8, Self::Size>) {
*buf.as_mut() = self.0.to_bytes();
}

fn deserialize(buf: &GenericArray<u8, Self::Size>) -> Self {
debug_assert!(CompressedRistretto((*buf).into()).decompress().is_some());
RP25519(CompressedRistretto((*buf).into()))
}
}

///## Panics
/// Panics when decompressing invalid curve point. This can happen when deserialize curve point
/// from bit array that does not have a valid representation on the curve
impl std::ops::Add for RP25519 {
type Output = Self;

fn add(self, rhs: Self) -> Self::Output {
Self((self.0.decompress().unwrap() + rhs.0.decompress().unwrap()).compress())
}
}

impl std::ops::AddAssign for RP25519 {
#[allow(clippy::assign_op_pattern)]
fn add_assign(&mut self, rhs: Self) {
*self = *self + rhs;
}
}

///## Panics
/// Panics when decompressing invalid curve point. This can happen when deserialize curve point
/// from bit array that does not have a valid representation on the curve
impl std::ops::Neg for RP25519 {
type Output = Self;

fn neg(self) -> Self::Output {
Self(self.0.decompress().unwrap().neg().compress())
}
}

///## Panics
/// Panics when decompressing invalid curve point. This can happen when deserialize curve point
/// from bit array that does not have a valid representation on the curve
impl std::ops::Sub for RP25519 {
type Output = Self;

fn sub(self, rhs: Self) -> Self::Output {
Self((self.0.decompress().unwrap() - rhs.0.decompress().unwrap()).compress())
}
}

impl std::ops::SubAssign for RP25519 {
#[allow(clippy::assign_op_pattern)]
fn sub_assign(&mut self, rhs: Self) {
*self = *self - rhs;
}
}

///Scalar Multiplication
/// allows to multiply curve points with scalars from Fp25519
///## Panics
/// Panics when decompressing invalid curve point. This can happen when deserialize curve point
/// from bit array that does not have a valid representation on the curve
impl std::ops::Mul<Fp25519> for RP25519 {
type Output = Self;

fn mul(self, rhs: Fp25519) -> RP25519 {
(self.0.decompress().unwrap() * Scalar::from(rhs))
.compress()
.into()
}
}

impl std::ops::MulAssign<Fp25519> for RP25519 {
#[allow(clippy::assign_op_pattern)]
fn mul_assign(&mut self, rhs: Fp25519) {
*self = *self * rhs;
}
}

impl From<Scalar> for RP25519 {
fn from(s: Scalar) -> Self {
RP25519(RistrettoPoint::mul_base(&s).compress())
}
}

impl From<Fp25519> for RP25519 {
fn from(s: Fp25519) -> Self {
RP25519(RistrettoPoint::mul_base(&s.into()).compress())
}
}

impl From<CompressedRistretto> for RP25519 {
fn from(s: CompressedRistretto) -> Self {
RP25519(s)
}
}

impl From<RP25519> for CompressedRistretto {
fn from(s: RP25519) -> Self {
s.0
}
}

///allows to convert curve points into unsigned integers, preserving high entropy
macro_rules! cp_hash_impl {
( $u_type:ty) => {
impl From<RP25519> for $u_type {
fn from(s: RP25519) -> Self {
use hkdf::Hkdf;
use sha2::Sha256;
let hk = Hkdf::<Sha256>::new(None, s.0.as_bytes());
let mut okm = <$u_type>::MIN.to_le_bytes();
//error invalid length from expand only happens when okm is very large
hk.expand(&[], &mut okm).unwrap();
<$u_type>::from_le_bytes(okm)
}
}
};
}

cp_hash_impl!(u128);

cp_hash_impl!(u64);

/// implementing random curve point generation for testing purposes,
/// in the actual IPA protocol, we generate them from scalars, i.e. Fp25519
#[cfg(test)]
impl rand::distributions::Distribution<RP25519> for rand::distributions::Standard {
fn sample<R: crate::rand::Rng + ?Sized>(&self, rng: &mut R) -> RP25519 {
let mut scalar_bytes = [0u8; 64];
rng.fill_bytes(&mut scalar_bytes);
RP25519(RistrettoPoint::from_uniform_bytes(&scalar_bytes).compress())
}
}

#[cfg(all(test, unit_test))]
mod test {
use curve25519_dalek::{constants, scalar::Scalar};
use generic_array::GenericArray;
use rand::{thread_rng, Rng};
use typenum::U32;

use crate::{
ff::{curve_points::RP25519, ec_prime_field::Fp25519, Serializable},
secret_sharing::WeakSharedValue,
};

cp_hash_impl!(u32);

///testing serialize and deserialize
#[test]
fn serde_25519() {
let mut rng = thread_rng();
let input = rng.gen::<RP25519>();
let mut a: GenericArray<u8, U32> = [0u8; 32].into();
input.serialize(&mut a);
let output = RP25519::deserialize(&a);
assert_eq!(input, output);
}

///testing conversion from scalar to Fp25519 and curve point, i.e. RP25519
#[test]
fn scalar_to_point() {
let a = Scalar::ONE;
let b: RP25519 = a.into();
let d: Fp25519 = a.into();
let c: RP25519 = RP25519::from(d);
assert_eq!(b, RP25519(constants::RISTRETTO_BASEPOINT_COMPRESSED));
assert_eq!(c, RP25519(constants::RISTRETTO_BASEPOINT_COMPRESSED));
}

///testing simple curve arithmetics to check that `curve25519_dalek` library is used correctly
#[test]
fn curve_arithmetics() {
let mut rng = thread_rng();
let fp_a = rng.gen::<Fp25519>();
let fp_b = rng.gen::<Fp25519>();
let fp_c = fp_a + fp_b;
let fp_d = RP25519::from(fp_a) + RP25519::from(fp_b);
assert_eq!(fp_d, RP25519::from(fp_c));
assert_ne!(fp_d, RP25519(constants::RISTRETTO_BASEPOINT_COMPRESSED));
let fp_e = rng.gen::<Fp25519>();
let fp_f = rng.gen::<Fp25519>();
let fp_g = fp_e * fp_f;
let fp_h = RP25519::from(fp_e) * fp_f;
assert_eq!(fp_h, RP25519::from(fp_g));
assert_ne!(fp_h, RP25519(constants::RISTRETTO_BASEPOINT_COMPRESSED));
assert_eq!(RP25519::ZERO, fp_h * Scalar::ZERO.into());
}

///testing curve to unsigned integer conversion has entropy (!= 0)
#[test]
fn curve_point_to_hash() {
let mut rng = thread_rng();
let fp_a = rng.gen::<RP25519>();
assert_ne!(0u64, u64::from(fp_a));
assert_ne!(0u32, u32::from(fp_a));
}
}
Loading

0 comments on commit c5b59f8

Please sign in to comment.