Skip to main content

world_id_authenticator/
prove.rs

1use secrecy::ExposeSecret;
2use world_id_primitives::{
3    Credential, FieldElement, ProofRequest, ProofResponse, ProofType, RequestItem, ResponseItem,
4    SessionId, SessionNullifier, ZeroKnowledgeProof,
5};
6use world_id_proof::{
7    AuthenticatorProofInput, FullOprfOutput, OprfEntrypoint, ProofCompression,
8    proof::{CircomGroth16Material, generate_nullifier_proof},
9};
10
11use crate::{
12    api_types::AccountInclusionProof,
13    authenticator::{Authenticator, CredentialInput, ProofResult},
14    error::AuthenticatorError,
15};
16#[cfg(not(target_arch = "wasm32"))]
17use world_id_primitives::OwnershipProof;
18use world_id_primitives::TREE_DEPTH;
19#[cfg(not(target_arch = "wasm32"))]
20use world_id_proof::{
21    circuit_inputs::OwnershipProofCircuitInput,
22    ownership_proof::generate_ownership_proof_with_prover,
23};
24
25#[expect(unused_imports, reason = "used for docs")]
26use world_id_primitives::Nullifier;
27
28impl Authenticator {
29    /// Gets an object to request OPRF computations to OPRF Nodes.
30    ///
31    /// # Arguments
32    /// - `account_inclusion_proof`: an optionally cached object can be passed to
33    ///   avoid an additional network call. If not passed, it'll be fetched from the indexer.
34    ///
35    /// # Errors
36    /// - Will return an error if there are no OPRF Nodes configured or if the threshold is invalid.
37    /// - Will return an error if proof materials are not loaded.
38    /// - Will return an error if there are issues fetching an inclusion proof.
39    async fn get_oprf_entrypoint<'a>(
40        &'a self,
41        query_material: &'a CircomGroth16Material,
42        account_inclusion_proof: Option<AccountInclusionProof<TREE_DEPTH>>,
43    ) -> Result<OprfEntrypoint<'a>, AuthenticatorError> {
44        // Check OPRF Config
45        let services = self.config.nullifier_oracle_urls();
46        if services.is_empty() {
47            return Err(AuthenticatorError::Generic(
48                "No nullifier oracle URLs configured".to_string(),
49            ));
50        }
51        let requested_threshold = self.config.nullifier_oracle_threshold();
52        if requested_threshold == 0 {
53            return Err(AuthenticatorError::InvalidConfig {
54                attribute: "nullifier_oracle_threshold".to_string(),
55                reason: "must be at least 1".to_string(),
56            });
57        }
58        let threshold = requested_threshold.min(services.len());
59
60        let authenticator_input = self
61            .prepare_authenticator_input(account_inclusion_proof)
62            .await?;
63
64        Ok(OprfEntrypoint::new(
65            services,
66            threshold,
67            query_material,
68            authenticator_input,
69            &self.ws_connector,
70        ))
71    }
72
73    async fn prepare_authenticator_input(
74        &self,
75        account_inclusion_proof: Option<AccountInclusionProof<TREE_DEPTH>>,
76    ) -> Result<AuthenticatorProofInput, AuthenticatorError> {
77        // Fetch inclusion_proof && authenticator key_set if not provided
78        let account_inclusion_proof = if let Some(account_inclusion_proof) = account_inclusion_proof
79        {
80            account_inclusion_proof
81        } else {
82            self.fetch_inclusion_proof().await?
83        };
84
85        let key_index = account_inclusion_proof
86            .authenticator_pubkeys
87            .iter()
88            .position(|pk| {
89                pk.as_ref()
90                    .is_some_and(|pk| pk.pk == self.offchain_pubkey().pk)
91            })
92            .ok_or(AuthenticatorError::PublicKeyNotFound)? as u64;
93
94        let authenticator_input = AuthenticatorProofInput::new(
95            account_inclusion_proof.authenticator_pubkeys,
96            account_inclusion_proof.inclusion_proof,
97            self.signer
98                .offchain_signer_private_key()
99                .expose_secret()
100                .clone(),
101            key_index,
102        );
103
104        Ok(authenticator_input)
105    }
106
107    /// Generates a nullifier for a World ID Proof (through OPRF Nodes).
108    ///
109    /// A [`Nullifier`] is a unique, one-time use, anonymous identifier for a World ID
110    /// on a specific RP context. See [`Nullifier`] for more details.
111    ///
112    /// # Arguments
113    /// - `proof_request`: the request received from the RP.
114    /// - `account_inclusion_proof`: an optionally cached object can be passed to
115    ///   avoid an additional network call. If not passed, it'll be fetched from the indexer.
116    ///
117    /// A Nullifier takes an `action` as input:
118    /// - If `proof_request` is for a Session Proof, a random internal `action` is generated. This
119    ///   is opaque to RPs, and verified internally in the verification contract.
120    /// - If `proof_request` is for a Uniqueness Proof, the `action` is provided by the RP,
121    ///   if not provided a default of [`FieldElement::ZERO`] is used.
122    ///
123    /// # Errors
124    ///
125    /// - Will raise a [`ProofError`](world_id_proof::ProofError) if there is any issue
126    ///   generating the nullifier. For example, network issues, unexpected incorrect responses
127    ///   from OPRF Nodes.
128    /// - Raises an error if the OPRF Nodes configuration is not correctly set.
129    pub async fn generate_nullifier(
130        &self,
131        proof_request: &ProofRequest,
132        account_inclusion_proof: Option<AccountInclusionProof<TREE_DEPTH>>,
133    ) -> Result<FullOprfOutput, AuthenticatorError> {
134        proof_request.validate_proof_type()?;
135        let mut rng = rand::rngs::OsRng;
136
137        let query_material = self
138            .zk_artifact_source
139            .query_material()
140            .map_err(AuthenticatorError::ZkArtifactError)?;
141        let oprf_entrypoint = self
142            .get_oprf_entrypoint(&query_material, account_inclusion_proof)
143            .await?;
144
145        Ok(oprf_entrypoint
146            .gen_nullifier(&mut rng, proof_request)
147            .await?)
148    }
149
150    /// Generates a blinding factor for a Credential sub (through OPRF Nodes). The credential
151    /// blinding factor enables every credential to have a different subject identifier, see
152    /// [`Credential::sub`] for more details.
153    ///
154    /// # Errors
155    ///
156    /// - Will raise a [`ProofError`](world_id_proof::ProofError) if there is any issue
157    ///   generating the blinding factor. For example, network issues, unexpected incorrect
158    ///   responses from OPRF Nodes.
159    /// - Raises an error if the OPRF Nodes configuration is not correctly set.
160    pub async fn generate_credential_blinding_factor(
161        &self,
162        issuer_schema_id: u64,
163    ) -> Result<FieldElement, AuthenticatorError> {
164        let mut rng = rand::rngs::OsRng;
165
166        // This is called sporadic enough that fetching fresh is reasonable
167        let query_material = self
168            .zk_artifact_source
169            .query_material()
170            .map_err(AuthenticatorError::ZkArtifactError)?;
171        let oprf_entrypoint = self.get_oprf_entrypoint(&query_material, None).await?;
172
173        let (blinding_factor, _share_epoch) = oprf_entrypoint
174            .gen_credential_blinding_factor(&mut rng, issuer_schema_id)
175            .await?;
176
177        Ok(blinding_factor)
178    }
179
180    /// Builds or resolves a [`SessionId`] object which can be used for Session Proofs. This has two uses:
181    /// 1. Creating a new Session, i.e. generating a [`SessionId`] for the first time.
182    /// 2. Reconstructing a session for a Session Proof, particularly if the `session_id_r_seed` is not cached.
183    ///
184    /// Internally, this derives the session randomness (`r`) using OPRF Nodes. For existing
185    /// sessions this re-derives the same `r` from [`SessionId::oprf_seed`]; it does not mint a
186    /// new session. The seed is used to compute the [`SessionId::commitment`] for Session Proofs.
187    ///
188    /// # Arguments
189    /// - `proof_request`: the request received from the RP to create or prove a session id.
190    /// - `session_id_r_seed`: the seed (see below) if it was already generated previously and it's cached.
191    /// - `account_inclusion_proof`: an optionally cached object can be passed to
192    ///   avoid an additional network call. If not passed, it'll be fetched from the indexer.
193    ///
194    /// # Returns
195    /// - `session_id`: The generated or resolved [`SessionId`].
196    /// - `session_id_r_seed`: The `r` value used for this session so the Authenticator can cache it.
197    ///
198    /// # Seed (`session_id_r_seed`)
199    /// - If a `session_id_r_seed` (`r`) is not provided, it'll be derived/re-derived with the OPRF nodes.
200    /// - Even if `r` has been generated before, the same `r` will be computed again for the same
201    ///   context (i.e. `rpId`, [`SessionId::oprf_seed`]). This means caching `r` is optional but RECOMMENDED.
202    /// -  Caching behavior is the responsibility of the Authenticator (and/or its relevant SDKs), not this crate.
203    /// - More information about the seed can be found in [`SessionId::from_r_seed`].
204    pub async fn build_session_id(
205        &self,
206        proof_request: &ProofRequest,
207        session_id_r_seed: Option<FieldElement>,
208        account_inclusion_proof: Option<AccountInclusionProof<TREE_DEPTH>>,
209    ) -> Result<(SessionId, FieldElement), AuthenticatorError> {
210        proof_request.validate_proof_type()?;
211        if !proof_request.is_session_proof() {
212            return Err(AuthenticatorError::PrimitiveError(
213                world_id_primitives::PrimitiveError::InvalidInput {
214                    attribute: "proof_type".to_string(),
215                    reason: "must be create_session or session".to_string(),
216                },
217            ));
218        }
219
220        let mut rng = rand::rngs::OsRng;
221
222        let oprf_seed = match proof_request.session_id {
223            Some(session_id) => session_id.oprf_seed,
224            None => SessionId::generate_oprf_seed(&mut rng),
225        };
226
227        let resolved_session_id_r_seed = match session_id_r_seed {
228            Some(seed) => seed,
229            None => {
230                let query_material = self
231                    .zk_artifact_source
232                    .query_material()
233                    .map_err(AuthenticatorError::ZkArtifactError)?;
234                let entrypoint = self
235                    .get_oprf_entrypoint(&query_material, account_inclusion_proof)
236                    .await?;
237                let oprf_output = entrypoint
238                    .derive_session_id_r_seed(&mut rng, proof_request, oprf_seed)
239                    .await?;
240                oprf_output.verifiable_oprf_output.output.into()
241            }
242        };
243
244        let session_id =
245            SessionId::from_r_seed(self.leaf_index(), resolved_session_id_r_seed, oprf_seed)?;
246
247        if let Some(request_session_id) = proof_request.session_id
248            && request_session_id != session_id
249        {
250            return Err(AuthenticatorError::SessionIdMismatch);
251        }
252
253        Ok((session_id, resolved_session_id_r_seed))
254    }
255
256    /// Generates a complete [`ProofResponse`] for
257    /// the given [`ProofRequest`] to respond to an RP request.
258    ///
259    /// This orchestrates session resolution, per-credential proof generation,
260    /// response assembly, and self-validation.
261    ///
262    /// # Typical flow
263    /// ```rust,ignore
264    /// // <- check request can be fulfilled with available credentials
265    /// let nullifier = authenticator.generate_nullifier(&request, None).await?;
266    /// // <- check replay guard using nullifier.oprf_output()
267    /// let (response, meta) = authenticator.generate_proof(&request, nullifier, &creds, ...).await?;
268    /// // <- cache `session_id_r_seed` (to speed future proofs) and `nullifier` (to prevent replays)
269    /// ```
270    ///
271    /// # Arguments
272    /// - `proof_request` — the RP's full request.
273    /// - `nullifier` — the OPRF nullifier output, obtained from
274    ///   [`generate_nullifier`](Self::generate_nullifier). The caller MUST check
275    ///   for replays before calling this method to avoid wasted computation.
276    /// - `credentials` — one [`CredentialInput`] per credential to prove,
277    ///   matched to request items by `issuer_schema_id`.
278    /// - `account_inclusion_proof` — a cached inclusion proof if available (a fresh one will be fetched otherwise)
279    /// - `session_id_r_seed` — a cached session `r` seed for Session Proofs. If not available, it will be
280    ///   re-computed.
281    ///
282    /// # Caller Responsibilities
283    /// 1. The caller must ensure the request can be fulfilled with the credentials which the user has available,
284    ///    and provide such credentials.
285    /// 2. The caller must ensure the nullifier has not been used before.
286    ///
287    /// # Errors
288    /// - [`AuthenticatorError::UnfullfilableRequest`] if the provided credentials
289    ///   cannot satisfy the request (including constraints).
290    /// - Other `AuthenticatorError` variants on proof circuit or validation failures.
291    pub async fn generate_proof(
292        &self,
293        proof_request: &ProofRequest,
294        nullifier: FullOprfOutput,
295        credentials: &[CredentialInput],
296        account_inclusion_proof: Option<AccountInclusionProof<TREE_DEPTH>>,
297        session_id_r_seed: Option<FieldElement>,
298    ) -> Result<ProofResult, AuthenticatorError> {
299        proof_request.validate_proof_type()?;
300
301        // 1. Determine request items to prove
302        let available: std::collections::HashSet<u64> = credentials
303            .iter()
304            .map(|c| c.credential.issuer_schema_id)
305            .collect();
306        let items_to_prove = proof_request
307            .credentials_to_prove(&available)
308            .ok_or(AuthenticatorError::UnfullfilableRequest)?;
309
310        // 2. Resolve session seed
311        let (resolved_session_id, resolved_session_seed) = match proof_request.proof_type {
312            ProofType::Uniqueness => (None, None),
313            ProofType::CreateSession => {
314                let (session_id, seed) = self
315                    .build_session_id(proof_request, None, account_inclusion_proof)
316                    .await?;
317                (Some(session_id), Some(seed))
318            }
319            ProofType::Session => {
320                let session_id = proof_request
321                    .session_id
322                    .expect("session proof must have session_id");
323                if let Some(seed) = session_id_r_seed {
324                    // Validate the cached seed produces the expected session ID
325                    let computed =
326                        SessionId::from_r_seed(self.leaf_index(), seed, session_id.oprf_seed)?;
327
328                    if computed != session_id {
329                        return Err(AuthenticatorError::SessionIdMismatch);
330                    }
331                    (Some(session_id), Some(seed))
332                } else {
333                    // Re-derive the same `r` from the existing session's `oprf_seed` when the
334                    // caller did not provide a cached seed.
335                    let (_session_id, seed) = self
336                        .build_session_id(proof_request, None, account_inclusion_proof)
337                        .await?;
338                    (Some(session_id), Some(seed))
339                }
340            }
341        };
342
343        let nullifier_material = self
344            .zk_artifact_source
345            .nullifier_material()
346            .map_err(AuthenticatorError::ZkArtifactError)?;
347
348        // 3. Generate per-credential proofs for the selected items
349        let creds_by_schema: std::collections::HashMap<u64, &CredentialInput> = credentials
350            .iter()
351            .map(|c| (c.credential.issuer_schema_id, c))
352            .collect();
353
354        let mut responses = Vec::with_capacity(items_to_prove.len());
355        for request_item in &items_to_prove {
356            let cred_input = creds_by_schema[&request_item.issuer_schema_id];
357
358            let response_item = self.generate_credential_proof(
359                &nullifier_material,
360                nullifier.clone(),
361                request_item,
362                &cred_input.credential,
363                cred_input.blinding_factor,
364                resolved_session_seed,
365                resolved_session_id,
366                proof_request.created_at,
367            )?;
368            responses.push(response_item);
369        }
370
371        // 4. Assemble response
372        let proof_response = ProofResponse {
373            id: proof_request.id.clone(),
374            version: proof_request.version,
375            session_id: resolved_session_id,
376            responses,
377            error: None,
378        };
379
380        // 5. Validate and return response
381        proof_request.validate_response(&proof_response)?;
382        Ok(ProofResult {
383            session_id_r_seed: resolved_session_seed,
384            proof_response,
385        })
386    }
387
388    /// Generates a single World ID Proof from a provided `[ProofRequest]` and `[Credential]`. This
389    /// method generates the raw proof to be translated into a Uniqueness Proof or a Session Proof for the RP.
390    ///
391    /// The correct entrypoint for an RP request is [`Self::generate_proof`].
392    ///
393    /// This assumes the RP's `[ProofRequest]` has already been parsed to determine
394    /// which `[Credential]` is appropriate for the request. This method responds to a
395    /// specific `[RequestItem]` (a `[ProofRequest]` may contain multiple items).
396    ///
397    /// # Arguments
398    /// - `oprf_nullifier`: The output representing the nullifier, generated from the `generate_nullifier` function. All proofs
399    ///   require this attribute.
400    /// - `request_item`: The specific `RequestItem` that is being resolved from the RP's `ProofRequest`.
401    /// - `credential`: The Credential to be used for the proof that fulfills the `RequestItem`.
402    /// - `credential_sub_blinding_factor`: The blinding factor for the Credential's sub.
403    /// - `session_id_r_seed`: The session ID random seed, obtained via [`build_session_id`](Self::build_session_id).
404    ///   For Uniqueness Proofs (when `session_id` is `None`), this value is ignored by the circuit.
405    /// - `session_id`: The expected session ID provided by the RP. Only needed for Session Proofs. Obtained from the RP's [`ProofRequest`].
406    /// - `request_timestamp`: The timestamp of the request. Obtained from the RP's [`ProofRequest`].
407    ///
408    /// # Errors
409    /// - Will error if the any of the provided parameters are not valid.
410    /// - Will error if any of the required network requests fail.
411    /// - Will error if the user does not have a registered World ID.
412    #[expect(clippy::too_many_arguments)]
413    fn generate_credential_proof(
414        &self,
415        nullifier_material: &CircomGroth16Material,
416        oprf_nullifier: FullOprfOutput,
417        request_item: &RequestItem,
418        credential: &Credential,
419        credential_sub_blinding_factor: FieldElement,
420        session_id_r_seed: Option<FieldElement>,
421        session_id: Option<SessionId>,
422        request_timestamp: u64,
423    ) -> Result<ResponseItem, AuthenticatorError> {
424        let mut rng = rand::rngs::OsRng;
425
426        let merkle_root: FieldElement = oprf_nullifier.query_proof_input.merkle_root.into();
427        let action_from_query: FieldElement = oprf_nullifier.query_proof_input.action.into();
428
429        let expires_at_min = request_item.effective_expires_at_min(request_timestamp);
430
431        let (proof, _public_inputs, nullifier) = generate_nullifier_proof(
432            nullifier_material,
433            &mut rng,
434            credential,
435            credential_sub_blinding_factor,
436            oprf_nullifier,
437            request_item,
438            session_id.map(|v| v.commitment),
439            session_id_r_seed,
440            expires_at_min,
441        )?;
442
443        let proof = ZeroKnowledgeProof::from_groth16_proof(&proof, merkle_root);
444
445        // Construct the appropriate response item based on proof type
446        let nullifier_fe: FieldElement = nullifier.into();
447        let response_item = if session_id.is_some() {
448            let session_nullifier = SessionNullifier::new(nullifier_fe, action_from_query)?;
449            ResponseItem::new_session(
450                request_item.identifier.clone(),
451                request_item.issuer_schema_id,
452                proof,
453                session_nullifier,
454                expires_at_min,
455            )
456        } else {
457            ResponseItem::new_uniqueness(
458                request_item.identifier.clone(),
459                request_item.issuer_schema_id,
460                proof,
461                nullifier_fe.into(),
462                expires_at_min,
463            )
464        };
465
466        Ok(response_item)
467    }
468
469    /// Generates an Ownership Proof (WIP-103) over a Credential's `sub`.
470    ///
471    /// This proof MUST only be shared with each relevant issuer. This is the responsibility of Authenticators.
472    ///
473    /// # Arguments
474    /// - `nonce`: The nonce of the request provided by the Issuer.
475    /// - `credential_blinding_factor`: The blinding factor generated for the credential.
476    /// - `sub`: The expected `sub` of the Credential in question.
477    /// - `account_inclusion_proof`: An optionally cached account inclusion proof. If not provided, a new inclusion proof will be fetched.
478    ///
479    /// # Returns
480    /// The [`OwnershipProof`] containing the ZKP and Merkle root.
481    #[cfg(not(target_arch = "wasm32"))]
482    pub async fn prove_credential_sub(
483        &self,
484        nonce: FieldElement,
485        credential_blinding_factor: FieldElement,
486        sub: FieldElement,
487        account_inclusion_proof: Option<AccountInclusionProof<TREE_DEPTH>>,
488    ) -> Result<OwnershipProof, AuthenticatorError> {
489        use world_id_proof::ownership_proof::DS_OWNERSHIP_PROOF;
490
491        let authenticator_input = self
492            .prepare_authenticator_input(account_inclusion_proof)
493            .await?;
494
495        let commitment = Credential::compute_sub(self.leaf_index(), credential_blinding_factor);
496
497        if commitment != sub {
498            return Err(AuthenticatorError::InvalidSubOrBlindingFactor);
499        }
500
501        let mut message = [
502            *FieldElement::from_be_bytes_mod_order(DS_OWNERSHIP_PROOF),
503            *commitment,
504            *nonce,
505        ];
506        poseidon2::bn254::t3::permutation_in_place(&mut message);
507
508        let signature = self
509            .signer
510            .offchain_signer_private_key()
511            .expose_secret()
512            .sign(message[1]);
513
514        let input = OwnershipProofCircuitInput {
515            key_index: authenticator_input.key_index,
516            key_set: authenticator_input.key_set.clone(),
517            inclusion_proof: authenticator_input.inclusion_proof.clone(),
518            nonce,
519            signature,
520            commitment_blinder: credential_blinding_factor,
521        };
522
523        let prover = self
524            .zk_artifact_source
525            .ownership_prover()
526            .map_err(AuthenticatorError::ZkArtifactError)?;
527
528        Ok(generate_ownership_proof_with_prover(input, prover)?)
529    }
530}
531
532#[cfg(test)]
533mod tests {
534    use crate::{
535        authenticator::Authenticator,
536        error::AuthenticatorError,
537        service_client::{ServiceClient, ServiceKind},
538    };
539    use alloy::primitives::address;
540    use ruint::aliases::U256;
541    use std::sync::Arc;
542    use taceo_oprf::client::Connector;
543    use world_id_primitives::{
544        Config, FieldElement, ServiceEndpoint, Signer, TREE_DEPTH, merkle::AccountInclusionProof,
545    };
546    use world_id_proof::artifacts::{ZkArtifactSource, dummy::DummyZkArtifactSource};
547    use world_id_test_utils::fixtures::single_leaf_merkle_fixture;
548
549    fn build_test_authenticator(
550        seed: &[u8; 32],
551        leaf_index: u64,
552        zk_artifact_source: Arc<dyn ZkArtifactSource>,
553    ) -> (Authenticator, AccountInclusionProof<TREE_DEPTH>) {
554        let signer = Signer::from_seed_bytes(seed).expect("valid seed");
555        let pubkey = signer.offchain_signer_pubkey();
556
557        let fixture =
558            single_leaf_merkle_fixture(vec![pubkey], leaf_index).expect("valid merkle fixture");
559        let account_inclusion_proof =
560            AccountInclusionProof::new(fixture.inclusion_proof, fixture.key_set);
561
562        let config = Config::new(
563            None,
564            1,
565            address!("0x0000000000000000000000000000000000000001"),
566            ServiceEndpoint::direct("http://indexer.example.com".to_string()),
567            ServiceEndpoint::direct("http://gateway.example.com".to_string()),
568            Vec::new(),
569            2,
570        )
571        .expect("valid config");
572
573        let http_client = reqwest::Client::new();
574        let authenticator = Authenticator {
575            config: config.clone(),
576            packed_account_data: U256::from(leaf_index),
577            signer,
578            registry: None,
579            indexer_client: ServiceClient::new(
580                http_client.clone(),
581                ServiceKind::Indexer,
582                config.indexer(),
583            )
584            .expect("valid indexer client"),
585            gateway_client: ServiceClient::new(http_client, ServiceKind::Gateway, config.gateway())
586                .expect("valid gateway client"),
587            ws_connector: Connector::Plain,
588            zk_artifact_source,
589        };
590
591        (authenticator, account_inclusion_proof)
592    }
593
594    #[tokio::test]
595    async fn test_prove_credential_sub_rejects_wrong_sub() {
596        let leaf_index = 1u64;
597        let (authenticator, inclusion_proof) =
598            build_test_authenticator(&[42u8; 32], leaf_index, Arc::new(DummyZkArtifactSource));
599
600        let blinding_factor = FieldElement::from(999u64);
601        let wrong_sub = FieldElement::from(123u64);
602
603        let result = authenticator
604            .prove_credential_sub(
605                FieldElement::from(1_234_567_890u64),
606                blinding_factor,
607                wrong_sub,
608                Some(inclusion_proof),
609            )
610            .await;
611
612        assert!(matches!(
613            result,
614            Err(AuthenticatorError::InvalidSubOrBlindingFactor)
615        ));
616    }
617
618    #[tokio::test]
619    #[cfg(all(
620        not(target_arch = "wasm32"),
621        feature = "embed-zkeys",
622        feature = "embed-ownership-prover"
623    ))]
624    async fn test_prove_credential_sub_succeeds_with_correct_sub() {
625        use world_id_primitives::Credential;
626        use world_id_proof::artifacts::{ZkArtifactSourceExt as _, embedded::EmbeddedZkArtifacts};
627
628        let leaf_index = 1u64;
629        let zk_artifact_source = EmbeddedZkArtifacts.cached();
630        let (authenticator, inclusion_proof) =
631            build_test_authenticator(&[42u8; 32], leaf_index, Arc::new(zk_artifact_source));
632
633        let blinding_factor = FieldElement::from(999u64);
634        let correct_sub = Credential::compute_sub(leaf_index, blinding_factor);
635        let nonce = FieldElement::from(1_234_567_890u64);
636
637        authenticator
638            .prove_credential_sub(nonce, blinding_factor, correct_sub, Some(inclusion_proof))
639            .await
640            .expect("proof generation should succeed");
641    }
642}