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,
24 providers::DynProvider,
25 signers::{Signature, SignerSync},
26};
27use ark_serialize::CanonicalSerialize;
28use eddsa_babyjubjub::{EdDSAPublicKey, EdDSASignature};
29use groth16_material::circom::CircomGroth16Material;
30use reqwest::StatusCode;
31use ruint::{aliases::U256, uint};
32use secrecy::ExposeSecret;
33use taceo_oprf::client::Connector;
34pub use world_id_primitives::{Config, TREE_DEPTH, authenticator::ProtocolSigner};
35use world_id_primitives::{
36 PrimitiveError, SessionId, ZeroKnowledgeProof,
37 authenticator::{
38 AuthenticatorPublicKeySet, SparseAuthenticatorPubkeysError,
39 decode_sparse_authenticator_pubkeys,
40 },
41 merkle::MerkleInclusionProof,
42};
43use world_id_proof::{
44 AuthenticatorProofInput, FullOprfOutput, OprfEntrypoint,
45 proof::{ProofError, generate_nullifier_proof},
46};
47
48#[expect(unused_imports, reason = "used for docs")]
49use world_id_primitives::Nullifier;
50
51static MASK_RECOVERY_COUNTER: U256 =
52 uint!(0xFFFFFFFF00000000000000000000000000000000000000000000000000000000_U256);
53static MASK_PUBKEY_ID: U256 =
54 uint!(0x00000000FFFFFFFF000000000000000000000000000000000000000000000000_U256);
55static MASK_LEAF_INDEX: U256 =
56 uint!(0x000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFF_U256);
57
58pub struct Authenticator {
60 pub config: Config,
62 pub packed_account_data: U256,
65 signer: Signer,
66 registry: Option<Arc<WorldIdRegistryInstance<DynProvider>>>,
67 http_client: reqwest::Client,
68 ws_connector: Connector,
69 query_material: Option<Arc<CircomGroth16Material>>,
70 nullifier_material: Option<Arc<CircomGroth16Material>>,
71}
72
73impl std::fmt::Debug for Authenticator {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 f.debug_struct("Authenticator")
77 .field("config", &self.config)
78 .finish_non_exhaustive()
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(seed: &[u8], config: Config) -> Result<Self, AuthenticatorError> {
100 let signer = Signer::from_seed_bytes(seed)?;
101
102 let registry: Option<Arc<WorldIdRegistryInstance<DynProvider>>> =
103 config.rpc_url().map(|rpc_url| {
104 let provider = alloy::providers::ProviderBuilder::new()
105 .with_chain_id(config.chain_id())
106 .connect_http(rpc_url.clone());
107 Arc::new(crate::registry::WorldIdRegistry::new(
108 *config.registry_address(),
109 alloy::providers::Provider::erased(provider),
110 ))
111 });
112
113 let http_client = reqwest::Client::new();
114
115 let packed_account_data = Self::get_packed_account_data(
116 signer.onchain_signer_address(),
117 registry.as_deref(),
118 &config,
119 &http_client,
120 )
121 .await?;
122
123 #[cfg(not(target_arch = "wasm32"))]
124 let ws_connector = {
125 let mut root_store = rustls::RootCertStore::empty();
126 root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
127 let rustls_config = rustls::ClientConfig::builder()
128 .with_root_certificates(root_store)
129 .with_no_client_auth();
130 Connector::Rustls(Arc::new(rustls_config))
131 };
132
133 #[cfg(target_arch = "wasm32")]
134 let ws_connector = Connector;
135
136 Ok(Self {
137 packed_account_data,
138 signer,
139 config,
140 registry,
141 http_client,
142 ws_connector,
143 query_material: None,
144 nullifier_material: None,
145 })
146 }
147
148 #[must_use]
153 pub fn with_proof_materials(
154 self,
155 query_material: Arc<CircomGroth16Material>,
156 nullifier_material: Arc<CircomGroth16Material>,
157 ) -> Self {
158 Self {
159 query_material: Some(query_material),
160 nullifier_material: Some(nullifier_material),
161 ..self
162 }
163 }
164
165 pub async fn register(
173 seed: &[u8],
174 config: Config,
175 recovery_address: Option<Address>,
176 ) -> Result<InitializingAuthenticator, AuthenticatorError> {
177 let http_client = reqwest::Client::new();
178 InitializingAuthenticator::new(seed, config, recovery_address, http_client).await
179 }
180
181 pub async fn init_or_register(
194 seed: &[u8],
195 config: Config,
196 recovery_address: Option<Address>,
197 ) -> Result<Self, AuthenticatorError> {
198 match Self::init(seed, config.clone()).await {
199 Ok(authenticator) => Ok(authenticator),
200 Err(AuthenticatorError::AccountDoesNotExist) => {
201 let http_client = reqwest::Client::new();
203 let initializing_authenticator = InitializingAuthenticator::new(
204 seed,
205 config.clone(),
206 recovery_address,
207 http_client,
208 )
209 .await?;
210
211 let backoff = backon::ExponentialBuilder::default()
212 .with_min_delay(std::time::Duration::from_millis(800))
213 .with_factor(1.5)
214 .without_max_times()
215 .with_total_delay(Some(std::time::Duration::from_secs(120)));
216
217 let poller = || async {
218 let poll_status = initializing_authenticator.poll_status().await;
219 let result = match poll_status {
220 Ok(GatewayRequestState::Finalized { .. }) => Ok(()),
221 Ok(GatewayRequestState::Failed { error_code, error }) => Err(
222 PollResult::TerminalError(AuthenticatorError::RegistrationError {
223 error_code: error_code.map(|v| v.to_string()).unwrap_or_default(),
224 error_message: error,
225 }),
226 ),
227 Err(AuthenticatorError::GatewayError { status, body }) => {
228 if status.is_client_error() {
229 Err(PollResult::TerminalError(
230 AuthenticatorError::GatewayError { status, body },
231 ))
232 } else {
233 Err(PollResult::Retryable)
234 }
235 }
236 _ => Err(PollResult::Retryable),
237 };
238
239 match result {
240 Ok(()) => match Self::init(seed, config.clone()).await {
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 pub fn danger_sign_challenge(&self, challenge: &[u8]) -> Result<Signature, AuthenticatorError> {
487 self.signer
488 .onchain_signer()
489 .sign_message_sync(challenge)
490 .map_err(|e| AuthenticatorError::Generic(format!("signature error: {e}")))
491 }
492
493 fn check_oprf_config(&self) -> Result<(&[String], usize), AuthenticatorError> {
498 let services = self.config.nullifier_oracle_urls();
499 if services.is_empty() {
500 return Err(AuthenticatorError::Generic(
501 "No nullifier oracle URLs configured".to_string(),
502 ));
503 }
504 let requested_threshold = self.config.nullifier_oracle_threshold();
505 if requested_threshold == 0 {
506 return Err(AuthenticatorError::InvalidConfig {
507 attribute: "nullifier_oracle_threshold",
508 reason: "must be at least 1".to_string(),
509 });
510 }
511 let threshold = requested_threshold.min(services.len());
512 Ok((services, threshold))
513 }
514
515 fn decode_indexer_pubkeys(
516 pubkeys: Vec<Option<U256>>,
517 ) -> Result<AuthenticatorPublicKeySet, AuthenticatorError> {
518 decode_sparse_authenticator_pubkeys(pubkeys).map_err(|e| match e {
519 SparseAuthenticatorPubkeysError::SlotOutOfBounds {
520 slot_index,
521 max_supported_slot,
522 } => AuthenticatorError::InvalidIndexerPubkeySlot {
523 slot_index,
524 max_supported_slot,
525 },
526 SparseAuthenticatorPubkeysError::InvalidCompressedPubkey { slot_index, reason } => {
527 PrimitiveError::Deserialization(format!(
528 "invalid authenticator public key returned by indexer at slot {slot_index}: {reason}"
529 ))
530 .into()
531 }
532 })
533 }
534
535 fn insert_or_reuse_authenticator_key(
536 key_set: &mut AuthenticatorPublicKeySet,
537 new_authenticator_pubkey: EdDSAPublicKey,
538 ) -> Result<usize, AuthenticatorError> {
539 if let Some(index) = key_set.iter().position(Option::is_none) {
540 key_set.try_set_at_index(index, new_authenticator_pubkey)?;
541 Ok(index)
542 } else {
543 key_set.try_push(new_authenticator_pubkey)?;
544 Ok(key_set.len() - 1)
545 }
546 }
547
548 pub async fn generate_nullifier(
565 &self,
566 proof_request: &ProofRequest,
567 inclusion_proof: MerkleInclusionProof<TREE_DEPTH>,
568 key_set: AuthenticatorPublicKeySet,
569 ) -> Result<FullOprfOutput, AuthenticatorError> {
570 let mut rng = rand::rngs::OsRng;
571
572 let (services, threshold) = self.check_oprf_config()?;
573
574 let query_material = self
575 .query_material
576 .as_ref()
577 .ok_or(AuthenticatorError::ProofMaterialsNotLoaded)?;
578
579 let key_index = key_set
580 .iter()
581 .position(|pk| {
582 pk.as_ref()
583 .is_some_and(|pk| pk.pk == self.offchain_pubkey().pk)
584 })
585 .ok_or(AuthenticatorError::PublicKeyNotFound)? as u64;
586
587 let authenticator_input = AuthenticatorProofInput::new(
588 key_set,
589 inclusion_proof,
590 self.signer
591 .offchain_signer_private_key()
592 .expose_secret()
593 .clone(),
594 key_index,
595 );
596
597 let oprf_entry_point = OprfEntrypoint::new(
598 services,
599 threshold,
600 query_material,
601 &authenticator_input,
602 &self.ws_connector,
603 );
604 Ok(oprf_entry_point
605 .gen_nullifier(&mut rng, proof_request)
606 .await?)
607 }
608
609 pub async fn generate_credential_blinding_factor(
618 &self,
619 issuer_schema_id: u64,
620 ) -> Result<FieldElement, AuthenticatorError> {
621 let mut rng = rand::rngs::OsRng;
622 let (services, threshold) = self.check_oprf_config()?;
623
624 let query_material = self
625 .query_material
626 .as_ref()
627 .ok_or(AuthenticatorError::ProofMaterialsNotLoaded)?;
628
629 let (inclusion_proof, key_set) = self.fetch_inclusion_proof().await?;
630 let key_index = key_set
631 .iter()
632 .position(|pk| {
633 pk.as_ref()
634 .is_some_and(|pk| pk.pk == self.offchain_pubkey().pk)
635 })
636 .ok_or(AuthenticatorError::PublicKeyNotFound)? as u64;
637
638 let authenticator_input = AuthenticatorProofInput::new(
639 key_set,
640 inclusion_proof,
641 self.signer
642 .offchain_signer_private_key()
643 .expose_secret()
644 .clone(),
645 key_index,
646 );
647
648 let oprf_entry_point = OprfEntrypoint::new(
649 services,
650 threshold,
651 query_material,
652 &authenticator_input,
653 &self.ws_connector,
654 );
655
656 let (blinding_factor, _share_epoch) = oprf_entry_point
657 .gen_credential_blinding_factor(&mut rng, issuer_schema_id)
658 .await?;
659
660 Ok(blinding_factor)
661 }
662
663 pub async fn generate_session_id(
679 &self,
680 proof_request: &ProofRequest,
681 session_id_r_seed: Option<FieldElement>,
682 ) -> Result<(SessionId, FieldElement), AuthenticatorError> {
683 let mut rng = rand::rngs::OsRng;
684
685 let session_id_r_seed = session_id_r_seed.unwrap_or(FieldElement::random(&mut rng));
687
688 let session_id = SessionId::from_r_seed(
689 self.leaf_index(),
690 session_id_r_seed,
691 proof_request.session_id.map(|v| v.oprf_seed()),
692 &mut rng,
693 )
694 .map_err(|_| AuthenticatorError::InvalidSessionId)?;
695
696 if let Some(request_session_id) = proof_request.session_id {
697 if request_session_id != session_id {
698 return Err(AuthenticatorError::SessionIdMismatch);
699 }
700 }
701
702 Ok((session_id, session_id_r_seed))
703 }
704
705 #[allow(clippy::too_many_arguments)]
729 pub fn generate_single_proof(
730 &self,
731 oprf_nullifier: FullOprfOutput,
732 request_item: &RequestItem,
733 credential: &Credential,
734 credential_sub_blinding_factor: FieldElement,
735 session_id_r_seed: FieldElement,
736 session_id: Option<SessionId>,
737 request_timestamp: u64,
738 ) -> Result<ResponseItem, AuthenticatorError> {
739 let mut rng = rand::rngs::OsRng;
740
741 let merkle_root: FieldElement = oprf_nullifier.query_proof_input.merkle_root.into();
742 let action_from_query: FieldElement = oprf_nullifier.query_proof_input.action.into();
743
744 let expires_at_min = request_item.effective_expires_at_min(request_timestamp);
745
746 let nullifier_material = self
747 .nullifier_material
748 .as_ref()
749 .ok_or(AuthenticatorError::ProofMaterialsNotLoaded)?;
750
751 let (proof, _public_inputs, nullifier) = generate_nullifier_proof(
752 nullifier_material,
753 &mut rng,
754 credential,
755 credential_sub_blinding_factor,
756 oprf_nullifier,
757 request_item,
758 session_id.map(|v| v.commitment()),
759 session_id_r_seed,
760 expires_at_min,
761 )?;
762
763 let proof = ZeroKnowledgeProof::from_groth16_proof(&proof, merkle_root);
764
765 let nullifier_fe: FieldElement = nullifier.into();
767 let response_item = if session_id.is_some() {
768 let session_nullifier = SessionNullifier::new(nullifier_fe, action_from_query);
769 ResponseItem::new_session(
770 request_item.identifier.clone(),
771 request_item.issuer_schema_id,
772 proof,
773 session_nullifier,
774 expires_at_min,
775 )
776 } else {
777 ResponseItem::new_uniqueness(
778 request_item.identifier.clone(),
779 request_item.issuer_schema_id,
780 proof,
781 nullifier_fe.into(),
782 expires_at_min,
783 )
784 };
785
786 Ok(response_item)
787 }
788
789 pub async fn insert_authenticator(
798 &self,
799 new_authenticator_pubkey: EdDSAPublicKey,
800 new_authenticator_address: Address,
801 ) -> Result<String, AuthenticatorError> {
802 let leaf_index = self.leaf_index();
803 let nonce = self.signing_nonce().await?;
804 let mut key_set = self.fetch_authenticator_pubkeys().await?;
805 let old_offchain_signer_commitment = key_set.leaf_hash();
806 let encoded_offchain_pubkey = new_authenticator_pubkey.to_ethereum_representation()?;
807 let index =
808 Self::insert_or_reuse_authenticator_key(&mut key_set, new_authenticator_pubkey)?;
809 let new_offchain_signer_commitment = key_set.leaf_hash();
810
811 let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
812
813 #[allow(clippy::cast_possible_truncation)]
814 let signature = sign_insert_authenticator(
816 &self.signer.onchain_signer(),
817 leaf_index,
818 new_authenticator_address,
819 index as u32,
820 encoded_offchain_pubkey,
821 new_offchain_signer_commitment.into(),
822 nonce,
823 &eip712_domain,
824 )
825 .map_err(|e| {
826 AuthenticatorError::Generic(format!("Failed to sign insert authenticator: {e}"))
827 })?;
828
829 #[allow(clippy::cast_possible_truncation)]
830 let req = InsertAuthenticatorRequest {
832 leaf_index,
833 new_authenticator_address,
834 pubkey_id: index as u32,
835 new_authenticator_pubkey: encoded_offchain_pubkey,
836 old_offchain_signer_commitment: old_offchain_signer_commitment.into(),
837 new_offchain_signer_commitment: new_offchain_signer_commitment.into(),
838 signature: signature.as_bytes().to_vec(),
839 nonce,
840 };
841
842 let resp = self
843 .http_client
844 .post(format!(
845 "{}/insert-authenticator",
846 self.config.gateway_url()
847 ))
848 .json(&req)
849 .send()
850 .await?;
851
852 let status = resp.status();
853 if status.is_success() {
854 let body: GatewayStatusResponse = resp.json().await?;
855 Ok(body.request_id)
856 } else {
857 let body_text = Self::response_body_or_fallback(resp).await;
858 Err(AuthenticatorError::GatewayError {
859 status,
860 body: body_text,
861 })
862 }
863 }
864
865 pub async fn update_authenticator(
874 &self,
875 old_authenticator_address: Address,
876 new_authenticator_address: Address,
877 new_authenticator_pubkey: EdDSAPublicKey,
878 index: u32,
879 ) -> Result<String, AuthenticatorError> {
880 let leaf_index = self.leaf_index();
881 let nonce = self.signing_nonce().await?;
882 let mut key_set = self.fetch_authenticator_pubkeys().await?;
883 let old_commitment: U256 = key_set.leaf_hash().into();
884 let encoded_offchain_pubkey = new_authenticator_pubkey.to_ethereum_representation()?;
885 key_set.try_set_at_index(index as usize, new_authenticator_pubkey)?;
886 let new_commitment: U256 = key_set.leaf_hash().into();
887
888 let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
889
890 let signature = sign_update_authenticator(
891 &self.signer.onchain_signer(),
892 leaf_index,
893 old_authenticator_address,
894 new_authenticator_address,
895 index,
896 encoded_offchain_pubkey,
897 new_commitment,
898 nonce,
899 &eip712_domain,
900 )
901 .map_err(|e| {
902 AuthenticatorError::Generic(format!("Failed to sign update authenticator: {e}"))
903 })?;
904
905 let req = UpdateAuthenticatorRequest {
906 leaf_index,
907 old_authenticator_address,
908 new_authenticator_address,
909 old_offchain_signer_commitment: old_commitment,
910 new_offchain_signer_commitment: new_commitment,
911 signature: signature.as_bytes().to_vec(),
912 nonce,
913 pubkey_id: index,
914 new_authenticator_pubkey: encoded_offchain_pubkey,
915 };
916
917 let resp = self
918 .http_client
919 .post(format!(
920 "{}/update-authenticator",
921 self.config.gateway_url()
922 ))
923 .json(&req)
924 .send()
925 .await?;
926
927 let status = resp.status();
928 if status.is_success() {
929 let gateway_resp: GatewayStatusResponse = resp.json().await?;
930 Ok(gateway_resp.request_id)
931 } else {
932 let body_text = Self::response_body_or_fallback(resp).await;
933 Err(AuthenticatorError::GatewayError {
934 status,
935 body: body_text,
936 })
937 }
938 }
939
940 pub async fn remove_authenticator(
949 &self,
950 authenticator_address: Address,
951 index: u32,
952 ) -> Result<String, AuthenticatorError> {
953 let leaf_index = self.leaf_index();
954 let nonce = self.signing_nonce().await?;
955 let mut key_set = self.fetch_authenticator_pubkeys().await?;
956 let old_commitment: U256 = key_set.leaf_hash().into();
957 let existing_pubkey = key_set
958 .get(index as usize)
959 .ok_or(AuthenticatorError::PublicKeyNotFound)?;
960
961 let encoded_old_offchain_pubkey = existing_pubkey.to_ethereum_representation()?;
962
963 key_set.try_clear_at_index(index as usize)?;
964 let new_commitment: U256 = key_set.leaf_hash().into();
965
966 let eip712_domain = domain(self.config.chain_id(), *self.config.registry_address());
967
968 let signature = sign_remove_authenticator(
969 &self.signer.onchain_signer(),
970 leaf_index,
971 authenticator_address,
972 index,
973 encoded_old_offchain_pubkey,
974 new_commitment,
975 nonce,
976 &eip712_domain,
977 )
978 .map_err(|e| {
979 AuthenticatorError::Generic(format!("Failed to sign remove authenticator: {e}"))
980 })?;
981
982 let req = RemoveAuthenticatorRequest {
983 leaf_index,
984 authenticator_address,
985 old_offchain_signer_commitment: old_commitment,
986 new_offchain_signer_commitment: new_commitment,
987 signature: signature.as_bytes().to_vec(),
988 nonce,
989 pubkey_id: Some(index),
990 authenticator_pubkey: Some(encoded_old_offchain_pubkey),
991 };
992
993 let resp = self
994 .http_client
995 .post(format!(
996 "{}/remove-authenticator",
997 self.config.gateway_url()
998 ))
999 .json(&req)
1000 .send()
1001 .await?;
1002
1003 let status = resp.status();
1004 if status.is_success() {
1005 let gateway_resp: GatewayStatusResponse = resp.json().await?;
1006 Ok(gateway_resp.request_id)
1007 } else {
1008 let body_text = Self::response_body_or_fallback(resp).await;
1009 Err(AuthenticatorError::GatewayError {
1010 status,
1011 body: body_text,
1012 })
1013 }
1014 }
1015}
1016
1017pub struct InitializingAuthenticator {
1020 request_id: String,
1021 http_client: reqwest::Client,
1022 config: Config,
1023}
1024
1025impl InitializingAuthenticator {
1026 #[must_use]
1028 pub fn request_id(&self) -> &str {
1029 &self.request_id
1030 }
1031
1032 async fn new(
1038 seed: &[u8],
1039 config: Config,
1040 recovery_address: Option<Address>,
1041 http_client: reqwest::Client,
1042 ) -> Result<Self, AuthenticatorError> {
1043 let signer = Signer::from_seed_bytes(seed)?;
1044
1045 let mut key_set = AuthenticatorPublicKeySet::default();
1046 key_set.try_push(signer.offchain_signer_pubkey())?;
1047 let leaf_hash = key_set.leaf_hash();
1048
1049 let offchain_pubkey_compressed = {
1050 let pk = signer.offchain_signer_pubkey().pk;
1051 let mut compressed_bytes = Vec::new();
1052 pk.serialize_compressed(&mut compressed_bytes)
1053 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
1054 U256::from_le_slice(&compressed_bytes)
1055 };
1056
1057 let req = CreateAccountRequest {
1058 recovery_address,
1059 authenticator_addresses: vec![signer.onchain_signer_address()],
1060 authenticator_pubkeys: vec![offchain_pubkey_compressed],
1061 offchain_signer_commitment: leaf_hash.into(),
1062 };
1063
1064 let resp = http_client
1065 .post(format!("{}/create-account", config.gateway_url()))
1066 .json(&req)
1067 .send()
1068 .await?;
1069
1070 let status = resp.status();
1071 if status.is_success() {
1072 let body: GatewayStatusResponse = resp.json().await?;
1073 Ok(Self {
1074 request_id: body.request_id,
1075 http_client,
1076 config,
1077 })
1078 } else {
1079 let body_text = Authenticator::response_body_or_fallback(resp).await;
1080 Err(AuthenticatorError::GatewayError {
1081 status,
1082 body: body_text,
1083 })
1084 }
1085 }
1086
1087 pub async fn poll_status(&self) -> Result<GatewayRequestState, AuthenticatorError> {
1093 let resp = self
1094 .http_client
1095 .get(format!(
1096 "{}/status/{}",
1097 self.config.gateway_url(),
1098 self.request_id
1099 ))
1100 .send()
1101 .await?;
1102
1103 let status = resp.status();
1104
1105 if status.is_success() {
1106 let body: GatewayStatusResponse = resp.json().await?;
1107 Ok(body.status)
1108 } else {
1109 let body_text = Authenticator::response_body_or_fallback(resp).await;
1110 Err(AuthenticatorError::GatewayError {
1111 status,
1112 body: body_text,
1113 })
1114 }
1115 }
1116}
1117
1118impl ProtocolSigner for Authenticator {
1119 fn sign(&self, message: FieldElement) -> EdDSASignature {
1120 self.signer
1121 .offchain_signer_private_key()
1122 .expose_secret()
1123 .sign(*message)
1124 }
1125}
1126
1127pub trait OnchainKeyRepresentable {
1129 fn to_ethereum_representation(&self) -> Result<U256, PrimitiveError>;
1136}
1137
1138impl OnchainKeyRepresentable for EdDSAPublicKey {
1139 fn to_ethereum_representation(&self) -> Result<U256, PrimitiveError> {
1141 let mut compressed_bytes = Vec::new();
1142 self.pk
1143 .serialize_compressed(&mut compressed_bytes)
1144 .map_err(|e| PrimitiveError::Serialization(e.to_string()))?;
1145 Ok(U256::from_le_slice(&compressed_bytes))
1146 }
1147}
1148
1149#[derive(Debug, thiserror::Error)]
1151pub enum AuthenticatorError {
1152 #[error(transparent)]
1154 PrimitiveError(#[from] PrimitiveError),
1155
1156 #[error("Account is not registered for this authenticator.")]
1159 AccountDoesNotExist,
1160
1161 #[error("Error interacting with EVM contract: {0}")]
1163 ContractError(#[from] alloy::contract::Error),
1164
1165 #[error("Network error: {0}")]
1167 NetworkError(#[from] reqwest::Error),
1168
1169 #[error("Public key not found.")]
1171 PublicKeyNotFound,
1172
1173 #[error("Gateway error (status {status}): {body}")]
1175 GatewayError {
1176 status: StatusCode,
1178 body: String,
1180 },
1181
1182 #[error("Indexer error (status {status}): {body}")]
1184 IndexerError {
1185 status: StatusCode,
1187 body: String,
1189 },
1190
1191 #[error("Account creation timed out")]
1193 Timeout,
1194
1195 #[error("Invalid configuration for {attribute}: {reason}")]
1197 InvalidConfig {
1198 attribute: &'static str,
1200 reason: String,
1202 },
1203
1204 #[error("The provided credential is not valid for the provided proof request")]
1206 InvalidCredentialForProofRequest,
1207
1208 #[error("Registration error ({error_code}): {error_message}")]
1212 RegistrationError {
1213 error_code: String,
1215 error_message: String,
1217 },
1218
1219 #[error(transparent)]
1221 ProofError(#[from] ProofError),
1222
1223 #[error(
1225 "Invalid indexer authenticator pubkey slot {slot_index}; max supported slot is {max_supported_slot}"
1226 )]
1227 InvalidIndexerPubkeySlot {
1228 slot_index: usize,
1230 max_supported_slot: usize,
1232 },
1233
1234 #[error("Proof materials not loaded. Call `with_proof_materials` before generating proofs.")]
1236 ProofMaterialsNotLoaded,
1237
1238 #[error("the expected session id and the generated session id do not match")]
1243 SessionIdMismatch,
1244
1245 #[error("invalid session id")]
1247 InvalidSessionId,
1248
1249 #[error("{0}")]
1251 Generic(String),
1252}
1253
1254#[derive(Debug)]
1255enum PollResult {
1256 Retryable,
1257 TerminalError(AuthenticatorError),
1258}
1259
1260#[cfg(test)]
1261mod tests {
1262 use super::*;
1263 use alloy::primitives::{U256, address};
1264 use world_id_primitives::authenticator::MAX_AUTHENTICATOR_KEYS;
1265
1266 fn test_pubkey(seed_byte: u8) -> EdDSAPublicKey {
1267 Signer::from_seed_bytes(&[seed_byte; 32])
1268 .unwrap()
1269 .offchain_signer_pubkey()
1270 }
1271
1272 fn encoded_test_pubkey(seed_byte: u8) -> U256 {
1273 test_pubkey(seed_byte).to_ethereum_representation().unwrap()
1274 }
1275
1276 #[test]
1277 fn test_insert_or_reuse_authenticator_key_reuses_empty_slot() {
1278 let mut key_set =
1279 AuthenticatorPublicKeySet::new(vec![test_pubkey(1), test_pubkey(2), test_pubkey(4)])
1280 .unwrap();
1281 key_set[1] = None;
1282 let new_key = test_pubkey(3);
1283
1284 let index =
1285 Authenticator::insert_or_reuse_authenticator_key(&mut key_set, new_key).unwrap();
1286
1287 assert_eq!(index, 1);
1288 assert_eq!(key_set.len(), 3);
1289 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(3).pk);
1290 }
1291
1292 #[test]
1293 fn test_insert_or_reuse_authenticator_key_appends_when_no_empty_slot() {
1294 let mut key_set = AuthenticatorPublicKeySet::new(vec![test_pubkey(1)]).unwrap();
1295 let new_key = test_pubkey(2);
1296
1297 let index =
1298 Authenticator::insert_or_reuse_authenticator_key(&mut key_set, new_key).unwrap();
1299
1300 assert_eq!(index, 1);
1301 assert_eq!(key_set.len(), 2);
1302 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(2).pk);
1303 }
1304
1305 #[test]
1306 fn test_decode_indexer_pubkeys_trims_trailing_empty_slots() {
1307 let mut encoded_pubkeys = vec![Some(encoded_test_pubkey(1)), Some(encoded_test_pubkey(2))];
1308 encoded_pubkeys.extend(vec![None; MAX_AUTHENTICATOR_KEYS + 5]);
1309
1310 let key_set = Authenticator::decode_indexer_pubkeys(encoded_pubkeys).unwrap();
1311
1312 assert_eq!(key_set.len(), 2);
1313 assert_eq!(key_set[0].as_ref().unwrap().pk, test_pubkey(1).pk);
1314 assert_eq!(key_set[1].as_ref().unwrap().pk, test_pubkey(2).pk);
1315 }
1316
1317 #[test]
1318 fn test_decode_indexer_pubkeys_rejects_used_slot_beyond_max() {
1319 let mut encoded_pubkeys = vec![None; MAX_AUTHENTICATOR_KEYS + 1];
1320 encoded_pubkeys[MAX_AUTHENTICATOR_KEYS] = Some(encoded_test_pubkey(1));
1321
1322 let error = Authenticator::decode_indexer_pubkeys(encoded_pubkeys).unwrap_err();
1323 assert!(matches!(
1324 error,
1325 AuthenticatorError::InvalidIndexerPubkeySlot {
1326 slot_index,
1327 max_supported_slot
1328 } if slot_index == MAX_AUTHENTICATOR_KEYS && max_supported_slot == MAX_AUTHENTICATOR_KEYS - 1
1329 ));
1330 }
1331
1332 #[tokio::test]
1335 async fn test_get_packed_account_data_from_indexer() {
1336 let mut server = mockito::Server::new_async().await;
1337 let indexer_url = server.url();
1338
1339 let test_address = address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0");
1340 let expected_packed_index = U256::from(42);
1341
1342 let mock = server
1343 .mock("POST", "/packed-account")
1344 .match_header("content-type", "application/json")
1345 .match_body(mockito::Matcher::JsonString(
1346 serde_json::json!({
1347 "authenticator_address": test_address
1348 })
1349 .to_string(),
1350 ))
1351 .with_status(200)
1352 .with_header("content-type", "application/json")
1353 .with_body(
1354 serde_json::json!({
1355 "packed_account_data": format!("{:#x}", expected_packed_index)
1356 })
1357 .to_string(),
1358 )
1359 .create_async()
1360 .await;
1361
1362 let config = Config::new(
1363 None,
1364 1,
1365 address!("0x0000000000000000000000000000000000000001"),
1366 indexer_url,
1367 "http://gateway.example.com".to_string(),
1368 Vec::new(),
1369 2,
1370 )
1371 .unwrap();
1372
1373 let http_client = reqwest::Client::new();
1374
1375 let result = Authenticator::get_packed_account_data(
1376 test_address,
1377 None, &config,
1379 &http_client,
1380 )
1381 .await
1382 .unwrap();
1383
1384 assert_eq!(result, expected_packed_index);
1385 mock.assert_async().await;
1386 drop(server);
1387 }
1388
1389 #[tokio::test]
1390 async fn test_get_packed_account_data_from_indexer_error() {
1391 let mut server = mockito::Server::new_async().await;
1392 let indexer_url = server.url();
1393
1394 let test_address = address!("0x0000000000000000000000000000000000000099");
1395
1396 let mock = server
1397 .mock("POST", "/packed-account")
1398 .with_status(400)
1399 .with_header("content-type", "application/json")
1400 .with_body(
1401 serde_json::json!({
1402 "code": "account_does_not_exist",
1403 "message": "There is no account for this authenticator address"
1404 })
1405 .to_string(),
1406 )
1407 .create_async()
1408 .await;
1409
1410 let config = Config::new(
1411 None,
1412 1,
1413 address!("0x0000000000000000000000000000000000000001"),
1414 indexer_url,
1415 "http://gateway.example.com".to_string(),
1416 Vec::new(),
1417 2,
1418 )
1419 .unwrap();
1420
1421 let http_client = reqwest::Client::new();
1422
1423 let result =
1424 Authenticator::get_packed_account_data(test_address, None, &config, &http_client).await;
1425
1426 assert!(matches!(
1427 result,
1428 Err(AuthenticatorError::AccountDoesNotExist)
1429 ));
1430 mock.assert_async().await;
1431 drop(server);
1432 }
1433
1434 #[tokio::test]
1435 #[cfg(not(target_arch = "wasm32"))]
1436 async fn test_signing_nonce_from_indexer() {
1437 let mut server = mockito::Server::new_async().await;
1438 let indexer_url = server.url();
1439
1440 let leaf_index = U256::from(1);
1441 let expected_nonce = U256::from(5);
1442
1443 let mock = server
1444 .mock("POST", "/signature-nonce")
1445 .match_header("content-type", "application/json")
1446 .match_body(mockito::Matcher::JsonString(
1447 serde_json::json!({
1448 "leaf_index": format!("{:#x}", leaf_index)
1449 })
1450 .to_string(),
1451 ))
1452 .with_status(200)
1453 .with_header("content-type", "application/json")
1454 .with_body(
1455 serde_json::json!({
1456 "signature_nonce": format!("{:#x}", expected_nonce)
1457 })
1458 .to_string(),
1459 )
1460 .create_async()
1461 .await;
1462
1463 let config = Config::new(
1464 None,
1465 1,
1466 address!("0x0000000000000000000000000000000000000001"),
1467 indexer_url,
1468 "http://gateway.example.com".to_string(),
1469 Vec::new(),
1470 2,
1471 )
1472 .unwrap();
1473
1474 let authenticator = Authenticator {
1475 config,
1476 packed_account_data: leaf_index, signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1478 registry: None, http_client: reqwest::Client::new(),
1480 ws_connector: Connector::Plain,
1481 query_material: None,
1482 nullifier_material: None,
1483 };
1484
1485 let nonce = authenticator.signing_nonce().await.unwrap();
1486
1487 assert_eq!(nonce, expected_nonce);
1488 mock.assert_async().await;
1489 drop(server);
1490 }
1491
1492 #[test]
1493 fn test_danger_sign_challenge_returns_valid_signature() {
1494 let authenticator = Authenticator {
1495 config: Config::new(
1496 None,
1497 1,
1498 address!("0x0000000000000000000000000000000000000001"),
1499 "http://indexer.example.com".to_string(),
1500 "http://gateway.example.com".to_string(),
1501 Vec::new(),
1502 2,
1503 )
1504 .unwrap(),
1505 packed_account_data: U256::from(1),
1506 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1507 registry: None,
1508 http_client: reqwest::Client::new(),
1509 ws_connector: Connector::Plain,
1510 query_material: None,
1511 nullifier_material: None,
1512 };
1513
1514 let challenge = b"test challenge";
1515 let signature = authenticator.danger_sign_challenge(challenge).unwrap();
1516
1517 let recovered = signature
1518 .recover_address_from_msg(challenge)
1519 .expect("should recover address");
1520 assert_eq!(recovered, authenticator.onchain_address());
1521 }
1522
1523 #[test]
1524 fn test_danger_sign_challenge_different_challenges_different_signatures() {
1525 let authenticator = Authenticator {
1526 config: Config::new(
1527 None,
1528 1,
1529 address!("0x0000000000000000000000000000000000000001"),
1530 "http://indexer.example.com".to_string(),
1531 "http://gateway.example.com".to_string(),
1532 Vec::new(),
1533 2,
1534 )
1535 .unwrap(),
1536 packed_account_data: U256::from(1),
1537 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1538 registry: None,
1539 http_client: reqwest::Client::new(),
1540 ws_connector: Connector::Plain,
1541 query_material: None,
1542 nullifier_material: None,
1543 };
1544
1545 let sig_a = authenticator.danger_sign_challenge(b"challenge A").unwrap();
1546 let sig_b = authenticator.danger_sign_challenge(b"challenge B").unwrap();
1547 assert_ne!(sig_a, sig_b);
1548 }
1549
1550 #[test]
1551 fn test_danger_sign_challenge_deterministic() {
1552 let authenticator = Authenticator {
1553 config: Config::new(
1554 None,
1555 1,
1556 address!("0x0000000000000000000000000000000000000001"),
1557 "http://indexer.example.com".to_string(),
1558 "http://gateway.example.com".to_string(),
1559 Vec::new(),
1560 2,
1561 )
1562 .unwrap(),
1563 packed_account_data: U256::from(1),
1564 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1565 registry: None,
1566 http_client: reqwest::Client::new(),
1567 ws_connector: Connector::Plain,
1568 query_material: None,
1569 nullifier_material: None,
1570 };
1571
1572 let challenge = b"deterministic test";
1573 let sig1 = authenticator.danger_sign_challenge(challenge).unwrap();
1574 let sig2 = authenticator.danger_sign_challenge(challenge).unwrap();
1575 assert_eq!(sig1, sig2);
1576 }
1577
1578 #[tokio::test]
1579 #[cfg(not(target_arch = "wasm32"))]
1580 async fn test_signing_nonce_from_indexer_error() {
1581 let mut server = mockito::Server::new_async().await;
1582 let indexer_url = server.url();
1583
1584 let mock = server
1585 .mock("POST", "/signature-nonce")
1586 .with_status(400)
1587 .with_header("content-type", "application/json")
1588 .with_body(
1589 serde_json::json!({
1590 "code": "invalid_leaf_index",
1591 "message": "Account index cannot be zero"
1592 })
1593 .to_string(),
1594 )
1595 .create_async()
1596 .await;
1597
1598 let config = Config::new(
1599 None,
1600 1,
1601 address!("0x0000000000000000000000000000000000000001"),
1602 indexer_url,
1603 "http://gateway.example.com".to_string(),
1604 Vec::new(),
1605 2,
1606 )
1607 .unwrap();
1608
1609 let authenticator = Authenticator {
1610 config,
1611 packed_account_data: U256::ZERO,
1612 signer: Signer::from_seed_bytes(&[1u8; 32]).unwrap(),
1613 registry: None,
1614 http_client: reqwest::Client::new(),
1615 ws_connector: Connector::Plain,
1616 query_material: None,
1617 nullifier_material: None,
1618 };
1619
1620 let result = authenticator.signing_nonce().await;
1621
1622 assert!(matches!(
1623 result,
1624 Err(AuthenticatorError::IndexerError { .. })
1625 ));
1626 mock.assert_async().await;
1627 drop(server);
1628 }
1629}