Crate voprf

source ·
Expand description

An implementation of a verifiable oblivious pseudorandom function (VOPRF)

Note: This implementation is in sync with RFC 9497.

§Overview

A verifiable oblivious pseudorandom function is a protocol that is evaluated between a client and a server. They must first agree on a finite cyclic group along with a point representation.

We will use the following choice in this example:

type CipherSuite = voprf::Ristretto255;

§Modes of Operation

VOPRF can be used in three modes:

  • Base Mode, which corresponds to a normal OPRF evaluation with no support for the verification of the OPRF outputs
  • Verifiable Mode, which corresponds to an OPRF evaluation where the outputs can be verified against a server public key (VOPRF)
  • Partially Oblivious Verifiable Mode, which corresponds to a VOPRF, where a public input can be supplied to the PRF computation

In all of these modes, the protocol begins with a client blinding, followed by a server evaluation, and finishes with a client finalization and server evaluation.

§Base Mode

In base mode, an OprfClient interacts with an OprfServer to compute the output of the OPRF.

§Server Setup

The protocol begins with a setup phase, in which the server must run OprfServer::new() to produce an instance of itself. This instance must be persisted on the server and used for online client evaluations.

use rand::rngs::OsRng;
use rand::RngCore;
use voprf::OprfServer;

let mut server_rng = OsRng;
let server = OprfServer::<CipherSuite>::new(&mut server_rng);

§Client Blinding

In the first step, the client chooses an input, and runs OprfClient::blind to produce an OprfClientBlindResult, which consists of a BlindedElement to be sent to the server and an OprfClient which must be persisted on the client for the final step of the VOPRF protocol.

use rand::rngs::OsRng;
use rand::RngCore;
use voprf::OprfClient;

let mut client_rng = OsRng;
let client_blind_result = OprfClient::<CipherSuite>::blind(b"input", &mut client_rng)
    .expect("Unable to construct client");

§Server Blind Evaluation

In the second step, the server takes as input the message from OprfClient::blind (a BlindedElement), and runs OprfServer::blind_evaluate to produce EvaluationElement to be sent to the client.

let server_evaluate_result = server.blind_evaluate(&client_blind_result.message);

§Client Finalization

In the final step on the client side, the client takes as input the message from OprfServer::evaluate (an EvaluationElement), and runs OprfClient::finalize to produce an output for the protocol.

let client_finalize_result = client_blind_result
    .state
    .finalize(b"input", &message)
    .expect("Unable to perform client finalization");

println!("VOPRF output: {:?}", client_finalize_result.to_vec());

§Server Evaluation

Optionally, if the server has direct access to the PRF input, then it need not perform the oblivious computation and can simply run OprfServer::evaluate to generate an output which matches the output produced by an execution of the oblivious protocol on the same input and key.

let client_finalize_result = client_blind_result
    .state
    .finalize(b"input", &message)
    .expect("Unable to perform client finalization");

let server_evaluate_result = server
    .evaluate(b"input")
    .expect("Unable to perform the server evaluation");

assert_eq!(client_finalize_result, server_evaluate_result);

§Verifiable Mode

In verifiable mode, a VoprfClient interacts with a VoprfServer to compute the output of the VOPRF. In order to verify the server’s computation, the client checks a server-generated proof against the server’s public key. If the proof fails to verify, then the client does not receive an output.

In batch mode, a single proof can be used for multiple VOPRF evaluations. See the batching section for more details on how to perform batch evaluations.

§Server Setup

The protocol begins with a setup phase, in which the server must run VoprfServer::new() to produce an instance of itself. This instance must be persisted on the server and used for online client evaluations.

use rand::rngs::OsRng;
use rand::RngCore;
use voprf::VoprfServer;

let mut server_rng = OsRng;
let server = VoprfServer::<CipherSuite>::new(&mut server_rng).unwrap();

// To be sent to the client
println!("Server public key: {:?}", server.get_public_key());

The public key should be sent to the client, since the client will need it in the final step of the protocol in order to complete the evaluation of the VOPRF.

§Client Blinding

In the first step, the client chooses an input, and runs VoprfClient::blind to produce a VoprfClientBlindResult, which consists of a BlindedElement to be sent to the server and a VoprfClient which must be persisted on the client for the final step of the VOPRF protocol.

use rand::rngs::OsRng;
use rand::RngCore;
use voprf::VoprfClient;

let mut client_rng = OsRng;
let client_blind_result = VoprfClient::<CipherSuite>::blind(b"input", &mut client_rng)
    .expect("Unable to construct client");

§Server Blind Evaluation

In the second step, the server takes as input the message from VoprfClient::blind (a BlindedElement), and runs VoprfServer::blind_evaluate to produce a VoprfServerEvaluateResult, which consists of an EvaluationElement to be sent to the client along with a proof.

let VoprfServerEvaluateResult { message, proof } =
    server.blind_evaluate(&mut server_rng, &client_blind_result.message);

§Client Finalization

In the final step, the client takes as input the message from VoprfServer::blind_evaluate (an EvaluationElement), the proof, and the server’s public key, and runs VoprfClient::finalize to produce an output for the protocol.

let client_finalize_result = client_blind_result
    .state
    .finalize(
        b"input",
        &server_evaluate_result.message,
        &server_evaluate_result.proof,
        server.get_public_key(),
    )
    .expect("Unable to perform client finalization");

println!("VOPRF output: {:?}", client_finalize_result.to_vec());

§Server Evaluation

Optionally, if the server has direct access to the PRF input, then it need not perform the oblivious computation and can simply run VoprfServer::evaluate to generate an output which matches the output produced by an execution of the oblivious protocol on the same input and key.

let client_finalize_result = client_blind_result
    .state
    .finalize(
        b"input",
        &server_evaluate_result.message,
        &server_evaluate_result.proof,
        server.get_public_key(),
    )
    .expect("Unable to perform client finalization");

let server_evaluate_result = server
    .evaluate(b"input")
    .expect("Unable to perform the server evaluation");

assert_eq!(client_finalize_result, server_evaluate_result);

§Advanced Usage

There are two additional (and optional) extensions to the core VOPRF protocol: support for batching of evaluations, and support for public metadata.

§Batching

It is sometimes desirable to generate only a single, constant-size proof for an unbounded number of VOPRF evaluations (on arbitrary inputs). VoprfClient and VoprfServer support a batch API for handling this case. In the following example, we show how to use the batch API to produce a single proof for 10 parallel VOPRF evaluations.

First, the client produces 10 blindings, storing their resulting states and messages:

let mut client_rng = OsRng;
let mut client_states = vec![];
let mut client_messages = vec![];
for _ in 0..10 {
    let client_blind_result = VoprfClient::<CipherSuite>::blind(b"input", &mut client_rng)
        .expect("Unable to construct client");
    client_states.push(client_blind_result.state);
    client_messages.push(client_blind_result.message);
}

Next, the server calls the VoprfServer::batch_blind_evaluate_prepare and VoprfServer::batch_blind_evaluate_finish function on a set of client messages, to produce a corresponding set of messages to be returned to the client (returned in the same order), along with a single proof:

let mut server_rng = OsRng;
let prepared_evaluation_elements = server.batch_blind_evaluate_prepare(client_messages.iter());
let prepared_elements: Vec<_> = prepared_evaluation_elements.collect();
let VoprfServerBatchEvaluateFinishResult { messages, proof } = server
    .batch_blind_evaluate_finish(&mut server_rng, client_messages.iter(), &prepared_elements)
    .expect("Unable to perform server batch evaluate");
let messages: Vec<_> = messages.collect();

If alloc is available, VoprfServer::batch_blind_evaluate can be called to avoid having to collect output manually:

let mut server_rng = OsRng;
let VoprfServerBatchEvaluateResult { messages, proof } = server
    .batch_blind_evaluate(&mut server_rng, &client_messages)
    .expect("Unable to perform server batch evaluate");

Then, the client calls VoprfClient::batch_finalize on the client states saved from the first step, along with the messages returned by the server, along with the server’s proof, in order to produce a vector of outputs if the proof verifies correctly.

let client_batch_finalize_result = VoprfClient::batch_finalize(
    &[b"input"; 10],
    &client_states,
    &messages,
    &proof,
    server.get_public_key(),
)
.expect("Unable to perform client batch finalization")
.collect::<Vec<_>>();

println!("VOPRF batch outputs: {:?}", client_batch_finalize_result);

§Metadata

The optional metadata parameter included in the POPRF mode allows clients and servers to cryptographically bind additional data to the VOPRF output. This metadata is known to both parties at the start of the protocol, and is inserted under the server’s blind evaluate step and the client’s finalize step. This metadata can be constructed with some type of higher-level domain separation to avoid cross-protocol attacks or related issues.

The API for POPRF mode is similar to VOPRF mode, except that a PoprfServer and PoprfClient are used, and that each of the functions accept an additional (and optional) info parameter which represents the public input. See https://www.rfc-editor.org/rfc/rfc9497#name-poprf-public-input for more detailed information on how this public input should be used.

§Features

  • The alloc feature requires Rust’s alloc crate and enables batching VOPRF evaluations.

  • The serde feature, enabled by default, provides convenience functions for serializing and deserializing with serde.

  • The danger feature, disabled by default, exposes functions for setting and getting internal values not available in the default API. These functions are intended for use in by higher-level cryptographic protocols that need access to these raw values and are able to perform the necessary validations on them (such as being valid group elements).

  • The ristretto255-ciphersuite features enables using Ristretto255 as a CipherSuite.

  • The ristretto255 feature enables using Ristretto255 as the underlying group for the Group choice. To select a specific backend see the curve25519-dalek documentation.

Structs§

Enums§

  • Represents an error in the manipulation of internal cryptographic data
  • Only used to implement Group.
  • Determines the mode of operation (either base mode or verifiable mode). This is only used for custom implementations for Group.

Traits§

  • Configures the underlying primitives used in VOPRF
  • A prime-order subgroup of a base field (EC, prime-order field …). This subgroup is noted additively — as in the RFC — in this trait.

Functions§

  • Corresponds to DeriveKeyPair() function from the VOPRF specification.

Type Aliases§