From 201fb57d9fa065a6358147c489500448ec3f57ba Mon Sep 17 00:00:00 2001 From: UneBaguette <28904802+UneBaguette@users.noreply.github.com> Date: Wed, 8 Jul 2026 23:43:11 +0200 Subject: [PATCH] release: 1.0.0 (#12) - Deduplicate serialization with impl_serde_scalar, impl_serde_elem, and impl_serde_scalar_elem macros - Move finalize_after_unblind to common.rs, shared by OPRF and VOPRF - Add shared test helpers (test_all_curves macro, prf function) - Update dependencies to stable releases Reviewed-on: https://dev.unebaguette.fr/vexahub/voprf-vx/pulls/12 Co-authored-by: UneBaguette <28904802+UneBaguette@users.noreply.github.com> Co-committed-by: UneBaguette <28904802+UneBaguette@users.noreply.github.com> --- CHANGELOG.md | 7 + Cargo.lock | 14 +- Cargo.toml | 10 +- README.md | 2 +- src/common.rs | 28 +++ src/error.rs | 2 +- src/group/ristretto.rs | 41 +++-- src/oprf.rs | 118 ++---------- src/poprf.rs | 48 +---- src/serialization.rs | 405 ++++++++++++++++------------------------- src/tests/helpers.rs | 25 +++ src/tests/macros.rs | 20 ++ src/tests/mod.rs | 5 + src/voprf.rs | 117 ++---------- 14 files changed, 313 insertions(+), 529 deletions(-) create mode 100644 src/tests/helpers.rs create mode 100644 src/tests/macros.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index edce1ce..bf49e4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 1.0.0 (July 8, 2026) + +* Deduplicated serialization with `impl_serde_scalar`, `impl_serde_elem`, and `impl_serde_scalar_elem` macros +* Moved `finalize_after_unblind` to `common.rs`, shared by OPRF and VOPRF +* Added shared test helpers (`test_all_curves` macro, `prf` function) +* Updated dependencies to stable releases + ## 1.0.0-rc.1 (July 3, 2026) * Reject trailing bytes in all `deserialize` methods diff --git a/Cargo.lock b/Cargo.lock index b456215..612d27c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,9 +346,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "memchr" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "num-traits" @@ -737,7 +737,7 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "voprf-vx" -version = "1.0.0-rc.1" +version = "1.0.0" dependencies = [ "curve25519-dalek", "derive-where", @@ -813,18 +813,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.52" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.52" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 9f25c27..ac41a0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ name = "voprf-vx" readme = "README.md" repository = "https://github.com/vexahub/voprf-vx/" rust-version = "1.87" -version = "1.0.0-rc.1" +version = "1.0.0" [features] alloc = [] @@ -22,7 +22,7 @@ serde = ["curve25519-dalek?/serde", "hybrid-array/serde", "dep:serde"] std = ["alloc"] [dependencies] -curve25519-dalek = { version = "5.0.0-rc.1", default-features = false, features = ["rand_core", "zeroize"], optional = true } +curve25519-dalek = { version = "5", default-features = false, features = ["rand_core", "zeroize"], optional = true } derive-where = { version = "1", features = ["zeroize-on-drop"] } digest = { version = "0.11", features = ["zeroize"] } displaydoc = { version = "0.2", default-features = false } @@ -36,17 +36,17 @@ serde = { version = "1", default-features = false, features = [ "derive", ], optional = true } sha2 = { version = "0.11", default-features = false, features = ["zeroize"], optional = true } -p256 = { version = "0.14.0-rc.15", default-features = false, features = ["hash2curve", "oprf"], optional = true } +p256 = { version = "0.14", default-features = false, features = ["hash2curve", "oprf"], optional = true } subtle = { version = "2.6", default-features = false } zeroize = { version = "1.5", default-features = false } [dev-dependencies] hex = "0.4" -p256 = { version = "0.14.0-rc.15", default-features = false, features = [ +p256 = { version = "0.14", default-features = false, features = [ "hash2curve", "oprf", ] } -p384 = { version = "0.14.0-rc.15", default-features = false, features = [ +p384 = { version = "0.14", default-features = false, features = [ "hash2curve", "oprf", ] } diff --git a/README.md b/README.md index b3ddc55..49e05a4 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Installation Add the following line to the dependencies of your `Cargo.toml`: ``` -voprf = { package = "voprf-vx", version = "1.0.0-rc.1" } +voprf-vx = "1.0.0" ``` ### Minimum Supported Rust Version diff --git a/src/common.rs b/src/common.rs index d9a932f..0173cf7 100644 --- a/src/common.rs +++ b/src/common.rs @@ -5,6 +5,7 @@ //! Common functionality between multiple OPRF modes. use core::convert::TryFrom; +use core::iter::Map; use core::ops::Add; use derive_where::derive_where; @@ -449,6 +450,33 @@ pub(crate) fn server_evaluate_hash_input( .finalize()) } +pub(crate) type FinalizeAfterUnblindResult<'a, C, I, IE> = Map< + IE, + fn((I, <::Group as Group>::Elem)) -> Result::Hash>>, +>; + +/// Returned values can only fail with [`Error::Input`]. +pub(crate) fn finalize_after_unblind< + 'a, + CS: CipherSuite, + I: AsRef<[u8]>, + IE: 'a + Iterator::Elem)>, +>( + inputs_and_unblinded_elements: IE, +) -> FinalizeAfterUnblindResult<'a, CS, I, IE> { + inputs_and_unblinded_elements.map(|(input, unblinded_element)| { + let elem_len = ::ElemLen::U16.to_be_bytes(); + + Ok(CS::Hash::new() + .chain_update(i2osp_2(input.as_ref().len()).map_err(|_| Error::Input)?) + .chain_update(input.as_ref()) + .chain_update(elem_len) + .chain_update(CS::Group::serialize_elem(unblinded_element)) + .chain_update(STR_FINALIZE) + .finalize()) + }) +} + pub(crate) struct Dst { dst_1: Array, dst_2: &'static [u8], diff --git a/src/error.rs b/src/error.rs index 6e0238d..a61175c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -18,7 +18,7 @@ pub enum Error { DeriveKeyPair, /// Failure to deserialize bytes Deserialization, - /// Batched items are more then [`u16::MAX`] or length don't match. + /// Batched items are more than [`u16::MAX`] or length don't match. Batch, /// In verifiable mode, occurs when the proof failed to verify ProofVerification, diff --git a/src/group/ristretto.rs b/src/group/ristretto.rs index 5c0b97b..dc5d285 100644 --- a/src/group/ristretto.rs +++ b/src/group/ristretto.rs @@ -54,16 +54,7 @@ impl Group for Ristretto255 { + IsLessOrEqual + IsGreaterOrEqual, Output = True>, { - let mut uniform_bytes = [0u8; 64]; - - as ExpandMsg>::expand_message( - input, - dst, - NonZeroU16::new(64).unwrap(), - ) - .map_err(|_| InternalError::Input)? - .fill_bytes(&mut uniform_bytes) - .map_err(|_| InternalError::Input)?; + let uniform_bytes = expand_uniform_bytes::(input, dst)?; Ok(RistrettoPoint::from_uniform_bytes(&uniform_bytes)) } @@ -77,16 +68,7 @@ impl Group for Ristretto255 { + IsLessOrEqual + IsGreaterOrEqual, Output = True>, { - let mut uniform_bytes = [0u8; 64]; - - as ExpandMsg>::expand_message( - input, - dst, - NonZeroU16::new(64).unwrap(), - ) - .map_err(|_| InternalError::Input)? - .fill_bytes(&mut uniform_bytes) - .map_err(|_| InternalError::Input)?; + let uniform_bytes = expand_uniform_bytes::(input, dst)?; Ok(Scalar::from_bytes_mod_order_wide(&uniform_bytes)) } @@ -150,3 +132,22 @@ impl Group for Ristretto255 { .ok_or(Error::Deserialization) } } + +// HELPERS + +fn expand_uniform_bytes(input: &[&[u8]], dst: &[&[u8]]) -> Result<[u8; 64], InternalError> +where + H: BlockSizeUser + Default + FixedOutput + HashMarker, + H::OutputSize: IsLess + + IsLessOrEqual + + IsGreaterOrEqual, Output = True>, +{ + let mut uniform_bytes = [0u8; 64]; + + as ExpandMsg>::expand_message(input, dst, NonZeroU16::new(64).unwrap()) + .map_err(|_| InternalError::Input)? + .fill_bytes(&mut uniform_bytes) + .map_err(|_| InternalError::Input)?; + + Ok(uniform_bytes) +} diff --git a/src/oprf.rs b/src/oprf.rs index b346835..cd08883 100644 --- a/src/oprf.rs +++ b/src/oprf.rs @@ -4,17 +4,16 @@ //! Contains the main OPRF API -use core::iter::{self, Map}; +use core::iter::{self}; use derive_where::derive_where; -use digest::{Digest, Output}; +use digest::Output; use hybrid_array::Array; -use hybrid_array::typenum::Unsigned; use rand_core::{TryCryptoRng, TryRng}; use crate::common::{ - BlindedElement, EvaluationElement, Mode, STR_FINALIZE, derive_key_internal, - deterministic_blind_unchecked, hash_to_group, i2osp_2, server_evaluate_hash_input, + BlindedElement, EvaluationElement, Mode, derive_key_internal, deterministic_blind_unchecked, + finalize_after_unblind, hash_to_group, server_evaluate_hash_input, }; #[cfg(feature = "serde")] use crate::serialization::serde::Scalar; @@ -120,7 +119,7 @@ impl OprfClient { ) -> Result> { let unblinded_element = evaluation_element.0 * &CS::Group::invert_scalar(self.blind); let mut outputs = - finalize_after_unblind::(iter::once((input, unblinded_element)), &[]); + finalize_after_unblind::(iter::once((input, unblinded_element))); outputs.next().unwrap() } @@ -217,43 +216,6 @@ pub struct OprfClientBlindResult { pub message: BlindedElement, } -///////////////////// -// Inner functions // -// =============== // -///////////////////// - -type FinalizeAfterUnblindResult<'a, C, I, IE> = Map< - IE, - fn((I, <::Group as Group>::Elem)) -> Result::Hash>>, ->; - -/// Returned values can only fail with [`Error::Input`]. -fn finalize_after_unblind< - 'a, - CS: CipherSuite, - I: AsRef<[u8]>, - IE: 'a + Iterator::Elem)>, ->( - inputs_and_unblinded_elements: IE, - _unused: &'a [u8], -) -> FinalizeAfterUnblindResult<'a, CS, I, IE> { - inputs_and_unblinded_elements.map(|(input, unblinded_element)| { - let elem_len = ::ElemLen::U16.to_be_bytes(); - - // hashInput = I2OSP(len(input), 2) || input || - // I2OSP(len(unblindedElement), 2) || unblindedElement || - // "Finalize" - // return Hash(hashInput) - Ok(CS::Hash::new() - .chain_update(i2osp_2(input.as_ref().len()).map_err(|_| Error::Input)?) - .chain_update(input.as_ref()) - .chain_update(elem_len) - .chain_update(CS::Group::serialize_elem(unblinded_element)) - .chain_update(STR_FINALIZE) - .finalize()) - }) -} - /////////// // Tests // // ===== // @@ -269,23 +231,7 @@ mod tests { use super::*; use crate::Group; use crate::common::{Dst, STR_HASH_TO_GROUP}; - - fn prf( - input: &[u8], - key: ::Scalar, - info: &[u8], - mode: Mode, - ) -> Output { - let dst = Dst::new::(STR_HASH_TO_GROUP, mode); - let point = CS::Group::hash_to_curve::(&[input], &dst.as_dst()).unwrap(); - - let res = point * &key; - - finalize_after_unblind::(iter::once((input, res)), info) - .next() - .unwrap() - .unwrap() - } + use crate::tests::helpers::prf; fn base_retrieval() { let input = b"input"; @@ -294,7 +240,7 @@ mod tests { let server = OprfServer::::new(&mut rng).unwrap(); let message = server.blind_evaluate(&client_blind_result.message); let client_finalize_result = client_blind_result.state.finalize(input, &message).unwrap(); - let res2 = prf::(input, server.get_private_key(), &[], Mode::Oprf); + let res2 = prf::(input, server.get_private_key(), Mode::Oprf); assert_eq!(client_finalize_result, res2); } @@ -310,7 +256,7 @@ mod tests { let dst = Dst::new::(STR_HASH_TO_GROUP, Mode::Oprf); let point = CS::Group::hash_to_curve::(&[&input], &dst.as_dst()).unwrap(); - let res2 = finalize_after_unblind::(iter::once((input.as_ref(), point)), &[]) + let res2 = finalize_after_unblind::(iter::once((input.as_ref(), point))) .next() .unwrap() .unwrap(); @@ -371,45 +317,11 @@ mod tests { assert!(message.serialize().iter().all(|&x| x == 0)); } - #[test] - fn test_functionality() -> Result<()> { - use p256::NistP256; - use p384::NistP384; - use p521::NistP521; - - #[cfg(feature = "ristretto255")] - { - use crate::Ristretto255; - - base_retrieval::(); - base_inversion_unsalted::(); - server_evaluate::(); - - zeroize_oprf_client::(); - zeroize_oprf_server::(); - } - - base_retrieval::(); - base_inversion_unsalted::(); - server_evaluate::(); - - zeroize_oprf_client::(); - zeroize_oprf_server::(); - - base_retrieval::(); - base_inversion_unsalted::(); - server_evaluate::(); - - zeroize_oprf_client::(); - zeroize_oprf_server::(); - - base_retrieval::(); - base_inversion_unsalted::(); - server_evaluate::(); - - zeroize_oprf_client::(); - zeroize_oprf_server::(); - - Ok(()) - } + crate::tests::test_all_curves!( + base_retrieval, + base_inversion_unsalted, + server_evaluate, + zeroize_oprf_client, + zeroize_oprf_server, + ); } diff --git a/src/poprf.rs b/src/poprf.rs index 20921c5..88d168a 100644 --- a/src/poprf.rs +++ b/src/poprf.rs @@ -873,45 +873,11 @@ mod tests { assert!(proof.serialize().iter().all(|&x| x == 0)); } - #[test] - fn test_functionality() -> Result<()> { - use p256::NistP256; - use p384::NistP384; - use p521::NistP521; - - #[cfg(feature = "ristretto255")] - { - use crate::Ristretto255; - - verifiable_retrieval::(); - verifiable_bad_public_key::(); - verifiable_server_evaluate::(); - - zeroize_verifiable_client::(); - zeroize_verifiable_server::(); - } - - verifiable_retrieval::(); - verifiable_bad_public_key::(); - verifiable_server_evaluate::(); - - zeroize_verifiable_client::(); - zeroize_verifiable_server::(); - - verifiable_retrieval::(); - verifiable_bad_public_key::(); - verifiable_server_evaluate::(); - - zeroize_verifiable_client::(); - zeroize_verifiable_server::(); - - verifiable_retrieval::(); - verifiable_bad_public_key::(); - verifiable_server_evaluate::(); - - zeroize_verifiable_client::(); - zeroize_verifiable_server::(); - - Ok(()) - } + crate::tests::test_all_curves!( + verifiable_retrieval, + verifiable_bad_public_key, + verifiable_server_evaluate, + zeroize_verifiable_client, + zeroize_verifiable_server, + ); } diff --git a/src/serialization.rs b/src/serialization.rs index 27b2cca..eadac06 100644 --- a/src/serialization.rs +++ b/src/serialization.rs @@ -13,255 +13,10 @@ use crate::{ PoprfClient, PoprfServer, Proof, Result, VoprfClient, VoprfServer, }; -////////////////////////////////////////////////////////// -// Serialization and Deserialization for High-Level API // -// ==================================================== // -////////////////////////////////////////////////////////// - -/// Length of [`OprfClient`] in bytes for serialization. -pub type OprfClientLen = <::Group as Group>::ScalarLen; - -impl OprfClient { - /// Serialization into bytes - pub fn serialize(&self) -> Array> { - CS::Group::serialize_scalar(self.blind) - } - - /// Deserialization from bytes - /// - /// # Errors - /// [`Error::Deserialization`] if failed to deserialize `input`. - pub fn deserialize(mut input: &[u8]) -> Result { - let blind = deserialize_scalar::(&mut input)?; - - if !input.is_empty() { - return Err(Error::Deserialization); - } - - Ok(Self { blind }) - } -} - -/// Length of [`VoprfClient`] in bytes for serialization. -pub type VoprfClientLen = Sum< - <::Group as Group>::ScalarLen, - <::Group as Group>::ElemLen, ->; - -impl VoprfClient { - /// Serialization into bytes - pub fn serialize(&self) -> Array> { - ::serialize_scalar(self.blind) - .concat(::serialize_elem(self.blinded_element)) - } - - /// Deserialization from bytes - /// - /// # Errors - /// [`Error::Deserialization`] if failed to deserialize `input`. - pub fn deserialize(mut input: &[u8]) -> Result { - let blind = deserialize_scalar::(&mut input)?; - let blinded_element = deserialize_elem::(&mut input)?; - - if !input.is_empty() { - return Err(Error::Deserialization); - } - - Ok(Self { - blind, - blinded_element, - }) - } -} - -/// Length of [`PoprfClient`] in bytes for serialization. -pub type PoprfClientLen = Sum< - <::Group as Group>::ScalarLen, - <::Group as Group>::ElemLen, ->; - -impl PoprfClient { - /// Serialization into bytes - pub fn serialize(&self) -> Array> { - ::serialize_scalar(self.blind) - .concat(::serialize_elem(self.blinded_element)) - } - - /// Deserialization from bytes - /// - /// # Errors - /// [`Error::Deserialization`] if failed to deserialize `input`. - pub fn deserialize(mut input: &[u8]) -> Result { - let blind = deserialize_scalar::(&mut input)?; - let blinded_element = deserialize_elem::(&mut input)?; - - if !input.is_empty() { - return Err(Error::Deserialization); - } - - Ok(Self { - blind, - blinded_element, - }) - } -} - -/// Length of [`OprfServer`] in bytes for serialization. -pub type OprfServerLen = <::Group as Group>::ScalarLen; - -impl OprfServer { - /// Serialization into bytes - pub fn serialize(&self) -> Array> { - CS::Group::serialize_scalar(self.sk) - } - - /// Deserialization from bytes - /// - /// # Errors - /// [`Error::Deserialization`] if failed to deserialize `input`. - pub fn deserialize(mut input: &[u8]) -> Result { - let sk = deserialize_scalar::(&mut input)?; - - if !input.is_empty() { - return Err(Error::Deserialization); - } - - Ok(Self { sk }) - } -} - -/// Length of [`VoprfServer`] in bytes for serialization. -pub type VoprfServerLen = Sum< - <::Group as Group>::ScalarLen, - <::Group as Group>::ElemLen, ->; - -impl VoprfServer { - /// Serialization into bytes - pub fn serialize(&self) -> Array> { - CS::Group::serialize_scalar(self.sk).concat(CS::Group::serialize_elem(self.pk)) - } - - /// Deserialization from bytes - /// - /// # Errors - /// [`Error::Deserialization`] if failed to deserialize `input`. - pub fn deserialize(mut input: &[u8]) -> Result { - let sk = deserialize_scalar::(&mut input)?; - let pk = deserialize_elem::(&mut input)?; - - if !input.is_empty() { - return Err(Error::Deserialization); - } - - Ok(Self { sk, pk }) - } -} - -/// Length of [`PoprfServer`] in bytes for serialization. -pub type PoprfServerLen = Sum< - <::Group as Group>::ScalarLen, - <::Group as Group>::ElemLen, ->; - -impl PoprfServer { - /// Serialization into bytes - pub fn serialize(&self) -> Array> { - CS::Group::serialize_scalar(self.sk).concat(CS::Group::serialize_elem(self.pk)) - } - - /// Deserialization from bytes - /// - /// # Errors - /// [`Error::Deserialization`] if failed to deserialize `input`. - pub fn deserialize(mut input: &[u8]) -> Result { - let sk = deserialize_scalar::(&mut input)?; - let pk = deserialize_elem::(&mut input)?; - - if !input.is_empty() { - return Err(Error::Deserialization); - } - - Ok(Self { sk, pk }) - } -} - -/// Length of [`Proof`] in bytes for serialization. -pub type ProofLen = Sum< - <::Group as Group>::ScalarLen, - <::Group as Group>::ScalarLen, ->; - -impl Proof { - /// Serialization into bytes - pub fn serialize(&self) -> Array> { - CS::Group::serialize_scalar(self.c_scalar) - .concat(CS::Group::serialize_scalar(self.s_scalar)) - } - - /// Deserialization from bytes - /// - /// # Errors - /// [`Error::Deserialization`] if failed to deserialize `input`. - pub fn deserialize(mut input: &[u8]) -> Result { - let c_scalar = deserialize_scalar::(&mut input)?; - let s_scalar = deserialize_scalar::(&mut input)?; - - if !input.is_empty() { - return Err(Error::Deserialization); - } - - Ok(Proof { c_scalar, s_scalar }) - } -} - -/// Length of [`BlindedElement`] in bytes for serialization. -pub type BlindedElementLen = <::Group as Group>::ElemLen; - -impl BlindedElement { - /// Serialization into bytes - pub fn serialize(&self) -> Array> { - CS::Group::serialize_elem(self.0) - } - - /// Deserialization from bytes - /// - /// # Errors - /// [`Error::Deserialization`] if failed to deserialize `input`. - pub fn deserialize(mut input: &[u8]) -> Result { - let value = deserialize_elem::(&mut input)?; - - if !input.is_empty() { - return Err(Error::Deserialization); - } - - Ok(Self(value)) - } -} - -/// Length of [`EvaluationElement`] in bytes for serialization. -pub type EvaluationElementLen = <::Group as Group>::ElemLen; - -impl EvaluationElement { - /// Serialization into bytes - pub fn serialize(&self) -> Array> { - CS::Group::serialize_elem(self.0) - } - - /// Deserialization from bytes - /// - /// # Errors - /// [`Error::Deserialization`] if failed to deserialize `input`. - pub fn deserialize(mut input: &[u8]) -> Result { - let value = deserialize_elem::(&mut input)?; - - if !input.is_empty() { - return Err(Error::Deserialization); - } - - Ok(Self(value)) - } -} +///////////////////////////// +// Deserialization Helpers // +// ======================= // +///////////////////////////// fn deserialize_elem(input: &mut &[u8]) -> Result { let input = input @@ -293,6 +48,158 @@ impl SliceExt for [T] { } } +////////////////////////////// +// Serialization Macros // +// ======================== // +////////////////////////////// + +macro_rules! impl_serde_scalar { + ($ty:ident, $len:ident, $field:ident) => { + /// Length in bytes for serialization. + pub type $len = <::Group as Group>::ScalarLen; + + impl $ty { + /// Serialization into bytes + pub fn serialize(&self) -> Array> { + CS::Group::serialize_scalar(self.$field) + } + + /// Deserialization from bytes + /// + /// # Errors + /// [`Error::Deserialization`] if failed to deserialize `input`. + pub fn deserialize(mut input: &[u8]) -> Result { + let $field = deserialize_scalar::(&mut input)?; + + if !input.is_empty() { + return Err(Error::Deserialization); + } + + Ok(Self { $field }) + } + } + }; +} + +macro_rules! impl_serde_scalar_elem { + ($ty:ident, $len:ident, $scalar_field:ident, $elem_field:ident) => { + /// Length in bytes for serialization. + pub type $len = Sum< + <::Group as Group>::ScalarLen, + <::Group as Group>::ElemLen, + >; + + impl $ty { + /// Serialization into bytes + pub fn serialize(&self) -> Array> { + ::serialize_scalar(self.$scalar_field) + .concat(::serialize_elem(self.$elem_field)) + } + + /// Deserialization from bytes + /// + /// # Errors + /// [`Error::Deserialization`] if failed to deserialize `input`. + pub fn deserialize(mut input: &[u8]) -> Result { + let $scalar_field = deserialize_scalar::(&mut input)?; + let $elem_field = deserialize_elem::(&mut input)?; + + if !input.is_empty() { + return Err(Error::Deserialization); + } + + Ok(Self { + $scalar_field, + $elem_field, + }) + } + } + }; +} + +macro_rules! impl_serde_elem { + ($ty:ident, $len:ident) => { + /// Length in bytes for serialization. + pub type $len = <::Group as Group>::ElemLen; + + impl $ty { + /// Serialization into bytes + pub fn serialize(&self) -> Array> { + CS::Group::serialize_elem(self.0) + } + + /// Deserialization from bytes + /// + /// # Errors + /// [`Error::Deserialization`] if failed to deserialize `input`. + pub fn deserialize(mut input: &[u8]) -> Result { + let value = deserialize_elem::(&mut input)?; + + if !input.is_empty() { + return Err(Error::Deserialization); + } + + Ok(Self(value)) + } + } + }; +} + +////////////////////////////////////////////////////////// +// Serialization and Deserialization for High-Level API // +// ==================================================== // +////////////////////////////////////////////////////////// + +impl_serde_scalar!(OprfClient, OprfClientLen, blind); +impl_serde_scalar!(OprfServer, OprfServerLen, sk); + +impl_serde_elem!(BlindedElement, BlindedElementLen); +impl_serde_elem!(EvaluationElement, EvaluationElementLen); + +impl_serde_scalar_elem!(VoprfClient, VoprfClientLen, blind, blinded_element); +impl_serde_scalar_elem!(PoprfClient, PoprfClientLen, blind, blinded_element); +impl_serde_scalar_elem!(VoprfServer, VoprfServerLen, sk, pk); +impl_serde_scalar_elem!(PoprfServer, PoprfServerLen, sk, pk); + +///////////////////// +// Proof (One-Off) // +// =============== // +///////////////////// + +/// Length of [`Proof`] in bytes for serialization. +pub type ProofLen = Sum< + <::Group as Group>::ScalarLen, + <::Group as Group>::ScalarLen, +>; + +impl Proof { + /// Serialization into bytes + pub fn serialize(&self) -> Array> { + CS::Group::serialize_scalar(self.c_scalar) + .concat(CS::Group::serialize_scalar(self.s_scalar)) + } + + /// Deserialization from bytes + /// + /// # Errors + /// [`Error::Deserialization`] if failed to deserialize `input`. + pub fn deserialize(mut input: &[u8]) -> Result { + let c_scalar = deserialize_scalar::(&mut input)?; + let s_scalar = deserialize_scalar::(&mut input)?; + + if !input.is_empty() { + return Err(Error::Deserialization); + } + + Ok(Proof { c_scalar, s_scalar }) + } +} + +/////////////////////////// +// Serde Support // +// ===================== // +/////////////////////////// + #[cfg(feature = "serde")] pub(crate) mod serde { use core::marker::PhantomData; diff --git a/src/tests/helpers.rs b/src/tests/helpers.rs new file mode 100644 index 0000000..d09a393 --- /dev/null +++ b/src/tests/helpers.rs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) VexaHub and contributors. + +use core::iter; + +use digest::Output; + +use crate::common::{Dst, Mode, STR_HASH_TO_GROUP, finalize_after_unblind}; +use crate::{CipherSuite, Group}; + +pub(crate) fn prf( + input: &[u8], + key: ::Scalar, + mode: Mode, +) -> Output { + let dst = Dst::new::(STR_HASH_TO_GROUP, mode); + let point = CS::Group::hash_to_curve::(&[input], &dst.as_dst()).unwrap(); + + let res = point * &key; + + finalize_after_unblind::(iter::once((input, res))) + .next() + .unwrap() + .unwrap() +} diff --git a/src/tests/macros.rs b/src/tests/macros.rs new file mode 100644 index 0000000..5ceb2a5 --- /dev/null +++ b/src/tests/macros.rs @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright (c) VexaHub and contributors. + +macro_rules! test_all_curves { + ($($test_fn:ident),+ $(,)?) => { + #[test] + fn test_functionality() -> $crate::Result<()> { + #[cfg(feature = "ristretto255")] + { + $( $test_fn::<$crate::Ristretto255>(); )+ + } + $( $test_fn::<::p256::NistP256>(); )+ + $( $test_fn::<::p384::NistP384>(); )+ + $( $test_fn::<::p521::NistP521>(); )+ + Ok(()) + } + }; +} + +pub(crate) use test_all_curves; diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 7bfdd3b..19b97ad 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -3,6 +3,11 @@ // Copyright (c) Meta Platforms, Inc. and affiliates. mod cfrg_vectors; +mod macros; mod mock_rng; mod parser; mod test_cfrg_vectors; + +pub(crate) mod helpers; + +pub(crate) use macros::test_all_curves; diff --git a/src/voprf.rs b/src/voprf.rs index 49c3222..b6a72eb 100644 --- a/src/voprf.rs +++ b/src/voprf.rs @@ -9,15 +9,14 @@ use alloc::vec::Vec; use core::iter::{self, Map, Repeat, Zip}; use derive_where::derive_where; -use digest::{Digest, Output}; +use digest::Output; use hybrid_array::Array; -use hybrid_array::typenum::Unsigned; use rand_core::{TryCryptoRng, TryRng}; use crate::common::{ - BlindedElement, EvaluationElement, Mode, PreparedEvaluationElement, Proof, STR_FINALIZE, - derive_keypair, deterministic_blind_unchecked, generate_proof, hash_to_group, i2osp_2, - server_evaluate_hash_input, verify_proof, + BlindedElement, EvaluationElement, FinalizeAfterUnblindResult, Mode, PreparedEvaluationElement, + Proof, derive_keypair, deterministic_blind_unchecked, finalize_after_unblind, generate_proof, + hash_to_group, server_evaluate_hash_input, verify_proof, }; #[cfg(feature = "serde")] use crate::serialization::serde::{Element, Scalar}; @@ -504,37 +503,6 @@ where .map(|(blind, x)| x.0 * &CS::Group::invert_scalar(blind))) } -type FinalizeAfterUnblindResult<'a, C, I, IE> = Map< - IE, - fn((I, <::Group as Group>::Elem)) -> Result::Hash>>, ->; - -/// Returned values can only fail with [`Error::Input`]. -fn finalize_after_unblind< - 'a, - CS: CipherSuite, - I: AsRef<[u8]>, - IE: 'a + Iterator::Elem)>, ->( - inputs_and_unblinded_elements: IE, -) -> FinalizeAfterUnblindResult<'a, CS, I, IE> { - inputs_and_unblinded_elements.map(|(input, unblinded_element)| { - let elem_len = ::ElemLen::U16.to_be_bytes(); - - // hashInput = I2OSP(len(input), 2) || input || - // I2OSP(len(unblindedElement), 2) || unblindedElement || - // "Finalize" - // return Hash(hashInput) - Ok(CS::Hash::new() - .chain_update(i2osp_2(input.as_ref().len()).map_err(|_| Error::Input)?) - .chain_update(input.as_ref()) - .chain_update(elem_len) - .chain_update(CS::Group::serialize_elem(unblinded_element)) - .chain_update(STR_FINALIZE) - .finalize()) - }) -} - /////////// // Tests // // ===== // @@ -551,22 +519,7 @@ mod tests { use super::*; use crate::Group; use crate::common::{Dst, STR_HASH_TO_GROUP}; - - fn prf( - input: &[u8], - key: ::Scalar, - mode: Mode, - ) -> Output { - let dst = Dst::new::(STR_HASH_TO_GROUP, mode); - let point = CS::Group::hash_to_curve::(&[input], &dst.as_dst()).unwrap(); - - let res = point * &key; - - finalize_after_unblind::(iter::once((input, res))) - .next() - .unwrap() - .unwrap() - } + use crate::tests::helpers::prf; fn verifiable_retrieval() { let input = b"input"; @@ -713,7 +666,7 @@ mod tests { // inputs let wrong_input = b"wrong input"; let server_evaluate = server.evaluate(wrong_input).unwrap(); - assert!(client_finalize != server_evaluate); + assert_ne!(client_finalize, server_evaluate); } fn zeroize_voprf_client() { @@ -750,53 +703,13 @@ mod tests { assert!(proof.serialize().iter().all(|&x| x == 0)); } - #[test] - fn test_functionality() -> Result<()> { - use p256::NistP256; - use p384::NistP384; - use p521::NistP521; - - #[cfg(feature = "ristretto255")] - { - use crate::Ristretto255; - - verifiable_retrieval::(); - verifiable_batch_retrieval::(); - verifiable_bad_public_key::(); - verifiable_batch_bad_public_key::(); - verifiable_server_evaluate::(); - - zeroize_voprf_client::(); - zeroize_voprf_server::(); - } - - verifiable_retrieval::(); - verifiable_batch_retrieval::(); - verifiable_bad_public_key::(); - verifiable_batch_bad_public_key::(); - verifiable_server_evaluate::(); - - zeroize_voprf_client::(); - zeroize_voprf_server::(); - - verifiable_retrieval::(); - verifiable_batch_retrieval::(); - verifiable_bad_public_key::(); - verifiable_batch_bad_public_key::(); - verifiable_server_evaluate::(); - - zeroize_voprf_client::(); - zeroize_voprf_server::(); - - verifiable_retrieval::(); - verifiable_batch_retrieval::(); - verifiable_bad_public_key::(); - verifiable_batch_bad_public_key::(); - verifiable_server_evaluate::(); - - zeroize_voprf_client::(); - zeroize_voprf_server::(); - - Ok(()) - } + crate::tests::test_all_curves!( + verifiable_retrieval, + verifiable_batch_retrieval, + verifiable_bad_public_key, + verifiable_batch_bad_public_key, + verifiable_server_evaluate, + zeroize_voprf_client, + zeroize_voprf_server, + ); }