Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
}
}
}
53 changes: 53 additions & 0 deletions walletkit-cli/src/commands/recovery_binding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//! `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_agent_address = environment.poh_recovery_agent_address();
let recovery_binding_manager =
RecoveryBindingManager::new(environment).unwrap();
recovery_binding_manager
.bind_recovery_agent(
&authenticator,
*leaf_index,
sub.clone(),
recovery_agent_address.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
49 changes: 43 additions & 6 deletions walletkit-core/src/issuers/recovery_bindings_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ 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),
Expand Down Expand Up @@ -66,6 +67,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 @@ -76,9 +78,19 @@ impl RecoveryBindingManager {
authenticator: &Authenticator,
leaf_index: u64,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Leaf index can be take from the authenticator, but I also noticed that it's sometimes passed as a param. What is the recommendation here?

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.

imo we should take it from the authenticator. Can we also take sub?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

we'd need blinding factor I think, I couldn't find a method to get the sub in the Authenticator

sub: String,
recovery_agent_address: 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(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: recovery_agent_address.clone(),
};
let security_token = Self::generate_recovery_agent_security_token(
authenticator,
&request,
Expand Down Expand Up @@ -109,7 +121,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 +246,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,9 +255,11 @@ 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("{}")
Expand All @@ -254,7 +279,12 @@ mod tests {
create_test_authenticator(&private_key_bytes, rpc_url).await;

let result = recovery_binding_manager
.bind_recovery_agent(&authenticator, leaf_index, sub.clone())
.bind_recovery_agent(
&authenticator,
leaf_index,
sub.clone(),
recovery_agent.clone(),
)
.await;
assert!(
result.is_ok(),
Expand Down Expand Up @@ -285,10 +315,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 +389,8 @@ mod tests {
})
.to_string(),
)
.expect_at_least(1)
.expect_at_most(2)
.create_async()
.await;
(mock_eth_server, mock)
Expand Down
Loading