Skip to main content

voprf_vx/
oprf.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (c) VexaHub and contributors.
3// Copyright (c) Meta Platforms, Inc. and affiliates.
4
5//! Contains the main OPRF API
6
7use core::iter::{self};
8
9use derive_where::derive_where;
10use digest::Output;
11use hybrid_array::Array;
12use rand_core::{TryCryptoRng, TryRng};
13
14use crate::common::{
15    BlindedElement, EvaluationElement, Mode, derive_key_internal, deterministic_blind_unchecked,
16    finalize_after_unblind, hash_to_group, server_evaluate_hash_input,
17};
18#[cfg(feature = "serde")]
19use crate::serialization::serde::Scalar;
20use crate::{CipherSuite, Error, Group, Result};
21
22///////////////
23// Constants //
24// ========= //
25///////////////
26
27////////////////////////////
28// High-level API Structs //
29// ====================== //
30////////////////////////////
31
32/// A client which engages with a [OprfServer] in base mode, meaning
33/// that the OPRF outputs are not verifiable.
34#[derive_where(Clone, ZeroizeOnDrop)]
35#[derive_where(Debug, Eq, Hash, Ord, PartialEq, PartialOrd; <CS::Group as Group>::Scalar)]
36#[cfg_attr(
37    feature = "serde",
38    derive(serde::Deserialize, serde::Serialize),
39    serde(bound = "")
40)]
41pub struct OprfClient<CS: CipherSuite> {
42    #[cfg_attr(feature = "serde", serde(with = "Scalar::<CS::Group>"))]
43    pub(crate) blind: <CS::Group as Group>::Scalar,
44}
45
46/// A server which engages with a [OprfClient] in base mode, meaning
47/// that the OPRF outputs are not verifiable.
48#[derive_where(Clone, ZeroizeOnDrop)]
49#[derive_where(Debug, Eq, Hash, Ord, PartialEq, PartialOrd; <CS::Group as Group>::Scalar)]
50#[cfg_attr(
51    feature = "serde",
52    derive(serde::Deserialize, serde::Serialize),
53    serde(bound = "")
54)]
55pub struct OprfServer<CS: CipherSuite> {
56    #[cfg_attr(feature = "serde", serde(with = "Scalar::<CS::Group>"))]
57    pub(crate) sk: <CS::Group as Group>::Scalar,
58}
59
60/////////////////////////
61// API Implementations //
62// =================== //
63/////////////////////////
64
65impl<CS: CipherSuite> OprfClient<CS> {
66    /// Computes the first step for the multiplicative blinding version of
67    /// DH-OPRF.
68    ///
69    /// # Errors
70    /// [`Error::Input`] if the `input` is empty or longer then [`u16::MAX`].
71    pub fn blind<R: TryRng + TryCryptoRng>(
72        input: &[u8],
73        blinding_factor_rng: &mut R,
74    ) -> Result<OprfClientBlindResult<CS>> {
75        let blind = CS::Group::random_scalar(blinding_factor_rng)?;
76        Self::deterministic_blind_unchecked_inner(input, blind)
77    }
78
79    /// Computes the first step for the multiplicative blinding version of
80    /// DH-OPRF, taking a blinding factor scalar as input instead of sampling
81    /// from an RNG.
82    ///
83    /// # Caution
84    ///
85    /// This should be used with caution, since it does not perform any checks
86    /// on the validity of the blinding factor!
87    ///
88    /// # Errors
89    /// [`Error::Input`] if the `input` is empty or longer then [`u16::MAX`].
90    #[cfg(any(feature = "danger", test))]
91    pub fn deterministic_blind_unchecked(
92        input: &[u8],
93        blind: <CS::Group as Group>::Scalar,
94    ) -> Result<OprfClientBlindResult<CS>> {
95        Self::deterministic_blind_unchecked_inner(input, blind)
96    }
97
98    /// Can only fail with [`Error::Input`].
99    fn deterministic_blind_unchecked_inner(
100        input: &[u8],
101        blind: <CS::Group as Group>::Scalar,
102    ) -> Result<OprfClientBlindResult<CS>> {
103        let blinded_element = deterministic_blind_unchecked::<CS>(input, &blind, Mode::Oprf)?;
104        Ok(OprfClientBlindResult {
105            state: Self { blind },
106            message: BlindedElement(blinded_element),
107        })
108    }
109
110    /// Computes the third step for the multiplicative blinding version of
111    /// DH-OPRF, in which the client unblinds the server's message.
112    ///
113    /// # Errors
114    /// [`Error::Input`] if the `input` is empty or longer then [`u16::MAX`].
115    pub fn finalize(
116        &self,
117        input: &[u8],
118        evaluation_element: &EvaluationElement<CS>,
119    ) -> Result<Output<CS::Hash>> {
120        let unblinded_element = evaluation_element.0 * &CS::Group::invert_scalar(self.blind);
121        let mut outputs =
122            finalize_after_unblind::<CS, _, _>(iter::once((input, unblinded_element)));
123        outputs.next().unwrap()
124    }
125
126    /// Only used for test functions
127    #[cfg(test)]
128    pub fn from_blind(blind: <CS::Group as Group>::Scalar) -> Self {
129        Self { blind }
130    }
131
132    /// Exposes the blind group element
133    #[cfg(feature = "danger")]
134    pub fn get_blind(&self) -> <CS::Group as Group>::Scalar {
135        self.blind
136    }
137}
138
139impl<CS: CipherSuite> OprfServer<CS> {
140    /// Produces a new instance of a [OprfServer] using a supplied RNG
141    ///
142    /// # Errors
143    /// [`Error::Protocol`] if the protocol fails and can't be completed.
144    pub fn new<R: TryRng + TryCryptoRng>(rng: &mut R) -> Result<Self> {
145        let mut seed = Array::<_, <CS::Group as Group>::ScalarLen>::default();
146        rng.try_fill_bytes(&mut seed).map_err(|_| Error::Protocol)?;
147        Self::new_from_seed(&seed, &[])
148    }
149
150    /// Produces a new instance of a [OprfServer] using a supplied set
151    /// of bytes to represent the server's private key
152    ///
153    /// # Errors
154    /// [`Error::Deserialization`] if the private key is not a valid point on
155    /// the group or zero.
156    pub fn new_with_key(private_key_bytes: &[u8]) -> Result<Self> {
157        let sk = CS::Group::deserialize_scalar(private_key_bytes)?;
158        Ok(Self { sk })
159    }
160
161    /// Produces a new instance of a [OprfServer] using a supplied set
162    /// of bytes which are used as a seed to derive the server's private key.
163    ///
164    /// Corresponds to DeriveKeyPair() function from the VOPRF specification.
165    ///
166    /// # Errors
167    /// - [`Error::DeriveKeyPair`] if the `input` and `seed` together are longer
168    ///   then `u16::MAX - 3`.
169    /// - [`Error::Protocol`] if the protocol fails and can't be completed.
170    pub fn new_from_seed(seed: &[u8], info: &[u8]) -> Result<Self> {
171        let sk = derive_key_internal::<CS>(seed, info, Mode::Oprf)?;
172        Ok(Self { sk })
173    }
174
175    /// Only used for tests
176    #[cfg(test)]
177    pub fn get_private_key(&self) -> <CS::Group as Group>::Scalar {
178        self.sk
179    }
180
181    /// Computes the second step for the multiplicative blinding version of
182    /// DH-OPRF. This message is sent from the server (who holds the OPRF key)
183    /// to the client.
184    pub fn blind_evaluate(&self, blinded_element: &BlindedElement<CS>) -> EvaluationElement<CS> {
185        EvaluationElement(blinded_element.0 * &self.sk)
186    }
187
188    /// Computes the output of the OPRF on the server side
189    ///
190    /// # Errors
191    /// [`Error::Input`]  if the `input` is longer then [`u16::MAX`].
192    pub fn evaluate(&self, input: &[u8]) -> Result<Output<<CS as CipherSuite>::Hash>> {
193        let input_element = hash_to_group::<CS>(input, Mode::Oprf)?;
194        if CS::Group::is_identity_elem(input_element).into() {
195            return Err(Error::Input);
196        };
197        let evaluated_element = input_element * &self.sk;
198
199        let issued_element = CS::Group::serialize_elem(evaluated_element);
200
201        server_evaluate_hash_input::<CS>(input, None, issued_element)
202    }
203}
204
205/////////////////////////
206// Convenience Structs //
207//==================== //
208/////////////////////////
209
210/// Contains the fields that are returned by a non-verifiable client blind
211#[derive_where(Debug; <CS::Group as Group>::Scalar, <CS::Group as Group>::Elem)]
212pub struct OprfClientBlindResult<CS: CipherSuite> {
213    /// The state to be persisted on the client
214    pub state: OprfClient<CS>,
215    /// The message to send to the server
216    pub message: BlindedElement<CS>,
217}
218
219///////////
220// Tests //
221// ===== //
222///////////
223
224#[cfg(test)]
225mod tests {
226    use core::ptr;
227
228    use rand::TryRng;
229    use rand::rngs::SysRng;
230
231    use super::*;
232    use crate::Group;
233    use crate::common::{Dst, STR_HASH_TO_GROUP};
234    use crate::tests::helpers::prf;
235
236    fn base_retrieval<CS: CipherSuite>() {
237        let input = b"input";
238        let mut rng = SysRng;
239        let client_blind_result = OprfClient::<CS>::blind(input, &mut rng).unwrap();
240        let server = OprfServer::<CS>::new(&mut rng).unwrap();
241        let message = server.blind_evaluate(&client_blind_result.message);
242        let client_finalize_result = client_blind_result.state.finalize(input, &message).unwrap();
243        let res2 = prf::<CS>(input, server.get_private_key(), Mode::Oprf);
244        assert_eq!(client_finalize_result, res2);
245    }
246
247    fn base_inversion_unsalted<CS: CipherSuite>() {
248        let mut rng = SysRng;
249        let mut input = [0u8; 64];
250        rng.try_fill_bytes(&mut input).unwrap();
251        let client_blind_result = OprfClient::<CS>::blind(&input, &mut rng).unwrap();
252        let client_finalize_result = client_blind_result
253            .state
254            .finalize(&input, &EvaluationElement(client_blind_result.message.0))
255            .unwrap();
256
257        let dst = Dst::new::<CS, _>(STR_HASH_TO_GROUP, Mode::Oprf);
258        let point = CS::Group::hash_to_curve::<CS::Hash>(&[&input], &dst.as_dst()).unwrap();
259        let res2 = finalize_after_unblind::<CS, _, _>(iter::once((input.as_ref(), point)))
260            .next()
261            .unwrap()
262            .unwrap();
263
264        assert_eq!(client_finalize_result, res2);
265    }
266
267    fn server_evaluate<CS: CipherSuite>() {
268        let input = b"input";
269        let mut rng = SysRng;
270        let client_blind_result = OprfClient::<CS>::blind(input, &mut rng).unwrap();
271        let server = OprfServer::<CS>::new(&mut rng).unwrap();
272        let server_result = server.blind_evaluate(&client_blind_result.message);
273
274        let client_finalize = client_blind_result
275            .state
276            .finalize(input, &server_result)
277            .unwrap();
278
279        // We expect the outputs from client and server to be equal given an identical
280        // input
281        let server_evaluate = server.evaluate(input).unwrap();
282        assert_eq!(client_finalize, server_evaluate);
283
284        // We expect the outputs from client and server to be different given different
285        // inputs
286        let wrong_input = b"wrong input";
287        let server_evaluate = server.evaluate(wrong_input).unwrap();
288        assert!(client_finalize != server_evaluate);
289    }
290
291    fn zeroize_oprf_client<CS: CipherSuite>() {
292        let input = b"input";
293        let mut rng = SysRng;
294        let client_blind_result = OprfClient::<CS>::blind(input, &mut rng).unwrap();
295
296        let mut state = client_blind_result.state;
297        unsafe { ptr::drop_in_place(&mut state) };
298        assert!(state.serialize().iter().all(|&x| x == 0));
299
300        let mut message = client_blind_result.message;
301        unsafe { ptr::drop_in_place(&mut message) };
302        assert!(message.serialize().iter().all(|&x| x == 0));
303    }
304
305    fn zeroize_oprf_server<CS: CipherSuite>() {
306        let input = b"input";
307        let mut rng = SysRng;
308        let client_blind_result = OprfClient::<CS>::blind(input, &mut rng).unwrap();
309        let server = OprfServer::<CS>::new(&mut rng).unwrap();
310        let mut message = server.blind_evaluate(&client_blind_result.message);
311
312        let mut state = server;
313        unsafe { ptr::drop_in_place(&mut state) };
314        assert!(state.serialize().iter().all(|&x| x == 0));
315
316        unsafe { ptr::drop_in_place(&mut message) };
317        assert!(message.serialize().iter().all(|&x| x == 0));
318    }
319
320    crate::tests::test_all_curves!(
321        base_retrieval,
322        base_inversion_unsalted,
323        server_evaluate,
324        zeroize_oprf_client,
325        zeroize_oprf_server,
326    );
327}