Skip to main content

ic_ec/
oprf.rs

1//! Oblivious pseudorandom functions, RFC 9497, with the P256-SHA256 suite.
2//!
3//! An OPRF lets a client learn `F(k, input)` for a server's key `k` without
4//! the server learning the input or the client learning the key. The client
5//! blinds its input, the server evaluates the blinded value, and the client
6//! removes the blind. It is the building block of OPAQUE, of password
7//! hardening services that hold a pepper the database never sees, and of
8//! privacy-preserving tokens.
9//!
10//! All three of RFC 9497's modes:
11//!
12//! - [`Mode::Oprf`]: the base protocol.
13//! - [`Mode::Voprf`]: the server also proves, with a discrete-logarithm
14//!   equality proof, that it used the key whose public half the client
15//!   holds, so it cannot single a client out with a different key.
16//! - [`Mode::Poprf`]: as VOPRF, with a public `info` string both sides see
17//!   bound into the evaluation -- a per-user or per-epoch tweak, which is
18//!   how a hardening service can rate-limit or rotate by tweak.
19//!
20//! ```
21//! use ic_ec::oprf::{blind, finalize, Mode, ServerKey};
22//! # struct Fixed(u8);
23//! # impl ic_core::traits::RandomSource for Fixed {
24//! #     fn fill(&mut self, out: &mut [u8]) -> ic_core::Result<()> { self.0 += 1; out.fill(self.0); Ok(()) }
25//! # }
26//! # let mut rng = Fixed(0);
27//! let server = ServerKey::generate(Mode::Oprf, &mut rng)?;
28//! let (state, blinded) = blind(Mode::Oprf, b"password", b"", None, &mut rng)?;
29//! let (evaluated, _) = server.blind_evaluate(&blinded, b"", &mut rng)?;
30//! let output = finalize(b"password", b"", &state, &blinded, &evaluated, None)?;
31//! assert_eq!(output, server.evaluate(b"password", b"")?);
32//! # Ok::<(), ic_core::Error>(())
33//! ```
34//!
35//! Elements are 33-byte compressed SEC1 points, scalars 32 bytes big-endian,
36//! a proof two scalars, and an output 32 bytes. Checked against every P-256
37//! vector in RFC 9497 appendix A.3.
38
39use crate::hash_to_curve::{hash_to_curve_point, hash_to_scalar};
40use crate::nist::arith::Field;
41use crate::nist::point::{AffinePoint, Point};
42use crate::p256::{Fn as Scalar, P256};
43use ic_core::traits::internal::Digest;
44use ic_core::traits::RandomSource;
45use ic_core::{ensure, Result, Zeroize};
46use ic_hash::Sha256;
47
48/// Bytes in a serialized element.
49pub const ELEMENT_LEN: usize = 33;
50/// Bytes in a serialized scalar.
51pub const SCALAR_LEN: usize = 32;
52/// Bytes in a serialized proof.
53pub const PROOF_LEN: usize = 64;
54/// Bytes in an output.
55pub const OUTPUT_LEN: usize = 32;
56/// The most elements one batch may hold. Batches are worked on the stack,
57/// a point is 96 bytes, and this crate builds for targets with a few
58/// kilobytes of it.
59pub const MAX_BATCH: usize = 64;
60
61/// The suite's identifier, as the context string names it.
62const IDENTIFIER: &[u8] = b"P256-SHA256";
63
64/// Which of RFC 9497's three protocols.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum Mode {
67    /// The base OPRF.
68    Oprf,
69    /// Verifiable: the server proves which key it used.
70    Voprf,
71    /// Partially oblivious: verifiable, with a public `info` bound in.
72    Poprf,
73}
74
75impl Mode {
76    const fn id(self) -> u8 {
77        match self {
78            Self::Oprf => 0,
79            Self::Voprf => 1,
80            Self::Poprf => 2,
81        }
82    }
83}
84
85/// `"OPRFV1-" || I2OSP(mode, 1) || "-" || "P256-SHA256"`.
86fn context(mode: Mode) -> [u8; 20] {
87    let mut c = [0u8; 20];
88    c[..7].copy_from_slice(b"OPRFV1-");
89    c[7] = mode.id();
90    c[8] = b'-';
91    c[9..].copy_from_slice(IDENTIFIER);
92    c
93}
94
95/// A domain separation tag: `prefix || contextString`.
96fn dst(prefix: &[u8], mode: Mode, out: &mut [u8; 40]) -> usize {
97    let ctx = context(mode);
98    out[..prefix.len()].copy_from_slice(prefix);
99    out[prefix.len()..prefix.len() + ctx.len()].copy_from_slice(&ctx);
100    prefix.len() + ctx.len()
101}
102
103fn hash_to_group(mode: Mode, input: &[u8]) -> Result<Point<P256>> {
104    let mut d = [0u8; 40];
105    let n = dst(b"HashToGroup-", mode, &mut d);
106    let p = hash_to_curve_point(&[input], &d[..n])?;
107    ensure!(
108        !bool::from(p.is_identity()),
109        InvalidParameter,
110        "oprf input hashes to the identity"
111    );
112    Ok(p)
113}
114
115fn hash_to_scalar_ctx(mode: Mode, parts: &[&[u8]]) -> Result<Scalar> {
116    let mut d = [0u8; 40];
117    let n = dst(b"HashToScalar-", mode, &mut d);
118    hash_to_scalar(parts, &d[..n])
119}
120
121fn serialize(p: &Point<P256>) -> Result<[u8; ELEMENT_LEN]> {
122    let affine = p.to_affine().ok_or(ic_core::err!(
123        InvalidParameter,
124        "oprf element is the identity"
125    ))?;
126    let mut out = [0u8; ELEMENT_LEN];
127    let _ = affine.write_compressed(&mut out);
128    Ok(out)
129}
130
131/// A compressed point on the curve, never the identity: RFC 9497 section
132/// 4.3's `DeserializeElement`.
133fn deserialize(bytes: &[u8]) -> Result<Point<P256>> {
134    ensure!(
135        bytes.len() == ELEMENT_LEN && (bytes[0] == 0x02 || bytes[0] == 0x03),
136        MalformedEncoding,
137        "oprf element must be a 33-byte compressed point"
138    );
139    let a = AffinePoint::<P256>::from_sec1(bytes).ok_or(ic_core::err!(
140        MalformedEncoding,
141        "oprf element is not on the curve"
142    ))?;
143    Ok(Point::from_affine(&a))
144}
145
146fn scalar_from(bytes: &[u8]) -> Result<Scalar> {
147    let mut b = [0u8; SCALAR_LEN];
148    ensure!(
149        bytes.len() == SCALAR_LEN,
150        InvalidLength,
151        "oprf scalar length"
152    );
153    b.copy_from_slice(bytes);
154    let s = Scalar::from_bytes(&b).ok_or(ic_core::err!(
155        MalformedEncoding,
156        "oprf scalar is not below the group order"
157    ));
158    b.zeroize();
159    s
160}
161
162/// A uniform nonzero scalar by rejection sampling, RFC 9497 section 4.7.1.
163fn random_scalar<R: RandomSource + ?Sized>(rng: &mut R) -> Result<Scalar> {
164    let mut b = [0u8; SCALAR_LEN];
165    for _ in 0..64 {
166        rng.fill(&mut b)?;
167        if let Some(s) = Scalar::from_bytes(&b) {
168            if !bool::from(s.is_zero()) {
169                b.zeroize();
170                return Ok(s);
171            }
172        }
173    }
174    b.zeroize();
175    Err(ic_core::err!(EntropyFailure, "oprf: no scalar in 64 draws"))
176}
177
178fn length_prefixed(h: &mut Sha256, bytes: &[u8]) {
179    h.update(&(bytes.len() as u16).to_be_bytes());
180    h.update(bytes);
181}
182
183/// The POPRF tweak `m = HashToScalar("Info" || I2OSP(len(info), 2) || info)`.
184fn tweak(info: &[u8]) -> Result<Scalar> {
185    ensure!(
186        info.len() <= u16::MAX as usize,
187        InvalidLength,
188        "oprf info too long"
189    );
190    hash_to_scalar_ctx(
191        Mode::Poprf,
192        &[b"Info", &(info.len() as u16).to_be_bytes(), info],
193    )
194}
195
196/// `ComputeComposites`: with the key, `Z = k * M`; without it, the sum of
197/// `d_i * D[i]`.
198fn composites(
199    mode: Mode,
200    k: Option<&Scalar>,
201    b: &Point<P256>,
202    c: &[Point<P256>],
203    d: &[Point<P256>],
204) -> Result<(Point<P256>, Point<P256>)> {
205    let bm = serialize(b)?;
206    let mut seed_dst = [0u8; 40];
207    let n = dst(b"Seed-", mode, &mut seed_dst);
208    let mut h = Sha256::new();
209    length_prefixed(&mut h, &bm);
210    length_prefixed(&mut h, &seed_dst[..n]);
211    let seed = h.finalize();
212    let mut m = Point::<P256>::identity();
213    let mut z = Point::<P256>::identity();
214    for (i, (ci, di)) in c.iter().zip(d.iter()).enumerate() {
215        let (cs, ds) = (serialize(ci)?, serialize(di)?);
216        let weight = hash_to_scalar_ctx(
217            mode,
218            &[
219                &(seed.as_ref().len() as u16).to_be_bytes(),
220                seed.as_ref(),
221                &(i as u16).to_be_bytes(),
222                &(cs.len() as u16).to_be_bytes(),
223                &cs,
224                &(ds.len() as u16).to_be_bytes(),
225                &ds,
226                b"Composite",
227            ],
228        )?;
229        m = ci.mul_scalar_vartime(&weight).add(&m);
230        if k.is_none() {
231            z = di.mul_scalar_vartime(&weight).add(&z);
232        }
233    }
234    if let Some(k) = k {
235        z = m.mul_scalar(k);
236    }
237    Ok((m, z))
238}
239
240fn challenge(
241    mode: Mode,
242    b: &Point<P256>,
243    m: &Point<P256>,
244    z: &Point<P256>,
245    t2: &Point<P256>,
246    t3: &Point<P256>,
247) -> Result<Scalar> {
248    let (bm, a0, a1, a2, a3) = (
249        serialize(b)?,
250        serialize(m)?,
251        serialize(z)?,
252        serialize(t2)?,
253        serialize(t3)?,
254    );
255    let len = (ELEMENT_LEN as u16).to_be_bytes();
256    hash_to_scalar_ctx(
257        mode,
258        &[
259            &len,
260            &bm,
261            &len,
262            &a0,
263            &len,
264            &a1,
265            &len,
266            &a2,
267            &len,
268            &a3,
269            b"Challenge",
270        ],
271    )
272}
273
274/// `GenerateProof(k, A, B, C, D)` with `A` the generator.
275fn generate_proof<R: RandomSource + ?Sized>(
276    mode: Mode,
277    k: &Scalar,
278    b: &Point<P256>,
279    c: &[Point<P256>],
280    d: &[Point<P256>],
281    rng: &mut R,
282) -> Result<[u8; PROOF_LEN]> {
283    let (m, z) = composites(mode, Some(k), b, c, d)?;
284    let r = random_scalar(rng)?;
285    let t2 = Point::<P256>::mul_generator(&r);
286    let t3 = m.mul_scalar(&r);
287    let ch = challenge(mode, b, &m, &z, &t2, &t3)?;
288    let s = r.sub(&ch.mul(k));
289    let mut proof = [0u8; PROOF_LEN];
290    proof[..32].copy_from_slice(&ch.to_bytes());
291    proof[32..].copy_from_slice(&s.to_bytes());
292    Ok(proof)
293}
294
295/// `VerifyProof(A, B, C, D, proof)` with `A` the generator.
296fn verify_proof(
297    mode: Mode,
298    b: &Point<P256>,
299    c: &[Point<P256>],
300    d: &[Point<P256>],
301    proof: &[u8; PROOF_LEN],
302) -> Result<()> {
303    let (m, z) = composites(mode, None, b, c, d)?;
304    let ch = scalar_from(&proof[..32])?;
305    let s = scalar_from(&proof[32..])?;
306    let t2 = Point::<P256>::mul_generator(&s).add(&b.mul_scalar_vartime(&ch));
307    let t3 = m.mul_scalar_vartime(&s).add(&z.mul_scalar_vartime(&ch));
308    let expected = challenge(mode, b, &m, &z, &t2, &t3)?;
309    ensure!(
310        bool::from(expected.ct_eq(&ch)),
311        AuthenticationFailed,
312        "oprf proof"
313    );
314    Ok(())
315}
316
317fn finalize_hash(input: &[u8], info: Option<&[u8]>, n: &Point<P256>) -> Result<[u8; OUTPUT_LEN]> {
318    ensure!(
319        input.len() <= u16::MAX as usize,
320        InvalidLength,
321        "oprf input too long"
322    );
323    let unblinded = serialize(n)?;
324    let mut h = Sha256::new();
325    length_prefixed(&mut h, input);
326    if let Some(info) = info {
327        length_prefixed(&mut h, info);
328    }
329    length_prefixed(&mut h, &unblinded);
330    h.update(b"Finalize");
331    let mut out = [0u8; OUTPUT_LEN];
332    out.copy_from_slice(h.finalize().as_ref());
333    Ok(out)
334}
335
336/// A server's key for one mode.
337pub struct ServerKey {
338    mode: Mode,
339    secret: [u8; SCALAR_LEN],
340    public: [u8; ELEMENT_LEN],
341}
342
343impl Drop for ServerKey {
344    fn drop(&mut self) {
345        self.secret.zeroize();
346    }
347}
348
349impl ServerKey {
350    fn from_scalar(mode: Mode, sk: &Scalar) -> Result<Self> {
351        ensure!(
352            !bool::from(sk.is_zero()),
353            InvalidParameter,
354            "oprf key is zero"
355        );
356        let public = serialize(&Point::<P256>::mul_generator(sk))?;
357        Ok(Self {
358            mode,
359            secret: sk.to_bytes(),
360            public,
361        })
362    }
363
364    fn scalar(&self) -> Result<Scalar> {
365        scalar_from(&self.secret)
366    }
367
368    /// A fresh key from `rng`. Pass `ic_drbg::Rng`.
369    pub fn generate<R: RandomSource + ?Sized>(mode: Mode, rng: &mut R) -> Result<Self> {
370        ic_core::module::operational()?;
371        Self::from_scalar(mode, &random_scalar(rng)?)
372    }
373
374    /// RFC 9497 section 3.2.1's `DeriveKeyPair`: a key from a 32-byte secret
375    /// seed and a public `info`.
376    pub fn derive(mode: Mode, seed: &[u8; 32], info: &[u8]) -> Result<Self> {
377        ic_core::module::operational()?;
378        ensure!(
379            info.len() <= u16::MAX as usize,
380            InvalidLength,
381            "oprf info too long"
382        );
383        let mut d = [0u8; 40];
384        let n = dst(b"DeriveKeyPair", mode, &mut d);
385        for counter in 0u8..=255 {
386            let sk = hash_to_scalar(
387                &[seed, &(info.len() as u16).to_be_bytes(), info, &[counter]],
388                &d[..n],
389            )?;
390            if !bool::from(sk.is_zero()) {
391                return Self::from_scalar(mode, &sk);
392            }
393        }
394        Err(ic_core::err!(
395            InvalidParameter,
396            "oprf DeriveKeyPair found no key"
397        ))
398    }
399
400    /// A key from its 32-byte scalar.
401    pub fn from_bytes(mode: Mode, secret: &[u8]) -> Result<Self> {
402        ic_core::module::operational()?;
403        Self::from_scalar(mode, &scalar_from(secret)?)
404    }
405
406    /// The 32-byte scalar, for storing the key.
407    pub fn to_bytes(&self) -> ic_core::Zeroizing<[u8; SCALAR_LEN]> {
408        ic_core::Zeroizing::new(self.secret)
409    }
410
411    /// The public key, which a VOPRF or POPRF client needs.
412    pub fn public_key(&self) -> [u8; ELEMENT_LEN] {
413        self.public
414    }
415
416    /// The mode this key serves.
417    pub fn mode(&self) -> Mode {
418        self.mode
419    }
420
421    /// Evaluate one blinded element; see [`ServerKey::blind_evaluate_batch`].
422    pub fn blind_evaluate<R: RandomSource + ?Sized>(
423        &self,
424        blinded: &[u8; ELEMENT_LEN],
425        info: &[u8],
426        rng: &mut R,
427    ) -> Result<([u8; ELEMENT_LEN], Option<[u8; PROOF_LEN]>)> {
428        let mut evaluated = [[0u8; ELEMENT_LEN]; 1];
429        let proof = self.blind_evaluate_batch(&[*blinded], info, rng, &mut evaluated)?;
430        Ok((evaluated[0], proof))
431    }
432
433    /// Evaluate blinded elements, writing one evaluated element for each, and
434    /// return the batched proof in the verifiable modes.
435    ///
436    /// `info` is the POPRF's public input and must be empty in the other two
437    /// modes. A POPRF `info` that hashes to the negated key -- which only
438    /// someone who knows the key can find -- is refused, as RFC 9497 says,
439    /// and is a sign the key is known.
440    pub fn blind_evaluate_batch<R: RandomSource + ?Sized>(
441        &self,
442        blinded: &[[u8; ELEMENT_LEN]],
443        info: &[u8],
444        rng: &mut R,
445        evaluated: &mut [[u8; ELEMENT_LEN]],
446    ) -> Result<Option<[u8; PROOF_LEN]>> {
447        ic_core::module::operational()?;
448        ensure!(
449            !blinded.is_empty() && blinded.len() == evaluated.len() && blinded.len() <= MAX_BATCH,
450            InvalidLength,
451            "oprf batch: 1 to MAX_BATCH elements, one output each"
452        );
453        ensure!(
454            self.mode == Mode::Poprf || info.is_empty(),
455            InvalidParameter,
456            "oprf info is only for the POPRF mode"
457        );
458        let sk = self.scalar()?;
459        // The POPRF evaluates under t = skS + m, inverted.
460        let k = if self.mode == Mode::Poprf {
461            let t = sk.add(&tweak(info)?);
462            ensure!(
463                !bool::from(t.is_zero()),
464                InvalidParameter,
465                "oprf: info maps to the key"
466            );
467            t.invert()
468        } else {
469            sk
470        };
471        for (b, e) in blinded.iter().zip(evaluated.iter_mut()) {
472            *e = serialize(&deserialize(b)?.mul_scalar(&k))?;
473        }
474        if self.mode == Mode::Oprf {
475            return Ok(None);
476        }
477        // In the POPRF the proof runs the other way: blinded = t * evaluated,
478        // under the tweaked key t * G.
479        let c_list = decode_all(if self.mode == Mode::Poprf {
480            evaluated
481        } else {
482            blinded
483        })?;
484        let d_list = decode_all(if self.mode == Mode::Poprf {
485            blinded
486        } else {
487            evaluated
488        })?;
489        let (proof_key, b) = if self.mode == Mode::Poprf {
490            let t = sk.add(&tweak(info)?);
491            (t, Point::<P256>::mul_generator(&t))
492        } else {
493            (sk, deserialize(&self.public)?)
494        };
495        let proof = generate_proof(
496            self.mode,
497            &proof_key,
498            &b,
499            &c_list[..blinded.len()],
500            &d_list[..blinded.len()],
501            rng,
502        )?;
503        Ok(Some(proof))
504    }
505
506    /// RFC 9497's `Evaluate`: the PRF output for `input` computed directly
507    /// with the key, as the client would arrive at it. `info` is the POPRF's
508    /// and must be empty otherwise.
509    pub fn evaluate(&self, input: &[u8], info: &[u8]) -> Result<[u8; OUTPUT_LEN]> {
510        ic_core::module::operational()?;
511        ensure!(
512            self.mode == Mode::Poprf || info.is_empty(),
513            InvalidParameter,
514            "oprf info is only for the POPRF mode"
515        );
516        let element = hash_to_group(self.mode, input)?;
517        let sk = self.scalar()?;
518        if self.mode == Mode::Poprf {
519            let t = sk.add(&tweak(info)?);
520            ensure!(
521                !bool::from(t.is_zero()),
522                InvalidParameter,
523                "oprf: info maps to the key"
524            );
525            finalize_hash(input, Some(info), &element.mul_scalar(&t.invert()))
526        } else {
527            finalize_hash(input, None, &element.mul_scalar(&sk))
528        }
529    }
530}
531
532/// Decode up to [`MAX_BATCH`] elements into a fixed array.
533fn decode_all(elements: &[[u8; ELEMENT_LEN]]) -> Result<[Point<P256>; MAX_BATCH]> {
534    ensure!(
535        elements.len() <= MAX_BATCH,
536        InvalidLength,
537        "oprf batch too large"
538    );
539    let mut out: [Point<P256>; MAX_BATCH] = core::array::from_fn(|_| Point::identity());
540    for (slot, e) in out.iter_mut().zip(elements.iter()) {
541        *slot = deserialize(e)?;
542    }
543    Ok(out)
544}
545
546/// What a client keeps between blinding and finalizing.
547pub struct BlindState {
548    mode: Mode,
549    blind: [u8; SCALAR_LEN],
550    /// The server's public key in the VOPRF, the tweaked key in the POPRF.
551    key: Option<[u8; ELEMENT_LEN]>,
552}
553
554impl Drop for BlindState {
555    fn drop(&mut self) {
556        self.blind.zeroize();
557    }
558}
559
560/// Blind `input` for evaluation.
561///
562/// The VOPRF and POPRF need the server's public key, to verify its proof
563/// later; the POPRF also binds the public `info`, which must be empty in the
564/// other modes. Returns the state to keep and the blinded element to send.
565pub fn blind<R: RandomSource + ?Sized>(
566    mode: Mode,
567    input: &[u8],
568    info: &[u8],
569    server_public_key: Option<&[u8; ELEMENT_LEN]>,
570    rng: &mut R,
571) -> Result<(BlindState, [u8; ELEMENT_LEN])> {
572    ic_core::module::operational()?;
573    ensure!(
574        mode == Mode::Poprf || info.is_empty(),
575        InvalidParameter,
576        "oprf info is only for the POPRF mode"
577    );
578    let key = match mode {
579        Mode::Oprf => None,
580        Mode::Voprf | Mode::Poprf => {
581            let pk = server_public_key.ok_or(ic_core::err!(
582                InvalidParameter,
583                "the verifiable modes need the server's public key"
584            ))?;
585            let pk_point = deserialize(pk)?;
586            if mode == Mode::Poprf {
587                let tweaked = Point::<P256>::mul_generator(&tweak(info)?).add(&pk_point);
588                ensure!(
589                    !bool::from(tweaked.is_identity()),
590                    InvalidParameter,
591                    "oprf: info maps to the negated key"
592                );
593                Some(serialize(&tweaked)?)
594            } else {
595                Some(*pk)
596            }
597        }
598    };
599    let r = random_scalar(rng)?;
600    let element = hash_to_group(mode, input)?;
601    let blinded = serialize(&element.mul_scalar(&r))?;
602    Ok((
603        BlindState {
604            mode,
605            blind: r.to_bytes(),
606            key,
607        },
608        blinded,
609    ))
610}
611
612/// Finish one evaluation; see [`finalize_batch`].
613pub fn finalize(
614    input: &[u8],
615    info: &[u8],
616    state: &BlindState,
617    blinded: &[u8; ELEMENT_LEN],
618    evaluated: &[u8; ELEMENT_LEN],
619    proof: Option<&[u8; PROOF_LEN]>,
620) -> Result<[u8; OUTPUT_LEN]> {
621    let mut out = [[0u8; OUTPUT_LEN]; 1];
622    finalize_batch(
623        &[input],
624        info,
625        core::slice::from_ref(state),
626        &[*blinded],
627        &[*evaluated],
628        proof,
629        &mut out,
630    )?;
631    Ok(out[0])
632}
633
634/// Unblind a batch of evaluations, writing one output for each input.
635///
636/// In the verifiable modes the server's proof is checked first, over the
637/// whole batch, and a bad proof is `AuthenticationFailed` with nothing
638/// written. Every state must come from [`blind`] in the same mode with the
639/// same key and `info`.
640pub fn finalize_batch(
641    inputs: &[&[u8]],
642    info: &[u8],
643    states: &[BlindState],
644    blinded: &[[u8; ELEMENT_LEN]],
645    evaluated: &[[u8; ELEMENT_LEN]],
646    proof: Option<&[u8; PROOF_LEN]>,
647    out: &mut [[u8; OUTPUT_LEN]],
648) -> Result<()> {
649    ic_core::module::operational()?;
650    let n = inputs.len();
651    ensure!(
652        (1..=MAX_BATCH).contains(&n)
653            && states.len() == n
654            && blinded.len() == n
655            && evaluated.len() == n
656            && out.len() == n,
657        InvalidLength,
658        "oprf batch: 1 to MAX_BATCH of each, equal in number"
659    );
660    let mode = states[0].mode;
661    ensure!(
662        states
663            .iter()
664            .all(|s| s.mode == mode && s.key == states[0].key),
665        InvalidParameter,
666        "oprf batch mixes modes or keys"
667    );
668    ensure!(
669        mode == Mode::Poprf || info.is_empty(),
670        InvalidParameter,
671        "oprf info is only for the POPRF mode"
672    );
673    if mode != Mode::Oprf {
674        let proof = proof.ok_or(ic_core::err!(
675            InvalidParameter,
676            "the verifiable modes need the server's proof"
677        ))?;
678        let key = deserialize(&states[0].key.ok_or(ic_core::err!(
679            InvalidParameter,
680            "the verifiable modes need the server's public key"
681        ))?)?;
682        let blinded_points = decode_all(blinded)?;
683        let evaluated_points = decode_all(evaluated)?;
684        let (c, d) = if mode == Mode::Poprf {
685            (&evaluated_points[..n], &blinded_points[..n])
686        } else {
687            (&blinded_points[..n], &evaluated_points[..n])
688        };
689        verify_proof(mode, &key, c, d, proof)?;
690    }
691    for i in 0..n {
692        let r = scalar_from(&states[i].blind)?;
693        let unblinded = deserialize(&evaluated[i])?.mul_scalar(&r.invert());
694        out[i] = finalize_hash(inputs[i], (mode == Mode::Poprf).then_some(info), &unblinded)?;
695    }
696    Ok(())
697}
698
699/// Check that `element` is a valid serialized element: compressed, on the
700/// curve, not the identity. For protocols built on this group, such as
701/// OPAQUE, which receive keys in the same encoding.
702pub fn validate_element(element: &[u8; ELEMENT_LEN]) -> Result<()> {
703    deserialize(element).map(|_| ())
704}
705
706/// Diffie-Hellman in this group as OPAQUE-3DH defines it for P-256:
707/// `private * public`, serialized as an element -- the compressed point,
708/// not the bare x-coordinate ordinary ECDH yields. `public` is validated.
709pub fn diffie_hellman(
710    private: &[u8; SCALAR_LEN],
711    public: &[u8; ELEMENT_LEN],
712) -> Result<[u8; ELEMENT_LEN]> {
713    ic_core::module::operational()?;
714    let k = scalar_from(private)?;
715    ensure!(
716        !bool::from(k.is_zero()),
717        InvalidParameter,
718        "oprf private key is zero"
719    );
720    serialize(&deserialize(public)?.mul_scalar(&k))
721}
722
723/// The known-answer test: RFC 9497 appendix A.3.1.1, the OPRF mode's first
724/// vector, evaluated with the derived key.
725pub fn self_test() -> Result<()> {
726    let seed = [0xa3u8; 32];
727    let key = ServerKey::derive(Mode::Oprf, &seed, b"test key")?;
728    let mut want_sk = [0u8; 32];
729    ic_core::codec::hex_decode(
730        b"159749d750713afe245d2d39ccfaae8381c53ce92d098a9375ee70739c7ac0bf",
731        &mut want_sk,
732    )?;
733    ensure!(
734        ic_core::ct::verify(key.to_bytes().as_ref(), &want_sk),
735        SelfTestFailed,
736        "oprf"
737    );
738    let mut want = [0u8; 32];
739    ic_core::codec::hex_decode(
740        b"a0b34de5fa4c5b6da07e72af73cc507cceeb48981b97b7285fc375345fe495dd",
741        &mut want,
742    )?;
743    ensure!(
744        ic_core::ct::verify(&key.evaluate(&[0x00], b"")?, &want),
745        SelfTestFailed,
746        "oprf"
747    );
748    Ok(())
749}
750
751#[cfg(test)]
752mod tests {
753    use super::*;
754
755    #[test]
756    fn the_self_test_passes() {
757        self_test().unwrap();
758    }
759
760    #[test]
761    fn the_context_string_is_rfc_9497_s() {
762        assert_eq!(&context(Mode::Poprf), b"OPRFV1-\x02-P256-SHA256");
763    }
764
765    /// Not the identity, not uncompressed, not off the curve.
766    #[test]
767    fn elements_are_validated() {
768        assert!(deserialize(&[0u8; 33]).is_err());
769        let mut uncompressed = [0u8; 65];
770        uncompressed[0] = 4;
771        assert!(deserialize(&uncompressed).is_err());
772        let mut off = [0u8; 33];
773        off[0] = 2;
774        off[32] = 7; // x = 7 is not: x^3 - 3x + b is a non-residue, checked by Euler's criterion
775        assert!(deserialize(&off).is_err());
776    }
777}