Skip to main content

world_id_authenticator/
prove.rs

1use secrecy::ExposeSecret;
2use world_id_primitives::{
3    Credential, FieldElement, ProofRequest, ProofResponse, RequestItem, ResponseItem, SessionId,
4    SessionNullifier, ZeroKnowledgeProof,
5};
6use world_id_proof::{
7    AuthenticatorProofInput, FullOprfOutput, OprfEntrypoint, proof::generate_nullifier_proof,
8};
9
10use crate::{
11    api_types::AccountInclusionProof,
12    authenticator::{Authenticator, CredentialInput, ProofResult},
13    error::AuthenticatorError,
14};
15use world_id_primitives::TREE_DEPTH;
16
17#[expect(unused_imports, reason = "used for docs")]
18use world_id_primitives::Nullifier;
19
20impl Authenticator {
21    /// Gets an object to request OPRF computations to OPRF Nodes.
22    ///
23    /// # Arguments
24    /// - `account_inclusion_proof`: an optionally cached object can be passed to
25    ///   avoid an additional network call. If not passed, it'll be fetched from the indexer.
26    ///
27    /// # Errors
28    /// - Will return an error if there are no OPRF Nodes configured or if the threshold is invalid.
29    /// - Will return an error if proof materials are not loaded.
30    /// - Will return an error if there are issues fetching an inclusion proof.
31    async fn get_oprf_entrypoint(
32        &self,
33        account_inclusion_proof: Option<AccountInclusionProof<TREE_DEPTH>>,
34    ) -> Result<OprfEntrypoint<'_>, AuthenticatorError> {
35        // Check OPRF Config
36        let services = self.config.nullifier_oracle_urls();
37        if services.is_empty() {
38            return Err(AuthenticatorError::Generic(
39                "No nullifier oracle URLs configured".to_string(),
40            ));
41        }
42        let requested_threshold = self.config.nullifier_oracle_threshold();
43        if requested_threshold == 0 {
44            return Err(AuthenticatorError::InvalidConfig {
45                attribute: "nullifier_oracle_threshold".to_string(),
46                reason: "must be at least 1".to_string(),
47            });
48        }
49        let threshold = requested_threshold.min(services.len());
50
51        let query_material = self
52            .query_material
53            .as_ref()
54            .ok_or(AuthenticatorError::ProofMaterialsNotLoaded)?;
55
56        // Fetch inclusion_proof && authenticator key_set if not provided
57        let account_inclusion_proof = if let Some(account_inclusion_proof) = account_inclusion_proof
58        {
59            account_inclusion_proof
60        } else {
61            self.fetch_inclusion_proof().await?
62        };
63
64        let key_index = account_inclusion_proof
65            .authenticator_pubkeys
66            .iter()
67            .position(|pk| {
68                pk.as_ref()
69                    .is_some_and(|pk| pk.pk == self.offchain_pubkey().pk)
70            })
71            .ok_or(AuthenticatorError::PublicKeyNotFound)? as u64;
72
73        let authenticator_input = AuthenticatorProofInput::new(
74            account_inclusion_proof.authenticator_pubkeys,
75            account_inclusion_proof.inclusion_proof,
76            self.signer
77                .offchain_signer_private_key()
78                .expose_secret()
79                .clone(),
80            key_index,
81        );
82
83        Ok(OprfEntrypoint::new(
84            services,
85            threshold,
86            query_material,
87            authenticator_input,
88            &self.ws_connector,
89        ))
90    }
91
92    /// Generates a nullifier for a World ID Proof (through OPRF Nodes).
93    ///
94    /// A [`Nullifier`] is a unique, one-time use, anonymous identifier for a World ID
95    /// on a specific RP context. See [`Nullifier`] for more details.
96    ///
97    /// # Arguments
98    /// - `proof_request`: the request received from the RP.
99    /// - `account_inclusion_proof`: an optionally cached object can be passed to
100    ///   avoid an additional network call. If not passed, it'll be fetched from the indexer.
101    ///
102    /// A Nullifier takes an `action` as input:
103    /// - If `proof_request` is for a Session Proof, a random internal `action` is generated. This
104    ///   is opaque to RPs, and verified internally in the verification contract.
105    /// - If `proof_request` is for a Uniqueness Proof, the `action` is provided by the RP,
106    ///   if not provided a default of [`FieldElement::ZERO`] is used.
107    ///
108    /// # Errors
109    ///
110    /// - Will raise a [`ProofError`](world_id_proof::ProofError) if there is any issue
111    ///   generating the nullifier. For example, network issues, unexpected incorrect responses
112    ///   from OPRF Nodes.
113    /// - Raises an error if the OPRF Nodes configuration is not correctly set.
114    pub async fn generate_nullifier(
115        &self,
116        proof_request: &ProofRequest,
117        account_inclusion_proof: Option<AccountInclusionProof<TREE_DEPTH>>,
118    ) -> Result<FullOprfOutput, AuthenticatorError> {
119        let mut rng = rand::rngs::OsRng;
120
121        let oprf_entrypoint = self.get_oprf_entrypoint(account_inclusion_proof).await?;
122
123        Ok(oprf_entrypoint
124            .gen_nullifier(&mut rng, proof_request)
125            .await?)
126    }
127
128    /// Generates a blinding factor for a Credential sub (through OPRF Nodes). The credential
129    /// blinding factor enables every credential to have a different subject identifier, see
130    /// [`Credential::sub`] for more details.
131    ///
132    /// # Errors
133    ///
134    /// - Will raise a [`ProofError`](world_id_proof::ProofError) if there is any issue
135    ///   generating the blinding factor. For example, network issues, unexpected incorrect
136    ///   responses from OPRF Nodes.
137    /// - Raises an error if the OPRF Nodes configuration is not correctly set.
138    pub async fn generate_credential_blinding_factor(
139        &self,
140        issuer_schema_id: u64,
141    ) -> Result<FieldElement, AuthenticatorError> {
142        let mut rng = rand::rngs::OsRng;
143
144        // This is called sporadic enough that fetching fresh is reasonable
145        let oprf_entrypoint = self.get_oprf_entrypoint(None).await?;
146
147        let (blinding_factor, _share_epoch) = oprf_entrypoint
148            .gen_credential_blinding_factor(&mut rng, issuer_schema_id)
149            .await?;
150
151        Ok(blinding_factor)
152    }
153
154    /// Builds a [`SessionId`] object which can be used for Session Proofs. This has two uses:
155    /// 1. Creating a new Sesssion, i.e. generating a [`SessionId`] for the first time.
156    /// 2. Reconstructing a session for a Session Proof, particularly if the `session_id_r_seed` is not cached.
157    ///
158    /// Internally, this generates the session's random seed (`r`) using OPRF Nodes. This seed is used to
159    /// compute the [`SessionId::commitment`] for Session Proofs.
160    ///
161    /// # Arguments
162    /// - `proof_request`: the request received from the RP to initialize a session id.
163    /// - `session_id_r_seed`: the seed (see below) if it was already generated previously and it's cached.
164    /// - `account_inclusion_proof`: an optionally cached object can be passed to
165    ///   avoid an additional network call. If not passed, it'll be fetched from the indexer.
166    ///
167    /// # Returns
168    /// - `session_id`: The generated [`SessionId`] to be shared with the requesting RP.
169    /// - `session_id_r_seed`: The `r` value used for this session so the Authenticator can cache it.
170    ///
171    /// # Seed (`session_id_r_seed`)
172    /// - If a `session_id_r_seed` (`r`) is not provided, it'll be derived/re-derived with the OPRF nodes.
173    /// - Even if `r` has been generated before, the same `r` will be computed again for the same
174    ///   context (i.e. `rpId`, [`SessionId::oprf_seed`]). This means caching `r` is optional but RECOMMENDED.
175    /// -  Caching behavior is the responsibility of the Authenticator (and/or its relevant SDKs), not this crate.
176    /// - More information about the seed can be found in [`SessionId::from_r_seed`].
177    pub async fn build_session_id(
178        &self,
179        proof_request: &ProofRequest,
180        session_id_r_seed: Option<FieldElement>,
181        account_inclusion_proof: Option<AccountInclusionProof<TREE_DEPTH>>,
182    ) -> Result<(SessionId, FieldElement), AuthenticatorError> {
183        let mut rng = rand::rngs::OsRng;
184
185        let oprf_seed = match proof_request.session_id {
186            Some(session_id) => session_id.oprf_seed,
187            None => SessionId::generate_oprf_seed(&mut rng),
188        };
189
190        let session_id_r_seed = match session_id_r_seed {
191            Some(seed) => seed,
192            None => {
193                let entrypoint = self.get_oprf_entrypoint(account_inclusion_proof).await?;
194                let oprf_output = entrypoint
195                    .gen_session_id_r_seed(&mut rng, proof_request, oprf_seed)
196                    .await?;
197                oprf_output.verifiable_oprf_output.output.into()
198            }
199        };
200
201        let session_id = SessionId::from_r_seed(self.leaf_index(), session_id_r_seed, oprf_seed)?;
202
203        if let Some(request_session_id) = proof_request.session_id {
204            if request_session_id != session_id {
205                return Err(AuthenticatorError::SessionIdMismatch);
206            }
207        }
208
209        Ok((session_id, session_id_r_seed))
210    }
211
212    /// Generates a complete [`ProofResponse`] for
213    /// the given [`ProofRequest`] to respond to an RP request.
214    ///
215    /// This orchestrates session resolution, per-credential proof generation,
216    /// response assembly, and self-validation.
217    ///
218    /// # Typical flow
219    /// ```rust,ignore
220    /// // <- check request can be fulfilled with available credentials
221    /// let nullifier = authenticator.generate_nullifier(&request, None).await?;
222    /// // <- check replay guard using nullifier.oprf_output()
223    /// let (response, meta) = authenticator.generate_proof(&request, nullifier, &creds, ...).await?;
224    /// // <- cache `session_id_r_seed` (to speed future proofs) and `nullifier` (to prevent replays)
225    /// ```
226    ///
227    /// # Arguments
228    /// - `proof_request` — the RP's full request.
229    /// - `nullifier` — the OPRF nullifier output, obtained from
230    ///   [`generate_nullifier`](Self::generate_nullifier). The caller MUST check
231    ///   for replays before calling this method to avoid wasted computation.
232    /// - `credentials` — one [`CredentialInput`] per credential to prove,
233    ///   matched to request items by `issuer_schema_id`.
234    /// - `account_inclusion_proof` — a cached inclusion proof if available (a fresh one will be fetched otherwise)
235    /// - `session_id_r_seed` — a cached session `r` seed for Session Proofs. If not available, it will be
236    ///   re-computed.
237    ///
238    /// # Caller Responsibilities
239    /// 1. The caller must ensure the request can be fulfilled with the credentials which the user has available,
240    ///    and provide such credentials.
241    /// 2. The caller must ensure the nullifier has not been used before.
242    ///
243    /// # Errors
244    /// - [`AuthenticatorError::UnfullfilableRequest`] if the provided credentials
245    ///   cannot satisfy the request (including constraints).
246    /// - Other `AuthenticatorError` variants on proof circuit or validation failures.
247    pub async fn generate_proof(
248        &self,
249        proof_request: &ProofRequest,
250        nullifier: FullOprfOutput,
251        credentials: &[CredentialInput],
252        account_inclusion_proof: Option<AccountInclusionProof<TREE_DEPTH>>,
253        session_id_r_seed: Option<FieldElement>,
254    ) -> Result<ProofResult, AuthenticatorError> {
255        // 1. Determine request items to prove
256        let available: std::collections::HashSet<u64> = credentials
257            .iter()
258            .map(|c| c.credential.issuer_schema_id)
259            .collect();
260        let items_to_prove = proof_request
261            .credentials_to_prove(&available)
262            .ok_or(AuthenticatorError::UnfullfilableRequest)?;
263
264        // 2. Resolve session seed
265        let resolved_session_seed = if proof_request.is_session_proof() {
266            if let Some(seed) = session_id_r_seed {
267                // Validate the cached seed produces the expected session ID
268                let session_id = proof_request
269                    .session_id
270                    .expect("session proof must have session_id");
271
272                let computed =
273                    SessionId::from_r_seed(self.leaf_index(), seed, session_id.oprf_seed)?;
274
275                if computed != session_id {
276                    return Err(AuthenticatorError::SessionIdMismatch);
277                }
278                Some(seed)
279            } else {
280                let (_session_id, seed) = self
281                    .build_session_id(proof_request, None, account_inclusion_proof)
282                    .await?;
283                Some(seed)
284            }
285        } else {
286            None
287        };
288
289        // 3. Generate per-credential proofs for the selected items
290        let creds_by_schema: std::collections::HashMap<u64, &CredentialInput> = credentials
291            .iter()
292            .map(|c| (c.credential.issuer_schema_id, c))
293            .collect();
294
295        let mut responses = Vec::with_capacity(items_to_prove.len());
296        for request_item in &items_to_prove {
297            let cred_input = creds_by_schema[&request_item.issuer_schema_id];
298
299            let response_item = self.generate_credential_proof(
300                nullifier.clone(),
301                request_item,
302                &cred_input.credential,
303                cred_input.blinding_factor,
304                resolved_session_seed,
305                proof_request.session_id,
306                proof_request.created_at,
307            )?;
308            responses.push(response_item);
309        }
310
311        // 4. Assemble response
312        let proof_response = ProofResponse {
313            id: proof_request.id.clone(),
314            version: proof_request.version,
315            session_id: proof_request.session_id,
316            responses,
317            error: None,
318        };
319
320        // 5. Validate and return response
321        proof_request.validate_response(&proof_response)?;
322        Ok(ProofResult {
323            session_id_r_seed: resolved_session_seed,
324            proof_response,
325        })
326    }
327
328    /// Generates a single World ID Proof from a provided `[ProofRequest]` and `[Credential]`. This
329    /// method generates the raw proof to be translated into a Uniqueness Proof or a Session Proof for the RP.
330    ///
331    /// The correct entrypoint for an RP request is [`Self::generate_proof`].
332    ///
333    /// This assumes the RP's `[ProofRequest]` has already been parsed to determine
334    /// which `[Credential]` is appropriate for the request. This method responds to a
335    /// specific `[RequestItem]` (a `[ProofRequest]` may contain multiple items).
336    ///
337    /// # Arguments
338    /// - `oprf_nullifier`: The output representing the nullifier, generated from the `generate_nullifier` function. All proofs
339    ///   require this attribute.
340    /// - `request_item`: The specific `RequestItem` that is being resolved from the RP's `ProofRequest`.
341    /// - `credential`: The Credential to be used for the proof that fulfills the `RequestItem`.
342    /// - `credential_sub_blinding_factor`: The blinding factor for the Credential's sub.
343    /// - `session_id_r_seed`: The session ID random seed, obtained via [`build_session_id`](Self::build_session_id).
344    ///   For Uniqueness Proofs (when `session_id` is `None`), this value is ignored by the circuit.
345    /// - `session_id`: The expected session ID provided by the RP. Only needed for Session Proofs. Obtained from the RP's [`ProofRequest`].
346    /// - `request_timestamp`: The timestamp of the request. Obtained from the RP's [`ProofRequest`].
347    ///
348    /// # Errors
349    /// - Will error if the any of the provided parameters are not valid.
350    /// - Will error if any of the required network requests fail.
351    /// - Will error if the user does not have a registered World ID.
352    #[expect(clippy::too_many_arguments)]
353    fn generate_credential_proof(
354        &self,
355        oprf_nullifier: FullOprfOutput,
356        request_item: &RequestItem,
357        credential: &Credential,
358        credential_sub_blinding_factor: FieldElement,
359        session_id_r_seed: Option<FieldElement>,
360        session_id: Option<SessionId>,
361        request_timestamp: u64,
362    ) -> Result<ResponseItem, AuthenticatorError> {
363        let mut rng = rand::rngs::OsRng;
364
365        let nullifier_material = self
366            .nullifier_material
367            .as_ref()
368            .ok_or(AuthenticatorError::ProofMaterialsNotLoaded)?;
369
370        let merkle_root: FieldElement = oprf_nullifier.query_proof_input.merkle_root.into();
371        let action_from_query: FieldElement = oprf_nullifier.query_proof_input.action.into();
372
373        let expires_at_min = request_item.effective_expires_at_min(request_timestamp);
374
375        let (proof, _public_inputs, nullifier) = generate_nullifier_proof(
376            nullifier_material,
377            &mut rng,
378            credential,
379            credential_sub_blinding_factor,
380            oprf_nullifier,
381            request_item,
382            session_id.map(|v| v.commitment),
383            session_id_r_seed,
384            expires_at_min,
385        )?;
386
387        let proof = ZeroKnowledgeProof::from_groth16_proof(&proof, merkle_root);
388
389        // Construct the appropriate response item based on proof type
390        let nullifier_fe: FieldElement = nullifier.into();
391        let response_item = if session_id.is_some() {
392            let session_nullifier = SessionNullifier::new(nullifier_fe, action_from_query)?;
393            ResponseItem::new_session(
394                request_item.identifier.clone(),
395                request_item.issuer_schema_id,
396                proof,
397                session_nullifier,
398                expires_at_min,
399            )
400        } else {
401            ResponseItem::new_uniqueness(
402                request_item.identifier.clone(),
403                request_item.issuer_schema_id,
404                proof,
405                nullifier_fe.into(),
406                expires_at_min,
407            )
408        };
409
410        Ok(response_item)
411    }
412}