Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 46 additions & 16 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.0", default-features = false }

# internal
walletkit-core = { version = "0.11.0", path = "walletkit-core", default-features = false }
Expand Down
164 changes: 162 additions & 2 deletions walletkit-core/src/authenticator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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<EdDSAPublicKey, WalletKitError> {
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 {
Expand Down Expand Up @@ -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<u8>,
new_authenticator_address: String,
) -> Result<String, WalletKitError> {
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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm I think this could cause a broken state, and as I think more about it, it may not be a problem with walletkit but with the protocol crate. inserting/updating/removing an authenticator alters the packed_account_data. this is saved in the CoreAuthenticator so certain subsequent operations will fail with hard to debug bugs

.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<u8>,
index: u32,
) -> Result<String, WalletKitError> {
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<String, WalletKitError> {
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<RegistrationStatus, WalletKitError> {
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"))]
Expand Down Expand Up @@ -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<u8> {
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::{
Expand Down
10 changes: 3 additions & 7 deletions walletkit-core/src/credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}

Expand Down
3 changes: 1 addition & 2 deletions walletkit-core/tests/proof_generation_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading