Skip to content
Merged
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
13 changes: 12 additions & 1 deletion walletkit-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ mod auth;
mod credential;
mod proof;
mod recovery_agent;
mod recovery_binding;
mod wallet;

use std::path::PathBuf;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};

use clap::{Parser, Subcommand};
use eyre::WrapErr as _;

use walletkit_core::storage::{cache_embedded_groth16_material, CredentialStore};
use walletkit_core::Authenticator;

Expand Down Expand Up @@ -104,6 +105,12 @@ pub enum Command {
#[command(subcommand)]
action: recovery_agent::RecoveryAgentCommand,
},
/// Recovery binding management.
#[command(name = "recovery-binding")]
RecoveryBinding {
#[command(subcommand)]
action: recovery_binding::RecoveryBindingCommand,
},
}

/// Resolves the wallet root directory, defaulting to `~/.walletkit`.
Expand Down Expand Up @@ -240,5 +247,9 @@ pub async fn run(cli: Cli) -> eyre::Result<()> {
Command::Credential { action } => credential::run(&cli, action).await,
Command::Proof { action } => proof::run(&cli, action).await,
Command::RecoveryAgent { action } => recovery_agent::run(&cli, action).await,
Command::RecoveryBinding { action } => {
let environment = resolve_environment(&cli)?;
recovery_binding::run(&cli, action, &environment).await
}
}
}
47 changes: 47 additions & 0 deletions walletkit-cli/src/commands/recovery_binding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//! `walletkit recovery-agent` subcommands — recovery agent management.

use clap::Subcommand;

use super::{init_authenticator, Cli};
use walletkit_core::issuers::RecoveryBindingManager;
use walletkit_core::Environment;

#[derive(Subcommand)]
pub enum RecoveryBindingCommand {
/// Register bindings for a recovery agent.
RegisterBindings {
leaf_index: u64,
/// Checksummed hex address of the recovery agent (e.g. "0x1234…").
sub: String,
},
UnregisterBindings {
leaf_index: u64,
sub: String,
},
}

pub async fn run(
cli: &Cli,
action: &RecoveryBindingCommand,
environment: &Environment,
) -> eyre::Result<()> {
let (authenticator, _store) = init_authenticator(cli).await?;

match action {
RecoveryBindingCommand::RegisterBindings { leaf_index, sub } => {
let recovery_binding_manager =
RecoveryBindingManager::new(environment).unwrap();
recovery_binding_manager
.bind_recovery_agent(&authenticator, leaf_index.clone(), sub.clone())
.await?;
}
RecoveryBindingCommand::UnregisterBindings { leaf_index, sub } => {
let recovery_binding_manager =
RecoveryBindingManager::new(environment).unwrap();
recovery_binding_manager
.unbind_recovery_agent(&authenticator, *leaf_index, sub.clone())
.await?;
}
}
Ok(())
}
34 changes: 34 additions & 0 deletions walletkit-core/src/issuers/pop_backend_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ pub struct ManageRecoveryBindingRequest {
/// The authenticator's leaf index in the World ID Merkle tree.
#[serde(rename = "leafIndex")]
pub leaf_index: u64,
/// The signature of the recovery agent update.
pub signature: String,
/// The nonce of the recovery agent update.
pub nonce: String,
/// The checksummed hex address of the recovery agent (e.g. `"0x1234…"`).
#[serde(rename = "recoveryAgent")]
pub recovery_agent: String,
}

#[derive(Deserialize)]
Expand Down Expand Up @@ -216,16 +223,23 @@ mod tests {
let mut server = mockito::Server::new_async().await;
let url = server.url();

let recovery_agent = "0x1234567890abcdef".to_string();
let request = ManageRecoveryBindingRequest {
sub: "test-sub-123".to_string(),
leaf_index: 42,
signature: "0x1234567890abcdef".to_string(),
nonce: "0x1234567890abcdef1".to_string(),
recovery_agent: recovery_agent.clone(),
};

let mock = server
.mock("POST", "/api/v1/recovery-binding")
.match_body(mockito::Matcher::Json(serde_json::json!({
"sub": "test-sub-123",
"leafIndex": 42,
"signature": "0x1234567890abcdef",
"nonce": "0x1234567890abcdef1",
"recoveryAgent": "0x1234567890abcdef",
})))
.match_header("X-Auth-Signature", "security_token")
.match_header("X-Auth-Challenge", "challenge")
Expand Down Expand Up @@ -254,16 +268,23 @@ mod tests {
let mut server = mockito::Server::new_async().await;
let url = server.url();

let recovery_agent = "0x1234567890abcdef".to_string();
let request = ManageRecoveryBindingRequest {
sub: "test-sub-123".to_string(),
leaf_index: 42,
signature: "0x1234567890abcdef".to_string(),
nonce: "0x1234567890abcdef1".to_string(),
recovery_agent: recovery_agent.clone(),
};

let mock = server
.mock("POST", "/api/v1/recovery-binding")
.match_body(mockito::Matcher::Json(serde_json::json!({
"sub": "test-sub-123",
"leafIndex": 42,
"signature": "0x1234567890abcdef",
"nonce": "0x1234567890abcdef1",
"recoveryAgent": "0x1234567890abcdef",
})))
.match_header("X-Auth-Signature", "security_token")
.match_header("X-Auth-Challenge", "challenge")
Expand Down Expand Up @@ -300,13 +321,19 @@ mod tests {
let request = ManageRecoveryBindingRequest {
sub: "test-sub-123".to_string(),
leaf_index: 42,
signature: "0x1234567890abcdef".to_string(),
nonce: "0x1234567890abcdef1".to_string(),
recovery_agent: "0x0000000000000000000000000000000000000000".to_string(),
};

let mock = server
.mock("DELETE", "/api/v1/recovery-binding")
.match_body(mockito::Matcher::Json(serde_json::json!({
"sub": "test-sub-123",
"leafIndex": 42,
"signature": "0x1234567890abcdef",
"nonce": "0x1234567890abcdef1",
"recoveryAgent": "0x0000000000000000000000000000000000000000",
})))
.match_header("X-Auth-Signature", "security_token")
.match_header("X-Auth-Challenge", "challenge")
Expand Down Expand Up @@ -335,16 +362,23 @@ mod tests {
let mut server = mockito::Server::new_async().await;
let url = server.url();

let recovery_agent = "0x0000000000000000000000000000000000000000".to_string();
let request = ManageRecoveryBindingRequest {
sub: "test-sub-123".to_string(),
leaf_index: 42,
signature: "0x1234567890abcdef".to_string(),
nonce: "0x1234567890abcdef1".to_string(),
recovery_agent: recovery_agent.clone(),
};

let mock = server
.mock("DELETE", "/api/v1/recovery-binding")
.match_body(mockito::Matcher::Json(serde_json::json!({
"sub": "test-sub-123",
"leafIndex": 42,
"signature": "0x1234567890abcdef",
"nonce": "0x1234567890abcdef1",
"recoveryAgent": "0x0000000000000000000000000000000000000000",
})))
.match_header("X-Auth-Signature", "security_token")
.match_header("X-Auth-Challenge", "challenge")
Expand Down
65 changes: 54 additions & 11 deletions walletkit-core/src/issuers/recovery_bindings_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ use crate::issuers::pop_backend_client::ManageRecoveryBindingRequest;
use crate::issuers::PopBackendClient;
use crate::Environment;
use alloy_primitives::keccak256;

use std::string::String;
const ZERO_ADDRESS: &str = "0x0000000000000000000000000000000000000000";
/// Client for registering and unregistering recovery agents with the `PoP` backend.
///
/// Each instance is bound to a specific [`Environment`] (staging or production),
/// which determines the backend URL used for all requests.
#[derive(uniffi::Object)]
pub struct RecoveryBindingManager {
pop_backend_client: PopBackendClient,
recovery_agent_address: String,
}

#[uniffi::export]
Expand All @@ -42,7 +44,8 @@ impl RecoveryBindingManager {
Environment::Production => "https://app.orb.worldcoin.org",
}
.to_string();
Self::new_with_base_url(base_url.as_str())
let recovery_agent_address = environment.poh_recovery_agent_address();
Self::new_with_base_url(base_url.as_str(), recovery_agent_address.as_str())
}

/// Creates a new `RecoveryBindingManager` for the specified base URL and user agent.
Expand All @@ -51,9 +54,15 @@ impl RecoveryBindingManager {
///
/// Returns an error if the HTTP client cannot be built.
#[uniffi::constructor]
pub fn new_with_base_url(base_url: &str) -> Result<Self, WalletKitError> {
pub fn new_with_base_url(
base_url: &str,
recovery_agent_address: &str,
) -> Result<Self, WalletKitError> {
let pop_backend_client = PopBackendClient::new(base_url.to_string());
Ok(Self { pop_backend_client })
Ok(Self {
pop_backend_client,
recovery_agent_address: recovery_agent_address.to_string(),
})
}
}

Expand All @@ -66,6 +75,7 @@ impl RecoveryBindingManager {
/// * `authenticator` — The authenticator whose signing key authorizes the request.
/// * `leaf_index` — The authenticator's leaf index in the World ID Merkle tree.
/// * `sub` — Hex-encoded subject identifier of the recovery agent to register.
/// * `new_recovery_agent` — The checksummed hex address of the new recovery agent (e.g. `"0x1234…"`).
///
/// # Errors
///
Expand All @@ -78,7 +88,18 @@ impl RecoveryBindingManager {
sub: String,
) -> Result<(), WalletKitError> {
let challenge = self.pop_backend_client.get_challenge().await?;
let request = ManageRecoveryBindingRequest { sub, leaf_index };
let sig_recovery_update = authenticator
.danger_sign_initiate_recovery_agent_update(
self.recovery_agent_address.clone(),
)
.await?;
let request = ManageRecoveryBindingRequest {
sub,
leaf_index,
signature: format!("0x{}", hex::encode(sig_recovery_update.signature)),
nonce: sig_recovery_update.nonce.to_string(),
recovery_agent: self.recovery_agent_address.clone(),
};
let security_token = Self::generate_recovery_agent_security_token(
authenticator,
&request,
Expand Down Expand Up @@ -109,7 +130,17 @@ impl RecoveryBindingManager {
leaf_index: u64,
sub: String,
) -> Result<(), WalletKitError> {
let request = ManageRecoveryBindingRequest { sub, leaf_index };
let recovery_agent = ZERO_ADDRESS.to_string();
let sig_recovery_update = authenticator
.danger_sign_initiate_recovery_agent_update(recovery_agent.clone())
.await?;
let request = ManageRecoveryBindingRequest {
sub,
leaf_index,
signature: format!("0x{}", hex::encode(sig_recovery_update.signature)),
nonce: sig_recovery_update.nonce.to_string(),
recovery_agent: recovery_agent.clone(),
};
let challenge = self.pop_backend_client.get_challenge().await?;
let security_token = Self::generate_recovery_agent_security_token(
authenticator,
Expand Down Expand Up @@ -224,6 +255,7 @@ mod tests {

// Mock the recovery binding registration endpoint
let url_path = "/api/v1/recovery-binding".to_string();
let recovery_agent = "0x1000000000000000000000000000000000000000".to_string();

let mock = pop_api_server
.mock("POST", url_path.as_str())
Expand All @@ -232,18 +264,22 @@ mod tests {
mockito::Matcher::Regex(".*".to_string()),
)
.match_header("X-Auth-Challenge", challenge.as_str())
.match_body(mockito::Matcher::Json(serde_json::json!({
.match_body(mockito::Matcher::PartialJson(serde_json::json!({
"sub": sub.as_str(),
"leafIndex": leaf_index,
"recoveryAgent": recovery_agent.as_str(),

})))
.with_status(201)
.with_body("{}")
.create_async()
.await;

let recovery_binding_manager =
RecoveryBindingManager::new_with_base_url(pop_api_server.url().as_str())
.unwrap();
let recovery_binding_manager = RecoveryBindingManager::new_with_base_url(
pop_api_server.url().as_str(),
recovery_agent.as_str(),
)
.unwrap();

let private_key =
"d1995ace62b15d907bfb351ffe3cac57a8a84089a1b034101d2d7c78da415d58";
Expand Down Expand Up @@ -285,10 +321,15 @@ mod tests {
.unwrap();
log::info!("message_bytes: {:?}", hex::encode(message_bytes.clone()));
assert_eq!(hex::encode(message_bytes.clone()), "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2000000000000002aabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890");

let signature = "0x01".to_string();
let nonce = "0x02".to_string();
let recovery_agent = "0x1000000000000000000000000000000000000000".to_string();
let request = ManageRecoveryBindingRequest {
sub: sub.clone(),
leaf_index,
signature: signature.clone(),
nonce: nonce.clone(),
recovery_agent: recovery_agent.clone(),
};
let (mock_eth_server, eth_mock) = create_mock_eth_server().await;
let rpc_url = mock_eth_server.url();
Expand Down Expand Up @@ -354,6 +395,8 @@ mod tests {
})
.to_string(),
)
.expect_at_least(1)
.expect_at_most(2)
.create_async()
.await;
(mock_eth_server, mock)
Expand Down
Loading