1use std::sync::Arc;
6
7use crate::api_types::{
8 AccountInclusionProof, CreateAccountRequest, GatewayRequestState, GatewayStatusResponse,
9 IndexerAuthenticatorPubkeysResponse, IndexerErrorCode, IndexerPackedAccountRequest,
10 IndexerPackedAccountResponse, IndexerQueryRequest, IndexerSignatureNonceResponse,
11 InsertAuthenticatorRequest, RemoveAuthenticatorRequest, ServiceApiError,
12 UpdateAuthenticatorRequest,
13};
14use world_id_primitives::{
15 Credential, FieldElement, ProofRequest, RequestItem, ResponseItem, SessionNullifier, Signer,
16};
17
18use crate::registry::{
19 WorldIdRegistry::WorldIdRegistryInstance, domain, sign_insert_authenticator,
20 sign_remove_authenticator, sign_update_authenticator,
21};
22use alloy::{
23 primitives::{Address, U256},
24 providers::DynProvider,
25 signers::{Signature, SignerSync},
26 uint,
27};
28use ark_serialize::CanonicalSerialize;
29use eddsa_babyjubjub::{EdDSAPublicKey, EdDSASignature};
30use groth16_material::circom::CircomGroth16Material;
31use reqwest::StatusCode;
32use secrecy::ExposeSecret;
33use taceo_oprf::client::Connector;
34pub use world_id_primitives::{Config, TREE_DEPTH, authenticator::ProtocolSigner};
35use world_id_primitives::{
36 PrimitiveError, ZeroKnowledgeProof,
37 authenticator::{
38 AuthenticatorPublicKeySet, SparseAuthenticatorPubkeysError,
39 decode_sparse_authenticator_pubkeys,
40 },
41 merkle::MerkleInclusionProof,
42};
43use world_id_proof::{
44 AuthenticatorProofInput,
45 credential_blinding_factor::OprfCredentialBlindingFactor,
46 nullifier::OprfNullifier,
47 proof::{ProofError, generate_nullifier_proof},
48};
49
50static MASK_RECOVERY_COUNTER: U256 =
51 uint!(0xFFFFFFFF00000000000000000000000000000000000000000000000000000000_U256);
52static MASK_PUBKEY_ID: U256 =
53 uint!(0x00000000FFFFFFFF000000000000000000000000000000000000000000000000_U256);
54static MASK_LEAF_INDEX: U256 =
55 uint!(0x000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFF_U256);
56
57pub struct Authenticator {
59 pub config: Config,
61 pub packed_account_data: U256,
64 signer: Signer,
65 registry: Option<Arc<WorldIdRegistryInstance<DynProvider>>>,
66 http_client: reqwest::Client,
67 ws_connector: Connector,
68 query_material: Arc<CircomGroth16Material>,
69 nullifier_material: Arc<CircomGroth16Material>,
70}
71
72#[expect(clippy::missing_fields_in_debug)]
73impl std::fmt::Debug for Authenticator {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 f.debug_struct("Authenticator")
76 .field("config", &self.config)
77 .field("packed_account_data", &self.packed_account_data)
78 .field("signer", &self.signer)
79 .finish()
80 }
81}
82
83impl Authenticator {
84 async fn response_body_or_fallback(response: reqwest::Response) -> String {
85 response
86 .text()
87 .await
88 .unwrap_or_else(|e| format!("Unable to read response body: {e}"))
89 }
90
91 pub async fn init(
101 seed: &[u8],
102 config: Config,
103 query_material: Arc<CircomGroth16Material>,
104 nullifier_material: Arc<CircomGroth16Material>,
105 ) -> Result<Self, AuthenticatorError> {
106 let signer = Signer::from_seed_bytes(seed)?;
107
108 let registry: Option<Arc<WorldIdRegistryInstance<DynProvider>>> =
109 config.rpc_url().map(|rpc_url| {
110 let provider = alloy::providers::ProviderBuilder::new()
111 .with_chain_id(config.chain_id())
112 .connect_http(rpc_url.clone());
113 Arc::new(crate::registry::WorldIdRegistry::new(
114 *config.registry_address(),
115 alloy::providers::Provider::erased(provider),
116 ))
117 });
118
119 let http_client = reqwest::Client::new();
120
121 let packed_account_data = Self::get_packed_account_data(
122 signer.onchain_signer_address(),
123 registry.as_deref(),
124 &config,
125 &http_client,
126 )
127 .await?;
128
129 #[cfg(not(target_arch = "wasm32"))]
130 let ws_connector = {
131 let mut root_store = rustls::RootCertStore::empty();
132 root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
133 let rustls_config = rustls::ClientConfig::builder()
134 .with_root_certificates(root_store)
135 .with_no_client_auth();
136 Connector::Rustls(Arc::new(rustls_config))
137 };
138
139 #[cfg(target_arch = "wasm32")]
140 let ws_connector = Connector;
141
142 Ok(Self {
143 packed_account_data,
144 signer,
145 config,
146 registry,
147 http_client,
148 ws_connector,
149 query_material,
150 nullifier_material,
151 })
152 }
153
154 pub async fn register(
162 seed: &[u8],
163 config: Config,
164 recovery_address: Option<Address>,
165 ) -> Result<InitializingAuthenticator, AuthenticatorError> {
166 let http_client = reqwest::Client::new();
167 InitializingAuthenticator::new(seed, config, recovery_address, http_client).await
168 }
169
170 pub async fn init_or_register(
183 seed: &[u8],
184 config: Config,
185 query_material: Arc<CircomGroth16Material>,
186 nullifier_material: Arc<CircomGroth16Material>,
187 recovery_address: Option<Address>,
188 ) -> Result<Self, AuthenticatorError> {
189 match Self::init(
190 seed,
191 config.clone(),
192 query_material.clone(),
193 nullifier_material.clone(),
194 )
195 .await
196 {
197 Ok(authenticator) => Ok(authenticator),
198 Err(AuthenticatorError::AccountDoesNotExist) => {
199 let http_client = reqwest::Client::new();
201 let initializing_authenticator = InitializingAuthenticator::new(
202 seed,
203 config.clone(),
204 recovery_address,
205 http_client,
206 )
207 .await?;
208
209 let backoff = backon::ExponentialBuilder::default()
210 .with_min_delay(std::time::Duration::from_millis(800))
211 .with_factor(1.5)
212 .without_max_times()
213 .with_total_delay(Some(std::time::Duration::from_secs(120)));
214
215 let poller = || async {
216 let poll_status = initializing_authenticator.poll_status().await;
217 let result = match poll_status {
218 Ok(GatewayRequestState::Finalized { .. }) => Ok(()),
219 Ok(GatewayRequestState::Failed { error_code, error }) => Err(
220 PollResult::TerminalError(AuthenticatorError::RegistrationError {
221 error_code: error_code.map(|v| v.to_string()).unwrap_or_default(),
222 error_message: error,
223 }),
224 ),
225 Err(AuthenticatorError::GatewayError { status, body }) => {
226 if status.is_client_error() {
227 Err(PollResult::TerminalError(
228 AuthenticatorError::GatewayError { status, body },
229 ))
230 } else {
231 Err(PollResult::Retryable)
232 }
233 }
234 _ => Err(PollResult::Retryable),
235 };
236
237 match result {
238 Ok(()) => match Self::init(
239 seed,
240 config.clone(),
241 query_material.clone(),
242 nullifier_material.clone(),
243 )
244 .await
245 {
246 Ok(auth) => Ok(auth),
247 Err(AuthenticatorError::AccountDoesNotExist) => {
248 Err(PollResult::Retryable)
249 }
250 Err(e) => Err(PollResult::TerminalError(e)),
251 },
252 Err(e) => Err(e),
253 }
254 };
255
256 let result = backon::Retryable::retry(poller, backoff)
257 .when(|e| matches!(e, PollResult::Retryable))
258 .await;
259
260 match result {
261 Ok(authenticator) => Ok(authenticator),
262 Err(PollResult::TerminalError(e)) => Err(e),
263 Err(PollResult::Retryable) => Err(AuthenticatorError::Timeout),
264 }
265 }
266 Err(e) => Err(e),
267 }
268 }
269
270 pub async fn get_packed_account_data(
278 onchain_signer_address: Address,
279 registry: Option<&WorldIdRegistryInstance<DynProvider>>,
280 config: &Config,
281 http_client: &reqwest::Client,
282 ) -> Result<U256, AuthenticatorError> {
283 let raw_index = if let Some(registry) = registry {
285 registry
287 .getPackedAccountData(onchain_signer_address)
288 .call()
289 .await?
290 } else {
291 let url = format!("{}/packed-account", config.indexer_url());
292 let req = IndexerPackedAccountRequest {
293 authenticator_address: onchain_signer_address,
294 };
295 let resp = http_client.post(&url).json(&req).send().await?;
296 let status = resp.status();
297 if !status.is_success() {
298 let body = Self::response_body_or_fallback(resp).await;
299 if let Ok(error_resp) =
300 serde_json::from_str::<ServiceApiError<IndexerErrorCode>>(&body)
301 {
302 return match error_resp.code {
303 IndexerErrorCode::AccountDoesNotExist => {
304 Err(AuthenticatorError::AccountDoesNotExist)
305 }
306 _ => Err(AuthenticatorError::IndexerError {
307 status,
308 body: error_resp.message,
309 }),
310 };
311 }
312 return Err(AuthenticatorError::IndexerError { status, body });
313 }
314
315 let response: IndexerPackedAccountResponse = resp.json().await?;
316 response.packed_account_data
317 };
318
319 if raw_index == U256::ZERO {
320 return Err(AuthenticatorError::AccountDoesNotExist);
321 }
322
323 Ok(raw_index)
324 }
325
326 #[must_use]
329 pub const fn onchain_address(&self) -> Address {
330 self.signer.onchain_signer_address()
331 }
332
333 #[must_use]
336 pub fn offchain_pubkey(&self) -> EdDSAPublicKey {
337 self.signer.offchain_signer_pubkey()
338 }
339
340 pub fn offchain_pubkey_compressed(&self) -> Result<U256, AuthenticatorError> {
345 let pk = self.signer.offchain_signer_pubkey().pk;
346 let mut compressed_bytes = Vec::new();
347 pk.serialize_compressed(&mut compressed_bytes)
348 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
349 Ok(U256::from_le_slice(&compressed_bytes))
350 }
351
352 #[must_use]
354 pub fn registry(&self) -> Option<Arc<WorldIdRegistryInstance<DynProvider>>> {
355 self.registry.clone()
356 }
357
358 #[must_use]
374 pub fn leaf_index(&self) -> u64 {
375 (self.packed_account_data & MASK_LEAF_INDEX).to::<u64>()
376 }
377
378 #[must_use]
382 pub fn recovery_counter(&self) -> U256 {
383 let recovery_counter = self.packed_account_data & MASK_RECOVERY_COUNTER;
384 recovery_counter >> 224
385 }
386
387 #[must_use]
391 pub fn pubkey_id(&self) -> U256 {
392 let pubkey_id = self.packed_account_data & MASK_PUBKEY_ID;
393 pubkey_id >> 192
394 }
395
396 pub async fn fetch_inclusion_proof(
402 &self,
403 ) -> Result<(MerkleInclusionProof<TREE_DEPTH>, AuthenticatorPublicKeySet), AuthenticatorError>
404 {
405 let url = format!("{}/inclusion-proof", self.config.indexer_url());
406 let req = IndexerQueryRequest {
407 leaf_index: self.leaf_index(),
408 };
409 let response = self.http_client.post(&url).json(&req).send().await?;
410 let status = response.status();
411 if !status.is_success() {
412 return Err(AuthenticatorError::IndexerError {
413 status,
414 body: Self::response_body_or_fallback(response).await,
415 });
416 }
417 let response = response.json::<AccountInclusionProof<TREE_DEPTH>>().await?;
418
419 Ok((response.inclusion_proof, response.authenticator_pubkeys))
420 }
421
422 pub async fn fetch_authenticator_pubkeys(
431 &self,
432 ) -> Result<AuthenticatorPublicKeySet, AuthenticatorError> {
433 let url = format!("{}/authenticator-pubkeys", self.config.indexer_url());
434 let req = IndexerQueryRequest {
435 leaf_index: self.leaf_index(),
436 };
437 let response = self.http_client.post(&url).json(&req).send().await?;
438 let status = response.status();
439 if !status.is_success() {
440 return Err(AuthenticatorError::IndexerError {
441 status,
442 body: Self::response_body_or_fallback(response).await,
443 });
444 }
445 let response = response
446 .json::<IndexerAuthenticatorPubkeysResponse>()
447 .await?;
448 Self::decode_indexer_pubkeys(response.authenticator_pubkeys)
449 }
450
451 pub async fn signing_nonce(&self) -> Result<U256, AuthenticatorError> {
456 let registry = self.registry();
457 if let Some(registry) = registry {
458 let nonce = registry.getSignatureNonce(self.leaf_index()).call().await?;
459 Ok(nonce)
460 } else {
461 let url = format!("{}/signature-nonce", self.config.indexer_url());
462 let req = IndexerQueryRequest {
463 leaf_index: self.leaf_index(),
464 };
465 let resp = self.http_client.post(&url).json(&req).send().await?;
466
467 let status = resp.status();
468 if !status.is_success() {
469 return Err(AuthenticatorError::IndexerError {
470 status,
471 body: Self::response_body_or_fallback(resp).await,
472 });
473 }
474
475 let response: IndexerSignatureNonceResponse = resp.json().await?;
476 Ok(response.signature_nonce)
477 }
478 }
479
480 pub fn danger_sign_challenge(&self, challenge: &[u8]) -> Result<Signature, AuthenticatorError> {
492 self.signer
493 .onchain_signer()
494 .sign_message_sync(challenge)
495 .map_err(|e| AuthenticatorError::Generic(format!("signature error: {e}")))
496 }
497
498 fn check_oprf_config(&self) -> Result<(&[String], usize), AuthenticatorError> {
503 let services = self.config.nullifier_oracle_urls();
504 if services.is_empty() {
505 return Err(AuthenticatorError::Generic(
506 "No nullifier oracle URLs configured".to_string(),
507 ));
508 }
509 let requested_threshold = self.config.nullifier_oracle_threshold();
510 if requested_threshold == 0 {
511 return Err(AuthenticatorError::InvalidConfig {
512 attribute: "nullifier_oracle_threshold",
513 reason: "must be at least 1".to_string(),
514 });
515 }
516 let threshold = requested_threshold.min(services.len());
517 Ok((services, threshold))
518 }
519
520 fn decode_indexer_pubkeys(
521 pubkeys: Vec<Option<U256>>,
522 ) -> Result<AuthenticatorPublicKeySet, AuthenticatorError> {
523 decode_sparse_authenticator_pubkeys(pubkeys).map_err(|e| match e {
524 SparseAuthenticatorPubkeysError::SlotOutOfBounds {
525 slot_index,
526 max_supported_slot,
527 } => AuthenticatorError::InvalidIndexerPubkeySlot {
528 slot_index,
529 max_supported_slot,
530 },
531 SparseAuthenticatorPubkeysError::InvalidCompressedPubkey { slot_index, reason } => {
532 PrimitiveError::Deserialization(format!(
533 "invalid authenticator public key returned by indexer at slot {slot_index}: {reason}"
534 ))
535 .into()
536 }
537 })
538 }
539
540 fn insert_or_reuse_authenticator_key(
541 key_set: &mut AuthenticatorPublicKeySet,
542 new_authenticator_pubkey: EdDSAPublicKey,
543 ) -> Result<usize, AuthenticatorError> {
544 if let Some(index) = key_set.iter().position(Option::is_none) {
545 key_set.try_set_at_index(index, new_authenticator_pubkey)?;
546 Ok(index)
547 } else {
548 key_set.try_push(new_authenticator_pubkey)?;
549 Ok(key_set.len() - 1)
550 }
551 }
552
553 pub async fn generate_nullifier(
565 &self,
566 proof_request: &ProofRequest,
567 inclusion_proof: MerkleInclusionProof<TREE_DEPTH>,
568 key_set: AuthenticatorPublicKeySet,
569 ) -> Result<OprfNullifier, AuthenticatorError> {
570 let (services, threshold) = self.check_oprf_config()?;
571 let key_index = key_set
572 .iter()
573 .position(|pk| {
574 pk.as_ref()
575 .is_some_and(|pk| pk.pk == self.offchain_pubkey().pk)
576 })
577 .ok_or(AuthenticatorError::PublicKeyNotFound)? as u64;
578
579 let authenticator_input = AuthenticatorProofInput::new(
580 key_set,
581 inclusion_proof,
582 self.signer
583 .offchain_signer_private_key()
584 .expose_secret()
585 .clone(),
586 key_index,
587 );
588
589 Ok(OprfNullifier::generate(
590 services,
591 threshold,
592 &self.query_material,
593 authenticator_input,
594 proof_request,
595 self.ws_connector.clone(),
596 )
597 .await?)
598 }
599
600 pub async fn generate_credential_blinding_factor(
609 &self,
610 issuer_schema_id: u64,
611 ) -> Result<FieldElement, AuthenticatorError> {
612 let (services, threshold) = self.check_oprf_config()?;
613
614 let (inclusion_proof, key_set) = self.fetch_inclusion_proof().await?;
615 let key_index = key_set
616 .iter()
617 .position(|pk| {
618 pk.as_ref()
619 .is_some_and(|pk| pk.pk == self.offchain_pubkey().pk)
620 })
621 .ok_or(AuthenticatorError::PublicKeyNotFound)? as u64;
622
623 let authenticator_input = AuthenticatorProofInput::new(
624 key_set,
625 inclusion_proof,
626 self.signer
627 .offchain_signer_private_key()
628 .expose_secret()
629 .clone(),
630 key_index,
631 );
632
633 let blinding_factor = OprfCredentialBlindingFactor::generate(
634 services,
635 threshold,
636 &self.query_material,
637 authenticator_input,
638 issuer_schema_id,
639 FieldElement::ZERO, self.ws_connector.clone(),
641 )
642 .await?;
643
644 Ok(blinding_factor.verifiable_oprf_output.output.into())
645 }
646
647 #[allow(clippy::too_many_arguments)]
668 pub fn generate_single_proof(
669 &self,
670 oprf_nullifier: OprfNullifier,
671 request_item: &RequestItem,
672 credential: &Credential,
673 credential_sub_blinding_factor: FieldElement,
674 session_id_r_seed: FieldElement,
675 session_id: Option<FieldElement>,
676 request_timestamp: u64,
677 ) -> Result<ResponseItem, AuthenticatorError> {
678 let mut rng = rand::rngs::OsRng;
679
680 let merkle_root: FieldElement = oprf_nullifier.query_proof_input.merkle_root.into();
681 let action_from_query: FieldElement = oprf_nullifier.query_proof_input.action.into();
682
683 let expires_at_min = request_item.effective_expires_at_min(request_timestamp);
684
685 let (proof, _public_inputs, nullifier) = generate_nullifier_proof(
686 &self.nullifier_material,
687 &mut rng,
688 credential,
689 credential_sub_blinding_factor,
690 oprf_nullifier,
691 request_item,
692 session_id,
693 session_id_r_seed,
694 expires_at_min,
695 )?;
696
697 let proof = ZeroKnowledgeProof::from_groth16_proof(&proof, merkle_root);
698
699 let nullifier_fe: FieldElement = nullifier.into();
701 let response_item = if session_id.is_some() {
702 let session_nullifier = SessionNullifier::new(nullifier_fe, action_from_query);
703 ResponseItem::new_session(
704 request_item.identifier.clone(),
705 request_item.issuer_schema_id,
706 proof,
707 session_nullifier,
708 expires_at_min,
709 )
710 } else {
711 ResponseItem::new_uniqueness(
712 request_item.identifier.clone(),
713 request_item.issuer_schema_id,
714 proof,
715 nullifier_fe.into(),
716 expires_at_min,
717 )
718 };
719
720 Ok(response_item)
721 }
722
723 pub async fn insert_authenticator(
732 &mut self,
733 new_authenticator_pubkey: EdDSAPublicKey,
734 new_authenticator_address: Address,
735 ) -> Result<String, AuthenticatorError> {
736 let leaf_index = self.leaf_index();
737 let nonce = self.signing_nonce().await?;
738 let mut key_set = self.fetch_authenticator_pubkeys().await?;
739 let old_offchain_signer_commitment = key_set.leaf_hash();
740 let encoded_offchain_pubkey = new_authenticator_pubkey.to_ethereum_representation()?;
741 let index =
742 Self::insert_or_reuse_authenticator_key(&mut key_set, new_authenticator_pubkey)?;
743 let new_offchain_signer_commitment = key_set.leaf_hash();
744
745 let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
746
747 #[allow(clippy::cast_possible_truncation)]
748 let signature = sign_insert_authenticator(
750 &self.signer.onchain_signer(),
751 leaf_index,
752 new_authenticator_address,
753 index as u32,
754 encoded_offchain_pubkey,
755 new_offchain_signer_commitment.into(),
756 nonce,
757 &eip712_domain,
758 )
759 .map_err(|e| {
760 AuthenticatorError::Generic(format!("Failed to sign insert authenticator: {e}"))
761 })?;
762
763 #[allow(clippy::cast_possible_truncation)]
764 let req = InsertAuthenticatorRequest {
766 leaf_index,
767 new_authenticator_address,
768 pubkey_id: index as u32,
769 new_authenticator_pubkey: encoded_offchain_pubkey,
770 old_offchain_signer_commitment: old_offchain_signer_commitment.into(),
771 new_offchain_signer_commitment: new_offchain_signer_commitment.into(),
772 signature: signature.as_bytes().to_vec(),
773 nonce,
774 };
775
776 let resp = self
777 .http_client
778 .post(format!(
779 "{}/insert-authenticator",
780 self.config.gateway_url()
781 ))
782 .json(&req)
783 .send()
784 .await?;
785
786 let status = resp.status();
787 if status.is_success() {
788 let body: GatewayStatusResponse = resp.json().await?;
789 Ok(body.request_id)
790 } else {
791 let body_text = Self::response_body_or_fallback(resp).await;
792 Err(AuthenticatorError::GatewayError {
793 status,
794 body: body_text,
795 })
796 }
797 }
798
799 pub async fn update_authenticator(
808 &mut self,
809 old_authenticator_address: Address,
810 new_authenticator_address: Address,
811 new_authenticator_pubkey: EdDSAPublicKey,
812 index: u32,
813 ) -> Result<String, AuthenticatorError> {
814 let leaf_index = self.leaf_index();
815 let nonce = self.signing_nonce().await?;
816 let mut key_set = self.fetch_authenticator_pubkeys().await?;
817 let old_commitment: U256 = key_set.leaf_hash().into();
818 let encoded_offchain_pubkey = new_authenticator_pubkey.to_ethereum_representation()?;
819 key_set.try_set_at_index(index as usize, new_authenticator_pubkey)?;
820 let new_commitment: U256 = key_set.leaf_hash().into();
821
822 let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
823
824 let signature = sign_update_authenticator(
825 &self.signer.onchain_signer(),
826 leaf_index,
827 old_authenticator_address,
828 new_authenticator_address,
829 index,
830 encoded_offchain_pubkey,
831 new_commitment,
832 nonce,
833 &eip712_domain,
834 )
835 .map_err(|e| {
836 AuthenticatorError::Generic(format!("Failed to sign update authenticator: {e}"))
837 })?;
838
839 let req = UpdateAuthenticatorRequest {
840 leaf_index,
841 old_authenticator_address,
842 new_authenticator_address,
843 old_offchain_signer_commitment: old_commitment,
844 new_offchain_signer_commitment: new_commitment,
845 signature: signature.as_bytes().to_vec(),
846 nonce,
847 pubkey_id: index,
848 new_authenticator_pubkey: encoded_offchain_pubkey,
849 };
850
851 let resp = self
852 .http_client
853 .post(format!(
854 "{}/update-authenticator",
855 self.config.gateway_url()
856 ))
857 .json(&req)
858 .send()
859 .await?;
860
861 let status = resp.status();
862 if status.is_success() {
863 let gateway_resp: GatewayStatusResponse = resp.json().await?;
864 Ok(gateway_resp.request_id)
865 } else {
866 let body_text = Self::response_body_or_fallback(resp).await;
867 Err(AuthenticatorError::GatewayError {
868 status,
869 body: body_text,
870 })
871 }
872 }
873
874 pub async fn remove_authenticator(
883 &mut self,
884 authenticator_address: Address,
885 index: u32,
886 ) -> Result<String, AuthenticatorError> {
887 let leaf_index = self.leaf_index();
888 let nonce = self.signing_nonce().await?;
889 let mut key_set = self.fetch_authenticator_pubkeys().await?;
890 let old_commitment: U256 = key_set.leaf_hash().into();
891 let existing_pubkey = key_set
892 .get(index as usize)
893 .ok_or(AuthenticatorError::PublicKeyNotFound)?;
894
895 let encoded_old_offchain_pubkey = existing_pubkey.to_ethereum_representation()?;
896
897 key_set.try_clear_at_index(index as usize)?;
898 let new_commitment: U256 = key_set.leaf_hash().into();
899
900 let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
901
902 let signature = sign_remove_authenticator(
903 &self.signer.onchain_signer(),
904 leaf_index,
905 authenticator_address,
906 index,
907 encoded_old_offchain_pubkey,
908 new_commitment,
909 nonce,
910 &eip712_domain,
911 )
912 .map_err(|e| {
913 AuthenticatorError::Generic(format!("Failed to sign remove authenticator: {e}"))
914 })?;
915
916 let req = RemoveAuthenticatorRequest {
917 leaf_index,
918 authenticator_address,
919 old_offchain_signer_commitment: old_commitment,
920 new_offchain_signer_commitment: new_commitment,
921 signature: signature.as_bytes().to_vec(),
922 nonce,
923 pubkey_id: Some(index),
924 authenticator_pubkey: Some(encoded_old_offchain_pubkey),
925 };
926
927 let resp = self
928 .http_client
929 .post(format!(
930 "{}/remove-authenticator",
931 self.config.gateway_url()
932 ))
933 .json(&req)
934 .send()
935 .await?;
936
937 let status = resp.status();
938 if status.is_success() {
939 let gateway_resp: GatewayStatusResponse = resp.json().await?;
940 Ok(gateway_resp.request_id)
941 } else {
942 let body_text = Self::response_body_or_fallback(resp).await;
943 Err(AuthenticatorError::GatewayError {
944 status,
945 body: body_text,
946 })
947 }
948 }
949}
950
951pub struct InitializingAuthenticator {
954 request_id: String,
955 http_client: reqwest::Client,
956 config: Config,
957}
958
959impl InitializingAuthenticator {
960 #[must_use]
962 pub fn request_id(&self) -> &str {
963 &self.request_id
964 }
965
966 async fn new(
972 seed: &[u8],
973 config: Config,
974 recovery_address: Option<Address>,
975 http_client: reqwest::Client,
976 ) -> Result<Self, AuthenticatorError> {
977 let signer = Signer::from_seed_bytes(seed)?;
978
979 let mut key_set = AuthenticatorPublicKeySet::default();
980 key_set.try_push(signer.offchain_signer_pubkey())?;
981 let leaf_hash = key_set.leaf_hash();
982
983 let offchain_pubkey_compressed = {
984 let pk = signer.offchain_signer_pubkey().pk;
985 let mut compressed_bytes = Vec::new();
986 pk.serialize_compressed(&mut compressed_bytes)
987 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
988 U256::from_le_slice(&compressed_bytes)
989 };
990
991 let req = CreateAccountRequest {
992 recovery_address,
993 authenticator_addresses: vec![signer.onchain_signer_address()],
994 authenticator_pubkeys: vec![offchain_pubkey_compressed],
995 offchain_signer_commitment: leaf_hash.into(),
996 };
997
998 let resp = http_client
999 .post(format!("{}/create-account", config.gateway_url()))
1000 .json(&req)
1001 .send()
1002 .await?;
1003
1004 let status = resp.status();
1005 if status.is_success() {
1006 let body: GatewayStatusResponse = resp.json().await?;
1007 Ok(Self {
1008 request_id: body.request_id,
1009 http_client,
1010 config,
1011 })
1012 } else {
1013 let body_text = Authenticator::response_body_or_fallback(resp).await;
1014 Err(AuthenticatorError::GatewayError {
1015 status,
1016 body: body_text,
1017 })
1018 }
1019 }
1020
1021 pub async fn poll_status(&self) -> Result<GatewayRequestState, AuthenticatorError> {
1027 let resp = self
1028 .http_client
1029 .get(format!(
1030 "{}/status/{}",
1031 self.config.gateway_url(),
1032 self.request_id
1033 ))
1034 .send()
1035 .await?;
1036
1037 let status = resp.status();
1038
1039 if status.is_success() {
1040 let body: GatewayStatusResponse = resp.json().await?;
1041 Ok(body.status)
1042 } else {
1043 let body_text = Authenticator::response_body_or_fallback(resp).await;
1044 Err(AuthenticatorError::GatewayError {
1045 status,
1046 body: body_text,
1047 })
1048 }
1049 }
1050}
1051
1052impl ProtocolSigner for Authenticator {
1053 fn sign(&self, message: FieldElement) -> EdDSASignature {
1054 self.signer
1055 .offchain_signer_private_key()
1056 .expose_secret()
1057 .sign(*message)
1058 }
1059}
1060
1061pub trait OnchainKeyRepresentable {
1063 fn to_ethereum_representation(&self) -> Result<U256, PrimitiveError>;
1070}
1071
1072impl OnchainKeyRepresentable for EdDSAPublicKey {
1073 fn to_ethereum_representation(&self) -> Result<U256, PrimitiveError> {
1075 let mut compressed_bytes = Vec::new();
1076 self.pk
1077 .serialize_compressed(&mut compressed_bytes)
1078 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
1079 Ok(U256::from_le_slice(&compressed_bytes))
1080 }
1081}
1082
1083#[derive(Debug, thiserror::Error)]
1085pub enum AuthenticatorError {
1086 #[error(transparent)]
1088 PrimitiveError(#[from] PrimitiveError),
1089
1090 #[error("Account is not registered for this authenticator.")]
1093 AccountDoesNotExist,
1094
1095 #[error("Error interacting with EVM contract: {0}")]
1097 ContractError(#[from] alloy::contract::Error),
1098
1099 #[error("Network error: {0}")]
1101 NetworkError(#[from] reqwest::Error),
1102
1103 #[error("Public key not found.")]
1105 PublicKeyNotFound,
1106
1107 #[error("Gateway error (status {status}): {body}")]
1109 GatewayError {
1110 status: StatusCode,
1112 body: String,
1114 },
1115
1116 #[error("Indexer error (status {status}): {body}")]
1118 IndexerError {
1119 status: StatusCode,
1121 body: String,
1123 },
1124
1125 #[error("Account creation timed out")]
1127 Timeout,
1128
1129 #[error("Invalid configuration for {attribute}: {reason}")]
1131 InvalidConfig {
1132 attribute: &'static str,
1134 reason: String,
1136 },
1137
1138 #[error("The provided credential is not valid for the provided proof request")]
1140 InvalidCredentialForProofRequest,
1141
1142 #[error("Registration error ({error_code}): {error_message}")]
1146 RegistrationError {
1147 error_code: String,
1149 error_message: String,
1151 },
1152
1153 #[error(transparent)]
1155 ProofError(#[from] ProofError),
1156
1157 #[error(
1159 "Invalid indexer authenticator pubkey slot {slot_index}; max supported slot is {max_supported_slot}"
1160 )]
1161 InvalidIndexerPubkeySlot {
1162 slot_index: usize,
1164 max_supported_slot: usize,
1166 },
1167
1168 #[error("{0}")]
1170 Generic(String),
1171}
1172
1173#[derive(Debug)]
1174enum PollResult {
1175 Retryable,
1176 TerminalError(AuthenticatorError),
1177}
1178
1179#[cfg(all(test, feature = "embed-zkeys"))]
1180mod tests {
1181 use super::*;
1182 use alloy::primitives::{U256, address};
1183 use std::sync::OnceLock;
1184 use world_id_primitives::authenticator::MAX_AUTHENTICATOR_KEYS;
1185
1186 fn test_materials() -> (Arc<CircomGroth16Material>, Arc<CircomGroth16Material>) {
1187 static QUERY: OnceLock<Arc<CircomGroth16Material>> = OnceLock::new();
1188 static NULLIFIER: OnceLock<Arc<CircomGroth16Material>> = OnceLock::new();
1189
1190 let query = QUERY.get_or_init(|| {
1191 Arc::new(world_id_proof::proof::load_embedded_query_material().unwrap())
1192 });
1193 let nullifier = NULLIFIER.get_or_init(|| {
1194 Arc::new(world_id_proof::proof::load_embedded_nullifier_material().unwrap())
1195 });
1196
1197 (Arc::clone(query), Arc::clone(nullifier))
1198 }
1199
1200 fn test_pubkey(seed_byte: u8) -> EdDSAPublicKey {
1201 Signer::from_seed_bytes(&[seed_byte; 32])
1202 .unwrap()
1203 .offchain_signer_pubkey()
1204 }
1205
1206 fn encoded_test_pubkey(seed_byte: u8) -> U256 {
1207 test_pubkey(seed_byte).to_ethereum_representation().unwrap()
1208 }
1209
1210 #[test]
1211 fn test_insert_or_reuse_authenticator_key_reuses_empty_slot() {
1212 let mut key_set =
1213 AuthenticatorPublicKeySet::new(vec![test_pubkey(1), test_pubkey(2), test_pubkey(4)])
1214 .unwrap();
1215 key_set[1] = None;
1216 let new_key = test_pubkey(3);
1217
1218 let index =
1219 Authenticator::insert_or_reuse_authenticator_key(&mut key_set, new_key).unwrap();
1220
1221 assert_eq!(index, 1);
1222 assert_eq!(key_set.len(), 3);
1223 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(3).pk);
1224 }
1225
1226 #[test]
1227 fn test_insert_or_reuse_authenticator_key_appends_when_no_empty_slot() {
1228 let mut key_set = AuthenticatorPublicKeySet::new(vec![test_pubkey(1)]).unwrap();
1229 let new_key = test_pubkey(2);
1230
1231 let index =
1232 Authenticator::insert_or_reuse_authenticator_key(&mut key_set, new_key).unwrap();
1233
1234 assert_eq!(index, 1);
1235 assert_eq!(key_set.len(), 2);
1236 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(2).pk);
1237 }
1238
1239 #[test]
1240 fn test_decode_indexer_pubkeys_trims_trailing_empty_slots() {
1241 let mut encoded_pubkeys = vec![Some(encoded_test_pubkey(1)), Some(encoded_test_pubkey(2))];
1242 encoded_pubkeys.extend(vec![None; MAX_AUTHENTICATOR_KEYS + 5]);
1243
1244 let key_set = Authenticator::decode_indexer_pubkeys(encoded_pubkeys).unwrap();
1245
1246 assert_eq!(key_set.len(), 2);
1247 assert_eq!(key_set[0].as_ref().unwrap().pk, test_pubkey(1).pk);
1248 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(2).pk);
1249 }
1250
1251 #[test]
1252 fn test_decode_indexer_pubkeys_rejects_used_slot_beyond_max() {
1253 let mut encoded_pubkeys = vec![None; MAX_AUTHENTICATOR_KEYS + 1];
1254 encoded_pubkeys[MAX_AUTHENTICATOR_KEYS] = Some(encoded_test_pubkey(1));
1255
1256 let error = Authenticator::decode_indexer_pubkeys(encoded_pubkeys).unwrap_err();
1257 assert!(matches!(
1258 error,
1259 AuthenticatorError::InvalidIndexerPubkeySlot {
1260 slot_index,
1261 max_supported_slot
1262 } if slot_index == MAX_AUTHENTICATOR_KEYS && max_supported_slot == MAX_AUTHENTICATOR_KEYS - 1
1263 ));
1264 }
1265
1266 #[tokio::test]
1269 async fn test_get_packed_account_data_from_indexer() {
1270 let mut server = mockito::Server::new_async().await;
1271 let indexer_url = server.url();
1272
1273 let test_address = address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0");
1274 let expected_packed_index = U256::from(42);
1275
1276 let mock = server
1277 .mock("POST", "/packed-account")
1278 .match_header("content-type", "application/json")
1279 .match_body(mockito::Matcher::JsonString(
1280 serde_json::json!({
1281 "authenticator_address": test_address
1282 })
1283 .to_string(),
1284 ))
1285 .with_status(200)
1286 .with_header("content-type", "application/json")
1287 .with_body(
1288 serde_json::json!({
1289 "packed_account_data": format!("{:#x}", expected_packed_index)
1290 })
1291 .to_string(),
1292 )
1293 .create_async()
1294 .await;
1295
1296 let config = Config::new(
1297 None,
1298 1,
1299 address!("0x0000000000000000000000000000000000000001"),
1300 indexer_url,
1301 "http://gateway.example.com".to_string(),
1302 Vec::new(),
1303 2,
1304 )
1305 .unwrap();
1306
1307 let http_client = reqwest::Client::new();
1308
1309 let result = Authenticator::get_packed_account_data(
1310 test_address,
1311 None, &config,
1313 &http_client,
1314 )
1315 .await
1316 .unwrap();
1317
1318 assert_eq!(result, expected_packed_index);
1319 mock.assert_async().await;
1320 drop(server);
1321 }
1322
1323 #[tokio::test]
1324 async fn test_get_packed_account_data_from_indexer_error() {
1325 let mut server = mockito::Server::new_async().await;
1326 let indexer_url = server.url();
1327
1328 let test_address = address!("0x0000000000000000000000000000000000000099");
1329
1330 let mock = server
1331 .mock("POST", "/packed-account")
1332 .with_status(400)
1333 .with_header("content-type", "application/json")
1334 .with_body(
1335 serde_json::json!({
1336 "code": "account_does_not_exist",
1337 "message": "There is no account for this authenticator address"
1338 })
1339 .to_string(),
1340 )
1341 .create_async()
1342 .await;
1343
1344 let config = Config::new(
1345 None,
1346 1,
1347 address!("0x0000000000000000000000000000000000000001"),
1348 indexer_url,
1349 "http://gateway.example.com".to_string(),
1350 Vec::new(),
1351 2,
1352 )
1353 .unwrap();
1354
1355 let http_client = reqwest::Client::new();
1356
1357 let result =
1358 Authenticator::get_packed_account_data(test_address, None, &config, &http_client).await;
1359
1360 assert!(matches!(
1361 result,
1362 Err(AuthenticatorError::AccountDoesNotExist)
1363 ));
1364 mock.assert_async().await;
1365 drop(server);
1366 }
1367
1368 #[tokio::test]
1369 #[cfg(not(target_arch = "wasm32"))]
1370 async fn test_signing_nonce_from_indexer() {
1371 let mut server = mockito::Server::new_async().await;
1372 let indexer_url = server.url();
1373
1374 let leaf_index = U256::from(1);
1375 let expected_nonce = U256::from(5);
1376
1377 let mock = server
1378 .mock("POST", "/signature-nonce")
1379 .match_header("content-type", "application/json")
1380 .match_body(mockito::Matcher::JsonString(
1381 serde_json::json!({
1382 "leaf_index": format!("{:#x}", leaf_index)
1383 })
1384 .to_string(),
1385 ))
1386 .with_status(200)
1387 .with_header("content-type", "application/json")
1388 .with_body(
1389 serde_json::json!({
1390 "signature_nonce": format!("{:#x}", expected_nonce)
1391 })
1392 .to_string(),
1393 )
1394 .create_async()
1395 .await;
1396
1397 let config = Config::new(
1398 None,
1399 1,
1400 address!("0x0000000000000000000000000000000000000001"),
1401 indexer_url,
1402 "http://gateway.example.com".to_string(),
1403 Vec::new(),
1404 2,
1405 )
1406 .unwrap();
1407
1408 let (query_material, nullifier_material) = test_materials();
1409 let authenticator = Authenticator {
1410 config,
1411 packed_account_data: leaf_index, signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1413 registry: None, http_client: reqwest::Client::new(),
1415 ws_connector: Connector::Plain,
1416 query_material,
1417 nullifier_material,
1418 };
1419
1420 let nonce = authenticator.signing_nonce().await.unwrap();
1421
1422 assert_eq!(nonce, expected_nonce);
1423 mock.assert_async().await;
1424 drop(server);
1425 }
1426
1427 #[test]
1428 fn test_danger_sign_challenge_returns_valid_signature() {
1429 let (query_material, nullifier_material) = test_materials();
1430 let authenticator = Authenticator {
1431 config: Config::new(
1432 None,
1433 1,
1434 address!("0x0000000000000000000000000000000000000001"),
1435 "http://indexer.example.com".to_string(),
1436 "http://gateway.example.com".to_string(),
1437 Vec::new(),
1438 2,
1439 )
1440 .unwrap(),
1441 packed_account_data: U256::from(1),
1442 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1443 registry: None,
1444 http_client: reqwest::Client::new(),
1445 ws_connector: Connector::Plain,
1446 query_material,
1447 nullifier_material,
1448 };
1449
1450 let challenge = b"test challenge";
1451 let signature = authenticator.danger_sign_challenge(challenge).unwrap();
1452
1453 let recovered = signature
1454 .recover_address_from_msg(challenge)
1455 .expect("should recover address");
1456 assert_eq!(recovered, authenticator.onchain_address());
1457 }
1458
1459 #[test]
1460 fn test_danger_sign_challenge_different_challenges_different_signatures() {
1461 let (query_material, nullifier_material) = test_materials();
1462 let authenticator = Authenticator {
1463 config: Config::new(
1464 None,
1465 1,
1466 address!("0x0000000000000000000000000000000000000001"),
1467 "http://indexer.example.com".to_string(),
1468 "http://gateway.example.com".to_string(),
1469 Vec::new(),
1470 2,
1471 )
1472 .unwrap(),
1473 packed_account_data: U256::from(1),
1474 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1475 registry: None,
1476 http_client: reqwest::Client::new(),
1477 ws_connector: Connector::Plain,
1478 query_material,
1479 nullifier_material,
1480 };
1481
1482 let sig_a = authenticator.danger_sign_challenge(b"challenge A").unwrap();
1483 let sig_b = authenticator.danger_sign_challenge(b"challenge B").unwrap();
1484 assert_ne!(sig_a, sig_b);
1485 }
1486
1487 #[test]
1488 fn test_danger_sign_challenge_deterministic() {
1489 let (query_material, nullifier_material) = test_materials();
1490 let authenticator = Authenticator {
1491 config: Config::new(
1492 None,
1493 1,
1494 address!("0x0000000000000000000000000000000000000001"),
1495 "http://indexer.example.com".to_string(),
1496 "http://gateway.example.com".to_string(),
1497 Vec::new(),
1498 2,
1499 )
1500 .unwrap(),
1501 packed_account_data: U256::from(1),
1502 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1503 registry: None,
1504 http_client: reqwest::Client::new(),
1505 ws_connector: Connector::Plain,
1506 query_material,
1507 nullifier_material,
1508 };
1509
1510 let challenge = b"deterministic test";
1511 let sig1 = authenticator.danger_sign_challenge(challenge).unwrap();
1512 let sig2 = authenticator.danger_sign_challenge(challenge).unwrap();
1513 assert_eq!(sig1, sig2);
1514 }
1515
1516 #[tokio::test]
1517 #[cfg(not(target_arch = "wasm32"))]
1518 async fn test_signing_nonce_from_indexer_error() {
1519 let mut server = mockito::Server::new_async().await;
1520 let indexer_url = server.url();
1521
1522 let mock = server
1523 .mock("POST", "/signature-nonce")
1524 .with_status(400)
1525 .with_header("content-type", "application/json")
1526 .with_body(
1527 serde_json::json!({
1528 "code": "invalid_leaf_index",
1529 "message": "Account index cannot be zero"
1530 })
1531 .to_string(),
1532 )
1533 .create_async()
1534 .await;
1535
1536 let config = Config::new(
1537 None,
1538 1,
1539 address!("0x0000000000000000000000000000000000000001"),
1540 indexer_url,
1541 "http://gateway.example.com".to_string(),
1542 Vec::new(),
1543 2,
1544 )
1545 .unwrap();
1546
1547 let (query_material, nullifier_material) = test_materials();
1548 let authenticator = Authenticator {
1549 config,
1550 packed_account_data: U256::ZERO,
1551 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1552 registry: None,
1553 http_client: reqwest::Client::new(),
1554 ws_connector: Connector::Plain,
1555 query_material,
1556 nullifier_material,
1557 };
1558
1559 let result = authenticator.signing_nonce().await;
1560
1561 assert!(matches!(
1562 result,
1563 Err(AuthenticatorError::IndexerError { .. })
1564 ));
1565 mock.assert_async().await;
1566 drop(server);
1567 }
1568}