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(
492 &mut self,
493 challenge: &[u8],
494 ) -> Result<Signature, AuthenticatorError> {
495 self.signer
496 .onchain_signer()
497 .sign_message_sync(challenge)
498 .map_err(|e| AuthenticatorError::Generic(format!("signature error: {e}")))
499 }
500
501 fn check_oprf_config(&self) -> Result<(&[String], usize), AuthenticatorError> {
506 let services = self.config.nullifier_oracle_urls();
507 if services.is_empty() {
508 return Err(AuthenticatorError::Generic(
509 "No nullifier oracle URLs configured".to_string(),
510 ));
511 }
512 let requested_threshold = self.config.nullifier_oracle_threshold();
513 if requested_threshold == 0 {
514 return Err(AuthenticatorError::InvalidConfig {
515 attribute: "nullifier_oracle_threshold",
516 reason: "must be at least 1".to_string(),
517 });
518 }
519 let threshold = requested_threshold.min(services.len());
520 Ok((services, threshold))
521 }
522
523 fn decode_indexer_pubkeys(
524 pubkeys: Vec<Option<U256>>,
525 ) -> Result<AuthenticatorPublicKeySet, AuthenticatorError> {
526 decode_sparse_authenticator_pubkeys(pubkeys).map_err(|e| match e {
527 SparseAuthenticatorPubkeysError::SlotOutOfBounds {
528 slot_index,
529 max_supported_slot,
530 } => AuthenticatorError::InvalidIndexerPubkeySlot {
531 slot_index,
532 max_supported_slot,
533 },
534 SparseAuthenticatorPubkeysError::InvalidCompressedPubkey { slot_index, reason } => {
535 PrimitiveError::Deserialization(format!(
536 "invalid authenticator public key returned by indexer at slot {slot_index}: {reason}"
537 ))
538 .into()
539 }
540 })
541 }
542
543 fn insert_or_reuse_authenticator_key(
544 key_set: &mut AuthenticatorPublicKeySet,
545 new_authenticator_pubkey: EdDSAPublicKey,
546 ) -> Result<usize, AuthenticatorError> {
547 if let Some(index) = key_set.iter().position(Option::is_none) {
548 key_set.try_set_at_index(index, new_authenticator_pubkey)?;
549 Ok(index)
550 } else {
551 key_set.try_push(new_authenticator_pubkey)?;
552 Ok(key_set.len() - 1)
553 }
554 }
555
556 pub async fn generate_nullifier(
568 &self,
569 proof_request: &ProofRequest,
570 inclusion_proof: MerkleInclusionProof<TREE_DEPTH>,
571 key_set: AuthenticatorPublicKeySet,
572 ) -> Result<OprfNullifier, AuthenticatorError> {
573 let (services, threshold) = self.check_oprf_config()?;
574 let key_index = key_set
575 .iter()
576 .position(|pk| {
577 pk.as_ref()
578 .is_some_and(|pk| pk.pk == self.offchain_pubkey().pk)
579 })
580 .ok_or(AuthenticatorError::PublicKeyNotFound)? as u64;
581
582 let authenticator_input = AuthenticatorProofInput::new(
583 key_set,
584 inclusion_proof,
585 self.signer
586 .offchain_signer_private_key()
587 .expose_secret()
588 .clone(),
589 key_index,
590 );
591
592 Ok(OprfNullifier::generate(
593 services,
594 threshold,
595 &self.query_material,
596 authenticator_input,
597 proof_request,
598 self.ws_connector.clone(),
599 )
600 .await?)
601 }
602
603 pub async fn generate_credential_blinding_factor(
612 &self,
613 issuer_schema_id: u64,
614 ) -> Result<FieldElement, AuthenticatorError> {
615 let (services, threshold) = self.check_oprf_config()?;
616
617 let (inclusion_proof, key_set) = self.fetch_inclusion_proof().await?;
618 let key_index = key_set
619 .iter()
620 .position(|pk| {
621 pk.as_ref()
622 .is_some_and(|pk| pk.pk == self.offchain_pubkey().pk)
623 })
624 .ok_or(AuthenticatorError::PublicKeyNotFound)? as u64;
625
626 let authenticator_input = AuthenticatorProofInput::new(
627 key_set,
628 inclusion_proof,
629 self.signer
630 .offchain_signer_private_key()
631 .expose_secret()
632 .clone(),
633 key_index,
634 );
635
636 let blinding_factor = OprfCredentialBlindingFactor::generate(
637 services,
638 threshold,
639 &self.query_material,
640 authenticator_input,
641 issuer_schema_id,
642 FieldElement::ZERO, self.ws_connector.clone(),
644 )
645 .await?;
646
647 Ok(blinding_factor.verifiable_oprf_output.output.into())
648 }
649
650 #[allow(clippy::too_many_arguments)]
671 pub fn generate_single_proof(
672 &self,
673 oprf_nullifier: OprfNullifier,
674 request_item: &RequestItem,
675 credential: &Credential,
676 credential_sub_blinding_factor: FieldElement,
677 session_id_r_seed: FieldElement,
678 session_id: Option<FieldElement>,
679 request_timestamp: u64,
680 ) -> Result<ResponseItem, AuthenticatorError> {
681 let mut rng = rand::rngs::OsRng;
682
683 let merkle_root: FieldElement = oprf_nullifier.query_proof_input.merkle_root.into();
684 let action_from_query: FieldElement = oprf_nullifier.query_proof_input.action.into();
685
686 let expires_at_min = request_item.effective_expires_at_min(request_timestamp);
687
688 let (proof, _public_inputs, nullifier) = generate_nullifier_proof(
689 &self.nullifier_material,
690 &mut rng,
691 credential,
692 credential_sub_blinding_factor,
693 oprf_nullifier,
694 request_item,
695 session_id,
696 session_id_r_seed,
697 expires_at_min,
698 )?;
699
700 let proof = ZeroKnowledgeProof::from_groth16_proof(&proof, merkle_root);
701
702 let nullifier_fe: FieldElement = nullifier.into();
704 let response_item = if session_id.is_some() {
705 let session_nullifier = SessionNullifier::new(nullifier_fe, action_from_query);
706 ResponseItem::new_session(
707 request_item.identifier.clone(),
708 request_item.issuer_schema_id,
709 proof,
710 session_nullifier,
711 expires_at_min,
712 )
713 } else {
714 ResponseItem::new_uniqueness(
715 request_item.identifier.clone(),
716 request_item.issuer_schema_id,
717 proof,
718 nullifier_fe.into(),
719 expires_at_min,
720 )
721 };
722
723 Ok(response_item)
724 }
725
726 pub async fn insert_authenticator(
735 &mut self,
736 new_authenticator_pubkey: EdDSAPublicKey,
737 new_authenticator_address: Address,
738 ) -> Result<String, AuthenticatorError> {
739 let leaf_index = self.leaf_index();
740 let nonce = self.signing_nonce().await?;
741 let mut key_set = self.fetch_authenticator_pubkeys().await?;
742 let old_offchain_signer_commitment = key_set.leaf_hash();
743 let encoded_offchain_pubkey = new_authenticator_pubkey.to_ethereum_representation()?;
744 let index =
745 Self::insert_or_reuse_authenticator_key(&mut key_set, new_authenticator_pubkey)?;
746 let new_offchain_signer_commitment = key_set.leaf_hash();
747
748 let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
749
750 #[allow(clippy::cast_possible_truncation)]
751 let signature = sign_insert_authenticator(
753 &self.signer.onchain_signer(),
754 leaf_index,
755 new_authenticator_address,
756 index as u32,
757 encoded_offchain_pubkey,
758 new_offchain_signer_commitment.into(),
759 nonce,
760 &eip712_domain,
761 )
762 .await
763 .map_err(|e| {
764 AuthenticatorError::Generic(format!("Failed to sign insert authenticator: {e}"))
765 })?;
766
767 #[allow(clippy::cast_possible_truncation)]
768 let req = InsertAuthenticatorRequest {
770 leaf_index,
771 new_authenticator_address,
772 pubkey_id: index as u32,
773 new_authenticator_pubkey: encoded_offchain_pubkey,
774 old_offchain_signer_commitment: old_offchain_signer_commitment.into(),
775 new_offchain_signer_commitment: new_offchain_signer_commitment.into(),
776 signature: signature.as_bytes().to_vec(),
777 nonce,
778 };
779
780 let resp = self
781 .http_client
782 .post(format!(
783 "{}/insert-authenticator",
784 self.config.gateway_url()
785 ))
786 .json(&req)
787 .send()
788 .await?;
789
790 let status = resp.status();
791 if status.is_success() {
792 let body: GatewayStatusResponse = resp.json().await?;
793 Ok(body.request_id)
794 } else {
795 let body_text = Self::response_body_or_fallback(resp).await;
796 Err(AuthenticatorError::GatewayError {
797 status,
798 body: body_text,
799 })
800 }
801 }
802
803 pub async fn update_authenticator(
812 &mut self,
813 old_authenticator_address: Address,
814 new_authenticator_address: Address,
815 new_authenticator_pubkey: EdDSAPublicKey,
816 index: u32,
817 ) -> Result<String, AuthenticatorError> {
818 let leaf_index = self.leaf_index();
819 let nonce = self.signing_nonce().await?;
820 let mut key_set = self.fetch_authenticator_pubkeys().await?;
821 let old_commitment: U256 = key_set.leaf_hash().into();
822 let encoded_offchain_pubkey = new_authenticator_pubkey.to_ethereum_representation()?;
823 key_set.try_set_at_index(index as usize, new_authenticator_pubkey)?;
824 let new_commitment: U256 = key_set.leaf_hash().into();
825
826 let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
827
828 let signature = sign_update_authenticator(
829 &self.signer.onchain_signer(),
830 leaf_index,
831 old_authenticator_address,
832 new_authenticator_address,
833 index,
834 encoded_offchain_pubkey,
835 new_commitment,
836 nonce,
837 &eip712_domain,
838 )
839 .await
840 .map_err(|e| {
841 AuthenticatorError::Generic(format!("Failed to sign update authenticator: {e}"))
842 })?;
843
844 let req = UpdateAuthenticatorRequest {
845 leaf_index,
846 old_authenticator_address,
847 new_authenticator_address,
848 old_offchain_signer_commitment: old_commitment,
849 new_offchain_signer_commitment: new_commitment,
850 signature: signature.as_bytes().to_vec(),
851 nonce,
852 pubkey_id: index,
853 new_authenticator_pubkey: encoded_offchain_pubkey,
854 };
855
856 let resp = self
857 .http_client
858 .post(format!(
859 "{}/update-authenticator",
860 self.config.gateway_url()
861 ))
862 .json(&req)
863 .send()
864 .await?;
865
866 let status = resp.status();
867 if status.is_success() {
868 let gateway_resp: GatewayStatusResponse = resp.json().await?;
869 Ok(gateway_resp.request_id)
870 } else {
871 let body_text = Self::response_body_or_fallback(resp).await;
872 Err(AuthenticatorError::GatewayError {
873 status,
874 body: body_text,
875 })
876 }
877 }
878
879 pub async fn remove_authenticator(
888 &mut self,
889 authenticator_address: Address,
890 index: u32,
891 ) -> Result<String, AuthenticatorError> {
892 let leaf_index = self.leaf_index();
893 let nonce = self.signing_nonce().await?;
894 let mut key_set = self.fetch_authenticator_pubkeys().await?;
895 let old_commitment: U256 = key_set.leaf_hash().into();
896 let existing_pubkey = key_set
897 .get(index as usize)
898 .ok_or(AuthenticatorError::PublicKeyNotFound)?;
899
900 let encoded_old_offchain_pubkey = existing_pubkey.to_ethereum_representation()?;
901
902 key_set.try_clear_at_index(index as usize)?;
903 let new_commitment: U256 = key_set.leaf_hash().into();
904
905 let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
906
907 let signature = sign_remove_authenticator(
908 &self.signer.onchain_signer(),
909 leaf_index,
910 authenticator_address,
911 index,
912 encoded_old_offchain_pubkey,
913 new_commitment,
914 nonce,
915 &eip712_domain,
916 )
917 .await
918 .map_err(|e| {
919 AuthenticatorError::Generic(format!("Failed to sign remove authenticator: {e}"))
920 })?;
921
922 let req = RemoveAuthenticatorRequest {
923 leaf_index,
924 authenticator_address,
925 old_offchain_signer_commitment: old_commitment,
926 new_offchain_signer_commitment: new_commitment,
927 signature: signature.as_bytes().to_vec(),
928 nonce,
929 pubkey_id: Some(index),
930 authenticator_pubkey: Some(encoded_old_offchain_pubkey),
931 };
932
933 let resp = self
934 .http_client
935 .post(format!(
936 "{}/remove-authenticator",
937 self.config.gateway_url()
938 ))
939 .json(&req)
940 .send()
941 .await?;
942
943 let status = resp.status();
944 if status.is_success() {
945 let gateway_resp: GatewayStatusResponse = resp.json().await?;
946 Ok(gateway_resp.request_id)
947 } else {
948 let body_text = Self::response_body_or_fallback(resp).await;
949 Err(AuthenticatorError::GatewayError {
950 status,
951 body: body_text,
952 })
953 }
954 }
955}
956
957pub struct InitializingAuthenticator {
960 request_id: String,
961 http_client: reqwest::Client,
962 config: Config,
963}
964
965impl InitializingAuthenticator {
966 #[must_use]
968 pub fn request_id(&self) -> &str {
969 &self.request_id
970 }
971
972 async fn new(
978 seed: &[u8],
979 config: Config,
980 recovery_address: Option<Address>,
981 http_client: reqwest::Client,
982 ) -> Result<Self, AuthenticatorError> {
983 let signer = Signer::from_seed_bytes(seed)?;
984
985 let mut key_set = AuthenticatorPublicKeySet::default();
986 key_set.try_push(signer.offchain_signer_pubkey())?;
987 let leaf_hash = key_set.leaf_hash();
988
989 let offchain_pubkey_compressed = {
990 let pk = signer.offchain_signer_pubkey().pk;
991 let mut compressed_bytes = Vec::new();
992 pk.serialize_compressed(&mut compressed_bytes)
993 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
994 U256::from_le_slice(&compressed_bytes)
995 };
996
997 let req = CreateAccountRequest {
998 recovery_address,
999 authenticator_addresses: vec![signer.onchain_signer_address()],
1000 authenticator_pubkeys: vec![offchain_pubkey_compressed],
1001 offchain_signer_commitment: leaf_hash.into(),
1002 };
1003
1004 let resp = http_client
1005 .post(format!("{}/create-account", config.gateway_url()))
1006 .json(&req)
1007 .send()
1008 .await?;
1009
1010 let status = resp.status();
1011 if status.is_success() {
1012 let body: GatewayStatusResponse = resp.json().await?;
1013 Ok(Self {
1014 request_id: body.request_id,
1015 http_client,
1016 config,
1017 })
1018 } else {
1019 let body_text = Authenticator::response_body_or_fallback(resp).await;
1020 Err(AuthenticatorError::GatewayError {
1021 status,
1022 body: body_text,
1023 })
1024 }
1025 }
1026
1027 pub async fn poll_status(&self) -> Result<GatewayRequestState, AuthenticatorError> {
1033 let resp = self
1034 .http_client
1035 .get(format!(
1036 "{}/status/{}",
1037 self.config.gateway_url(),
1038 self.request_id
1039 ))
1040 .send()
1041 .await?;
1042
1043 let status = resp.status();
1044
1045 if status.is_success() {
1046 let body: GatewayStatusResponse = resp.json().await?;
1047 Ok(body.status)
1048 } else {
1049 let body_text = Authenticator::response_body_or_fallback(resp).await;
1050 Err(AuthenticatorError::GatewayError {
1051 status,
1052 body: body_text,
1053 })
1054 }
1055 }
1056}
1057
1058impl ProtocolSigner for Authenticator {
1059 fn sign(&self, message: FieldElement) -> EdDSASignature {
1060 self.signer
1061 .offchain_signer_private_key()
1062 .expose_secret()
1063 .sign(*message)
1064 }
1065}
1066
1067pub trait OnchainKeyRepresentable {
1069 fn to_ethereum_representation(&self) -> Result<U256, PrimitiveError>;
1076}
1077
1078impl OnchainKeyRepresentable for EdDSAPublicKey {
1079 fn to_ethereum_representation(&self) -> Result<U256, PrimitiveError> {
1081 let mut compressed_bytes = Vec::new();
1082 self.pk
1083 .serialize_compressed(&mut compressed_bytes)
1084 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
1085 Ok(U256::from_le_slice(&compressed_bytes))
1086 }
1087}
1088
1089#[derive(Debug, thiserror::Error)]
1091pub enum AuthenticatorError {
1092 #[error(transparent)]
1094 PrimitiveError(#[from] PrimitiveError),
1095
1096 #[error("Account is not registered for this authenticator.")]
1099 AccountDoesNotExist,
1100
1101 #[error("Account already exists for this authenticator.")]
1103 AccountAlreadyExists,
1104
1105 #[error("Error interacting with EVM contract: {0}")]
1107 ContractError(#[from] alloy::contract::Error),
1108
1109 #[error("Network error: {0}")]
1111 NetworkError(#[from] reqwest::Error),
1112
1113 #[error("Public key not found.")]
1115 PublicKeyNotFound,
1116
1117 #[error("Gateway error (status {status}): {body}")]
1119 GatewayError {
1120 status: StatusCode,
1122 body: String,
1124 },
1125
1126 #[error("Indexer error (status {status}): {body}")]
1128 IndexerError {
1129 status: StatusCode,
1131 body: String,
1133 },
1134
1135 #[error("Account creation timed out")]
1137 Timeout,
1138
1139 #[error("Invalid configuration for {attribute}: {reason}")]
1141 InvalidConfig {
1142 attribute: &'static str,
1144 reason: String,
1146 },
1147
1148 #[error("The provided credential is not valid for the provided proof request")]
1150 InvalidCredentialForProofRequest,
1151
1152 #[error("Registration error ({error_code}): {error_message}")]
1156 RegistrationError {
1157 error_code: String,
1159 error_message: String,
1161 },
1162
1163 #[error(transparent)]
1165 ProofError(#[from] ProofError),
1166
1167 #[error(
1169 "Invalid indexer authenticator pubkey slot {slot_index}; max supported slot is {max_supported_slot}"
1170 )]
1171 InvalidIndexerPubkeySlot {
1172 slot_index: usize,
1174 max_supported_slot: usize,
1176 },
1177
1178 #[error("{0}")]
1180 Generic(String),
1181}
1182
1183#[derive(Debug)]
1184enum PollResult {
1185 Retryable,
1186 TerminalError(AuthenticatorError),
1187}
1188
1189#[cfg(all(test, feature = "embed-zkeys"))]
1190mod tests {
1191 use super::*;
1192 use alloy::primitives::{U256, address};
1193 use std::sync::OnceLock;
1194 use world_id_primitives::authenticator::MAX_AUTHENTICATOR_KEYS;
1195
1196 fn test_materials() -> (Arc<CircomGroth16Material>, Arc<CircomGroth16Material>) {
1197 static QUERY: OnceLock<Arc<CircomGroth16Material>> = OnceLock::new();
1198 static NULLIFIER: OnceLock<Arc<CircomGroth16Material>> = OnceLock::new();
1199
1200 let query = QUERY.get_or_init(|| {
1201 Arc::new(world_id_proof::proof::load_embedded_query_material().unwrap())
1202 });
1203 let nullifier = NULLIFIER.get_or_init(|| {
1204 Arc::new(world_id_proof::proof::load_embedded_nullifier_material().unwrap())
1205 });
1206
1207 (Arc::clone(query), Arc::clone(nullifier))
1208 }
1209
1210 fn test_pubkey(seed_byte: u8) -> EdDSAPublicKey {
1211 Signer::from_seed_bytes(&[seed_byte; 32])
1212 .unwrap()
1213 .offchain_signer_pubkey()
1214 }
1215
1216 fn encoded_test_pubkey(seed_byte: u8) -> U256 {
1217 test_pubkey(seed_byte).to_ethereum_representation().unwrap()
1218 }
1219
1220 #[test]
1221 fn test_insert_or_reuse_authenticator_key_reuses_empty_slot() {
1222 let mut key_set =
1223 AuthenticatorPublicKeySet::new(vec![test_pubkey(1), test_pubkey(2), test_pubkey(4)])
1224 .unwrap();
1225 key_set[1] = None;
1226 let new_key = test_pubkey(3);
1227
1228 let index =
1229 Authenticator::insert_or_reuse_authenticator_key(&mut key_set, new_key).unwrap();
1230
1231 assert_eq!(index, 1);
1232 assert_eq!(key_set.len(), 3);
1233 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(3).pk);
1234 }
1235
1236 #[test]
1237 fn test_insert_or_reuse_authenticator_key_appends_when_no_empty_slot() {
1238 let mut key_set = AuthenticatorPublicKeySet::new(vec![test_pubkey(1)]).unwrap();
1239 let new_key = test_pubkey(2);
1240
1241 let index =
1242 Authenticator::insert_or_reuse_authenticator_key(&mut key_set, new_key).unwrap();
1243
1244 assert_eq!(index, 1);
1245 assert_eq!(key_set.len(), 2);
1246 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(2).pk);
1247 }
1248
1249 #[test]
1250 fn test_decode_indexer_pubkeys_trims_trailing_empty_slots() {
1251 let mut encoded_pubkeys = vec![Some(encoded_test_pubkey(1)), Some(encoded_test_pubkey(2))];
1252 encoded_pubkeys.extend(vec![None; MAX_AUTHENTICATOR_KEYS + 5]);
1253
1254 let key_set = Authenticator::decode_indexer_pubkeys(encoded_pubkeys).unwrap();
1255
1256 assert_eq!(key_set.len(), 2);
1257 assert_eq!(key_set[0].as_ref().unwrap().pk, test_pubkey(1).pk);
1258 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(2).pk);
1259 }
1260
1261 #[test]
1262 fn test_decode_indexer_pubkeys_rejects_used_slot_beyond_max() {
1263 let mut encoded_pubkeys = vec![None; MAX_AUTHENTICATOR_KEYS + 1];
1264 encoded_pubkeys[MAX_AUTHENTICATOR_KEYS] = Some(encoded_test_pubkey(1));
1265
1266 let error = Authenticator::decode_indexer_pubkeys(encoded_pubkeys).unwrap_err();
1267 assert!(matches!(
1268 error,
1269 AuthenticatorError::InvalidIndexerPubkeySlot {
1270 slot_index,
1271 max_supported_slot
1272 } if slot_index == MAX_AUTHENTICATOR_KEYS && max_supported_slot == MAX_AUTHENTICATOR_KEYS - 1
1273 ));
1274 }
1275
1276 #[tokio::test]
1279 async fn test_get_packed_account_data_from_indexer() {
1280 let mut server = mockito::Server::new_async().await;
1281 let indexer_url = server.url();
1282
1283 let test_address = address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0");
1284 let expected_packed_index = U256::from(42);
1285
1286 let mock = server
1287 .mock("POST", "/packed-account")
1288 .match_header("content-type", "application/json")
1289 .match_body(mockito::Matcher::JsonString(
1290 serde_json::json!({
1291 "authenticator_address": test_address
1292 })
1293 .to_string(),
1294 ))
1295 .with_status(200)
1296 .with_header("content-type", "application/json")
1297 .with_body(
1298 serde_json::json!({
1299 "packed_account_data": format!("{:#x}", expected_packed_index)
1300 })
1301 .to_string(),
1302 )
1303 .create_async()
1304 .await;
1305
1306 let config = Config::new(
1307 None,
1308 1,
1309 address!("0x0000000000000000000000000000000000000001"),
1310 indexer_url,
1311 "http://gateway.example.com".to_string(),
1312 Vec::new(),
1313 2,
1314 )
1315 .unwrap();
1316
1317 let http_client = reqwest::Client::new();
1318
1319 let result = Authenticator::get_packed_account_data(
1320 test_address,
1321 None, &config,
1323 &http_client,
1324 )
1325 .await
1326 .unwrap();
1327
1328 assert_eq!(result, expected_packed_index);
1329 mock.assert_async().await;
1330 drop(server);
1331 }
1332
1333 #[tokio::test]
1334 async fn test_get_packed_account_data_from_indexer_error() {
1335 let mut server = mockito::Server::new_async().await;
1336 let indexer_url = server.url();
1337
1338 let test_address = address!("0x0000000000000000000000000000000000000099");
1339
1340 let mock = server
1341 .mock("POST", "/packed-account")
1342 .with_status(400)
1343 .with_header("content-type", "application/json")
1344 .with_body(
1345 serde_json::json!({
1346 "code": "account_does_not_exist",
1347 "message": "There is no account for this authenticator address"
1348 })
1349 .to_string(),
1350 )
1351 .create_async()
1352 .await;
1353
1354 let config = Config::new(
1355 None,
1356 1,
1357 address!("0x0000000000000000000000000000000000000001"),
1358 indexer_url,
1359 "http://gateway.example.com".to_string(),
1360 Vec::new(),
1361 2,
1362 )
1363 .unwrap();
1364
1365 let http_client = reqwest::Client::new();
1366
1367 let result =
1368 Authenticator::get_packed_account_data(test_address, None, &config, &http_client).await;
1369
1370 assert!(matches!(
1371 result,
1372 Err(AuthenticatorError::AccountDoesNotExist)
1373 ));
1374 mock.assert_async().await;
1375 drop(server);
1376 }
1377
1378 #[tokio::test]
1379 #[cfg(not(target_arch = "wasm32"))]
1380 async fn test_signing_nonce_from_indexer() {
1381 let mut server = mockito::Server::new_async().await;
1382 let indexer_url = server.url();
1383
1384 let leaf_index = U256::from(1);
1385 let expected_nonce = U256::from(5);
1386
1387 let mock = server
1388 .mock("POST", "/signature-nonce")
1389 .match_header("content-type", "application/json")
1390 .match_body(mockito::Matcher::JsonString(
1391 serde_json::json!({
1392 "leaf_index": format!("{:#x}", leaf_index)
1393 })
1394 .to_string(),
1395 ))
1396 .with_status(200)
1397 .with_header("content-type", "application/json")
1398 .with_body(
1399 serde_json::json!({
1400 "signature_nonce": format!("{:#x}", expected_nonce)
1401 })
1402 .to_string(),
1403 )
1404 .create_async()
1405 .await;
1406
1407 let config = Config::new(
1408 None,
1409 1,
1410 address!("0x0000000000000000000000000000000000000001"),
1411 indexer_url,
1412 "http://gateway.example.com".to_string(),
1413 Vec::new(),
1414 2,
1415 )
1416 .unwrap();
1417
1418 let (query_material, nullifier_material) = test_materials();
1419 let authenticator = Authenticator {
1420 config,
1421 packed_account_data: leaf_index, signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1423 registry: None, http_client: reqwest::Client::new(),
1425 ws_connector: Connector::Plain,
1426 query_material,
1427 nullifier_material,
1428 };
1429
1430 let nonce = authenticator.signing_nonce().await.unwrap();
1431
1432 assert_eq!(nonce, expected_nonce);
1433 mock.assert_async().await;
1434 drop(server);
1435 }
1436
1437 #[test]
1438 fn test_danger_sign_challenge_returns_valid_signature() {
1439 let (query_material, nullifier_material) = test_materials();
1440 let mut authenticator = Authenticator {
1441 config: Config::new(
1442 None,
1443 1,
1444 address!("0x0000000000000000000000000000000000000001"),
1445 "http://indexer.example.com".to_string(),
1446 "http://gateway.example.com".to_string(),
1447 Vec::new(),
1448 2,
1449 )
1450 .unwrap(),
1451 packed_account_data: U256::from(1),
1452 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1453 registry: None,
1454 http_client: reqwest::Client::new(),
1455 ws_connector: Connector::Plain,
1456 query_material,
1457 nullifier_material,
1458 };
1459
1460 let challenge = b"test challenge";
1461 let signature = authenticator.danger_sign_challenge(challenge).unwrap();
1462
1463 let recovered = signature
1464 .recover_address_from_msg(challenge)
1465 .expect("should recover address");
1466 assert_eq!(recovered, authenticator.onchain_address());
1467 }
1468
1469 #[test]
1470 fn test_danger_sign_challenge_different_challenges_different_signatures() {
1471 let (query_material, nullifier_material) = test_materials();
1472 let mut authenticator = Authenticator {
1473 config: Config::new(
1474 None,
1475 1,
1476 address!("0x0000000000000000000000000000000000000001"),
1477 "http://indexer.example.com".to_string(),
1478 "http://gateway.example.com".to_string(),
1479 Vec::new(),
1480 2,
1481 )
1482 .unwrap(),
1483 packed_account_data: U256::from(1),
1484 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1485 registry: None,
1486 http_client: reqwest::Client::new(),
1487 ws_connector: Connector::Plain,
1488 query_material,
1489 nullifier_material,
1490 };
1491
1492 let sig_a = authenticator.danger_sign_challenge(b"challenge A").unwrap();
1493 let sig_b = authenticator.danger_sign_challenge(b"challenge B").unwrap();
1494 assert_ne!(sig_a, sig_b);
1495 }
1496
1497 #[test]
1498 fn test_danger_sign_challenge_deterministic() {
1499 let (query_material, nullifier_material) = test_materials();
1500 let mut authenticator = Authenticator {
1501 config: Config::new(
1502 None,
1503 1,
1504 address!("0x0000000000000000000000000000000000000001"),
1505 "http://indexer.example.com".to_string(),
1506 "http://gateway.example.com".to_string(),
1507 Vec::new(),
1508 2,
1509 )
1510 .unwrap(),
1511 packed_account_data: U256::from(1),
1512 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1513 registry: None,
1514 http_client: reqwest::Client::new(),
1515 ws_connector: Connector::Plain,
1516 query_material,
1517 nullifier_material,
1518 };
1519
1520 let challenge = b"deterministic test";
1521 let sig1 = authenticator.danger_sign_challenge(challenge).unwrap();
1522 let sig2 = authenticator.danger_sign_challenge(challenge).unwrap();
1523 assert_eq!(sig1, sig2);
1524 }
1525
1526 #[tokio::test]
1527 #[cfg(not(target_arch = "wasm32"))]
1528 async fn test_signing_nonce_from_indexer_error() {
1529 let mut server = mockito::Server::new_async().await;
1530 let indexer_url = server.url();
1531
1532 let mock = server
1533 .mock("POST", "/signature-nonce")
1534 .with_status(400)
1535 .with_header("content-type", "application/json")
1536 .with_body(
1537 serde_json::json!({
1538 "code": "invalid_leaf_index",
1539 "message": "Account index cannot be zero"
1540 })
1541 .to_string(),
1542 )
1543 .create_async()
1544 .await;
1545
1546 let config = Config::new(
1547 None,
1548 1,
1549 address!("0x0000000000000000000000000000000000000001"),
1550 indexer_url,
1551 "http://gateway.example.com".to_string(),
1552 Vec::new(),
1553 2,
1554 )
1555 .unwrap();
1556
1557 let (query_material, nullifier_material) = test_materials();
1558 let authenticator = Authenticator {
1559 config,
1560 packed_account_data: U256::ZERO,
1561 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1562 registry: None,
1563 http_client: reqwest::Client::new(),
1564 ws_connector: Connector::Plain,
1565 query_material,
1566 nullifier_material,
1567 };
1568
1569 let result = authenticator.signing_nonce().await;
1570
1571 assert!(matches!(
1572 result,
1573 Err(AuthenticatorError::IndexerError { .. })
1574 ));
1575 mock.assert_async().await;
1576 drop(server);
1577 }
1578}