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 uint,
26};
27use ark_serialize::CanonicalSerialize;
28use eddsa_babyjubjub::{EdDSAPublicKey, EdDSASignature};
29use groth16_material::circom::CircomGroth16Material;
30use reqwest::StatusCode;
31use secrecy::ExposeSecret;
32use taceo_oprf::client::Connector;
33pub use world_id_primitives::{Config, TREE_DEPTH, authenticator::ProtocolSigner};
34use world_id_primitives::{
35 PrimitiveError, ZeroKnowledgeProof,
36 authenticator::{
37 AuthenticatorPublicKeySet, SparseAuthenticatorPubkeysError,
38 decode_sparse_authenticator_pubkeys,
39 },
40 merkle::MerkleInclusionProof,
41};
42use world_id_proof::{
43 AuthenticatorProofInput,
44 credential_blinding_factor::OprfCredentialBlindingFactor,
45 nullifier::OprfNullifier,
46 proof::{ProofError, generate_nullifier_proof},
47};
48
49static MASK_RECOVERY_COUNTER: U256 =
50 uint!(0xFFFFFFFF00000000000000000000000000000000000000000000000000000000_U256);
51static MASK_PUBKEY_ID: U256 =
52 uint!(0x00000000FFFFFFFF000000000000000000000000000000000000000000000000_U256);
53static MASK_LEAF_INDEX: U256 =
54 uint!(0x000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFF_U256);
55
56pub struct Authenticator {
58 pub config: Config,
60 pub packed_account_data: U256,
63 signer: Signer,
64 registry: Option<Arc<WorldIdRegistryInstance<DynProvider>>>,
65 http_client: reqwest::Client,
66 ws_connector: Connector,
67 query_material: Arc<CircomGroth16Material>,
68 nullifier_material: Arc<CircomGroth16Material>,
69}
70
71#[expect(clippy::missing_fields_in_debug)]
72impl std::fmt::Debug for Authenticator {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 f.debug_struct("Authenticator")
75 .field("config", &self.config)
76 .field("packed_account_data", &self.packed_account_data)
77 .field("signer", &self.signer)
78 .finish()
79 }
80}
81
82impl Authenticator {
83 async fn response_body_or_fallback(response: reqwest::Response) -> String {
84 response
85 .text()
86 .await
87 .unwrap_or_else(|e| format!("Unable to read response body: {e}"))
88 }
89
90 pub async fn init(
100 seed: &[u8],
101 config: Config,
102 query_material: Arc<CircomGroth16Material>,
103 nullifier_material: Arc<CircomGroth16Material>,
104 ) -> Result<Self, AuthenticatorError> {
105 let signer = Signer::from_seed_bytes(seed)?;
106
107 let registry = config.rpc_url().map_or_else(
108 || None,
109 |rpc_url| {
110 let provider = alloy::providers::ProviderBuilder::new()
111 .with_chain_id(config.chain_id())
112 .connect_http(rpc_url.clone());
113 Some(crate::registry::WorldIdRegistry::new(
114 *config.registry_address(),
115 alloy::providers::Provider::erased(provider),
116 ))
117 },
118 );
119
120 let http_client = reqwest::Client::new();
121
122 let packed_account_data = Self::get_packed_account_data(
123 signer.onchain_signer_address(),
124 registry.as_ref(),
125 &config,
126 &http_client,
127 )
128 .await?;
129
130 let mut root_store = rustls::RootCertStore::empty();
131 root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
132 let rustls_config = rustls::ClientConfig::builder()
133 .with_root_certificates(root_store)
134 .with_no_client_auth();
135 let ws_connector = Connector::Rustls(Arc::new(rustls_config));
136
137 Ok(Self {
138 packed_account_data,
139 signer,
140 config,
141 registry: registry.map(Arc::new),
142 http_client,
143 ws_connector,
144 query_material,
145 nullifier_material,
146 })
147 }
148
149 pub async fn register(
157 seed: &[u8],
158 config: Config,
159 recovery_address: Option<Address>,
160 ) -> Result<InitializingAuthenticator, AuthenticatorError> {
161 let http_client = reqwest::Client::new();
162 InitializingAuthenticator::new(seed, config, recovery_address, http_client).await
163 }
164
165 pub async fn init_or_register(
178 seed: &[u8],
179 config: Config,
180 query_material: Arc<CircomGroth16Material>,
181 nullifier_material: Arc<CircomGroth16Material>,
182 recovery_address: Option<Address>,
183 ) -> Result<Self, AuthenticatorError> {
184 match Self::init(
185 seed,
186 config.clone(),
187 query_material.clone(),
188 nullifier_material.clone(),
189 )
190 .await
191 {
192 Ok(authenticator) => Ok(authenticator),
193 Err(AuthenticatorError::AccountDoesNotExist) => {
194 let http_client = reqwest::Client::new();
196 let initializing_authenticator = InitializingAuthenticator::new(
197 seed,
198 config.clone(),
199 recovery_address,
200 http_client,
201 )
202 .await?;
203
204 let backoff = backon::ExponentialBuilder::default()
205 .with_min_delay(std::time::Duration::from_millis(800))
206 .with_factor(1.5)
207 .without_max_times()
208 .with_total_delay(Some(std::time::Duration::from_secs(120)));
209
210 let poller = || async {
211 let poll_status = initializing_authenticator.poll_status().await;
212 let result = match poll_status {
213 Ok(GatewayRequestState::Finalized { .. }) => Ok(()),
214 Ok(GatewayRequestState::Failed { error_code, error }) => Err(
215 PollResult::TerminalError(AuthenticatorError::RegistrationError {
216 error_code: error_code.map(|v| v.to_string()).unwrap_or_default(),
217 error_message: error,
218 }),
219 ),
220 Err(AuthenticatorError::GatewayError { status, body }) => {
221 if status.is_client_error() {
222 Err(PollResult::TerminalError(
223 AuthenticatorError::GatewayError { status, body },
224 ))
225 } else {
226 Err(PollResult::Retryable)
227 }
228 }
229 _ => Err(PollResult::Retryable),
230 };
231
232 match result {
233 Ok(()) => match Self::init(
234 seed,
235 config.clone(),
236 query_material.clone(),
237 nullifier_material.clone(),
238 )
239 .await
240 {
241 Ok(auth) => Ok(auth),
242 Err(AuthenticatorError::AccountDoesNotExist) => {
243 Err(PollResult::Retryable)
244 }
245 Err(e) => Err(PollResult::TerminalError(e)),
246 },
247 Err(e) => Err(e),
248 }
249 };
250
251 let result = backon::Retryable::retry(poller, backoff)
252 .when(|e| matches!(e, PollResult::Retryable))
253 .await;
254
255 match result {
256 Ok(authenticator) => Ok(authenticator),
257 Err(PollResult::TerminalError(e)) => Err(e),
258 Err(PollResult::Retryable) => Err(AuthenticatorError::Timeout),
259 }
260 }
261 Err(e) => Err(e),
262 }
263 }
264
265 pub async fn get_packed_account_data(
273 onchain_signer_address: Address,
274 registry: Option<&WorldIdRegistryInstance<DynProvider>>,
275 config: &Config,
276 http_client: &reqwest::Client,
277 ) -> Result<U256, AuthenticatorError> {
278 let raw_index = if let Some(registry) = registry {
280 registry
282 .getPackedAccountData(onchain_signer_address)
283 .call()
284 .await?
285 } else {
286 let url = format!("{}/packed-account", config.indexer_url());
287 let req = IndexerPackedAccountRequest {
288 authenticator_address: onchain_signer_address,
289 };
290 let resp = http_client.post(&url).json(&req).send().await?;
291 let status = resp.status();
292 if !status.is_success() {
293 let body = Self::response_body_or_fallback(resp).await;
294 if let Ok(error_resp) =
295 serde_json::from_str::<ServiceApiError<IndexerErrorCode>>(&body)
296 {
297 return match error_resp.code {
298 IndexerErrorCode::AccountDoesNotExist => {
299 Err(AuthenticatorError::AccountDoesNotExist)
300 }
301 _ => Err(AuthenticatorError::IndexerError {
302 status,
303 body: error_resp.message,
304 }),
305 };
306 }
307 return Err(AuthenticatorError::IndexerError { status, body });
308 }
309
310 let response: IndexerPackedAccountResponse = resp.json().await?;
311 response.packed_account_data
312 };
313
314 if raw_index == U256::ZERO {
315 return Err(AuthenticatorError::AccountDoesNotExist);
316 }
317
318 Ok(raw_index)
319 }
320
321 #[must_use]
324 pub const fn onchain_address(&self) -> Address {
325 self.signer.onchain_signer_address()
326 }
327
328 #[must_use]
331 pub fn offchain_pubkey(&self) -> EdDSAPublicKey {
332 self.signer.offchain_signer_pubkey()
333 }
334
335 pub fn offchain_pubkey_compressed(&self) -> Result<U256, AuthenticatorError> {
340 let pk = self.signer.offchain_signer_pubkey().pk;
341 let mut compressed_bytes = Vec::new();
342 pk.serialize_compressed(&mut compressed_bytes)
343 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
344 Ok(U256::from_le_slice(&compressed_bytes))
345 }
346
347 #[must_use]
349 pub fn registry(&self) -> Option<Arc<WorldIdRegistryInstance<DynProvider>>> {
350 self.registry.clone()
351 }
352
353 #[must_use]
369 pub fn leaf_index(&self) -> u64 {
370 (self.packed_account_data & MASK_LEAF_INDEX).to::<u64>()
371 }
372
373 #[must_use]
377 pub fn recovery_counter(&self) -> U256 {
378 let recovery_counter = self.packed_account_data & MASK_RECOVERY_COUNTER;
379 recovery_counter >> 224
380 }
381
382 #[must_use]
386 pub fn pubkey_id(&self) -> U256 {
387 let pubkey_id = self.packed_account_data & MASK_PUBKEY_ID;
388 pubkey_id >> 192
389 }
390
391 pub async fn fetch_inclusion_proof(
397 &self,
398 ) -> Result<(MerkleInclusionProof<TREE_DEPTH>, AuthenticatorPublicKeySet), AuthenticatorError>
399 {
400 let url = format!("{}/inclusion-proof", self.config.indexer_url());
401 let req = IndexerQueryRequest {
402 leaf_index: self.leaf_index(),
403 };
404 let response = self.http_client.post(&url).json(&req).send().await?;
405 let status = response.status();
406 if !status.is_success() {
407 return Err(AuthenticatorError::IndexerError {
408 status,
409 body: Self::response_body_or_fallback(response).await,
410 });
411 }
412 let response = response.json::<AccountInclusionProof<TREE_DEPTH>>().await?;
413
414 Ok((response.inclusion_proof, response.authenticator_pubkeys))
415 }
416
417 pub async fn fetch_authenticator_pubkeys(
426 &self,
427 ) -> Result<AuthenticatorPublicKeySet, AuthenticatorError> {
428 let url = format!("{}/authenticator-pubkeys", self.config.indexer_url());
429 let req = IndexerQueryRequest {
430 leaf_index: self.leaf_index(),
431 };
432 let response = self.http_client.post(&url).json(&req).send().await?;
433 let status = response.status();
434 if !status.is_success() {
435 return Err(AuthenticatorError::IndexerError {
436 status,
437 body: Self::response_body_or_fallback(response).await,
438 });
439 }
440 let response = response
441 .json::<IndexerAuthenticatorPubkeysResponse>()
442 .await?;
443 Self::decode_indexer_pubkeys(response.authenticator_pubkeys)
444 }
445
446 pub async fn signing_nonce(&self) -> Result<U256, AuthenticatorError> {
451 let registry = self.registry();
452 if let Some(registry) = registry {
453 let nonce = registry.getSignatureNonce(self.leaf_index()).call().await?;
454 Ok(nonce)
455 } else {
456 let url = format!("{}/signature-nonce", self.config.indexer_url());
457 let req = IndexerQueryRequest {
458 leaf_index: self.leaf_index(),
459 };
460 let resp = self.http_client.post(&url).json(&req).send().await?;
461
462 let status = resp.status();
463 if !status.is_success() {
464 return Err(AuthenticatorError::IndexerError {
465 status,
466 body: Self::response_body_or_fallback(resp).await,
467 });
468 }
469
470 let response: IndexerSignatureNonceResponse = resp.json().await?;
471 Ok(response.signature_nonce)
472 }
473 }
474
475 fn check_oprf_config(&self) -> Result<(&[String], usize), AuthenticatorError> {
480 let services = self.config.nullifier_oracle_urls();
481 if services.is_empty() {
482 return Err(AuthenticatorError::Generic(
483 "No nullifier oracle URLs configured".to_string(),
484 ));
485 }
486 let requested_threshold = self.config.nullifier_oracle_threshold();
487 if requested_threshold == 0 {
488 return Err(AuthenticatorError::InvalidConfig {
489 attribute: "nullifier_oracle_threshold",
490 reason: "must be at least 1".to_string(),
491 });
492 }
493 let threshold = requested_threshold.min(services.len());
494 Ok((services, threshold))
495 }
496
497 fn decode_indexer_pubkeys(
498 pubkeys: Vec<Option<U256>>,
499 ) -> Result<AuthenticatorPublicKeySet, AuthenticatorError> {
500 decode_sparse_authenticator_pubkeys(pubkeys).map_err(|e| match e {
501 SparseAuthenticatorPubkeysError::SlotOutOfBounds {
502 slot_index,
503 max_supported_slot,
504 } => AuthenticatorError::InvalidIndexerPubkeySlot {
505 slot_index,
506 max_supported_slot,
507 },
508 SparseAuthenticatorPubkeysError::InvalidCompressedPubkey { slot_index, reason } => {
509 PrimitiveError::Deserialization(format!(
510 "invalid authenticator public key returned by indexer at slot {slot_index}: {reason}"
511 ))
512 .into()
513 }
514 })
515 }
516
517 fn insert_or_reuse_authenticator_key(
518 key_set: &mut AuthenticatorPublicKeySet,
519 new_authenticator_pubkey: EdDSAPublicKey,
520 ) -> Result<usize, AuthenticatorError> {
521 if let Some(index) = key_set.iter().position(Option::is_none) {
522 key_set.try_set_at_index(index, new_authenticator_pubkey)?;
523 Ok(index)
524 } else {
525 key_set.try_push(new_authenticator_pubkey)?;
526 Ok(key_set.len() - 1)
527 }
528 }
529
530 pub async fn generate_nullifier(
542 &self,
543 proof_request: &ProofRequest,
544 inclusion_proof: MerkleInclusionProof<TREE_DEPTH>,
545 key_set: AuthenticatorPublicKeySet,
546 ) -> Result<OprfNullifier, AuthenticatorError> {
547 let (services, threshold) = self.check_oprf_config()?;
548 let key_index = key_set
549 .iter()
550 .position(|pk| {
551 pk.as_ref()
552 .is_some_and(|pk| pk.pk == self.offchain_pubkey().pk)
553 })
554 .ok_or(AuthenticatorError::PublicKeyNotFound)? as u64;
555
556 let authenticator_input = AuthenticatorProofInput::new(
557 key_set,
558 inclusion_proof,
559 self.signer
560 .offchain_signer_private_key()
561 .expose_secret()
562 .clone(),
563 key_index,
564 );
565
566 Ok(OprfNullifier::generate(
567 services,
568 threshold,
569 &self.query_material,
570 authenticator_input,
571 proof_request,
572 self.ws_connector.clone(),
573 )
574 .await?)
575 }
576
577 pub async fn generate_credential_blinding_factor(
586 &self,
587 issuer_schema_id: u64,
588 ) -> Result<FieldElement, AuthenticatorError> {
589 let (services, threshold) = self.check_oprf_config()?;
590
591 let (inclusion_proof, key_set) = self.fetch_inclusion_proof().await?;
592 let key_index = key_set
593 .iter()
594 .position(|pk| {
595 pk.as_ref()
596 .is_some_and(|pk| pk.pk == self.offchain_pubkey().pk)
597 })
598 .ok_or(AuthenticatorError::PublicKeyNotFound)? as u64;
599
600 let authenticator_input = AuthenticatorProofInput::new(
601 key_set,
602 inclusion_proof,
603 self.signer
604 .offchain_signer_private_key()
605 .expose_secret()
606 .clone(),
607 key_index,
608 );
609
610 let blinding_factor = OprfCredentialBlindingFactor::generate(
611 services,
612 threshold,
613 &self.query_material,
614 authenticator_input,
615 issuer_schema_id,
616 FieldElement::ZERO, self.ws_connector.clone(),
618 )
619 .await?;
620
621 Ok(blinding_factor.verifiable_oprf_output.output.into())
622 }
623
624 #[allow(clippy::too_many_arguments)]
645 pub fn generate_single_proof(
646 &self,
647 oprf_nullifier: OprfNullifier,
648 request_item: &RequestItem,
649 credential: &Credential,
650 credential_sub_blinding_factor: FieldElement,
651 session_id_r_seed: FieldElement,
652 session_id: Option<FieldElement>,
653 request_timestamp: u64,
654 ) -> Result<ResponseItem, AuthenticatorError> {
655 let mut rng = rand::rngs::OsRng;
656
657 let merkle_root: FieldElement = oprf_nullifier.query_proof_input.merkle_root.into();
658 let action_from_query: FieldElement = oprf_nullifier.query_proof_input.action.into();
659
660 let expires_at_min = request_item.effective_expires_at_min(request_timestamp);
661
662 let (proof, _public_inputs, nullifier) = generate_nullifier_proof(
663 &self.nullifier_material,
664 &mut rng,
665 credential,
666 credential_sub_blinding_factor,
667 oprf_nullifier,
668 request_item,
669 session_id,
670 session_id_r_seed,
671 expires_at_min,
672 )?;
673
674 let proof = ZeroKnowledgeProof::from_groth16_proof(&proof, merkle_root);
675
676 let nullifier_fe: FieldElement = nullifier.into();
678 let response_item = if session_id.is_some() {
679 let session_nullifier = SessionNullifier::new(nullifier_fe, action_from_query);
680 ResponseItem::new_session(
681 request_item.identifier.clone(),
682 request_item.issuer_schema_id,
683 proof,
684 session_nullifier,
685 expires_at_min,
686 )
687 } else {
688 ResponseItem::new_uniqueness(
689 request_item.identifier.clone(),
690 request_item.issuer_schema_id,
691 proof,
692 nullifier_fe.into(),
693 expires_at_min,
694 )
695 };
696
697 Ok(response_item)
698 }
699
700 pub async fn insert_authenticator(
709 &mut self,
710 new_authenticator_pubkey: EdDSAPublicKey,
711 new_authenticator_address: Address,
712 ) -> Result<String, AuthenticatorError> {
713 let leaf_index = self.leaf_index();
714 let nonce = self.signing_nonce().await?;
715 let mut key_set = self.fetch_authenticator_pubkeys().await?;
716 let old_offchain_signer_commitment = key_set.leaf_hash();
717 let encoded_offchain_pubkey = new_authenticator_pubkey.to_ethereum_representation()?;
718 let index =
719 Self::insert_or_reuse_authenticator_key(&mut key_set, new_authenticator_pubkey)?;
720 let new_offchain_signer_commitment = key_set.leaf_hash();
721
722 let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
723
724 #[allow(clippy::cast_possible_truncation)]
725 let signature = sign_insert_authenticator(
727 &self.signer.onchain_signer(),
728 leaf_index,
729 new_authenticator_address,
730 index as u32,
731 encoded_offchain_pubkey,
732 new_offchain_signer_commitment.into(),
733 nonce,
734 &eip712_domain,
735 )
736 .await
737 .map_err(|e| {
738 AuthenticatorError::Generic(format!("Failed to sign insert authenticator: {e}"))
739 })?;
740
741 #[allow(clippy::cast_possible_truncation)]
742 let req = InsertAuthenticatorRequest {
744 leaf_index,
745 new_authenticator_address,
746 pubkey_id: index as u32,
747 new_authenticator_pubkey: encoded_offchain_pubkey,
748 old_offchain_signer_commitment: old_offchain_signer_commitment.into(),
749 new_offchain_signer_commitment: new_offchain_signer_commitment.into(),
750 signature: signature.as_bytes().to_vec(),
751 nonce,
752 };
753
754 let resp = self
755 .http_client
756 .post(format!(
757 "{}/insert-authenticator",
758 self.config.gateway_url()
759 ))
760 .json(&req)
761 .send()
762 .await?;
763
764 let status = resp.status();
765 if status.is_success() {
766 let body: GatewayStatusResponse = resp.json().await?;
767 Ok(body.request_id)
768 } else {
769 let body_text = Self::response_body_or_fallback(resp).await;
770 Err(AuthenticatorError::GatewayError {
771 status,
772 body: body_text,
773 })
774 }
775 }
776
777 pub async fn update_authenticator(
786 &mut self,
787 old_authenticator_address: Address,
788 new_authenticator_address: Address,
789 new_authenticator_pubkey: EdDSAPublicKey,
790 index: u32,
791 ) -> Result<String, AuthenticatorError> {
792 let leaf_index = self.leaf_index();
793 let nonce = self.signing_nonce().await?;
794 let mut key_set = self.fetch_authenticator_pubkeys().await?;
795 let old_commitment: U256 = key_set.leaf_hash().into();
796 let encoded_offchain_pubkey = new_authenticator_pubkey.to_ethereum_representation()?;
797 key_set.try_set_at_index(index as usize, new_authenticator_pubkey)?;
798 let new_commitment: U256 = key_set.leaf_hash().into();
799
800 let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
801
802 let signature = sign_update_authenticator(
803 &self.signer.onchain_signer(),
804 leaf_index,
805 old_authenticator_address,
806 new_authenticator_address,
807 index,
808 encoded_offchain_pubkey,
809 new_commitment,
810 nonce,
811 &eip712_domain,
812 )
813 .await
814 .map_err(|e| {
815 AuthenticatorError::Generic(format!("Failed to sign update authenticator: {e}"))
816 })?;
817
818 let req = UpdateAuthenticatorRequest {
819 leaf_index,
820 old_authenticator_address,
821 new_authenticator_address,
822 old_offchain_signer_commitment: old_commitment,
823 new_offchain_signer_commitment: new_commitment,
824 signature: signature.as_bytes().to_vec(),
825 nonce,
826 pubkey_id: index,
827 new_authenticator_pubkey: encoded_offchain_pubkey,
828 };
829
830 let resp = self
831 .http_client
832 .post(format!(
833 "{}/update-authenticator",
834 self.config.gateway_url()
835 ))
836 .json(&req)
837 .send()
838 .await?;
839
840 let status = resp.status();
841 if status.is_success() {
842 let gateway_resp: GatewayStatusResponse = resp.json().await?;
843 Ok(gateway_resp.request_id)
844 } else {
845 let body_text = Self::response_body_or_fallback(resp).await;
846 Err(AuthenticatorError::GatewayError {
847 status,
848 body: body_text,
849 })
850 }
851 }
852
853 pub async fn remove_authenticator(
862 &mut self,
863 authenticator_address: Address,
864 index: u32,
865 ) -> Result<String, AuthenticatorError> {
866 let leaf_index = self.leaf_index();
867 let nonce = self.signing_nonce().await?;
868 let mut key_set = self.fetch_authenticator_pubkeys().await?;
869 let old_commitment: U256 = key_set.leaf_hash().into();
870 let existing_pubkey = key_set
871 .get(index as usize)
872 .ok_or(AuthenticatorError::PublicKeyNotFound)?;
873
874 let encoded_old_offchain_pubkey = existing_pubkey.to_ethereum_representation()?;
875
876 key_set.try_clear_at_index(index as usize)?;
877 let new_commitment: U256 = key_set.leaf_hash().into();
878
879 let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
880
881 let signature = sign_remove_authenticator(
882 &self.signer.onchain_signer(),
883 leaf_index,
884 authenticator_address,
885 index,
886 encoded_old_offchain_pubkey,
887 new_commitment,
888 nonce,
889 &eip712_domain,
890 )
891 .await
892 .map_err(|e| {
893 AuthenticatorError::Generic(format!("Failed to sign remove authenticator: {e}"))
894 })?;
895
896 let req = RemoveAuthenticatorRequest {
897 leaf_index,
898 authenticator_address,
899 old_offchain_signer_commitment: old_commitment,
900 new_offchain_signer_commitment: new_commitment,
901 signature: signature.as_bytes().to_vec(),
902 nonce,
903 pubkey_id: Some(index),
904 authenticator_pubkey: Some(encoded_old_offchain_pubkey),
905 };
906
907 let resp = self
908 .http_client
909 .post(format!(
910 "{}/remove-authenticator",
911 self.config.gateway_url()
912 ))
913 .json(&req)
914 .send()
915 .await?;
916
917 let status = resp.status();
918 if status.is_success() {
919 let gateway_resp: GatewayStatusResponse = resp.json().await?;
920 Ok(gateway_resp.request_id)
921 } else {
922 let body_text = Self::response_body_or_fallback(resp).await;
923 Err(AuthenticatorError::GatewayError {
924 status,
925 body: body_text,
926 })
927 }
928 }
929}
930
931pub struct InitializingAuthenticator {
934 request_id: String,
935 http_client: reqwest::Client,
936 config: Config,
937}
938
939impl InitializingAuthenticator {
940 #[must_use]
942 pub fn request_id(&self) -> &str {
943 &self.request_id
944 }
945
946 async fn new(
952 seed: &[u8],
953 config: Config,
954 recovery_address: Option<Address>,
955 http_client: reqwest::Client,
956 ) -> Result<Self, AuthenticatorError> {
957 let signer = Signer::from_seed_bytes(seed)?;
958
959 let mut key_set = AuthenticatorPublicKeySet::default();
960 key_set.try_push(signer.offchain_signer_pubkey())?;
961 let leaf_hash = key_set.leaf_hash();
962
963 let offchain_pubkey_compressed = {
964 let pk = signer.offchain_signer_pubkey().pk;
965 let mut compressed_bytes = Vec::new();
966 pk.serialize_compressed(&mut compressed_bytes)
967 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
968 U256::from_le_slice(&compressed_bytes)
969 };
970
971 let req = CreateAccountRequest {
972 recovery_address,
973 authenticator_addresses: vec![signer.onchain_signer_address()],
974 authenticator_pubkeys: vec![offchain_pubkey_compressed],
975 offchain_signer_commitment: leaf_hash.into(),
976 };
977
978 let resp = http_client
979 .post(format!("{}/create-account", config.gateway_url()))
980 .json(&req)
981 .send()
982 .await?;
983
984 let status = resp.status();
985 if status.is_success() {
986 let body: GatewayStatusResponse = resp.json().await?;
987 Ok(Self {
988 request_id: body.request_id,
989 http_client,
990 config,
991 })
992 } else {
993 let body_text = Authenticator::response_body_or_fallback(resp).await;
994 Err(AuthenticatorError::GatewayError {
995 status,
996 body: body_text,
997 })
998 }
999 }
1000
1001 pub async fn poll_status(&self) -> Result<GatewayRequestState, AuthenticatorError> {
1007 let resp = self
1008 .http_client
1009 .get(format!(
1010 "{}/status/{}",
1011 self.config.gateway_url(),
1012 self.request_id
1013 ))
1014 .send()
1015 .await?;
1016
1017 let status = resp.status();
1018
1019 if status.is_success() {
1020 let body: GatewayStatusResponse = resp.json().await?;
1021 Ok(body.status)
1022 } else {
1023 let body_text = Authenticator::response_body_or_fallback(resp).await;
1024 Err(AuthenticatorError::GatewayError {
1025 status,
1026 body: body_text,
1027 })
1028 }
1029 }
1030}
1031
1032impl ProtocolSigner for Authenticator {
1033 fn sign(&self, message: FieldElement) -> EdDSASignature {
1034 self.signer
1035 .offchain_signer_private_key()
1036 .expose_secret()
1037 .sign(*message)
1038 }
1039}
1040
1041pub trait OnchainKeyRepresentable {
1043 fn to_ethereum_representation(&self) -> Result<U256, PrimitiveError>;
1050}
1051
1052impl OnchainKeyRepresentable for EdDSAPublicKey {
1053 fn to_ethereum_representation(&self) -> Result<U256, PrimitiveError> {
1055 let mut compressed_bytes = Vec::new();
1056 self.pk
1057 .serialize_compressed(&mut compressed_bytes)
1058 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
1059 Ok(U256::from_le_slice(&compressed_bytes))
1060 }
1061}
1062
1063#[derive(Debug, thiserror::Error)]
1065pub enum AuthenticatorError {
1066 #[error(transparent)]
1068 PrimitiveError(#[from] PrimitiveError),
1069
1070 #[error("Account is not registered for this authenticator.")]
1073 AccountDoesNotExist,
1074
1075 #[error("Account already exists for this authenticator.")]
1077 AccountAlreadyExists,
1078
1079 #[error("Error interacting with EVM contract: {0}")]
1081 ContractError(#[from] alloy::contract::Error),
1082
1083 #[error("Network error: {0}")]
1085 NetworkError(#[from] reqwest::Error),
1086
1087 #[error("Public key not found.")]
1089 PublicKeyNotFound,
1090
1091 #[error("Gateway error (status {status}): {body}")]
1093 GatewayError {
1094 status: StatusCode,
1096 body: String,
1098 },
1099
1100 #[error("Indexer error (status {status}): {body}")]
1102 IndexerError {
1103 status: StatusCode,
1105 body: String,
1107 },
1108
1109 #[error("Account creation timed out")]
1111 Timeout,
1112
1113 #[error("Invalid configuration for {attribute}: {reason}")]
1115 InvalidConfig {
1116 attribute: &'static str,
1118 reason: String,
1120 },
1121
1122 #[error("The provided credential is not valid for the provided proof request")]
1124 InvalidCredentialForProofRequest,
1125
1126 #[error("Registration error ({error_code}): {error_message}")]
1130 RegistrationError {
1131 error_code: String,
1133 error_message: String,
1135 },
1136
1137 #[error(transparent)]
1139 ProofError(#[from] ProofError),
1140
1141 #[error(
1143 "Invalid indexer authenticator pubkey slot {slot_index}; max supported slot is {max_supported_slot}"
1144 )]
1145 InvalidIndexerPubkeySlot {
1146 slot_index: usize,
1148 max_supported_slot: usize,
1150 },
1151
1152 #[error("{0}")]
1154 Generic(String),
1155}
1156
1157#[derive(Debug)]
1158enum PollResult {
1159 Retryable,
1160 TerminalError(AuthenticatorError),
1161}
1162
1163#[cfg(all(test, feature = "embed-zkeys"))]
1164mod tests {
1165 use super::*;
1166 use alloy::primitives::{U256, address};
1167 use std::sync::OnceLock;
1168 use world_id_primitives::authenticator::MAX_AUTHENTICATOR_KEYS;
1169
1170 fn test_materials() -> (Arc<CircomGroth16Material>, Arc<CircomGroth16Material>) {
1171 static QUERY: OnceLock<Arc<CircomGroth16Material>> = OnceLock::new();
1172 static NULLIFIER: OnceLock<Arc<CircomGroth16Material>> = OnceLock::new();
1173
1174 let query = QUERY.get_or_init(|| {
1175 Arc::new(world_id_proof::proof::load_embedded_query_material().unwrap())
1176 });
1177 let nullifier = NULLIFIER.get_or_init(|| {
1178 Arc::new(world_id_proof::proof::load_embedded_nullifier_material().unwrap())
1179 });
1180
1181 (Arc::clone(query), Arc::clone(nullifier))
1182 }
1183
1184 fn test_pubkey(seed_byte: u8) -> EdDSAPublicKey {
1185 Signer::from_seed_bytes(&[seed_byte; 32])
1186 .unwrap()
1187 .offchain_signer_pubkey()
1188 }
1189
1190 fn encoded_test_pubkey(seed_byte: u8) -> U256 {
1191 test_pubkey(seed_byte).to_ethereum_representation().unwrap()
1192 }
1193
1194 #[test]
1195 fn test_insert_or_reuse_authenticator_key_reuses_empty_slot() {
1196 let mut key_set =
1197 AuthenticatorPublicKeySet::new(vec![test_pubkey(1), test_pubkey(2), test_pubkey(4)])
1198 .unwrap();
1199 key_set[1] = None;
1200 let new_key = test_pubkey(3);
1201
1202 let index =
1203 Authenticator::insert_or_reuse_authenticator_key(&mut key_set, new_key).unwrap();
1204
1205 assert_eq!(index, 1);
1206 assert_eq!(key_set.len(), 3);
1207 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(3).pk);
1208 }
1209
1210 #[test]
1211 fn test_insert_or_reuse_authenticator_key_appends_when_no_empty_slot() {
1212 let mut key_set = AuthenticatorPublicKeySet::new(vec![test_pubkey(1)]).unwrap();
1213 let new_key = test_pubkey(2);
1214
1215 let index =
1216 Authenticator::insert_or_reuse_authenticator_key(&mut key_set, new_key).unwrap();
1217
1218 assert_eq!(index, 1);
1219 assert_eq!(key_set.len(), 2);
1220 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(2).pk);
1221 }
1222
1223 #[test]
1224 fn test_decode_indexer_pubkeys_trims_trailing_empty_slots() {
1225 let mut encoded_pubkeys = vec![Some(encoded_test_pubkey(1)), Some(encoded_test_pubkey(2))];
1226 encoded_pubkeys.extend(vec![None; MAX_AUTHENTICATOR_KEYS + 5]);
1227
1228 let key_set = Authenticator::decode_indexer_pubkeys(encoded_pubkeys).unwrap();
1229
1230 assert_eq!(key_set.len(), 2);
1231 assert_eq!(key_set[0].as_ref().unwrap().pk, test_pubkey(1).pk);
1232 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(2).pk);
1233 }
1234
1235 #[test]
1236 fn test_decode_indexer_pubkeys_rejects_used_slot_beyond_max() {
1237 let mut encoded_pubkeys = vec![None; MAX_AUTHENTICATOR_KEYS + 1];
1238 encoded_pubkeys[MAX_AUTHENTICATOR_KEYS] = Some(encoded_test_pubkey(1));
1239
1240 let error = Authenticator::decode_indexer_pubkeys(encoded_pubkeys).unwrap_err();
1241 assert!(matches!(
1242 error,
1243 AuthenticatorError::InvalidIndexerPubkeySlot {
1244 slot_index,
1245 max_supported_slot
1246 } if slot_index == MAX_AUTHENTICATOR_KEYS && max_supported_slot == MAX_AUTHENTICATOR_KEYS - 1
1247 ));
1248 }
1249
1250 #[tokio::test]
1253 async fn test_get_packed_account_data_from_indexer() {
1254 let mut server = mockito::Server::new_async().await;
1255 let indexer_url = server.url();
1256
1257 let test_address = address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0");
1258 let expected_packed_index = U256::from(42);
1259
1260 let mock = server
1261 .mock("POST", "/packed-account")
1262 .match_header("content-type", "application/json")
1263 .match_body(mockito::Matcher::JsonString(
1264 serde_json::json!({
1265 "authenticator_address": test_address
1266 })
1267 .to_string(),
1268 ))
1269 .with_status(200)
1270 .with_header("content-type", "application/json")
1271 .with_body(
1272 serde_json::json!({
1273 "packed_account_data": format!("{:#x}", expected_packed_index)
1274 })
1275 .to_string(),
1276 )
1277 .create_async()
1278 .await;
1279
1280 let config = Config::new(
1281 None,
1282 1,
1283 address!("0x0000000000000000000000000000000000000001"),
1284 indexer_url,
1285 "http://gateway.example.com".to_string(),
1286 Vec::new(),
1287 2,
1288 )
1289 .unwrap();
1290
1291 let http_client = reqwest::Client::new();
1292
1293 let result = Authenticator::get_packed_account_data(
1294 test_address,
1295 None, &config,
1297 &http_client,
1298 )
1299 .await
1300 .unwrap();
1301
1302 assert_eq!(result, expected_packed_index);
1303 mock.assert_async().await;
1304 drop(server);
1305 }
1306
1307 #[tokio::test]
1308 async fn test_get_packed_account_data_from_indexer_error() {
1309 let mut server = mockito::Server::new_async().await;
1310 let indexer_url = server.url();
1311
1312 let test_address = address!("0x0000000000000000000000000000000000000099");
1313
1314 let mock = server
1315 .mock("POST", "/packed-account")
1316 .with_status(400)
1317 .with_header("content-type", "application/json")
1318 .with_body(
1319 serde_json::json!({
1320 "code": "account_does_not_exist",
1321 "message": "There is no account for this authenticator address"
1322 })
1323 .to_string(),
1324 )
1325 .create_async()
1326 .await;
1327
1328 let config = Config::new(
1329 None,
1330 1,
1331 address!("0x0000000000000000000000000000000000000001"),
1332 indexer_url,
1333 "http://gateway.example.com".to_string(),
1334 Vec::new(),
1335 2,
1336 )
1337 .unwrap();
1338
1339 let http_client = reqwest::Client::new();
1340
1341 let result =
1342 Authenticator::get_packed_account_data(test_address, None, &config, &http_client).await;
1343
1344 assert!(matches!(
1345 result,
1346 Err(AuthenticatorError::AccountDoesNotExist)
1347 ));
1348 mock.assert_async().await;
1349 drop(server);
1350 }
1351
1352 #[tokio::test]
1353 async fn test_signing_nonce_from_indexer() {
1354 let mut server = mockito::Server::new_async().await;
1355 let indexer_url = server.url();
1356
1357 let leaf_index = U256::from(1);
1358 let expected_nonce = U256::from(5);
1359
1360 let mock = server
1361 .mock("POST", "/signature-nonce")
1362 .match_header("content-type", "application/json")
1363 .match_body(mockito::Matcher::JsonString(
1364 serde_json::json!({
1365 "leaf_index": format!("{:#x}", leaf_index)
1366 })
1367 .to_string(),
1368 ))
1369 .with_status(200)
1370 .with_header("content-type", "application/json")
1371 .with_body(
1372 serde_json::json!({
1373 "signature_nonce": format!("{:#x}", expected_nonce)
1374 })
1375 .to_string(),
1376 )
1377 .create_async()
1378 .await;
1379
1380 let config = Config::new(
1381 None,
1382 1,
1383 address!("0x0000000000000000000000000000000000000001"),
1384 indexer_url,
1385 "http://gateway.example.com".to_string(),
1386 Vec::new(),
1387 2,
1388 )
1389 .unwrap();
1390
1391 let (query_material, nullifier_material) = test_materials();
1392 let authenticator = Authenticator {
1393 config,
1394 packed_account_data: leaf_index, signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1396 registry: None, http_client: reqwest::Client::new(),
1398 ws_connector: Connector::Plain,
1399 query_material,
1400 nullifier_material,
1401 };
1402
1403 let nonce = authenticator.signing_nonce().await.unwrap();
1404
1405 assert_eq!(nonce, expected_nonce);
1406 mock.assert_async().await;
1407 drop(server);
1408 }
1409
1410 #[tokio::test]
1411 async fn test_signing_nonce_from_indexer_error() {
1412 let mut server = mockito::Server::new_async().await;
1413 let indexer_url = server.url();
1414
1415 let mock = server
1416 .mock("POST", "/signature-nonce")
1417 .with_status(400)
1418 .with_header("content-type", "application/json")
1419 .with_body(
1420 serde_json::json!({
1421 "code": "invalid_leaf_index",
1422 "message": "Account index cannot be zero"
1423 })
1424 .to_string(),
1425 )
1426 .create_async()
1427 .await;
1428
1429 let config = Config::new(
1430 None,
1431 1,
1432 address!("0x0000000000000000000000000000000000000001"),
1433 indexer_url,
1434 "http://gateway.example.com".to_string(),
1435 Vec::new(),
1436 2,
1437 )
1438 .unwrap();
1439
1440 let (query_material, nullifier_material) = test_materials();
1441 let authenticator = Authenticator {
1442 config,
1443 packed_account_data: U256::ZERO,
1444 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1445 registry: None,
1446 http_client: reqwest::Client::new(),
1447 ws_connector: Connector::Plain,
1448 query_material,
1449 nullifier_material,
1450 };
1451
1452 let result = authenticator.signing_nonce().await;
1453
1454 assert!(matches!(
1455 result,
1456 Err(AuthenticatorError::IndexerError { .. })
1457 ));
1458 mock.assert_async().await;
1459 drop(server);
1460 }
1461}