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}