diff --git a/Cargo.lock b/Cargo.lock index f11b572ac..a637b1157 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5220,17 +5220,26 @@ name = "taceo-oprf" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be862ba49094098f945f1375f704a2c47b4e267a6e564462a43ad142b4b1469e" +dependencies = [ + "taceo-oprf-types 0.10.1", +] + +[[package]] +name = "taceo-oprf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f2d8a89181967875c3e130dca3d5e906e720b30568b82c68653c3048b1bb02" dependencies = [ "taceo-oprf-client", "taceo-oprf-core", - "taceo-oprf-types", + "taceo-oprf-types 0.11.0", ] [[package]] name = "taceo-oprf-client" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "696c4b03e8b570d1d0486f7bc2273b85951b316d7f720fc2f95e79f2b053f649" +checksum = "b1624edfd12063146c6f3614c659cb178a8ae82bfac72b751a72133136000866" dependencies = [ "ark-ec", "ciborium", @@ -5241,7 +5250,7 @@ dependencies = [ "serde", "taceo-ark-babyjubjub", "taceo-oprf-core", - "taceo-oprf-types", + "taceo-oprf-types 0.11.0", "taceo-poseidon2", "thiserror 2.0.18", "tokio", @@ -5293,6 +5302,27 @@ dependencies = [ "uuid", ] +[[package]] +name = "taceo-oprf-types" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b92ebe8693dfb75f2f1e7861785792747495f8f2be058ff07698f16046cb5fad" +dependencies = [ + "alloy", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "async-trait", + "eyre", + "http", + "serde", + "taceo-ark-babyjubjub", + "taceo-ark-serde-compat", + "taceo-circom-types", + "taceo-groth16-sol", + "taceo-oprf-core", + "uuid", +] + [[package]] name = "taceo-poseidon2" version = "0.2.1" @@ -6079,7 +6109,7 @@ dependencies = [ "sha2", "strum", "subtle", - "taceo-oprf", + "taceo-oprf 0.8.0", "thiserror 2.0.18", "tokio", "tokio-test", @@ -6688,9 +6718,9 @@ dependencies = [ [[package]] name = "world-id-authenticator" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07d061f3e1150294b0eaab3c3a8311343c144995097c6f08cd7babbe8610faca" +checksum = "f1713b3c70d6f2822f8ec0c12e313a74616fce2aa86c5cb54a991d13be5fe625" dependencies = [ "alloy", "anyhow", @@ -6707,7 +6737,7 @@ dependencies = [ "taceo-ark-babyjubjub", "taceo-eddsa-babyjubjub", "taceo-groth16-material", - "taceo-oprf", + "taceo-oprf 0.10.0", "taceo-poseidon2", "thiserror 2.0.18", "tokio", @@ -6718,9 +6748,9 @@ dependencies = [ [[package]] name = "world-id-core" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88fbe3660167654a014924faeeec87d7aebb9ff13a43dbb51958045f0aa3ed6" +checksum = "c0f0b7dab893d2588e13b3d3a0c6259467c7079b7cbe4a41db5a6bf4a04f46be" dependencies = [ "taceo-eddsa-babyjubjub", "world-id-authenticator", @@ -6730,9 +6760,9 @@ dependencies = [ [[package]] name = "world-id-primitives" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "022157c68f1c49bb3d7256b9199174d6de6cc9d4b07c5ea757f2f6b35597b4a2" +checksum = "8a38588112438f94966aef95559b48a0c3d4ae627c0ebc680ae4739ca0538c29" dependencies = [ "alloy", "alloy-primitives", @@ -6758,7 +6788,7 @@ dependencies = [ "taceo-eddsa-babyjubjub", "taceo-groth16-material", "taceo-groth16-sol", - "taceo-oprf", + "taceo-oprf 0.10.0", "taceo-poseidon2", "thiserror 2.0.18", "url", @@ -6767,9 +6797,9 @@ dependencies = [ [[package]] name = "world-id-proof" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091e4902d84a827900db7f030fc6bb3f9705e0abe6d20aca943148727bb625b8" +checksum = "c0685fc6fcb0fef10a7caa80f7d4ac09727ee582934c2da39bdd82a423ab7abe" dependencies = [ "ark-bn254", "ark-ec", @@ -6785,7 +6815,7 @@ dependencies = [ "taceo-circom-types", "taceo-eddsa-babyjubjub", "taceo-groth16-material", - "taceo-oprf", + "taceo-oprf 0.10.0", "taceo-poseidon2", "tar", "thiserror 2.0.18", diff --git a/Cargo.toml b/Cargo.toml index ea2552f46..71d9b0f1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ alloy-core = { version = "1", default-features = false, features = [ ] } alloy-primitives = { version = "1", default-features = false } uniffi = { version = "0.31", features = ["tokio"] } -world-id-core = { version = "0.6", default-features = false } +world-id-core = { version = "0.7", default-features = false } # internal walletkit-core = { version = "0.11.0", path = "walletkit-core", default-features = false } diff --git a/walletkit-core/src/authenticator/mod.rs b/walletkit-core/src/authenticator/mod.rs index 90b03f47e..34d33ffb8 100644 --- a/walletkit-core/src/authenticator/mod.rs +++ b/walletkit-core/src/authenticator/mod.rs @@ -10,7 +10,7 @@ use std::sync::Arc; use world_id_core::{ api_types::{GatewayErrorCode, GatewayRequestState}, primitives::Config, - Authenticator as CoreAuthenticator, Credential as CoreCredential, + Authenticator as CoreAuthenticator, Credential as CoreCredential, EdDSAPublicKey, InitializingAuthenticator as CoreInitializingAuthenticator, }; @@ -121,6 +121,27 @@ fn load_nullifier_material_from_cache( )) } +/// Parses a compressed off-chain `EdDSA` public key from a 32-byte little-endian byte slice. +/// +/// # Errors +/// Returns an error if the bytes are not exactly 32 bytes or cannot be decompressed. +fn parse_compressed_pubkey(bytes: &[u8]) -> Result { + let compressed: [u8; 32] = + bytes.try_into().map_err(|_| WalletKitError::InvalidInput { + attribute: "new_authenticator_pubkey_bytes".to_string(), + reason: format!( + "Expected 32 bytes for compressed public key, got {}", + bytes.len() + ), + })?; + EdDSAPublicKey::from_compressed_bytes(compressed).map_err(|e| { + WalletKitError::InvalidInput { + attribute: "new_authenticator_pubkey_bytes".to_string(), + reason: format!("Invalid compressed public key: {e}"), + } + }) +} + /// The Authenticator is the main component with which users interact with the World ID Protocol. #[derive(Debug, uniffi::Object)] pub struct Authenticator { @@ -220,6 +241,92 @@ impl Authenticator { let signature = self.inner.danger_sign_challenge(challenge)?; Ok(signature.as_bytes().to_vec()) } + + /// Inserts a new authenticator to the account. + /// + /// # Errors + /// Returns an error if the pubkey bytes, address, or network call fails. + pub async fn insert_authenticator( + &self, + new_authenticator_pubkey_bytes: Vec, + new_authenticator_address: String, + ) -> Result { + let new_address = Address::parse_from_ffi( + &new_authenticator_address, + "new_authenticator_address", + )?; + let new_pubkey = parse_compressed_pubkey(&new_authenticator_pubkey_bytes)?; + Ok(self + .inner + .insert_authenticator(new_pubkey, new_address) + .await? + .to_string()) + } + + /// Updates an existing authenticator at the given slot index. + /// + /// # Errors + /// Returns an error if the address, pubkey bytes, index, or network call fails. + pub async fn update_authenticator( + &self, + old_authenticator_address: String, + new_authenticator_address: String, + new_authenticator_pubkey_bytes: Vec, + index: u32, + ) -> Result { + let old_address = Address::parse_from_ffi( + &old_authenticator_address, + "old_authenticator_address", + )?; + let new_address = Address::parse_from_ffi( + &new_authenticator_address, + "new_authenticator_address", + )?; + let new_pubkey = parse_compressed_pubkey(&new_authenticator_pubkey_bytes)?; + Ok(self + .inner + .update_authenticator(old_address, new_address, new_pubkey, index) + .await? + .to_string()) + } + + /// Removes an authenticator from the account at the given slot index. + /// + /// # Errors + /// Returns an error if the address or network call fails. + pub async fn remove_authenticator( + &self, + authenticator_address: String, + index: u32, + ) -> Result { + let auth_address = + Address::parse_from_ffi(&authenticator_address, "authenticator_address")?; + Ok(self + .inner + .remove_authenticator(auth_address, index) + .await? + .to_string()) + } + + /// Polls the status of a gateway operation (insert, update, or remove authenticator). + /// + /// Use the request ID returned by [`insert_authenticator`](Self::insert_authenticator), + /// [`update_authenticator`](Self::update_authenticator), or + /// [`remove_authenticator`](Self::remove_authenticator) to track the operation. + /// + /// # Errors + /// Will error if the network request fails or the gateway returns an error. + pub async fn poll_operation_status( + &self, + request_id: String, + ) -> Result { + use world_id_core::api_types::GatewayRequestId; + + let raw = request_id.strip_prefix("gw_").unwrap_or(&request_id); + let id = GatewayRequestId::new(raw); + let status = self.inner.poll_status(&id).await?; + Ok(status.into()) + } } #[cfg(not(feature = "storage"))] @@ -534,8 +641,61 @@ impl InitializingAuthenticator { } } -#[cfg(all(test, feature = "storage"))] +#[cfg(test)] mod tests { + use super::*; + use world_id_core::OnchainKeyRepresentable; + + fn test_pubkey(seed_byte: u8) -> EdDSAPublicKey { + let signer = + world_id_core::primitives::Signer::from_seed_bytes(&[seed_byte; 32]) + .unwrap(); + signer.offchain_signer_pubkey() + } + + fn compressed_pubkey_bytes(seed_byte: u8) -> Vec { + let pk = test_pubkey(seed_byte); + let u256 = pk.to_ethereum_representation().unwrap(); + u256.to_le_bytes_vec() + } + + // ── Compressed pubkey parsing ── + + #[test] + fn test_parse_compressed_pubkey_valid() { + let bytes = compressed_pubkey_bytes(1); + assert_eq!(bytes.len(), 32); + let result = parse_compressed_pubkey(&bytes); + assert!(result.is_ok()); + } + + #[test] + fn test_parse_compressed_pubkey_wrong_length() { + let short = vec![0u8; 16]; + let result = parse_compressed_pubkey(&short); + assert!(matches!( + result, + Err(WalletKitError::InvalidInput { attribute, .. }) + if attribute == "new_authenticator_pubkey_bytes" + )); + } + + #[test] + fn test_parse_compressed_pubkey_roundtrip() { + let original = test_pubkey(42); + let bytes = { + let u256 = original.to_ethereum_representation().unwrap(); + u256.to_le_bytes_vec() + }; + let recovered = parse_compressed_pubkey(&bytes).unwrap(); + assert_eq!(original.pk, recovered.pk); + } +} + +// ── Storage-dependent tests ── + +#[cfg(all(test, feature = "storage"))] +mod storage_tests { use super::*; use crate::storage::cache_embedded_groth16_material; use crate::storage::tests_utils::{ diff --git a/walletkit-core/src/credential.rs b/walletkit-core/src/credential.rs index e478c7e48..4b72c297b 100644 --- a/walletkit-core/src/credential.rs +++ b/walletkit-core/src/credential.rs @@ -52,14 +52,10 @@ impl Credential { self.0.expires_at } - /// Returns the credential's `associated_data_hash` field element. - /// - /// This is a Poseidon2 commitment to the associated data (e.g. a PCP archive) - /// set by the issuer at issuance time. Returns `FieldElement::ZERO` if no - /// associated data was committed to. + /// Returns the associated-data commitment field element for this credential. #[must_use] - pub fn associated_data_hash(&self) -> FieldElement { - self.0.associated_data_hash.into() + pub fn associated_data_commitment(&self) -> FieldElement { + self.0.associated_data_commitment.into() } } diff --git a/walletkit-core/tests/proof_generation_integration.rs b/walletkit-core/tests/proof_generation_integration.rs index def118da1..1d9e3eb73 100644 --- a/walletkit-core/tests/proof_generation_integration.rs +++ b/walletkit-core/tests/proof_generation_integration.rs @@ -33,10 +33,9 @@ use alloy::signers::{local::PrivateKeySigner, SignerSync}; use alloy::sol; use alloy_primitives::U160; use eyre::{Context as _, Result}; -use taceo_oprf::types::OprfKeyId; use walletkit_core::storage::cache_embedded_groth16_material; use walletkit_core::{defaults::DefaultConfig, Authenticator, Environment}; -use world_id_core::primitives::{rp::RpId, FieldElement, Nullifier}; +use world_id_core::primitives::{rp::RpId, FieldElement, Nullifier, OprfKeyId}; use world_id_core::{ requests::{ProofRequest, RequestItem, RequestVersion}, Authenticator as CoreAuthenticator, EdDSAPrivateKey,