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,
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 async fn new(
946 seed: &[u8],
947 config: Config,
948 recovery_address: Option<Address>,
949 http_client: reqwest::Client,
950 ) -> Result<Self, AuthenticatorError> {
951 let signer = Signer::from_seed_bytes(seed)?;
952
953 let mut key_set = AuthenticatorPublicKeySet::default();
954 key_set.try_push(signer.offchain_signer_pubkey())?;
955 let leaf_hash = key_set.leaf_hash();
956
957 let offchain_pubkey_compressed = {
958 let pk = signer.offchain_signer_pubkey().pk;
959 let mut compressed_bytes = Vec::new();
960 pk.serialize_compressed(&mut compressed_bytes)
961 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
962 U256::from_le_slice(&compressed_bytes)
963 };
964
965 let req = CreateAccountRequest {
966 recovery_address,
967 authenticator_addresses: vec![signer.onchain_signer_address()],
968 authenticator_pubkeys: vec![offchain_pubkey_compressed],
969 offchain_signer_commitment: leaf_hash.into(),
970 };
971
972 let resp = http_client
973 .post(format!("{}/create-account", config.gateway_url()))
974 .json(&req)
975 .send()
976 .await?;
977
978 let status = resp.status();
979 if status.is_success() {
980 let body: GatewayStatusResponse = resp.json().await?;
981 Ok(Self {
982 request_id: body.request_id,
983 http_client,
984 config,
985 })
986 } else {
987 let body_text = Authenticator::response_body_or_fallback(resp).await;
988 Err(AuthenticatorError::GatewayError {
989 status,
990 body: body_text,
991 })
992 }
993 }
994
995 pub async fn poll_status(&self) -> Result<GatewayRequestState, AuthenticatorError> {
1001 let resp = self
1002 .http_client
1003 .get(format!(
1004 "{}/status/{}",
1005 self.config.gateway_url(),
1006 self.request_id
1007 ))
1008 .send()
1009 .await?;
1010
1011 let status = resp.status();
1012
1013 if status.is_success() {
1014 let body: GatewayStatusResponse = resp.json().await?;
1015 Ok(body.status)
1016 } else {
1017 let body_text = Authenticator::response_body_or_fallback(resp).await;
1018 Err(AuthenticatorError::GatewayError {
1019 status,
1020 body: body_text,
1021 })
1022 }
1023 }
1024}
1025
1026impl ProtocolSigner for Authenticator {
1027 fn sign(&self, message: FieldElement) -> EdDSASignature {
1028 self.signer
1029 .offchain_signer_private_key()
1030 .expose_secret()
1031 .sign(*message)
1032 }
1033}
1034
1035pub trait OnchainKeyRepresentable {
1037 fn to_ethereum_representation(&self) -> Result<U256, PrimitiveError>;
1044}
1045
1046impl OnchainKeyRepresentable for EdDSAPublicKey {
1047 fn to_ethereum_representation(&self) -> Result<U256, PrimitiveError> {
1049 let mut compressed_bytes = Vec::new();
1050 self.pk
1051 .serialize_compressed(&mut compressed_bytes)
1052 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
1053 Ok(U256::from_le_slice(&compressed_bytes))
1054 }
1055}
1056
1057#[derive(Debug, thiserror::Error)]
1059pub enum AuthenticatorError {
1060 #[error(transparent)]
1062 PrimitiveError(#[from] PrimitiveError),
1063
1064 #[error("Account is not registered for this authenticator.")]
1067 AccountDoesNotExist,
1068
1069 #[error("Account already exists for this authenticator.")]
1071 AccountAlreadyExists,
1072
1073 #[error("Error interacting with EVM contract: {0}")]
1075 ContractError(#[from] alloy::contract::Error),
1076
1077 #[error("Network error: {0}")]
1079 NetworkError(#[from] reqwest::Error),
1080
1081 #[error("Public key not found.")]
1083 PublicKeyNotFound,
1084
1085 #[error("Gateway error (status {status}): {body}")]
1087 GatewayError {
1088 status: StatusCode,
1090 body: String,
1092 },
1093
1094 #[error("Indexer error (status {status}): {body}")]
1096 IndexerError {
1097 status: StatusCode,
1099 body: String,
1101 },
1102
1103 #[error("Account creation timed out")]
1105 Timeout,
1106
1107 #[error("Invalid configuration for {attribute}: {reason}")]
1109 InvalidConfig {
1110 attribute: &'static str,
1112 reason: String,
1114 },
1115
1116 #[error("The provided credential is not valid for the provided proof request")]
1118 InvalidCredentialForProofRequest,
1119
1120 #[error("Registration error ({error_code}): {error_message}")]
1124 RegistrationError {
1125 error_code: String,
1127 error_message: String,
1129 },
1130
1131 #[error(transparent)]
1133 ProofError(#[from] ProofError),
1134
1135 #[error(
1137 "Invalid indexer authenticator pubkey slot {slot_index}; max supported slot is {max_supported_slot}"
1138 )]
1139 InvalidIndexerPubkeySlot {
1140 slot_index: usize,
1142 max_supported_slot: usize,
1144 },
1145
1146 #[error("{0}")]
1148 Generic(String),
1149}
1150
1151#[derive(Debug)]
1152enum PollResult {
1153 Retryable,
1154 TerminalError(AuthenticatorError),
1155}
1156
1157#[cfg(all(test, feature = "embed-zkeys"))]
1158mod tests {
1159 use super::*;
1160 use alloy::primitives::{U256, address};
1161 use std::sync::OnceLock;
1162 use world_id_primitives::authenticator::MAX_AUTHENTICATOR_KEYS;
1163
1164 fn test_materials() -> (Arc<CircomGroth16Material>, Arc<CircomGroth16Material>) {
1165 static QUERY: OnceLock<Arc<CircomGroth16Material>> = OnceLock::new();
1166 static NULLIFIER: OnceLock<Arc<CircomGroth16Material>> = OnceLock::new();
1167
1168 let query = QUERY.get_or_init(|| {
1169 Arc::new(world_id_proof::proof::load_embedded_query_material().unwrap())
1170 });
1171 let nullifier = NULLIFIER.get_or_init(|| {
1172 Arc::new(world_id_proof::proof::load_embedded_nullifier_material().unwrap())
1173 });
1174
1175 (Arc::clone(query), Arc::clone(nullifier))
1176 }
1177
1178 fn test_pubkey(seed_byte: u8) -> EdDSAPublicKey {
1179 Signer::from_seed_bytes(&[seed_byte; 32])
1180 .unwrap()
1181 .offchain_signer_pubkey()
1182 }
1183
1184 fn encoded_test_pubkey(seed_byte: u8) -> U256 {
1185 test_pubkey(seed_byte).to_ethereum_representation().unwrap()
1186 }
1187
1188 #[test]
1189 fn test_insert_or_reuse_authenticator_key_reuses_empty_slot() {
1190 let mut key_set =
1191 AuthenticatorPublicKeySet::new(vec![test_pubkey(1), test_pubkey(2), test_pubkey(4)])
1192 .unwrap();
1193 key_set[1] = None;
1194 let new_key = test_pubkey(3);
1195
1196 let index =
1197 Authenticator::insert_or_reuse_authenticator_key(&mut key_set, new_key).unwrap();
1198
1199 assert_eq!(index, 1);
1200 assert_eq!(key_set.len(), 3);
1201 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(3).pk);
1202 }
1203
1204 #[test]
1205 fn test_insert_or_reuse_authenticator_key_appends_when_no_empty_slot() {
1206 let mut key_set = AuthenticatorPublicKeySet::new(vec![test_pubkey(1)]).unwrap();
1207 let new_key = test_pubkey(2);
1208
1209 let index =
1210 Authenticator::insert_or_reuse_authenticator_key(&mut key_set, new_key).unwrap();
1211
1212 assert_eq!(index, 1);
1213 assert_eq!(key_set.len(), 2);
1214 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(2).pk);
1215 }
1216
1217 #[test]
1218 fn test_decode_indexer_pubkeys_trims_trailing_empty_slots() {
1219 let mut encoded_pubkeys = vec![Some(encoded_test_pubkey(1)), Some(encoded_test_pubkey(2))];
1220 encoded_pubkeys.extend(vec![None; MAX_AUTHENTICATOR_KEYS + 5]);
1221
1222 let key_set = Authenticator::decode_indexer_pubkeys(encoded_pubkeys).unwrap();
1223
1224 assert_eq!(key_set.len(), 2);
1225 assert_eq!(key_set[0].as_ref().unwrap().pk, test_pubkey(1).pk);
1226 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(2).pk);
1227 }
1228
1229 #[test]
1230 fn test_decode_indexer_pubkeys_rejects_used_slot_beyond_max() {
1231 let mut encoded_pubkeys = vec![None; MAX_AUTHENTICATOR_KEYS + 1];
1232 encoded_pubkeys[MAX_AUTHENTICATOR_KEYS] = Some(encoded_test_pubkey(1));
1233
1234 let error = Authenticator::decode_indexer_pubkeys(encoded_pubkeys).unwrap_err();
1235 assert!(matches!(
1236 error,
1237 AuthenticatorError::InvalidIndexerPubkeySlot {
1238 slot_index,
1239 max_supported_slot
1240 } if slot_index == MAX_AUTHENTICATOR_KEYS && max_supported_slot == MAX_AUTHENTICATOR_KEYS - 1
1241 ));
1242 }
1243
1244 #[tokio::test]
1247 async fn test_get_packed_account_data_from_indexer() {
1248 let mut server = mockito::Server::new_async().await;
1249 let indexer_url = server.url();
1250
1251 let test_address = address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0");
1252 let expected_packed_index = U256::from(42);
1253
1254 let mock = server
1255 .mock("POST", "/packed-account")
1256 .match_header("content-type", "application/json")
1257 .match_body(mockito::Matcher::JsonString(
1258 serde_json::json!({
1259 "authenticator_address": test_address
1260 })
1261 .to_string(),
1262 ))
1263 .with_status(200)
1264 .with_header("content-type", "application/json")
1265 .with_body(
1266 serde_json::json!({
1267 "packed_account_data": format!("{:#x}", expected_packed_index)
1268 })
1269 .to_string(),
1270 )
1271 .create_async()
1272 .await;
1273
1274 let config = Config::new(
1275 None,
1276 1,
1277 address!("0x0000000000000000000000000000000000000001"),
1278 indexer_url,
1279 "http://gateway.example.com".to_string(),
1280 Vec::new(),
1281 2,
1282 )
1283 .unwrap();
1284
1285 let http_client = reqwest::Client::new();
1286
1287 let result = Authenticator::get_packed_account_data(
1288 test_address,
1289 None, &config,
1291 &http_client,
1292 )
1293 .await
1294 .unwrap();
1295
1296 assert_eq!(result, expected_packed_index);
1297 mock.assert_async().await;
1298 drop(server);
1299 }
1300
1301 #[tokio::test]
1302 async fn test_get_packed_account_data_from_indexer_error() {
1303 let mut server = mockito::Server::new_async().await;
1304 let indexer_url = server.url();
1305
1306 let test_address = address!("0x0000000000000000000000000000000000000099");
1307
1308 let mock = server
1309 .mock("POST", "/packed-account")
1310 .with_status(400)
1311 .with_header("content-type", "application/json")
1312 .with_body(
1313 serde_json::json!({
1314 "code": "account_does_not_exist",
1315 "message": "There is no account for this authenticator address"
1316 })
1317 .to_string(),
1318 )
1319 .create_async()
1320 .await;
1321
1322 let config = Config::new(
1323 None,
1324 1,
1325 address!("0x0000000000000000000000000000000000000001"),
1326 indexer_url,
1327 "http://gateway.example.com".to_string(),
1328 Vec::new(),
1329 2,
1330 )
1331 .unwrap();
1332
1333 let http_client = reqwest::Client::new();
1334
1335 let result =
1336 Authenticator::get_packed_account_data(test_address, None, &config, &http_client).await;
1337
1338 assert!(matches!(
1339 result,
1340 Err(AuthenticatorError::AccountDoesNotExist)
1341 ));
1342 mock.assert_async().await;
1343 drop(server);
1344 }
1345
1346 #[tokio::test]
1347 async fn test_signing_nonce_from_indexer() {
1348 let mut server = mockito::Server::new_async().await;
1349 let indexer_url = server.url();
1350
1351 let leaf_index = U256::from(1);
1352 let expected_nonce = U256::from(5);
1353
1354 let mock = server
1355 .mock("POST", "/signature-nonce")
1356 .match_header("content-type", "application/json")
1357 .match_body(mockito::Matcher::JsonString(
1358 serde_json::json!({
1359 "leaf_index": format!("{:#x}", leaf_index)
1360 })
1361 .to_string(),
1362 ))
1363 .with_status(200)
1364 .with_header("content-type", "application/json")
1365 .with_body(
1366 serde_json::json!({
1367 "signature_nonce": format!("{:#x}", expected_nonce)
1368 })
1369 .to_string(),
1370 )
1371 .create_async()
1372 .await;
1373
1374 let config = Config::new(
1375 None,
1376 1,
1377 address!("0x0000000000000000000000000000000000000001"),
1378 indexer_url,
1379 "http://gateway.example.com".to_string(),
1380 Vec::new(),
1381 2,
1382 )
1383 .unwrap();
1384
1385 let (query_material, nullifier_material) = test_materials();
1386 let authenticator = Authenticator {
1387 config,
1388 packed_account_data: leaf_index, signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1390 registry: None, http_client: reqwest::Client::new(),
1392 ws_connector: Connector::Plain,
1393 query_material,
1394 nullifier_material,
1395 };
1396
1397 let nonce = authenticator.signing_nonce().await.unwrap();
1398
1399 assert_eq!(nonce, expected_nonce);
1400 mock.assert_async().await;
1401 drop(server);
1402 }
1403
1404 #[tokio::test]
1405 async fn test_signing_nonce_from_indexer_error() {
1406 let mut server = mockito::Server::new_async().await;
1407 let indexer_url = server.url();
1408
1409 let mock = server
1410 .mock("POST", "/signature-nonce")
1411 .with_status(400)
1412 .with_header("content-type", "application/json")
1413 .with_body(
1414 serde_json::json!({
1415 "code": "invalid_leaf_index",
1416 "message": "Account index cannot be zero"
1417 })
1418 .to_string(),
1419 )
1420 .create_async()
1421 .await;
1422
1423 let config = Config::new(
1424 None,
1425 1,
1426 address!("0x0000000000000000000000000000000000000001"),
1427 indexer_url,
1428 "http://gateway.example.com".to_string(),
1429 Vec::new(),
1430 2,
1431 )
1432 .unwrap();
1433
1434 let (query_material, nullifier_material) = test_materials();
1435 let authenticator = Authenticator {
1436 config,
1437 packed_account_data: U256::ZERO,
1438 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1439 registry: None,
1440 http_client: reqwest::Client::new(),
1441 ws_connector: Connector::Plain,
1442 query_material,
1443 nullifier_material,
1444 };
1445
1446 let result = authenticator.signing_nonce().await;
1447
1448 assert!(matches!(
1449 result,
1450 Err(AuthenticatorError::IndexerError { .. })
1451 ));
1452 mock.assert_async().await;
1453 drop(server);
1454 }
1455}