Skip to main content

voprf_vx/
common.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//! Common functionality between multiple OPRF modes.
6
7use core::convert::TryFrom;
8use core::iter::Map;
9use core::ops::Add;
10
11use derive_where::derive_where;
12use digest::{Digest, Output, OutputSizeUser};
13use hybrid_array::typenum::{IsLess, U2, U9, U256, Unsigned};
14use hybrid_array::{Array, ArrayN, ArraySize};
15use rand_core::{TryCryptoRng, TryRng};
16use subtle::ConstantTimeEq;
17
18#[cfg(feature = "serde")]
19use crate::serialization::serde::{Element, Scalar};
20use crate::{CipherSuite, Error, Group, InternalError, Result};
21
22///////////////
23// Constants //
24// ========= //
25///////////////
26
27pub(crate) const STR_FINALIZE: [u8; 8] = *b"Finalize";
28pub(crate) const STR_SEED: ArrayN<u8, 5> = Array(*b"Seed-");
29pub(crate) const STR_DERIVE_KEYPAIR: ArrayN<u8, 13> = Array(*b"DeriveKeyPair");
30pub(crate) const STR_COMPOSITE: [u8; 9] = *b"Composite";
31pub(crate) const STR_CHALLENGE: [u8; 9] = *b"Challenge";
32pub(crate) const STR_INFO: [u8; 4] = *b"Info";
33pub(crate) const STR_OPRF: [u8; 7] = *b"OPRFV1-";
34pub(crate) const STR_HASH_TO_SCALAR: ArrayN<u8, 13> = Array(*b"HashToScalar-");
35pub(crate) const STR_HASH_TO_GROUP: ArrayN<u8, 12> = Array(*b"HashToGroup-");
36
37/// Determines the mode of operation (either base mode or verifiable mode). This
38/// is only used for custom implementations for [`Group`].
39#[derive(Clone, Copy, Debug)]
40pub enum Mode {
41    /// Non-verifiable mode.
42    Oprf,
43    /// Verifiable mode.
44    Voprf,
45    /// Partially-oblivious mode.
46    Poprf,
47}
48
49impl Mode {
50    /// Mode as it is represented in a context string.
51    pub fn to_u8(self) -> u8 {
52        match self {
53            Mode::Oprf => 0,
54            Mode::Voprf => 1,
55            Mode::Poprf => 2,
56        }
57    }
58}
59
60////////////////////////////
61// High-level API Structs //
62// ====================== //
63////////////////////////////
64
65/// The first client message sent from a client (either verifiable or not) to a
66/// server (either verifiable or not).
67#[derive_where(Clone, ZeroizeOnDrop)]
68#[derive_where(Debug, Eq, Hash, Ord, PartialEq, PartialOrd; <CS::Group as Group>::Elem)]
69#[cfg_attr(
70    feature = "serde",
71    derive(serde::Deserialize, serde::Serialize),
72    serde(bound = "")
73)]
74pub struct BlindedElement<CS: CipherSuite>(
75    #[cfg_attr(feature = "serde", serde(with = "Element::<CS::Group>"))]
76    pub(crate)  <CS::Group as Group>::Elem,
77);
78
79/// The server's response to the [BlindedElement] message from a client (either
80/// verifiable or not) to a server (either verifiable or not).
81#[derive_where(Clone, ZeroizeOnDrop)]
82#[derive_where(Debug, Eq, Hash, Ord, PartialEq, PartialOrd; <CS::Group as Group>::Elem)]
83#[cfg_attr(
84    feature = "serde",
85    derive(serde::Deserialize, serde::Serialize),
86    serde(bound = "")
87)]
88pub struct EvaluationElement<CS: CipherSuite>(
89    #[cfg_attr(feature = "serde", serde(with = "Element::<CS::Group>"))]
90    pub(crate)  <CS::Group as Group>::Elem,
91);
92
93/// Contains prepared [`EvaluationElement`]s by a server batch evaluate
94/// preparation.
95#[derive_where(Clone, ZeroizeOnDrop)]
96#[derive_where(Debug, Eq, Hash, Ord, PartialEq, PartialOrd; <CS::Group as Group>::Elem)]
97#[cfg_attr(
98    feature = "serde",
99    derive(serde::Deserialize, serde::Serialize),
100    serde(bound = "")
101)]
102pub struct PreparedEvaluationElement<CS: CipherSuite>(pub(crate) EvaluationElement<CS>);
103
104/// A proof produced by a server that the OPRF output matches against a server
105/// public key.
106#[derive_where(Clone, ZeroizeOnDrop)]
107#[derive_where(Debug, Eq, Hash, Ord, PartialEq, PartialOrd; <CS::Group as Group>::Scalar)]
108#[cfg_attr(
109    feature = "serde",
110    derive(serde::Deserialize, serde::Serialize),
111    serde(bound = "")
112)]
113pub struct Proof<CS: CipherSuite> {
114    #[cfg_attr(feature = "serde", serde(with = "Scalar::<CS::Group>"))]
115    pub(crate) c_scalar: <CS::Group as Group>::Scalar,
116    #[cfg_attr(feature = "serde", serde(with = "Scalar::<CS::Group>"))]
117    pub(crate) s_scalar: <CS::Group as Group>::Scalar,
118}
119
120/////////////////////
121// Proof Functions //
122// =============== //
123/////////////////////
124
125/// Can only fail with [`Error::Batch`].
126#[allow(clippy::many_single_char_names)]
127pub(crate) fn generate_proof<CS: CipherSuite, R: TryRng + TryCryptoRng>(
128    rng: &mut R,
129    k: <CS::Group as Group>::Scalar,
130    a: <CS::Group as Group>::Elem,
131    b: <CS::Group as Group>::Elem,
132    cs: impl ExactSizeIterator<Item = <CS::Group as Group>::Elem>,
133    ds: impl ExactSizeIterator<Item = <CS::Group as Group>::Elem>,
134    mode: Mode,
135) -> Result<Proof<CS>> {
136    // https://www.rfc-editor.org/rfc/rfc9497#section-2.2.1
137
138    let (m, z) = compute_composites::<CS, _, _>(Some(k), b, cs, ds, mode)?;
139
140    let r = CS::Group::random_scalar(rng)?;
141    let t2 = a * &r;
142    let t3 = m * &r;
143
144    // Bm = GG.SerializeElement(B)
145    let bm = CS::Group::serialize_elem(b);
146    // a0 = GG.SerializeElement(M)
147    let a0 = CS::Group::serialize_elem(m);
148    // a1 = GG.SerializeElement(Z)
149    let a1 = CS::Group::serialize_elem(z);
150    // a2 = GG.SerializeElement(t2)
151    let a2 = CS::Group::serialize_elem(t2);
152    // a3 = GG.SerializeElement(t3)
153    let a3 = CS::Group::serialize_elem(t3);
154
155    let elem_len = <CS::Group as Group>::ElemLen::U16.to_be_bytes();
156
157    // h2Input = I2OSP(len(Bm), 2) || Bm ||
158    //           I2OSP(len(a0), 2) || a0 ||
159    //           I2OSP(len(a1), 2) || a1 ||
160    //           I2OSP(len(a2), 2) || a2 ||
161    //           I2OSP(len(a3), 2) || a3 ||
162    //           "Challenge"
163    let h2_input = [
164        &elem_len,
165        bm.as_slice(),
166        &elem_len,
167        &a0,
168        &elem_len,
169        &a1,
170        &elem_len,
171        &a2,
172        &elem_len,
173        &a3,
174        &STR_CHALLENGE,
175    ];
176
177    let dst = Dst::new::<CS, _>(STR_HASH_TO_SCALAR, mode);
178    // This can't fail, the size of the `input` is known.
179    let c_scalar = CS::Group::hash_to_scalar::<CS::Hash>(&h2_input, &dst.as_dst()).unwrap();
180    let s_scalar = r - &(c_scalar * &k);
181
182    Ok(Proof { c_scalar, s_scalar })
183}
184
185/// Can only fail with [`Error::ProofVerification`] or [`Error::Batch`].
186#[allow(clippy::many_single_char_names)]
187pub(crate) fn verify_proof<CS: CipherSuite>(
188    a: <CS::Group as Group>::Elem,
189    b: <CS::Group as Group>::Elem,
190    cs: impl ExactSizeIterator<Item = <CS::Group as Group>::Elem>,
191    ds: impl ExactSizeIterator<Item = <CS::Group as Group>::Elem>,
192    proof: &Proof<CS>,
193    mode: Mode,
194) -> Result<()> {
195    // https://www.rfc-editor.org/rfc/rfc9497#section-2.2.2
196    let (m, z) = compute_composites::<CS, _, _>(None, b, cs, ds, mode)?;
197    let t2 = (a * &proof.s_scalar) + &(b * &proof.c_scalar);
198    let t3 = (m * &proof.s_scalar) + &(z * &proof.c_scalar);
199
200    // Bm = GG.SerializeElement(B)
201    let bm = CS::Group::serialize_elem(b);
202    // a0 = GG.SerializeElement(M)
203    let a0 = CS::Group::serialize_elem(m);
204    // a1 = GG.SerializeElement(Z)
205    let a1 = CS::Group::serialize_elem(z);
206    // a2 = GG.SerializeElement(t2)
207    let a2 = CS::Group::serialize_elem(t2);
208    // a3 = GG.SerializeElement(t3)
209    let a3 = CS::Group::serialize_elem(t3);
210
211    let elem_len = <CS::Group as Group>::ElemLen::U16.to_be_bytes();
212
213    // h2Input = I2OSP(len(Bm), 2) || Bm ||
214    //           I2OSP(len(a0), 2) || a0 ||
215    //           I2OSP(len(a1), 2) || a1 ||
216    //           I2OSP(len(a2), 2) || a2 ||
217    //           I2OSP(len(a3), 2) || a3 ||
218    //           "Challenge"
219    let h2_input = [
220        &elem_len,
221        bm.as_slice(),
222        &elem_len,
223        &a0,
224        &elem_len,
225        &a1,
226        &elem_len,
227        &a2,
228        &elem_len,
229        &a3,
230        &STR_CHALLENGE,
231    ];
232
233    let dst = Dst::new::<CS, _>(STR_HASH_TO_SCALAR, mode);
234    // This can't fail, the size of the `input` is known.
235    let c = CS::Group::hash_to_scalar::<CS::Hash>(&h2_input, &dst.as_dst()).unwrap();
236
237    match c.ct_eq(&proof.c_scalar).into() {
238        true => Ok(()),
239        false => Err(Error::ProofVerification),
240    }
241}
242
243type ComputeCompositesResult<CS> = (
244    <<CS as CipherSuite>::Group as Group>::Elem,
245    <<CS as CipherSuite>::Group as Group>::Elem,
246);
247
248/// Can only fail with [`Error::Batch`].
249fn compute_composites<
250    CS: CipherSuite,
251    IC: Iterator<Item = <CS::Group as Group>::Elem> + ExactSizeIterator,
252    ID: Iterator<Item = <CS::Group as Group>::Elem> + ExactSizeIterator,
253>(
254    k_option: Option<<CS::Group as Group>::Scalar>,
255    b: <CS::Group as Group>::Elem,
256    c_slice: IC,
257    d_slice: ID,
258    mode: Mode,
259) -> Result<ComputeCompositesResult<CS>> {
260    // https://www.rfc-editor.org/rfc/rfc9497#section-2.2.1
261
262    let elem_len = <CS::Group as Group>::ElemLen::U16.to_be_bytes();
263
264    if c_slice.len() != d_slice.len() {
265        return Err(Error::Batch);
266    }
267
268    let len = u16::try_from(c_slice.len()).map_err(|_| Error::Batch)?;
269
270    // seedDST = "Seed-" || contextString
271    let seed_dst = Dst::new::<CS, _>(STR_SEED, mode);
272
273    // h1Input = I2OSP(len(Bm), 2) || Bm ||
274    //           I2OSP(len(seedDST), 2) || seedDST
275    // seed = Hash(h1Input)
276    let seed = CS::Hash::new()
277        .chain_update(elem_len)
278        .chain_update(CS::Group::serialize_elem(b))
279        .chain_update(seed_dst.i2osp_2())
280        .chain_update_multi(&seed_dst.as_dst())
281        .finalize();
282    let seed_len = i2osp_2_array::<<CS::Hash as OutputSizeUser>::OutputSize>();
283
284    let mut m = CS::Group::identity_elem();
285    let mut z = CS::Group::identity_elem();
286
287    for (i, (c, d)) in (0..len).zip(c_slice.zip(d_slice)) {
288        // Ci = GG.SerializeElement(Cs[i])
289        let ci = CS::Group::serialize_elem(c);
290        // Di = GG.SerializeElement(Ds[i])
291        let di = CS::Group::serialize_elem(d);
292        // h2Input = I2OSP(len(seed), 2) || seed || I2OSP(i, 2) ||
293        //           I2OSP(len(Ci), 2) || Ci ||
294        //           I2OSP(len(Di), 2) || Di ||
295        //           "Composite"
296        let h2_input = [
297            seed_len.as_slice(),
298            &seed,
299            &i.to_be_bytes(),
300            &elem_len,
301            &ci,
302            &elem_len,
303            &di,
304            &STR_COMPOSITE,
305        ];
306
307        let dst = Dst::new::<CS, _>(STR_HASH_TO_SCALAR, mode);
308        // This can't fail, the size of the `input` is known.
309        let di = CS::Group::hash_to_scalar::<CS::Hash>(&h2_input, &dst.as_dst()).unwrap();
310        m = c * &di + &m;
311        z = match k_option {
312            Some(_) => z,
313            None => d * &di + &z,
314        };
315    }
316
317    z = match k_option {
318        Some(k) => m * &k,
319        None => z,
320    };
321
322    Ok((m, z))
323}
324
325/////////////////////
326// Inner Functions //
327// =============== //
328/////////////////////
329
330/// Can only fail with [`Error::DeriveKeyPair`] and [`Error::Protocol`].
331pub(crate) fn derive_key_internal<CS: CipherSuite>(
332    seed: &[u8],
333    info: &[u8],
334    mode: Mode,
335) -> Result<<CS::Group as Group>::Scalar, Error> {
336    let dst = Dst::new::<CS, _>(STR_DERIVE_KEYPAIR, mode);
337
338    let info_len = i2osp_2(info.len()).map_err(|_| Error::DeriveKeyPair)?;
339
340    for counter in 0_u8..=u8::MAX {
341        // deriveInput = seed || I2OSP(len(info), 2) || info
342        // skS = G.HashToScalar(deriveInput || I2OSP(counter, 1), DST = "DeriveKeyPair"
343        // || contextString)
344        let sk_s = CS::Group::hash_to_scalar::<CS::Hash>(
345            &[seed, &info_len, info, &counter.to_be_bytes()],
346            &dst.as_dst(),
347        )
348        .map_err(|_| Error::DeriveKeyPair)?;
349
350        if !bool::from(CS::Group::is_zero_scalar(sk_s)) {
351            return Ok(sk_s);
352        }
353    }
354
355    Err(Error::Protocol)
356}
357
358/// Corresponds to DeriveKeyPair() function from the VOPRF specification.
359///
360/// # Errors
361/// - [`Error::DeriveKeyPair`] if the `input` and `seed` together are longer
362///   then `u16::MAX - 3`.
363/// - [`Error::Protocol`] if the protocol fails and can't be completed.
364#[cfg(feature = "danger")]
365pub fn derive_key<CS: CipherSuite>(
366    seed: &[u8],
367    info: &[u8],
368    mode: Mode,
369) -> Result<<CS::Group as Group>::Scalar, Error> {
370    derive_key_internal::<CS>(seed, info, mode)
371}
372
373type DeriveKeypairResult<CS> = (
374    <<CS as CipherSuite>::Group as Group>::Scalar,
375    <<CS as CipherSuite>::Group as Group>::Elem,
376);
377
378/// Can only fail with [`Error::DeriveKeyPair`] and [`Error::Protocol`].
379pub(crate) fn derive_keypair<CS: CipherSuite>(
380    seed: &[u8],
381    info: &[u8],
382    mode: Mode,
383) -> Result<DeriveKeypairResult<CS>, Error> {
384    let sk_s = derive_key_internal::<CS>(seed, info, mode)?;
385    let pk_s = CS::Group::base_elem() * &sk_s;
386
387    Ok((sk_s, pk_s))
388}
389
390/// Inner function for blind that assumes that the blinding factor has already
391/// been chosen, and therefore takes it as input. Does not check if the blinding
392/// factor is non-zero.
393///
394/// Can only fail with [`Error::Input`].
395pub(crate) fn deterministic_blind_unchecked<CS: CipherSuite>(
396    input: &[u8],
397    blind: &<CS::Group as Group>::Scalar,
398    mode: Mode,
399) -> Result<<CS::Group as Group>::Elem> {
400    let hashed_point = hash_to_group::<CS>(input, mode)?;
401
402    // Identity element would nullify blinding, revealing the input.
403    if CS::Group::is_identity_elem(hashed_point).into() {
404        return Err(Error::Input);
405    }
406
407    Ok(hashed_point * blind)
408}
409
410/// Hashes `input` to a point on the curve
411pub(crate) fn hash_to_group<CS: CipherSuite>(
412    input: &[u8],
413    mode: Mode,
414) -> Result<<CS::Group as Group>::Elem> {
415    let dst = Dst::new::<CS, _>(STR_HASH_TO_GROUP, mode);
416    CS::Group::hash_to_curve::<CS::Hash>(&[input], &dst.as_dst()).map_err(|_| Error::Input)
417}
418
419/// Internal function that finalizes the hash input for OPRF, VOPRF & POPRF.
420/// Returned values can only fail with [`Error::Input`].
421pub(crate) fn server_evaluate_hash_input<CS: CipherSuite>(
422    input: &[u8],
423    info: Option<&[u8]>,
424    issued_element: Array<u8, <<CS as CipherSuite>::Group as Group>::ElemLen>,
425) -> Result<Output<CS::Hash>> {
426    // OPRF & VOPRF
427    // hashInput = I2OSP(len(input), 2) || input ||
428    //             I2OSP(len(issuedElement), 2) || issuedElement ||
429    //             "Finalize"
430    // return Hash(hashInput)
431    //
432    // POPRF
433    // hashInput = I2OSP(len(input), 2) || input ||
434    //             I2OSP(len(info), 2) || info ||
435    //             I2OSP(len(issuedElement), 2) || issuedElement ||
436    //             "Finalize"
437
438    let mut hash = CS::Hash::new()
439        .chain_update(i2osp_2(input.as_ref().len()).map_err(|_| Error::Input)?)
440        .chain_update(input.as_ref());
441    if let Some(info) = info {
442        hash = hash
443            .chain_update(i2osp_2(info.as_ref().len()).map_err(|_| Error::Input)?)
444            .chain_update(info.as_ref());
445    }
446    Ok(hash
447        .chain_update(i2osp_2(issued_element.as_slice().len()).map_err(|_| Error::Input)?)
448        .chain_update(issued_element)
449        .chain_update(STR_FINALIZE)
450        .finalize())
451}
452
453pub(crate) type FinalizeAfterUnblindResult<'a, C, I, IE> = Map<
454    IE,
455    fn((I, <<C as CipherSuite>::Group as Group>::Elem)) -> Result<Output<<C as CipherSuite>::Hash>>,
456>;
457
458/// Returned values can only fail with [`Error::Input`].
459pub(crate) fn finalize_after_unblind<
460    'a,
461    CS: CipherSuite,
462    I: AsRef<[u8]>,
463    IE: 'a + Iterator<Item = (I, <CS::Group as Group>::Elem)>,
464>(
465    inputs_and_unblinded_elements: IE,
466) -> FinalizeAfterUnblindResult<'a, CS, I, IE> {
467    inputs_and_unblinded_elements.map(|(input, unblinded_element)| {
468        let elem_len = <CS::Group as Group>::ElemLen::U16.to_be_bytes();
469
470        Ok(CS::Hash::new()
471            .chain_update(i2osp_2(input.as_ref().len()).map_err(|_| Error::Input)?)
472            .chain_update(input.as_ref())
473            .chain_update(elem_len)
474            .chain_update(CS::Group::serialize_elem(unblinded_element))
475            .chain_update(STR_FINALIZE)
476            .finalize())
477    })
478}
479
480pub(crate) struct Dst<L: ArraySize> {
481    dst_1: Array<u8, L>,
482    dst_2: &'static [u8],
483}
484
485impl<L: ArraySize> Dst<L> {
486    pub(crate) fn new<CS, TL>(par_1: Array<u8, TL>, mode: Mode) -> Self
487    where
488        CS: CipherSuite,
489        TL: ArraySize + Add<U9, Output = L>,
490    {
491        // Generates the contextString parameter as defined in
492        // <https://www.rfc-editor.org/rfc/rfc9497#section-3.1>
493        let par_2 = ArrayN::<u8, 7>::from(STR_OPRF)
494            .concat(ArrayN::<u8, 1>::from([mode.to_u8()]))
495            .concat(ArrayN::<u8, 1>::from([b'-']));
496
497        let dst_1 = par_1.concat(par_2);
498        let dst_2 = CS::ID;
499
500        assert!(
501            L::USIZE + dst_2.len() <= u16::MAX.into(),
502            "constructed DST longer then {}",
503            u16::MAX
504        );
505
506        Self { dst_1, dst_2 }
507    }
508
509    pub(crate) fn as_dst(&self) -> [&[u8]; 2] {
510        [&self.dst_1, self.dst_2]
511    }
512
513    pub(crate) fn i2osp_2(&self) -> [u8; 2] {
514        u16::try_from(L::USIZE + self.dst_2.len())
515            .unwrap()
516            .to_be_bytes()
517    }
518}
519
520trait DigestExt {
521    fn chain_update_multi(self, data: &[&[u8]]) -> Self;
522}
523
524impl<T> DigestExt for T
525where
526    T: Digest,
527{
528    fn chain_update_multi(mut self, datas: &[&[u8]]) -> Self {
529        for data in datas {
530            self.update(data)
531        }
532
533        self
534    }
535}
536
537///////////////////////
538// Utility Functions //
539// ================= //
540///////////////////////
541
542pub(crate) fn i2osp_2(input: usize) -> Result<[u8; 2], InternalError> {
543    u16::try_from(input)
544        .map(|input| input.to_be_bytes())
545        .map_err(|_| InternalError::I2osp)
546}
547
548pub(crate) fn i2osp_2_array<L: ArraySize + IsLess<U256>>() -> Array<u8, U2> {
549    L::U16.to_be_bytes().into()
550}