# The Server Helped, and Never Saw: A Field Guide to Oblivious Pseudorandom Functions

> How a server applies its secret key to your secret input without ever seeing it: OPRF, VOPRF, and POPRF (RFC 9497), the engine of OPAQUE and Privacy Pass.

*Published: 2026-07-13*
*Canonical: https://paragmali.com/blog/the-server-helped-and-never-saw-a-field-guide-to-oblivious-p*
*© Parag Mali. All rights reserved.*

---
<TLDR>
**An oblivious pseudorandom function (OPRF) lets a server apply its secret key to your secret input so that it learns neither the input nor the output.** That single move is the engine under OPAQUE logins, Privacy Pass tokens, and private password-breach checks [@rfc9497]. The trick is David Chaum's 1982 blinding transplanted from RSA into a Diffie-Hellman group [@chaum82]: hash your input to a curve point, multiply by a fresh random blind, let the server multiply by its key, then divide the blind back out. RFC 9497 (2023) pins this down as three modes -- OPRF, VOPRF, and POPRF -- that answer three questions: is it blind, which key produced the answer, and how many times can one key be asked before it bleeds out [@rfc9497]. This is the field guide to that engine: every generation that built it, every real break that shaped it, and every way it still gets misused in production code.
</TLDR>

## 1. The Server Hardened Your Password Without Ever Seeing It

You want to make a stolen password database worthless. The standard defense is to key every stored password with a secret the attacker cannot exfiltrate -- a "pepper" that lives only in your server's memory -- so a dumped hash is uncrackable without also breaking the keyholder. This is the logic behind server-assisted password hardening, and it is exactly what OPAQUE was designed to formalize [@opaque-paper].

But look closely at the obvious way to apply that key. To hash a password under the pepper, the server has to be handed the password. That is the one thing you were trying not to do. The service that hardens your secret must first be shown your secret. It feels like a law of nature: to key a value, you have to see the value.

It is not a law of nature. Here is the whole resolution, and it fits in four lines.

The client hashes its password to a point on an elliptic curve, $P = H(pw)$. It picks a fresh uniformly random scalar $r$ and sends not $P$ but $r \cdot P$, a uniformly random point. The server multiplies by its secret key: $k \cdot (r \cdot P)$. The client divides the blind back out to get $k \cdot P$, and hashes that: $F(k, pw) = H_2(pw,\, k \cdot H(pw))$.

Now state exactly what the server saw. It saw $r \cdot P$: a uniformly random point on the curve, and nothing else. It never saw the password. It never saw the output. It applied its key to a value it could not read, and handed back a value it could not read either.

<Definition term="Oblivious Pseudorandom Function (OPRF)">
A two-party protocol that computes $F(k, x)$ where the server holds the key $k$, the client holds the input $x$, and when the protocol finishes the server has learned neither $x$ nor the output, while the client has learned only $F(k, x)$ and nothing about $k$ [@rfc9497].
</Definition>

<Definition term="Pseudorandom Function (PRF)">
A keyed function $F(k, x)$ whose outputs are indistinguishable from those of a truly random function to anyone who does not hold $k$. It is the "keyed lock": only the keyholder can compute it, and its outputs reveal nothing about the key [@rfc9497].
</Definition>

<Definition term="Blinding factor">
The fresh, uniformly random scalar $r$ that the client multiplies into $H(x)$ before sending it. Because $r$ is uniform and independent of the input, the transmitted point $r \cdot H(x)$ is itself uniform and carries no information about $x$ [@rfc9497].
</Definition>

<Mermaid caption="The OPRF round-trip: the client blinds its input to a uniform point, the server applies its key, and the client unblinds and hashes out. The server only ever sees a random point.">
sequenceDiagram
    participant C as Client (holds input x)
    participant S as Server (holds key k)
    Note over C: P holds HashToGroup(x)
    Note over C: pick fresh random blind r
    Note over C: blindedElement holds r times P
    C->>S: send blindedElement, a uniform random point
    Note over S: evaluatedElement holds k times blindedElement
    S->>C: return evaluatedElement
    Note over C: divide out r to recover k times P
    Note over C: output holds Hash of x with that point
</Mermaid>

> **Key idea:** The oblivious PRF applies a secret key to a secret input so that neither party learns the other's secret. Everything else in this guide is a variation on that one move, or a story about what happens when one half of it is done wrong.

That two-line trick is not a party piece. The *same* two lines are how Privacy Pass hands you a token that a server can validate but cannot link back to you [@dgstv18]; how Google's Password Checkup tells you a credential appears in a breach corpus without Google learning the credential [@thomas19]; and how two organizations compute the intersection of their customer lists without either revealing its list [@fipr05]. Replace "the component that used to have to see the secret" with a blind, verifiable, rate-limited oracle that does not, and you have described every one of these systems at once.

Because the move is always the same, you can interrogate any deployment with the same three questions. **Is it blind?** Does the server ever see the input, or link two evaluations of it? **Which key?** Can the client prove the answer was produced by the one key the server committed to? **How many asks?** How many blind evaluations can one key answer before an attacker can reconstruct it? Hold onto those three questions. They are the spine of this article, and by the end you will see that every named failure in the primitive's history is one of them answered wrong.

The 2HashDH shape $F(k, x) = H_2(x,\, H_1(x)^k)$ we just wrote by hand is the actual construction that ships today [@jkk14]. If it feels like Diffie-Hellman run one-sided, that is exactly right: Part 18 argued that nobody has broken [the discrete logarithm](/blog/nobody-broke-the-discrete-log-a-field-guide-to-diffie-hellma/), and this is its constructive twin -- the same group structure, run so that a server *applies* a secret rather than agreeing on one. If you are wondering why we [harden passwords](/blog/the-one-job-of-a-password-hash-why-fast-is-the-enemy-and-mem/) at all, Part 12 is the prerequisite. Here we take the hardening as given and ask how a server can perform it blind.

<RunnableCode lang="js" title="The blind hides the input; unblinding recovers the same value (structural model, not real crypto)">{`
// STRUCTURAL MODEL, NOT REAL CRYPTO. It shows only the SHAPE of blind/evaluate/unblind.
// Model an elliptic-curve group of prime order q: a "point" is an integer mod q,
// and "scalar multiplication" scalar*point is multiplication mod q. BigInt avoids overflow.
const q = 2147483647n;                       // a prime group order (toy)
const mod = (a) => ((a % q) + q) % q;
const scalarMul = (s, point) => mod(s * point);
function invScalar(a) {                       // inverse of a scalar mod q (q prime), via Fermat
  let r = 1n, b = mod(a), e = q - 2n;
  while (e > 0n) { if (e & 1n) r = mod(r * b); b = mod(b * b); e >>= 1n; }
  return r;
}

const k  = 1337n;                             // SERVER secret key (client never learns it)
const Hx = 991n;                              // H(x): the hashed input, as a group element
const r  = 42424243n;                         // CLIENT fresh random blind

const blinded   = scalarMul(r, Hx);           // client SENDS r*H(x): looks uniformly random
const evaluated = scalarMul(k, blinded);      // server returns k*(r*H(x))
const unblinded = scalarMul(invScalar(r), evaluated); // client removes r: recovers k*H(x)
const direct    = scalarMul(k, Hx);           // what F would be if the server had seen x

console.log('server saw            = ' + blinded.toString() + ' (independent of H(x)=' + Hx.toString() + ')');
console.log('client recovered      = ' + unblinded.toString());
console.log('equals direct k*H(x)  = ' + (unblinded === direct));
`}</RunnableCode>

That trick is forty years old, and for its first two decades it did not even have a name. To understand why it is *secure*, and why there turned out to be *three* versions of it, we have to start where the blinding started: in 1982, with digital cash.

## 2. Blinding, Before It Had a Name

The move is older than the problem it now solves. In 1982, David Chaum wanted untraceable digital cash: a way for a bank to sign a coin without learning which coin it signed, so the coin could later be spent without the bank linking it back to the withdrawal. His solution was blinding [@chaum82]. The client multiplies its message by a random factor, the bank signs the blinded message under its [RSA key](/blog/rsa-is-a-trapdoor-not-a-cryptosystem-oaep-pss-and-the-25-yea/), and the client divides the factor out to recover a valid signature on the original. Blind, let the keyholder act, unblind. Structurally, that is the exact template an OPRF runs on.<Sidenote>Chaum's 1982 construction used multiplicative blinding in an RSA group, and its output was a publicly verifiable *signature*. The OPRF performs the same blind-act-unblind dance in a Diffie-Hellman group, and its output is a pseudorandom value only the querier can read. Part 14 covers the RSA lineage in full.</Sidenote>

So if the trick existed in 1982, why did the OPRF take until 2005 to be named and 2023 to be standardized? Because Chaum's version produced the wrong kind of output, and the right kind needed a second ingredient that did not exist yet.

Watch the shape of what Chaum built. A blind *signature* is public: anyone with the bank's public key can verify it. That is perfect for a coin you want a merchant to check, and exactly wrong for a pepper you want no one to be able to reproduce. To harden a password, you do not want a verifiable signature on it; you want a pseudorandom value that only the keyholder could have produced and only the querier can read.

Chaum had the blinding. He did not have a *pseudorandom* thing to blind. This is why "Blind RSA," standardized decades later as RFC 9474, is not an OPRF but its publicly verifiable cousin -- a distinction that will matter enormously when we reach Privacy Pass [@rfc9474].

The second ingredient arrived in 1997. Moni Naor and Omer Reingold built a pseudorandom function out of pure number theory: $F_k(x)$ computed as a single group exponentiation whose exponent is a subset-product of the key, $g^{\prod a_i^{x_i}}$, provably secure under the Decisional Diffie-Hellman assumption [@naorreingold97]. What mattered was not the security proof but the *shape*. Because the function was built from group exponentiations, it had enough algebraic structure that a blinded input could pass through it and come out blinded -- the homomorphic property that makes oblivious evaluation possible at all. A random hash function scrambles its input beyond recovery; an algebraically structured PRF lets the blind survive the trip.

Now the two ingredients were on the table. In 2005, Michael Freedman, Yuval Ishai, Benny Pinkas, and Omer Reingold fused them, gave first constructions, and coined the term: the **Oblivious Pseudorandom Function** [@fipr05]. They were not thinking about passwords. They wanted private keyword search and private set intersection -- letting one party query another's database, or intersect two sets, without either side revealing its contents.

The abstraction they named is the one we still use: transplant Chaum's blinding out of RSA, point it at a Diffie-Hellman-structured PRF, and you get a value that is pseudorandom rather than a signature, and blind rather than seen.<Sidenote>RFC 9497 standardizes *multiplicative* blinding, the variant described here: its `Blind` step computes `blindedElement = blind * inputElement`, and that multiplicative form is what ships.</Sidenote>

<Mermaid caption="From Chaum's 1982 blinding to the standardized primitive: the ingredients (blinding, non-interactive proofs, an algebraic PRF) accumulate for two decades before the OPRF is named and then standardized.">
flowchart LR
    A["1982 Chaum blinding in RSA"] --> B["1986 Fiat-Shamir transform"]
    B --> C["1992 Chaum-Pedersen DLEQ proof"]
    C --> D["1997 Naor-Reingold algebraic PRF"]
    D --> E["2005 FIPR names the OPRF"]
    E --> F["2009 Jarecki-Liu efficient DH OPRF"]
    F --> G["2014 2HashDH, the workhorse"]
    G --> H["2018 Privacy Pass VOPRF"]
    H --> I["2022 3HashSDHI POPRF"]
    I --> J["2023 RFC 9497 standard"]
    J --> K["2025 RFC 9807 OPAQUE"]
</Mermaid>

Two other pieces on that timeline were quietly waiting to become load-bearing. In 1986, Fiat and Shamir showed how to turn an interactive proof into a one-shot, non-interactive one by replacing the verifier's random challenge with a hash of the transcript [@fiatshamir86]. In 1992, Chaum and Pedersen gave a zero-knowledge proof that two discrete logarithms are equal [@chaumped92]. Neither was about OPRFs. Both become the machinery of *verifiability* when, twenty-six years later, Privacy Pass needs a client to check which key the server used. Keep them in view; they return in Section 5.

The problem was defined and it had a name. What it did not have was a construction cheap enough to ship. And the first one that looked practical turned out to have a slow poison built into it.

## 3. The First Construction That Got Weaker With Use

Naming a primitive is not the same as having a recipe worth shipping. The constructions that came with the 2005 definition were general and powerful and, for a deployed service, awkward. The oblivious-transfer and oblivious-polynomial-evaluation approaches were round-heavy. The Naor-Reingold-based ones inherited the PRF's per-bit structure: they consumed the input one bit at a time, so their cost scaled with the input length rather than staying flat.<Sidenote>The linear-in-input-length cost of the early Naor-Reingold-style OPRFs is an inference from that PRF's per-bit construction rather than a benchmark quoted from the 2005 paper; treat it as a structural signpost, not a measured figure.</Sidenote> For private set intersection in a research prototype, fine. For a server answering millions of blind queries a second, not yet.

The efficient turn came in 2009. Stanislaw Jarecki and Xiaomin Liu built the first OPRF that was both fast and *committed* -- verifiable -- in a prime-order group [@jareckiliu09]. Instead of the exponentiation-per-bit structure, it used a Dodis-Yampolskiy-style PRF of the form $g^{1/(k+x)}$: the server inverts $k + x$ in the exponent, so evaluation is a single group operation regardless of input length.

And because the construction committed to the key, the client got assurance not just that *some* key was applied but that it was the server's key. In the language of our three questions, this one answered Q1 and Q2 at once. It looked like the deployable OPRF.

Then look at the assumption it rested on. Security of the $g^{1/(k+x)}$ family reduces to a *q-strong-Diffie-Hellman* assumption: hardness holds as long as the attacker cannot exploit a growing collection of key-dependent samples. And three years earlier, in 2006, Jung Hee Cheon had shown precisely how to exploit them. Cheon's analysis proved that the strong-Diffie-Hellman family weakens as the attacker obtains more samples tied to the same secret -- security *degrades with the number of queries* [@cheon06].

Sit with what that means for an OPRF specifically. An OPRF's entire job is to be offered as a service: to answer blind query after blind query, indefinitely. But every answer this construction gave was another key-dependent sample, and Cheon's result said each sample chipped away at the secret. The primitive got *weaker the more it was used*. A lock that loosens every time you turn the key is a strange thing to install on a vault you intend to open a billion times.

> **Note:** Most cryptographic primitives are static: a hash function is exactly as strong after a trillion evaluations as after one. The 2009 Jarecki-Liu OPRF was not. Its security was a budget that the act of answering queries spent down, because each blind evaluation handed the attacker another sample against the strong-Diffie-Hellman assumption it relied on [@cheon06]. For a primitive whose whole purpose is to be queried without limit, that is a design-level contradiction, not a tuning problem. This is our third question -- *how many asks?* -- appearing for the first time, and appearing as a wound rather than a footnote.

This is worth marking as special: it is the only true assumption-level break in the whole story. Everything else that supersedes something in the lineage does so because it is cheaper, or answers one more question. Only here does a construction fall because the mathematical ground under it gave way. The strong-Diffie-Hellman assumption was the wrong foundation, and no engineering on top could fix a foundation that eroded by design.

The fix was not a cleverer attack defense. It was to stop building on an assumption that decays -- to hash the input to *one* group element and re-base the whole construction on an assumption that does not weaken sample by sample. That single stroke produced the workhorse that every deployment now runs, and it is where the OPRF stops being a research object and becomes infrastructure.

## 4. Moving the Server From Trusted to Blind

Here is the whole next fifteen years compressed into one sentence: each generation moves the server one step from "must be trusted, sees the secret" toward "blind, verifiable, query-limited," and -- the part that surprises people -- most of those steps are *additive*. A later construction rarely throws out its predecessor. It bolts one more guarantee onto it. To read the lineage correctly you have to separate two kinds of arrow: **superseded by**, a genuine replacement, and **extended by**, an additive answer to one more question with the old construction still running underneath.

### The workhorse: 2HashDH

In 2014, Stanislaw Jarecki, Aggelos Kiayias, and Hugo Krawczyk wrote down the construction we built by hand in Section 1: $F(k, x) = H_2(x,\, H_1(x)^k)$ [@jkk14]. Hash the input into the group with $H_1$, exponentiate by the key, hash out with $H_2$.

It is round-optimal -- one message each way, and you provably cannot do it in fewer. And its security rests on the **One-More Gap Diffie-Hellman** assumption rather than the strong-Diffie-Hellman family, which is the whole point: One-More Gap security is not a function of a growing pile of key-dependent samples, so it sidesteps the Cheon erosion that doomed the 2009 construction.<Sidenote>"One-More" assumptions bound how much an attacker can produce relative to how many genuine evaluations it was given -- it cannot get "one more" than it was handed. This framing is what breaks the dependence on a growing q-tuple that made the strong-Diffie-Hellman family decay under Cheon's attack.</Sidenote> Because the input is hashed to a single group element, cost is flat in input length. Jarecki, Kiayias, Krawczyk, and Jiayu Xu put it on composable footing two years later [@jkkx16].

<Definition term="2HashDH">
The construction $F(k, x) = H_2(x,\, H_1(x)^k)$: hash the input into a prime-order group, raise it to the secret key, and hash the result back out with the input bound in. It is the OPRF and VOPRF core standardized in RFC 9497 [@jkk14], [@rfc9497].
</Definition>

This is the OPRF that ships. But notice what it does *not* do. It answers Q1 -- the server never sees the input -- and nothing more. The client has no way to tell *which* key the server used. A malicious server can quietly assign a different key to each client and, later, use the distinct responses to tell which client was which. In an anonymity system that is fatal: the primitive designed to hide you becomes the tag that identifies you. 2HashDH left Q2 open. The next generation did not replace it; it extended it.

### Verifiability you can batch: the VOPRF

In 2018, Alex Davidson, Ian Goldberg, Nick Sullivan, George Tankersley, and Filippo Valsorda shipped Privacy Pass, and with it the deployed **VOPRF** [@dgstv18]. The addition is precisely the 1992 machinery we set aside. The server publishes a commitment to its key -- a public key $pk_S = k \cdot G$ -- and alongside each response attaches a Chaum-Pedersen proof that the *same* $k$ relates its public key and its answer, made non-interactive by Fiat-Shamir [@chaumped92], [@fiatshamir86]. The client verifies before it accepts. Answering Q2, at last.

The engineering move that made it deployable was batching: many evaluations fold into a *single* proof via a random linear combination, so a client redeeming a thousand tokens verifies one proof, not a thousand [@dgstv18]. That is what let Privacy Pass run at content-delivery-network scale. That scale is what surfaced the primitive's real-world failure modes -- key consistency, tagging, rotation, double-spend -- that no research prototype had stress-tested.

<Definition term="VOPRF (Verifiable OPRF)">
An OPRF augmented with a proof that the server applied the key committed in its published public key, so a client can reject an answer computed under any other key. In RFC 9497 the proof is a batchable Chaum-Pedersen DLEQ [@rfc9497], [@dgstv18].
</Definition>

The VOPRF still left one gap. Its verifiability binds *one* key to *one* public key, which means one key cannot cleanly serve many domains, tenants, or rotation epochs -- to separate them you would need a different key per context, and a different public key per context is, again, a tag. What was missing was a way to fold a *public* label into the evaluation without changing the key.

### Partial obliviousness: heavy, then light

The first answer came early and expensive. In 2015, Adam Everspaugh, Rahul Chatterjee, Samuel Scott, Ari Juels, and Thomas Ristenpart built Pythia, the first **partially oblivious** PRF: a public per-tenant tweak let one server key serve many domains and support bulk key rotation, while the password stayed oblivious [@pythia]. It worked, but it needed bilinear pairings -- special curves, heavier operations, a bigger dependency. Powerful, but not the thing you reach for by default.

The light answer came in 2022. Nirvan Tyagi, Sofia Celi, Thomas Ristenpart, Nick Sullivan, Stefano Tessaro, and Christopher Wood built **3HashSDHI**: the first POPRF that needs no pairings [@tcrstw22]. It folds the public input `info` into the key by inversion. The server computes $t = k + H(\text{info})$ and returns $t^{-1} \cdot \text{blindedElement}$, with a DLEQ proof against the tweaked key. The private input stays oblivious; `info` is public and known to both sides. And it runs, in the authors' words, as fast as the standards-track 2HashDH protocol [@tcrstw22].

<Definition term="POPRF (Partially Oblivious PRF)">
A VOPRF that also binds a *public* input `info` into the evaluation, computing $F(k, x, \text{info})$ while keeping $x$ oblivious. One published key can then serve many public contexts -- tenants, rotation epochs, rate-limit buckets. A POPRF with a fixed `info` is functionally a VOPRF [@rfc9497], [@tcrstw22].
</Definition>

Here is the detail worth savoring. To fold in `info`, 3HashSDHI uses key inversion -- $t^{-1}$ where $t = k + H(\text{info})$ -- which is the *same* Dodis-Yampolskiy inversion structure Jarecki and Liu used back in 2009 [@tcrstw22], [@jareckiliu09]. The mechanism that got the field burned by Cheon's attack was never actually wrong. What was wrong was the *assumption it had been analyzed under*. Tyagi and co-authors re-proved the same inversion under a One-More Gap assumption that reduces to plain discrete log in the algebraic group model, and the old idea walked back onto the field rehabilitated [@tcrstw22].

The lesson is one every protocol designer should internalize: a construction can be sound while the security assumption used to justify it is not, and better analysis can resurrect a "broken" mechanism untouched.

<Mermaid caption="The OPRF lineage as a ladder: solid arrows are genuine replacements (superseded by), dashed reasoning is additive (extended by). Only one step, Cheon's erosion of strong-Diffie-Hellman, is a cryptanalytic break. KKRT16 is a separate mechanism for the same goal.">
flowchart TD
    G0["Gen 0: Chaum blinding plus Naor-Reingold PRF"] -->|superseded| G1["Gen 1: FIPR names the OPRF, generic"]
    G1 -->|superseded on cost| G2["Gen 2: Jarecki-Liu committed DH OPRF"]
    G2 -->|superseded by Cheon erosion| G3["Gen 3: 2HashDH, answers Q1"]
    G3 -->|extended, adds Q2| G4["Gen 4: VOPRF with batched DLEQ"]
    G4 -->|extended, adds public info| G5["Gen 5: POPRF 3HashSDHI"]
    G5 --> RFC["RFC 9497: three modes, one interface"]
    G3 -.->|parallel mechanism| K["KKRT16 OT-extension OPRF, for PSI"]
</Mermaid>

### The other lineage, in one breath

Not every OPRF is a Diffie-Hellman OPRF. In 2016, Vladimir Kolesnikov, Ranjit Kumaresan, Mike Rosulek, and Ni Trieu built a completely different one: a batched OPRF from oblivious-transfer extension, symmetric-key rather than public-key, and enormously fast in bulk [@kkrt16]. It is not verifiable, not publicly reusable, and only semi-honest secure, but it intersects two sets of a million ($2^{20}$) elements each in about 3.8 seconds [@kkrt16], and OT-extension OPRFs of this kind were for years the throughput workhorse of high-volume semi-honest PSI, until VOLE-based PSI overtook them in communication for very large sets [@rindalvolepsi]. The takeaway for a practitioner: "OPRF" names a *goal*, and at least two very different mechanisms reach it. Do not assume every system labeled OPRF is the standardized Diffie-Hellman family.

| Approach | Year | Key idea | Questions answered | Fate |
|----------|------|----------|--------------------|------|
| Chaum blind signature | 1982 | Blind, sign, unblind in RSA | none (a signature, not a PRF) | Ancestor; became Blind RSA [@chaum82] |
| FIPR generic OPRF | 2005 | Obliviously evaluate an algebraic PRF; names "OPRF" | Q1 | Superseded on cost [@fipr05] |
| Jarecki-Liu | 2009 | $g^{1/(k+x)}$, committed | Q1 + Q2 | Superseded: strong-DH erodes with use [@jareckiliu09] |
| 2HashDH | 2014 | $H_2(x, H_1(x)^k)$; One-More Gap CDH | Q1 | Active core of RFC 9497 [@jkk14] |
| VOPRF | 2018 | 2HashDH + batched Chaum-Pedersen DLEQ | Q1 + Q2 | Active; extends 2HashDH [@dgstv18] |
| Pythia | 2015 | Verifiable partially oblivious PRF, pairings | Q1 + Q2 + info | Superseded by 3HashSDHI [@pythia] |
| 3HashSDHI | 2022 | $(k + H(\text{info}))^{-1}$; no pairings | Q1 + Q2 + info | Active POPRF of RFC 9497 [@tcrstw22] |
| KKRT16 | 2016 | Batched OPRF from OT-extension (symmetric) | Q1 (relaxed, semi-honest) | Active, niche: high-volume PSI [@kkrt16] |

Twenty years of constructions now answered all three questions. But each was a separate paper with its own parameters, its own group choices, and its own foot-guns. The last move was not a new construction. It was a standard that unified them and, unusually for a specification, wrote the one unavoidable cost down as a number.

## 5. RFC 9497: Three Modes, One Interface

In December 2023, the IRTF's Crypto Forum Research Group published RFC 9497, written by Alex Davidson, Armando Faz-Hernandez, Nick Sullivan, and Christopher Wood [@rfc9497]. It did what twenty years of individual papers had not: it turned the whole family into a single misuse-resistant interface, fixed five ciphersuites, mandated the safety checks, and -- the honest move no construction paper had made -- wrote the one attack it cannot design away down as a number.

Start with the interface, because all three modes share it. Over a prime-order group with generator $G$, and using the [hash-to-group machinery of RFC 9380](/blog/the-doorway-into-the-group-a-field-guide-to-hashing-a-string/), the protocol is three calls [@rfc9497], [@rfc9380]:

```
Blind(x):
    blind           <- random nonzero scalar               # fresh, uniform, per call
    blindedElement  <- blind * HashToGroup(x)              # a uniformly random point
    if blindedElement == Identity: abort                   # element validation
    return blind, blindedElement

BlindEvaluate(k, blindedElement, [info]):
    OPRF / VOPRF:  evaluatedElement <- k * blindedElement
    POPRF:         t <- k + HashToScalar(info)
                   evaluatedElement <- t^(-1) * blindedElement
    # VOPRF and POPRF also emit a DLEQ proof (see below)

Finalize(x, blind, evaluatedElement, [info]):
    N <- blind^(-1) * evaluatedElement    # OPRF/VOPRF: = k * HashToGroup(x); POPRF: = HashToGroup(x)^(1/(k+H(info)))
    return Hash( len(x) || x || [len(info) || info ||] len(N) || N || "Finalize" )
```

The server sees only `blindedElement`, a uniformly random point independent of $x$; the client recovers exactly one PRF value and nothing about $k$ [@rfc9497]. That is the same 2HashDH we have been circling, now with every byte of the output hash pinned down.<Sidenote>`HashToGroup` is RFC 9380 hash-to-curve, the subject of Part 16. Its defining property is that the output point has an *unknown* discrete logarithm: no one, including the client, knows a scalar relating it to the generator. That unknown-discrete-log property is exactly why blinding hides the input and why the server cannot invert the map.</Sidenote>

### The three modes are three answers

This is where the second shift in understanding lands. The three modes are not three products. They are three points on one axis, distinguished by exactly which of our questions they answer.

The **base OPRF** answers Q1 and stops. `BlindEvaluate` is just $k \cdot \text{blindedElement}$, with no proof. The client trusts that *some* key was applied; it gets no assurance about which. Cheapest mode, smallest messages, right only when a wrong key can hurt no one but the querier [@rfc9497].

The **VOPRF** answers Q1 and Q2. The server commits to a public key $pk_S = k \cdot G$ and attaches a proof; the client verifies before finalizing [@rfc9497]. The **POPRF** answers Q1 and Q2 and binds a public `info`, via the key-inversion tweak. A POPRF with a fixed `info` collapses back to a VOPRF [@rfc9497]. Base to verifiable to partially oblivious: blind, then blind-plus-which-key, then blind-plus-which-key-plus-public-tweak.

> **Key idea:** Three modes are three answers to three questions. OPRF answers "is it blind?" VOPRF adds "which key?" POPRF adds "and bind this public label." You do not choose a mode by taste; you choose it by counting how many of the three questions your threat model forces you to answer.

<Mermaid caption="The three modes as an additive ladder: each mode answers one more question than the last, and the earlier construction keeps running underneath.">
flowchart LR
    Q1["Q1: is it blind?"] --> OPRF["OPRF: blind only"]
    OPRF -->|add DLEQ proof against one committed key| VOPRF["VOPRF: blind plus which key"]
    VOPRF -->|fold in a public info tweak| POPRF["POPRF: blind, which key, public label"]
</Mermaid>

### Verifiability, in depth

The proof is the 1992 machinery finally doing its job. A Chaum-Pedersen DLEQ proof establishes that two discrete logarithms are equal: that the $k$ relating $pk_S$ to $G$ is the *same* $k$ relating `evaluatedElement` to `blindedElement` [@rfc9497], [@chaumped92]. If the server used any other key, no valid proof exists. Fiat-Shamir turns the interactive proof into a single non-interactive object the client checks offline, so verifiability costs no extra round [@fiatshamir86].<Sidenote>The DLEQ proof is a sigma protocol made non-interactive by Fiat-Shamir -- the same transform behind [Schnorr signatures](/blog/the-math-held-the-interface-leaked-a-field-guide-to-digital-/), which Part 17 covers. The client recomputes the challenge as a hash of the transcript and checks one equation; there is no back-and-forth with the server.</Sidenote>

<Definition term="DLEQ proof (Chaum-Pedersen)">
A non-interactive zero-knowledge proof that two discrete logarithms are equal -- here, that the server's public key and its response were produced under the same secret key. It is what makes a VOPRF verifiable, and via a random linear combination it can prove a whole batch of responses with one object [@rfc9497], [@chaumped92].
</Definition>

Batching is what makes verifiability affordable. Many evaluations fold into one proof through a random linear combination, so a client redeeming a batch verifies a single object, not one proof per element [@rfc9497], [@dgstv18]. That is one proof object per batch, not $n$ -- but verifying it still forms one linear combination across the batch, so the win is skipping $n$ separate proof checks, not making verification constant-time in the batch size. That is what let anonymous-token issuance scale.

There is a subtlety that quietly carries the entire security of any anonymity system built on a VOPRF: verifiability is only as strong as *key consistency*. Proving the server used the key in its public key is worthless if the server hands a *different* public key to every client, because then the unique key is itself a deanonymizing tag. Verifiability protects you only when everyone is served under *one* transparently published key [@rfc9497]. We will return to this in the practical guide; for now, note that "add a proof" and "pin one key" are two requirements, not one.

<Mermaid caption="The VOPRF round-trip: the server attaches a DLEQ proof that its committed public key and its response share the same secret key, and the client verifies before finalizing.">
sequenceDiagram
    participant C as Client (input x)
    participant S as Server (key k, public pkS)
    Note over C: blindedElement holds blind times HashToGroup(x)
    C->>S: send blindedElement
    Note over S: evaluatedElement holds k times blindedElement
    Note over S: GenerateProof binds pkS and the answer to one k
    S->>C: return evaluatedElement and DLEQ proof
    Note over C: VerifyProof against pkS, abort on failure
    Note over C: unblind, then Finalize with domain separation
</Mermaid>

### The machinery that makes it safe

A specification earns "misuse-resistant" in the details, and RFC 9497's details are where the accumulated scar tissue lives. Hashing and group operations are delegated, not reinvented: `HashToGroup` and `HashToScalar` come from RFC 9380, and the groups themselves from RFC 9496 -- [ristretto255 and decaf448](/blog/the-curve-was-hard-the-gap-was-soft-a-field-guide-to-using-e/) -- or the NIST curves [@rfc9497], [@rfc9380], [@rfc9496].<Sidenote>ristretto255 and decaf448 are prime-order abstractions built over Edwards curves whose raw order has a small cofactor. They remove the cofactor pitfalls that Part 15 covers -- the small-subgroup and equivalent-point traps -- which is exactly why RFC 9497 prefers them and why it mandates rejecting the identity element on deserialization.</Sidenote>

Three requirements are worth naming because dropping any one silently breaks the guarantee:

> **Note:** RFC 9497 requires that every deserialized element be *validated* -- on-curve, in the right range, and not the identity -- or an attacker can probe the key with malformed inputs [@rfc9497]. It requires that operations on secret data, *including* `GenerateProof` and `BlindEvaluate`, run in *constant time*, or the server leaks its key through timing [@rfc9497]. And it requires *domain separation*: the literal `"Finalize"` tag and the mode context string keep one application's evaluations from colliding with another's [@rfc9497]. None of these is optional hardening; each is load-bearing for a property the primitive claims.

The [domain separation](/blog/one-secret-is-not-one-key-the-discipline-of-key-derivation-w/) is the discipline Part 13 argued for, applied here: the output hash binds the input length, the input, the point, and a fixed label, so no two contexts can be coaxed into producing the same value. Inputs are bounded -- shorter than $2^{16}-1$ bytes, with a two-byte length prefix -- so longer inputs must be pre-hashed [@rfc9497].

<Definition term="Unlinkability (unconditional input secrecy)">
The guarantee that the server cannot link two evaluations of the same input, nor learn anything about the input. Because the blind is fresh and uniform, this holds *even against an adversary with unbounded computation* -- it is information-theoretic, not merely computational [@rfc9497], [@dgstv18].
</Definition>

<RunnableCode lang="js" title="Domain separation: the Finalize tag and length prefixes stop cross-context collisions (structural model)">{`
// STRUCTURAL MODEL. Shows why Finalize binds lengths + a "Finalize" tag, not just N.
function toyHash(str) {                       // a tiny non-cryptographic stand-in
  let h = 5381 >>> 0;
  for (const ch of str) h = (Math.imul(h, 33) ^ ch.charCodeAt(0)) >>> 0;
  return (h >>> 0).toString(16).padStart(8, '0');
}
// length-prefixed encoding: len(x) || x, so boundaries are unambiguous
function lp(s) { return String(s.length).padStart(4, '0') + s; }

// RFC 9497 Finalize binds: len(x) || x || len(N) || N || "Finalize"
function finalize(x, N) { return toyHash(lp(x) + lp(N) + 'Finalize'); }

// Without length prefixes, ("ab","c") and ("a","bc") would collide. With them, they don't:
console.log('naive  ab|c   =', toyHash('ab' + 'c'));
console.log('naive  a|bc   =', toyHash('a' + 'bc'), '(collides above)');
console.log('finalize ab,c =', finalize('ab', 'c'));
console.log('finalize a,bc =', finalize('a', 'bc'), '(distinct)');
`}</RunnableCode>

Finally, the honesty. RFC 9497 section 7.2.3 states outright that every `BlindEvaluate` is a static-Diffie-Hellman oracle sample, and quantifies the erosion: the best-known attacks reduce the group's security by $\log_2(Q)/2$ bits, where $Q$ is the number of calls [@rfc9497], [@browngallant04], [@cheon06]. That is the domesticated ghost of the very failure that killed the 2009 construction -- no longer a surprise, but a budget line. We give it the full treatment it deserves in Section 8. First: who actually runs this, and what looks like it but is not.

## 6. Who Runs This Today

The OPRF is quietly load-bearing. You almost certainly used one this week without knowing it, because it lives one layer below the protocols you *do* know. Here are the four deployed consumers, each at "exactly where and why it calls the OPRF," and each paired with the look-alike an expert must not confuse it with.

**OPAQUE, standardized as RFC 9807 in July 2025.** This is the flagship. OPAQUE is an augmented password-authenticated key exchange: the server authenticates you by password without ever seeing the password, even at registration, and with no cleartext salt an attacker could precompute against [@rfc9807], [@opaque-paper]. The OPRF is the tool that makes the password oblivious, and it is what defeats precomputation attacks -- there is nothing static to build a rainbow table against.

RFC 9807 derives a *per-user* OPRF key from a single seed, keyed by each `credential_identifier`, so a server compromise does not let an attacker enumerate accounts, and it discusses a threshold-OPRF defense that forces an attacker online before any dictionary attack can even begin [@rfc9807]. The RFC names WhatsApp's end-to-end encrypted backups as a driving use case, and Meta's audited `opaque-ke` is the reference implementation [@rfc9807], [@opaque-ke]. If Part 12 explained why offline dictionary attacks are the thing to fear, OPAQUE is the OPRF's answer to them.

**Privacy Pass, RFCs 9576, 9577, and 9578, June 2024.** The in-scope token type is `0x0001 = VOPRF(P-384, SHA-384)`; RFC 9578 says plainly that its privately verifiable issuance is based on the OPRF [@rfc9578], [@rfc9576], [@rfc9577]. This is the VOPRF most engineers meet by name. But Privacy Pass is *not one thing*, and this is the boundary the whole article has been protecting.

<Aside label="Privacy Pass is not an OPRF (only half of it is)">
RFC 9578 defines two interchangeable token types. Token `0x0001` is a VOPRF, and it is an OPRF. Token `0x0002` is Blind RSA (2048-bit), standardized in RFC 9474, and it is a blind *signature* -- Chaum's 1982 construction, publicly verifiable, and emphatically *not* an OPRF [@rfc9578], [@rfc9474]. The most visible Privacy Pass deployment in the world, Apple's Private Access Tokens, uses the Blind-RSA token, not the VOPRF one. The two make a real, standardized trade-off: VOPRF tokens are issuer-verifiable only, small, and easy to rotate; Blind-RSA tokens are publicly verifiable with the RSA public key but larger and heavier to rotate [@rfc9578]. So never write "Privacy Pass equals OPRF." One of its two token types is not an OPRF at all -- a point the earlier field-guide entry on anonymous credentials shipping develops further.
</Aside>

**Password breach checking.** Google's Password Checkup performs a blinded, OPRF-based lookup: it checks your credential against a corpus of breached ones without Google learning the credential [@thomas19], [@google-blog]. Microsoft's Password Monitor in Edge does something related but not identical, and the difference matters.

<Aside label="Password Monitor is OPRF plus homomorphic encryption, not OPRF alone">
It is tempting to describe Microsoft Password Monitor as "an OPRF breach check," but that undersells the machinery. The OPRF is one layer; the actual set-matching runs on *homomorphic encryption* -- Labeled Private Set Intersection built on the Microsoft SEAL library, evaluating the match under encryption [@msft-monitor]. The OPRF hides the query; the fully homomorphic layer performs the private lookup against the breach set. Do not attribute the set intersection to the OPRF alone. And note the well-known *non-OPRF* alternative for the same job: Have I Been Pwned's k-anonymity range API, where the client sends the first five hex digits of a SHA-1 hash and receives the whole bucket to search locally [@cloudflare-kanon]. That leaks a hash prefix by design; it is prefix-bucketed hashing, simpler and cache-friendly, but not oblivious. The earlier entry on [Edge's two password cryptographies](/blog/edge-two-password-cryptographies/) traces this contrast in detail.
</Aside>

**OPRF-based private set intersection.** OPRF-PSI is *one approach among several* -- plain Diffie-Hellman PSI, oblivious-transfer and circuit-based PSI, and homomorphic-encryption PSI all compete for different workloads [@fipr05]. Even within OPRF-PSI, the two mechanisms from Section 4 occupy different corners: the public-key 2HashDH family when you want few messages and verifiability, and the symmetric KKRT16 OPRF when you want raw throughput over enormous sets [@kkrt16].

| Consumer | What the OPRF does there | The non-OPRF look-alike to not confuse it with |
|----------|-------------------------|-----------------------------------------------|
| OPAQUE (RFC 9807) | Makes the password oblivious; defeats precomputation; per-user keys block enumeration [@rfc9807] | A plain salted or peppered hash, where the server *must* see the password |
| Privacy Pass token `0x0001` | VOPRF issuance of unlinkable, issuer-verifiable tokens [@rfc9578] | Token `0x0002` = Blind RSA, a publicly verifiable blind *signature* (Apple PAT) [@rfc9474] |
| Google Password Checkup | Blinded OPRF lookup against a breach corpus [@thomas19] | HIBP k-anonymity range API: leaks a SHA-1 prefix, not oblivious [@cloudflare-kanon] |
| Microsoft Password Monitor | OPRF hides the query; FHE Labeled PSI does the match [@msft-monitor] | Assuming the OPRF alone does the set intersection |
| OPRF-based PSI | Oblivious set membership via 2HashDH or KKRT16 [@kkrt16] | DH-PSI, OT/circuit-PSI, and FHE-PSI, which are different approaches |

The production substrates are a short list: Cloudflare's CIRCL in Go, the `voprf` crate and `opaque-ke` in Rust, and curve25519-dalek providing the ristretto255 group backend [@circl], [@voprf-crate], [@opaque-ke], [@dalek]. Four deployments, one primitive, three look-alikes that are emphatically not it. When you actually have to choose, the decision comes down to a small matrix.

## 7. The Decision Matrix

Given a real problem, each decision has a crisp rule. Here they are, head to head.

### Which mode?

The primary choice is the mode, and it falls straight out of the three questions.

| Dimension | OPRF (base) | VOPRF | POPRF |
|-----------|-------------|-------|-------|
| Questions answered | Q1 (blind) | Q1 + Q2 (which key) | Q1 + Q2 + public tweak |
| Extra output | none | DLEQ proof (batchable) | DLEQ proof (batchable) |
| Public input `info` | no | no | yes, $F(k, x, \text{info})$ |
| Construction | 2HashDH | 2HashDH + DLEQ | 3HashSDHI |
| Assumption | One-More Gap CDH | One-More Gap CDH | One-More Gap SDHI (to q-DL, AGM) |
| Client trust in key | must trust | verifies | verifies |
| Best suited for | trusted-key internal oracle | anonymity systems, OPAQUE | per-tenant, rotation, buckets |

Sources: RFC 9497 sections 3.3.1 through 3.3.3, plus DGSTV18 and TCRSTW22 [@rfc9497], [@dgstv18], [@tcrstw22]. **The rule:** if you trust the server's key by construction and need only obliviousness, use the base OPRF. If any adversarial server could hand you a wrong or per-client key -- anonymity systems, OPAQUE -- use the VOPRF. If you need to bind a *public* label such as a per-tenant salt, rotation epoch, or rate-limit bucket, use the POPRF, and never put a secret in `info`.

### Which ciphersuite?

Pick the ciphersuite by your query budget, because the group size is what buys you static-Diffie-Hellman headroom (Section 8).

| Ciphersuite | Group | Nominal security | Use when |
|-------------|-------|------------------|----------|
| ristretto255-SHA512 | ristretto255 | 128-bit | Default: prime-order, fast, no cofactor traps |
| decaf448-SHAKE256 | decaf448 | 224-bit | Internet-facing oracle needing static-DH headroom |
| P-256-SHA256 | NIST P-256 | 128-bit | FIPS or interop constraints |
| P-384-SHA384 | NIST P-384 | 192-bit | Higher budget; Privacy Pass `0x0001` uses this |
| P-521-SHA512 | NIST P-521 | 256-bit | Maximum static-Diffie-Hellman headroom |

Source: RFC 9497 sections 4.1 through 4.5 [@rfc9497]. **The rule:** default to ristretto255-SHA512. If you expose an *unmetered public* oracle on a 128-bit group, the $\log_2(Q)/2$ erosion argues for decaf448, P-384, or P-521 -- RFC 9497 recommends exactly these for applications that cannot tolerate discrete-logarithm security below 128 bits [@rfc9497]. Choose P-256-SHA256 only when FIPS or interoperability forces it. And never roll your own group or hash-to-curve; use RFC 9380 and RFC 9496 [@rfc9380], [@rfc9496].

### Which token cryptography?

If you are issuing anonymous tokens, the choice between the two Privacy Pass token types is a genuine trade-off, not a default.

| Dimension | VOPRF token `0x0001` | Blind-RSA token `0x0002` |
|-----------|----------------------|--------------------------|
| Primitive | OPRF (VOPRF, P-384/SHA-384) | Blind *signature* (RSA-2048), not an OPRF |
| Verifiability | Issuer-verifiable only (secret key) | Publicly verifiable (RSA public key) |
| Token size | Small | Larger |
| Key rotation | Easy | Heavier (RSA keypair) |
| Flagship deployment | CDN anonymous rate-limiting | Apple Private Access Tokens |
| Spec | RFC 9578, RFC 9497 | RFC 9578, RFC 9474 |

Sources: RFC 9578 and RFC 9474 [@rfc9578], [@rfc9474]. **The rule:** if only the issuer needs to verify tokens and you want small, easily rotated tokens, use the VOPRF token. If a *third party* must verify a token without contacting the issuer, you need public verifiability, and that means a blind signature -- the Blind-RSA token -- which is not an OPRF at all.

### The remaining two, briefly

For private set intersection, choose the public-key 2HashDH OPRF when you want few messages and malicious-secure verifiability, and the symmetric KKRT16 OPRF when you want to intersect enormous sets fast under a semi-honest model [@jkk14], [@kkrt16]. For breach checking, choose an OPRF when query privacy is a hard requirement, and k-anonymity when simplicity and cache-friendliness outweigh the leak of a hash prefix [@thomas19], [@cloudflare-kanon].

Every row above assumes one thing worth making explicit, because it is the property the whole family exists to deliver. Against a plain salted or peppered server-side hash, the server *must see the secret* to key it. That is the single compromise the OPRF removes, and each decision in this matrix is a variation on how much structure you are willing to add on top of "the server never saw it." Which is worth asking: what, precisely, can this primitive prove -- and what can it provably not?

## 8. What the Assumptions Buy, and What They Cost

If the discrete logarithm has never been broken, what can still go wrong with an OPRF -- and what can never be fixed? The answer separates cleanly into what the primitive provably delivers, what it rests on, and the one price it can never stop paying.

**What it delivers.** RFC 9497 section 7.1 enumerates the guarantees [@rfc9497]. Pseudorandomness: for a random key, the output is indistinguishable from a random function. Nonmalleability: you cannot turn one evaluation into another. Verifiability, in the VOPRF and POPRF modes. Partial obliviousness, in the POPRF. And the one worth pausing on -- *unconditional input secrecy*. Because the blind is fresh and uniform, the point the server receives is uniform and independent of the input, so input secrecy holds even against an adversary with unbounded computation [@rfc9497]. That is not "secure until someone finds a faster algorithm." It is information-theoretic. The secret was never in the message, so there is nothing for computation to extract.

**What it rests on.** The OPRF and VOPRF rest on the well-studied One-More Gap Computational Diffie-Hellman assumption; the POPRF on One-More Gap Strong-Diffie-Hellman-Inversion [@rfc9497], [@jkk14], [@tcrstw22]. The POPRF's assumption comes with a load-bearing result: it is shown to be implied by the plain $q$-discrete-logarithm assumption in the algebraic group model [@tcrstw22]. All three are discrete-log-family assumptions believed hard in the groups actually used, so the deployed OPRF is anchored to the same discrete-log problem Part 18 argued has stood unbroken for half a century. That is about as good a foundation as applied cryptography offers.

**What it costs, forever.** Now the third question comes due. Every `BlindEvaluate` is, by construction, a static-Diffie-Hellman oracle sample: the server computes $x \mapsto x^k$ for a fixed secret $k$, and Brown-Gallant and Cheon showed that collecting such samples recovers $k$ at a cost that drops with the count [@rfc9497], [@browngallant04], [@cheon06].

<Definition term="Static Diffie-Hellman oracle">
A service that, for a fixed secret key $k$, answers queries of the form $x \mapsto x^k$. Each answer is a sample that erodes the key: after $Q$ queries, the best-known attacks recover $k$ at a cost reduced by $\log_2(Q)/2$ bits (the Brown-Gallant and Cheon bound). Offering an OPRF *is* offering this oracle [@browngallant04], [@cheon06].
</Definition>

The specification does not bury this. It states it:

<PullQuote>
"Best-known attacks reduce the security of the prime-order group instantiation by log_2(Q) / 2 bits, where Q is the number of BlindEvaluate calls." -- RFC 9497, section 7.2.3
</PullQuote>

Work the arithmetic and the design pressure becomes concrete. On a 128-bit group, effective key security is $128 - \log_2(Q)/2$ bits. Answer $2^{40}$ queries and you have shed 20 bits, down to 108. Answer $2^{60}$ and you are at 98. This is not an implementation flaw you can patch; it is a lower bound on the attacker's advantage that *no construction in this family removes*, because it follows from the primitive doing the one thing it exists to do.

| BlindEvaluate calls $Q$ | Bits lost, $\log_2(Q)/2$ | Effective security on a 128-bit group |
|-------------------------|--------------------------|----------------------------------------|
| $2^{20}$ | 10 | 118 bits |
| $2^{40}$ | 20 | 108 bits |
| $2^{60}$ | 30 | 98 bits |
| $2^{80}$ | 40 | 88 bits |

> **Note:** "Keep this key secret forever" and "answer unlimited public blind queries on a 128-bit group" cannot both hold. The static-Diffie-Hellman erosion is inherent to exposing the oracle, so an unmetered public OPRF on a 128-bit group provably cannot retain 128-bit key security [@rfc9497], [@browngallant04]. The only resolutions are operational: rate-limit `BlindEvaluate`, rotate the key on a schedule, or move to a larger group (decaf448, P-384, P-521). You are choosing a budget, not eliminating a cost.

<Aside label="The failure that domesticated itself">
Look at what just happened across five sections. The strong-Diffie-Hellman decay that doomed the 2009 Jarecki-Liu construction was treated then as a fatal flaw -- the reason to abandon a whole generation [@jareckiliu09], [@cheon06]. The *same physics* reappears in RFC 9497, but now it is a written-down budget with a formula and a mitigation list [@rfc9497]. Nothing about the mathematics changed. What changed is the engineering posture: a phenomenon that was a silent, disqualifying weakness became an explicit, managed policy knob. This is often how a field matures -- not by defeating a limit, but by naming it precisely enough to live with it.
</Aside>

Two more facts bound the theory. Round-optimality: two messages is provably the minimum, and 2HashDH achieves it, so there is no efficiency left to win there.<MarginNote>The two-message floor is structural: the client must send at least one blinded value and receive at least one response to recover an output, so no protocol of this shape can use fewer round-trips.</MarginNote> And two honest open edges. RFC 9497 admits that the exact multi-key, batched protocol the industry actually deploys has no security proof:

<PullQuote>
"There is currently no security analysis available for the VOPRF protocol described in this document in a setting with multiple server keys or batching." -- RFC 9497, section 7.2.1
</PullQuote>

The deployed reality -- many issuer keys, batched proofs -- outruns the single-key proof it is built on [@rfc9497]. And the whole family rests on discrete log, so a scalable quantum computer running Shor's algorithm breaks obliviousness and unforgeability alike. Two of these limits are not settled facts but open frontiers -- and the sharpest one opened with a construction that was broken within a year of being proposed.

## 9. Where It Is Still Moving

For the single-key, non-batched, classical setting, the theory is essentially closed: round-optimal, constant communication, unconditional input secrecy, a clean assumption reducing to discrete log. The live research is almost entirely about the edges the deployments already stand on.

**The post-quantum frontier, which opened with a cautionary tale.** Because every deployed OPRF falls to Shor, the sharpest open problem is a post-quantum replacement -- and the first serious attempt is exactly why the field is cautious.

In 2020, Dan Boneh, Dmitry Kogan, and Katharine Woo built the first substantial post-quantum OPRF from supersingular isogenies [@bkw20]. Within a year, Andrea Basso, Peter Kutas, Simon-Philipp Merz, Christophe Petit, and Antonio Sanso cryptanalyzed it, giving a polynomial-time attack and a subexponential one that survived the obvious countermeasures -- together breaking the parameters the authors had proposed [@basso21].

A construction proven secure under a named assumption was undone because the assumption itself did not hold. It is the cleanest possible reminder that a proof is only as good as what it reduces to.<Sidenote>The isogeny paper actually offered *two* constructions. Its second one revived the 1997 Naor-Reingold PRF, reinterpreted over commutative group actions (the CSIDH setting) [@bkw20], [@naorreingold97]. The oldest algebraic PRF in this story turned out to be the one with a plausible post-quantum future -- a twenty-three-year-old idea made newly relevant by a change of group.</Sidenote>

The other post-quantum directions are advancing but heavy. Martin Albrecht, Alex Davidson, Amit Deo, and Nigel Smart gave a round-optimal verifiable OPRF from ideal lattices in 2021 [@adds21]; Albrecht and Kamil Doruk Gur made it "practical-ish and thresholdisable" in 2024, cutting its lattice bandwidth roughly fourfold and adding a threshold variant [@albrechtgur24]. And a general framework arrived in 2025: Ward Beullens, Lucas Dodgson, Sebastian Faller, and Julia Hesse compiled quantum-safe OPRFs from secure multi-party evaluation of simple functions, reporting a working construction in about 0.57 seconds with under a megabyte of communication [@beullens25].

That is a genuine milestone -- and it is still orders of magnitude heavier than the sub-millisecond classical evaluation it aims to replace. An efficient, *verifiable*, *partially oblivious* post-quantum OPRF, matching the classical profile on all three counts at once, does not yet exist.

**The proof the industry is running ahead of.** Section 8's second pull quote is also an open problem: prove the multi-key, batched VOPRF that Privacy Pass actually deploys [@rfc9497]. Batching is a well-used heuristic, but the formal guarantee still covers only the single-key case.

**Removing the single point of failure.** A threshold or distributed OPRF splits the key across several servers so no one of them holds it, which is the structural answer to server compromise. RFC 9807 names exactly this as the defense that forces an attacker online before any dictionary attack can start [@rfc9807]. Round-optimal, misuse-resistant threshold OPRF specifications are still maturing.

**The limits that are policy, not cryptanalysis.** Two open problems are engineering rather than mathematics. The static-Diffie-Hellman budget is a permanent tax on any public oracle; the open question is operational -- what query cap and rotation cadence keep a given group above 128 bits -- not whether the bound can be improved, since Brown-Gallant and Cheon are believed essentially tight for generic groups [@browngallant04], [@cheon06].

And verifiability quietly collapses if a malicious issuer hands each user a different public key, so making "one key for everyone" *auditable* needs key-transparency infrastructure that does not yet ship as a turnkey component [@rfc9497]. Formally verified constant-time implementations sit in the same category: RFC 9497 mandates constant-time secret operations, but the guarantee is only as strong as the machine-checked code behind it [@rfc9497].

None of these block you from shipping today. They tell you what to watch. What you ship today has a small, boring set of rules that separate a correct deployment from a broken one -- and unlike the open problems, those rules are entirely settled.

## 10. The Rules, Made Operational

Here is the whole deployment discipline in a form a reviewer can apply without rereading the RFC. It has three parts: pick two things, always do a handful, and never do another handful.

**Pick the mode by the questions.** Trust the key by construction: base OPRF. Must verify which key: VOPRF. Must bind a public label: POPRF, and never smuggle a secret into `info`, which is public by definition [@rfc9497], [@tcrstw22]. **Pick the ciphersuite by the query budget.** Default to ristretto255-SHA512; for an internet-facing unmetered oracle move to decaf448, P-384, or P-521; for FIPS or interop use P-256-SHA256. Never roll your own group or hash-to-curve -- use RFC 9380 and RFC 9496 [@rfc9497], [@rfc9380], [@rfc9496].

<Mermaid caption="Operational mode selection: answer two or three yes/no questions to reach a mode, then one more to size the group.">
flowchart TD
    Start["Choose an OPRF deployment"] --> A&#123;"Could the server be adversarial about which key it uses?"&#125;
    A -->|no, key trusted| B["Base OPRF"]
    A -->|yes| C&#123;"Must you bind a public label such as tenant or epoch?"&#125;
    C -->|no| D["VOPRF"]
    C -->|yes| E["POPRF"]
    B --> F&#123;"Unmetered public oracle?"&#125;
    D --> F
    E --> F
    F -->|no| G["ristretto255-SHA512 default"]
    F -->|yes| H["decaf448 or P-384 or P-521"]
</Mermaid>

> **Note:** Every call: draw a *fresh, uniform blind*, and a fresh nonce for the DLEQ proof. Every received element: *validate* it -- on the curve, in range, and not the identity -- before you touch it [@rfc9497]. Every secret operation, including `BlindEvaluate` and `GenerateProof`: run it in *constant time* [@rfc9497]. Every output: *domain-separate* with the `"Finalize"` tag and mode context, and keep inputs within the length limit [@rfc9497]. For a VOPRF or POPRF: publish and pin *one* public key, transparently, for everyone. For a POPRF: treat an `InverseError` as a signal that the tweak collided and it is time to rotate [@tcrstw22]. And bound $Q$: rate-limit `BlindEvaluate` or rotate keys so the static-Diffie-Hellman loss stays inside budget [@rfc9497], [@browngallant04].

The "never" list is just the failure catalog from the last nine sections, re-read as review rules. Each entry maps to one of the three questions answered wrong.

| Anti-pattern | Why it breaks | Question violated | The fix |
|--------------|---------------|-------------------|---------|
| Sending the un-blinded input | The server sees the input; obliviousness is gone | Q1 | Always apply the fresh blind before sending [@rfc9497] |
| A MAC or HKDF "used as an OPRF" | If the server computes it, it *saw* the input | Q1 | Use a real two-party OPRF; the blind is the point [@rfc9497] |
| Reusing the blind across calls | Two evaluations become linkable, and the input can leak | Q1 | Fresh, uniform blind every call [@rfc9497] |
| Skipping element validation | Malformed points enable key-probing attacks | Q1/Q3 | Reject off-curve, out-of-range, and identity elements [@rfc9497] |
| Dropping or unsoundly batching the DLEQ | A malicious server can use a per-client key and tag you | Q2 | Verify the proof; combine batches soundly [@rfc9497], [@dgstv18] |
| A per-user public key | The unique key is itself a deanonymizing tag | Q2 | Pin one transparently published key for all [@rfc9497] |
| Non-constant-time secret ops | The key leaks through timing | Q3 | Constant-time group ops, proof, and evaluate [@rfc9497] |
| Treating POPRF `info` as secret | `info` is public by definition; obliviousness assumed of it fails | Q1 | Put only public labels in `info` [@tcrstw22] |
| An unmetered oracle on a 128-bit group | The key erodes by $\log_2(Q)/2$ bits, unbounded | Q3 | Rate-limit, rotate, or use a bigger group [@rfc9497], [@browngallant04] |
| Using an OPRF where you need public verifiability | A third party cannot verify a VOPRF token | (wrong primitive) | Use a blind signature (Blind RSA), not an OPRF [@rfc9474] |

<RunnableCode lang="js" title="Why a fresh blind is non-negotiable: reuse makes two evaluations linkable (structural model)">{`
// STRUCTURAL MODEL. Shows why reusing the blind across calls destroys unlinkability.
// Same additive group model: a "point" is an integer mod q, scalar*point is mod q.
const q = 2147483647;
const mod = (a) => ((a % q) + q) % q;
const scalarMul = (s, point) => mod(s * point);

const Hx = 991;                              // H(x): the SAME secret input, evaluated twice
const blindedWith = (r) => scalarMul(r, Hx); // what goes on the wire: r*H(x)

// BAD: reuse the same blind r for both calls
const rFixed = 55555;
const wire1_bad = blindedWith(rFixed);
const wire2_bad = blindedWith(rFixed);
console.log('reused blind -> wire equal? ', wire1_bad === wire2_bad, '(server links the two as one input)');

// GOOD: a fresh uniform blind each call
const wire1_ok = blindedWith(37337);
const wire2_ok = blindedWith(88123);
console.log('fresh blind  -> wire equal? ', wire1_ok === wire2_ok, '(server cannot link them)');
`}</RunnableCode>

Most of those "nevers" are the same handful of mistakes phrased for different audiences: do not let the server see the input, do not let it pick a per-client key, and do not let it answer unlimited queries. It is worth answering the questions engineers actually ask about them head on.

<Spoiler kind="hint" label="Want to run a real one?">
The `voprf` Rust crate and Cloudflare's CIRCL both implement all three modes against the RFC 9497 test vectors [@voprf-crate], [@circl]. A good first exercise: run the base OPRF, print the blinded element on two evaluations of the same input, and confirm the two wire values differ because the blind is fresh. Then switch to the verifiable mode, tamper with one byte of the server's response, and watch `VerifyProof` reject it. That single afternoon teaches more about Q1 and Q2 than any diagram.
</Spoiler>

## 11. Questions Engineers Actually Ask

<FAQ title="Frequently asked questions about oblivious PRFs">
<FAQItem question="Isn't an OPRF just an HMAC the server computes for me?">
No, and the difference is the whole primitive. If the server computes `HMAC(k, x)` for you, it *saw* `x`. That is exactly the compromise the OPRF removes: it applies the key to a blinded value it cannot read, so it never sees the input or the output [@rfc9497]. An HMAC that the server evaluates on your plaintext input is not oblivious by any definition.
</FAQItem>
<FAQItem question="OPRF or VOPRF -- does the difference actually matter?">
In any setting with an untrusted server, yes, decisively. The base OPRF answers only "is it blind?" Without the DLEQ proof, a malicious server can hand each client a *different* key and later use the distinct responses to tell clients apart -- deanonymizing the very users an anonymity system is meant to protect [@rfc9497], [@dgstv18]. The VOPRF's proof, plus one transparently pinned public key, is what closes that gap.
</FAQItem>
<FAQItem question="Why is there a limit on how many times I can call it?">
Because every `BlindEvaluate` is a static-Diffie-Hellman oracle sample, and collecting samples recovers the key at a cost that drops by $\log_2(Q)/2$ bits after $Q$ calls [@rfc9497], [@browngallant04], [@cheon06]. It is inherent to offering a Diffie-Hellman oracle, not an implementation defect. Rate-limit, rotate keys, or use a larger group to keep the loss inside budget.
</FAQItem>
<FAQItem question="Is Privacy Pass an OPRF?">
Half of it. Privacy Pass token type `0x0001` is a VOPRF, which is an OPRF. Token type `0x0002` is Blind RSA -- a blind *signature*, not an OPRF -- and it is the type behind Apple's Private Access Tokens, the most visible deployment [@rfc9578], [@rfc9474]. So "Privacy Pass equals OPRF" is wrong; one of its two token types is a different primitive entirely.
</FAQItem>
<FAQItem question="Is Google's Password Checkup the same as Have I Been Pwned?">
No. Checkup performs a blinded, OPRF-based lookup, so Google does not learn the credential being checked [@thomas19]. Have I Been Pwned's range API uses k-anonymity: you send the first five hex digits of a SHA-1 hash and receive the whole bucket to search locally [@cloudflare-kanon]. That leaks a hash prefix by design. It is simpler and cache-friendly, but it is not oblivious.
</FAQItem>
<FAQItem question="Can I reuse the blind to save a random draw?">
No. Reusing the blind across calls makes two evaluations of the same input produce the same on-wire value, so the server can link them, and in some settings it can leak the input [@rfc9497]. The blind and the proof nonce must be freshly and uniformly drawn every single call. This is non-negotiable.
</FAQItem>
<FAQItem question="Is the deployed OPRF quantum-safe?">
No. Every RFC 9497 ciphersuite rests on discrete-log hardness, which Shor's algorithm breaks, so a scalable quantum computer defeats both obliviousness and unforgeability [@rfc9497]. Post-quantum OPRFs are an open frontier: the first serious isogeny construction was broken within a year [@bkw20], [@basso21], and lattice and MPC candidates remain far heavier than classical evaluation [@albrechtgur24], [@beullens25]. And to preempt the related question: no, a POPRF's `info` is not secret -- it is public by definition [@tcrstw22].
</FAQItem>
</FAQ>

Read back across the whole story and the shape is unmistakable. Every named failure was one of three questions answered wrong. FIPR's early constructions answered "is it blind?" but too expensively to ship. Jarecki and Liu answered "which key?" too, and then the ground under "how many asks?" gave way beneath them. A base OPRF deployed where a VOPRF was needed answers "which key?" wrong and tags its users. A reused blind answers "is it blind?" wrong. An unmetered public oracle answers "how many asks?" wrong. The broken isogeny construction answered all three -- until its assumption did not hold. The diagnostic is not a teaching device bolted onto the history; it *is* the history.

And the resolution is a single move, assembled from primitives this series has already built. Take Chaum's 1982 blinding from the RSA world of Part 14, run it in the Diffie-Hellman group of Part 18 -- the discrete log that Part 18 argued nobody has broken, here run one-sided so a server *applies* a secret rather than agreeing on one -- hash the input into the group with the hash-to-curve of Part 16, and pin the key with the equal-discrete-log proof descended from the sigma protocols of Part 17.

Four earlier building blocks, one new primitive. Later parts of this guide return to its consumers as topics in their own right -- the augmented PAKE, anonymous credentials, private set intersection -- because each is, underneath, this same engine.

<PullQuote>
The server helped, and it still never saw.
</PullQuote>

That is the sentence to keep. A pseudorandom function is a lock only the keyholder can open, which is exactly what made it dangerous to offer as a service. The oblivious PRF removed the danger without removing the help: it lets a server apply its secret key to your secret input, prove it used the right key, and do so within a query budget it is honest about -- all without ever seeing what it operated on. Blind, verifiable, rate-limited. The server helped, and it still never saw.

<StudyGuide slug="oblivious-pseudorandom-functions-oprf-voprf-poprf" keyTerms={[
  { term: "OPRF", definition: "A two-party protocol computing F(k, x) where the server holds k and the client holds x, and the server learns neither x nor the output while the client learns only F(k, x)." },
  { term: "VOPRF", definition: "An OPRF plus a proof that the server used the key committed in its published public key, so the client can reject any other key." },
  { term: "POPRF", definition: "A VOPRF that also binds a public input info into the evaluation, F(k, x, info), keeping x oblivious while one key serves many public contexts." },
  { term: "2HashDH", definition: "The construction F(k, x) = H2(x, H1(x)^k): hash into the group, exponentiate by the key, hash out; the OPRF/VOPRF core of RFC 9497." },
  { term: "DLEQ proof", definition: "A Chaum-Pedersen non-interactive proof that two discrete logarithms are equal, binding the server's response to its committed key; batchable via a random linear combination." },
  { term: "Blinding factor", definition: "The fresh, uniform random scalar r that multiplies HashToGroup(x) so the transmitted point is uniform and independent of the input." },
  { term: "Static Diffie-Hellman oracle", definition: "A service answering x to x^k for fixed secret k; each query erodes k by the Brown-Gallant and Cheon bound, log2(Q)/2 bits after Q calls." },
  { term: "Unlinkability", definition: "Unconditional input secrecy: because the blind is uniform, the server cannot link or learn anything about the input, even with unbounded computation." }
]} questions={[
  { q: "Which of the three questions does a base OPRF leave unanswered, and what attack does that enable?", a: "It does not answer 'which key?' A malicious server can use a per-client key and later tag which client made a request, breaking anonymity." },
  { q: "Why does a POPRF's info have to be public?", a: "The partial-obliviousness guarantee only protects the private input x; info is folded into the key as a public tweak and is known to both parties by construction." },
  { q: "Why can an unmetered public OPRF on a 128-bit group not keep 128-bit key security?", a: "Every BlindEvaluate is a static-Diffie-Hellman sample, so Q queries erode the key by log2(Q)/2 bits; keeping the key forever and answering unlimited queries are provably incompatible." }
]} />
