identity.rs
1 use std::path::PathBuf; 2 use std::sync::Arc; 3 4 use auths_core::config::{EnvironmentConfig, KeychainConfig}; 5 use auths_core::crypto::signer::encrypt_keypair; 6 use auths_core::signing::PrefilledPassphraseProvider; 7 use auths_core::storage::keychain::{ 8 IdentityDID, KeyAlias, KeyRole, KeyStorage, get_platform_keychain_with_config, 9 }; 10 use auths_id::identity::helpers::encode_seed_as_pkcs8; 11 use auths_id::identity::helpers::extract_seed_bytes; 12 use auths_id::identity::initialize::initialize_registry_identity; 13 use auths_id::storage::attestation::AttestationSource; 14 use auths_sdk::context::AuthsContext; 15 use auths_sdk::device::{link_device, revoke_device}; 16 use auths_sdk::types::DeviceLinkConfig; 17 use auths_storage::git::GitRegistryBackend; 18 use auths_storage::git::RegistryAttestationStorage; 19 use auths_storage::git::RegistryConfig; 20 use auths_storage::git::RegistryIdentityStorage; 21 use auths_verifier::clock::SystemClock; 22 use auths_verifier::core::Capability; 23 use auths_verifier::types::DeviceDID; 24 use pyo3::exceptions::PyRuntimeError; 25 use pyo3::prelude::*; 26 use ring::rand::SystemRandom; 27 use ring::signature::{Ed25519KeyPair, KeyPair}; 28 29 #[allow(clippy::disallowed_methods)] // Presentation boundary: env var read is intentional 30 pub(crate) fn resolve_passphrase(passphrase: Option<String>) -> String { 31 passphrase.unwrap_or_else(|| std::env::var("AUTHS_PASSPHRASE").unwrap_or_default()) 32 } 33 34 pub(crate) fn make_keychain_config(passphrase: &str) -> EnvironmentConfig { 35 EnvironmentConfig { 36 auths_home: None, 37 keychain: KeychainConfig { 38 backend: Some("file".to_string()), 39 file_path: None, 40 passphrase: Some(passphrase.to_string()), 41 }, 42 ssh_agent_socket: None, 43 } 44 } 45 46 /// Resolve a DID-or-alias string to a KeyAlias. 47 /// 48 /// If the input starts with "did:", look up the primary key alias 49 /// for that identity in the keychain. Otherwise treat it as a direct alias. 50 pub(crate) fn resolve_key_alias( 51 identity_ref: &str, 52 keychain: &(dyn KeyStorage + Send + Sync), 53 ) -> PyResult<KeyAlias> { 54 if identity_ref.starts_with("did:") { 55 let did = IdentityDID::new_unchecked(identity_ref.to_string()); 56 let aliases = keychain 57 .list_aliases_for_identity_with_role(&did, KeyRole::Primary) 58 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_KEY_NOT_FOUND] Key lookup failed: {e}")))?; 59 aliases 60 .into_iter() 61 .next() 62 .ok_or_else(|| { 63 PyRuntimeError::new_err(format!( 64 "[AUTHS_KEY_NOT_FOUND] No primary key found for identity '{identity_ref}'" 65 )) 66 }) 67 } else { 68 KeyAlias::new(identity_ref) 69 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_KEY_NOT_FOUND] Invalid key alias: {e}"))) 70 } 71 } 72 73 #[pyclass] 74 #[derive(Clone)] 75 pub struct DelegatedAgentBundle { 76 #[pyo3(get)] 77 pub agent_did: String, 78 #[pyo3(get)] 79 pub key_alias: String, 80 #[pyo3(get)] 81 pub attestation_json: String, 82 #[pyo3(get)] 83 pub public_key_hex: String, 84 #[pyo3(get)] 85 pub repo_path: Option<String>, 86 } 87 88 #[pymethods] 89 impl DelegatedAgentBundle { 90 fn __repr__(&self) -> String { 91 format!("DelegatedAgentBundle(agent_did='{}')", self.agent_did) 92 } 93 } 94 95 #[pyclass] 96 #[derive(Clone)] 97 pub struct AgentIdentityBundle { 98 #[pyo3(get)] 99 pub agent_did: String, 100 #[pyo3(get)] 101 pub key_alias: String, 102 #[pyo3(get)] 103 pub attestation_json: String, 104 #[pyo3(get)] 105 pub public_key_hex: String, 106 #[pyo3(get)] 107 pub repo_path: Option<String>, 108 } 109 110 #[pymethods] 111 impl AgentIdentityBundle { 112 fn __repr__(&self) -> String { 113 format!("AgentIdentityBundle(agent_did='{}')", self.agent_did) 114 } 115 } 116 117 /// Create a new identity in the registry. 118 /// 119 /// Args: 120 /// * `key_alias`: Alias for the identity key in the keychain. 121 /// * `repo_path`: Path to the auths repository. 122 /// * `passphrase`: Optional passphrase for the keychain (reads AUTHS_PASSPHRASE if None). 123 /// 124 /// Usage: 125 /// ```ignore 126 /// let (did, alias, pub_key) = create_identity(py, "my-identity", "~/.auths", None)?; 127 /// ``` 128 #[pyfunction] 129 pub fn create_identity( 130 py: Python<'_>, 131 key_alias: &str, 132 repo_path: &str, 133 passphrase: Option<String>, 134 ) -> PyResult<(String, String, String)> { 135 let passphrase_str = resolve_passphrase(passphrase); 136 let env_config = make_keychain_config(&passphrase_str); 137 let alias = KeyAlias::new(key_alias) 138 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_KEY_NOT_FOUND] Invalid key alias: {e}")))?; 139 let provider = PrefilledPassphraseProvider::new(&passphrase_str); 140 141 let repo = PathBuf::from(shellexpand::tilde(repo_path).as_ref()); 142 let config = RegistryConfig::single_tenant(&repo); 143 let backend = GitRegistryBackend::from_config_unchecked(config); 144 backend 145 .init_if_needed() 146 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_REGISTRY_ERROR] Failed to initialize registry: {e}")))?; 147 let backend = Arc::new(backend); 148 149 let keychain = get_platform_keychain_with_config(&env_config) 150 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_KEYCHAIN_ERROR] Keychain error: {e}")))?; 151 152 py.allow_threads(|| { 153 let (identity_did, result_alias) = 154 initialize_registry_identity(backend, &alias, &provider, keychain.as_ref(), None) 155 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_IDENTITY_ERROR] Identity creation failed: {e}")))?; 156 157 // Extract public key so callers can verify signatures immediately 158 let pub_bytes = auths_core::storage::keychain::extract_public_key_bytes( 159 keychain.as_ref(), 160 &result_alias, 161 &provider, 162 ) 163 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_CRYPTO_ERROR] Public key extraction failed: {e}")))?; 164 165 Ok((identity_did.to_string(), result_alias.to_string(), hex::encode(pub_bytes))) 166 }) 167 } 168 169 /// Create a standalone agent identity with its own KERI identity (did:keri:). 170 /// 171 /// Args: 172 /// * `agent_name`: Human-readable agent name. 173 /// * `capabilities`: Capabilities to grant. 174 /// * `repo_path`: Path to the auths repository. 175 /// * `passphrase`: Optional passphrase for the keychain. 176 /// 177 /// Usage: 178 /// ```ignore 179 /// let bundle = create_agent_identity(py, "ci-bot", vec!["sign".into()], "~/.auths", None)?; 180 /// ``` 181 #[pyfunction] 182 #[pyo3(signature = (agent_name, capabilities, repo_path, passphrase=None))] 183 pub fn create_agent_identity( 184 py: Python<'_>, 185 agent_name: &str, 186 capabilities: Vec<String>, 187 repo_path: &str, 188 passphrase: Option<String>, 189 ) -> PyResult<AgentIdentityBundle> { 190 let passphrase_str = resolve_passphrase(passphrase); 191 let env_config = make_keychain_config(&passphrase_str); 192 let alias = KeyAlias::new_unchecked(format!("{}-agent", agent_name)); 193 let provider = PrefilledPassphraseProvider::new(&passphrase_str); 194 195 let repo = PathBuf::from(shellexpand::tilde(repo_path).as_ref()); 196 let config = RegistryConfig::single_tenant(&repo); 197 let backend = GitRegistryBackend::from_config_unchecked(config); 198 backend 199 .init_if_needed() 200 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_REGISTRY_ERROR] Failed to initialize registry: {e}")))?; 201 let backend = Arc::new(backend); 202 203 let keychain = get_platform_keychain_with_config(&env_config) 204 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_KEYCHAIN_ERROR] Keychain error: {e}")))?; 205 206 // Validate capabilities 207 let _parsed_caps: Vec<Capability> = capabilities 208 .iter() 209 .map(|c| { 210 Capability::parse(c) 211 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_INVALID_INPUT] Invalid capability '{c}': {e}"))) 212 }) 213 .collect::<PyResult<Vec<_>>>()?; 214 215 let parsed_caps_for_att = _parsed_caps; 216 217 py.allow_threads(|| { 218 let (identity_did, result_alias) = 219 initialize_registry_identity(backend.clone(), &alias, &provider, keychain.as_ref(), None) 220 .map_err(|e| { 221 PyRuntimeError::new_err(format!("[AUTHS_IDENTITY_ERROR] Agent identity creation failed: {e}")) 222 })?; 223 224 // Extract public key 225 let pub_bytes = auths_core::storage::keychain::extract_public_key_bytes( 226 keychain.as_ref(), 227 &result_alias, 228 &provider, 229 ) 230 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_CRYPTO_ERROR] Public key extraction failed: {e}")))?; 231 232 // Build a self-attestation for the standalone agent 233 let attestation_json = { 234 let device_did = DeviceDID::from_ed25519( 235 pub_bytes.as_slice().try_into().map_err(|_| { 236 PyRuntimeError::new_err("[AUTHS_CRYPTO_ERROR] Invalid public key length") 237 })?, 238 ); 239 let att = serde_json::json!({ 240 "version": 1, 241 "rid": repo.file_name().unwrap_or_default().to_string_lossy(), 242 "issuer": identity_did.to_string(), 243 "subject": device_did.to_string(), 244 "device_public_key": hex::encode(&pub_bytes), 245 "capabilities": parsed_caps_for_att.iter().map(|c| c.as_str()).collect::<Vec<_>>(), 246 "timestamp": chrono::Utc::now().to_rfc3339(), 247 "note": format!("Agent: {}", alias), 248 }); 249 serde_json::to_string(&att) 250 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_SERIALIZATION_ERROR] Serialization failed: {e}")))? 251 }; 252 253 Ok(AgentIdentityBundle { 254 agent_did: identity_did.to_string(), 255 key_alias: result_alias.to_string(), 256 attestation_json, 257 public_key_hex: hex::encode(pub_bytes), 258 repo_path: Some(repo.to_string_lossy().to_string()), 259 }) 260 }) 261 } 262 263 /// Delegate an agent under a parent identity using device-link delegation. 264 /// 265 /// Generates a new Ed25519 keypair for the agent, stores it in the keychain, 266 /// and creates a parent-signed attestation delegating capabilities to the agent. 267 /// Returns a `did:key:` identifier for the delegated agent. 268 /// 269 /// Args: 270 /// * `agent_name`: Human-readable agent name. 271 /// * `capabilities`: Capabilities to grant. 272 /// * `parent_repo_path`: Path to the parent identity's repository. 273 /// * `passphrase`: Optional passphrase for the keychain. 274 /// * `expires_in_days`: Optional expiry in days. 275 /// * `identity_did`: DID of the parent identity (did:keri:...). 276 /// 277 /// Usage: 278 /// ```ignore 279 /// let bundle = delegate_agent(py, "ci-bot", vec!["sign".into()], "~/.auths", None, None, Some("did:keri:E..."))?; 280 /// ``` 281 #[pyfunction] 282 #[pyo3(signature = (agent_name, capabilities, parent_repo_path, passphrase=None, expires_in_days=None, identity_did=None))] 283 pub fn delegate_agent( 284 py: Python<'_>, 285 agent_name: &str, 286 capabilities: Vec<String>, 287 parent_repo_path: &str, 288 passphrase: Option<String>, 289 expires_in_days: Option<u32>, 290 identity_did: Option<String>, 291 ) -> PyResult<DelegatedAgentBundle> { 292 let passphrase_str = resolve_passphrase(passphrase); 293 let env_config = make_keychain_config(&passphrase_str); 294 let provider = Arc::new(PrefilledPassphraseProvider::new(&passphrase_str)); 295 let clock = Arc::new(SystemClock); 296 297 let repo = PathBuf::from(shellexpand::tilde(parent_repo_path).as_ref()); 298 let config = RegistryConfig::single_tenant(&repo); 299 let backend = Arc::new( 300 GitRegistryBackend::open_existing(config) 301 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_REGISTRY_ERROR] Failed to open registry: {e}")))?, 302 ); 303 304 let keychain = get_platform_keychain_with_config(&env_config) 305 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_KEYCHAIN_ERROR] Keychain error: {e}")))?; 306 307 // Resolve parent identity key alias 308 let parent_alias = if let Some(ref did) = identity_did { 309 resolve_key_alias(did, keychain.as_ref())? 310 } else { 311 let aliases = keychain 312 .list_aliases() 313 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_KEYCHAIN_ERROR] Keychain error: {e}")))?; 314 aliases 315 .into_iter() 316 .find(|a| !a.as_str().contains("--next-")) 317 .ok_or_else(|| PyRuntimeError::new_err("[AUTHS_KEY_NOT_FOUND] No identity key found in keychain"))? 318 }; 319 320 // Generate a new Ed25519 keypair for the agent 321 let agent_alias = KeyAlias::new_unchecked(format!("{}-agent", agent_name)); 322 let rng = SystemRandom::new(); 323 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng) 324 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_CRYPTO_ERROR] Key generation failed: {e}")))?; 325 let keypair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()) 326 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_CRYPTO_ERROR] Key parsing failed: {e}")))?; 327 let agent_pubkey = keypair.public_key().as_ref().to_vec(); 328 329 // Get parent identity DID for key storage association 330 let (parent_did, _, _) = keychain 331 .load_key(&parent_alias) 332 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_KEY_NOT_FOUND] Key load failed: {e}")))?; 333 334 // Encrypt and store the agent key 335 let seed = extract_seed_bytes(pkcs8.as_ref()) 336 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_CRYPTO_ERROR] Seed extraction failed: {e}")))?; 337 let seed_pkcs8 = encode_seed_as_pkcs8(seed) 338 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_CRYPTO_ERROR] PKCS8 encoding failed: {e}")))?; 339 let encrypted = encrypt_keypair(&seed_pkcs8, &passphrase_str) 340 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_CRYPTO_ERROR] Key encryption failed: {e}")))?; 341 keychain 342 .store_key(&agent_alias, &parent_did, KeyRole::DelegatedAgent, &encrypted) 343 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_KEYCHAIN_ERROR] Key storage failed: {e}")))?; 344 345 // Parse capabilities 346 let parsed_caps: Vec<Capability> = capabilities 347 .iter() 348 .map(|c| { 349 Capability::parse(c) 350 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_INVALID_INPUT] Invalid capability '{c}': {e}"))) 351 }) 352 .collect::<PyResult<Vec<_>>>()?; 353 354 let link_config = DeviceLinkConfig { 355 identity_key_alias: parent_alias, 356 device_key_alias: Some(agent_alias.clone()), 357 device_did: None, 358 capabilities: parsed_caps, 359 expires_in_days, 360 note: Some(format!("Agent: {}", agent_name)), 361 payload: None, 362 }; 363 364 let keychain: Arc<dyn KeyStorage + Send + Sync> = Arc::from(keychain); 365 let identity_storage = Arc::new(RegistryIdentityStorage::new(&repo)); 366 let attestation_storage = Arc::new(RegistryAttestationStorage::new(&repo)); 367 368 let ctx = AuthsContext::builder() 369 .registry(backend) 370 .key_storage(keychain) 371 .clock(clock.clone()) 372 .identity_storage(identity_storage) 373 .attestation_sink(attestation_storage.clone()) 374 .attestation_source(attestation_storage.clone()) 375 .passphrase_provider(provider) 376 .build(); 377 378 py.allow_threads(|| { 379 let result = link_device(link_config, &ctx, clock.as_ref()) 380 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_IDENTITY_ERROR] Agent provisioning failed: {e}")))?; 381 382 let device_did = DeviceDID(result.device_did.to_string()); 383 let attestations = attestation_storage 384 .load_attestations_for_device(&device_did) 385 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_REGISTRY_ERROR] Failed to load attestation: {e}")))?; 386 387 let attestation = attestations 388 .last() 389 .ok_or_else(|| PyRuntimeError::new_err("[AUTHS_REGISTRY_ERROR] No attestation found after provisioning"))?; 390 391 let attestation_json = serde_json::to_string(attestation) 392 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_SERIALIZATION_ERROR] Serialization failed: {e}")))?; 393 394 Ok(DelegatedAgentBundle { 395 agent_did: result.device_did.to_string(), 396 key_alias: agent_alias.to_string(), 397 attestation_json, 398 public_key_hex: hex::encode(&agent_pubkey), 399 repo_path: Some(repo.to_string_lossy().to_string()), 400 }) 401 }) 402 } 403 404 /// Link a device to an identity. 405 /// 406 /// Args: 407 /// * `identity_key_alias`: Alias of the identity key in the keychain. 408 /// * `capabilities`: Capabilities to grant to the device. 409 /// * `passphrase`: Optional passphrase for the keychain. 410 /// * `repo_path`: Path to the auths repository. 411 /// * `expires_in_days`: Optional expiration period in days. 412 /// 413 /// Usage: 414 /// ```ignore 415 /// let (device_did, att_id) = link_device_ffi(py, "my-id", vec!["sign".into()], None, "~/.auths", None)?; 416 /// ``` 417 #[pyfunction] 418 #[pyo3(signature = (identity_key_alias, capabilities, repo_path, passphrase=None, expires_in_days=None))] 419 pub fn link_device_to_identity( 420 py: Python<'_>, 421 identity_key_alias: &str, 422 capabilities: Vec<String>, 423 repo_path: &str, 424 passphrase: Option<String>, 425 expires_in_days: Option<u32>, 426 ) -> PyResult<(String, String)> { 427 let passphrase_str = resolve_passphrase(passphrase); 428 let env_config = make_keychain_config(&passphrase_str); 429 let provider = Arc::new(PrefilledPassphraseProvider::new(&passphrase_str)); 430 let clock = Arc::new(SystemClock); 431 432 let repo = PathBuf::from(shellexpand::tilde(repo_path).as_ref()); 433 let config = RegistryConfig::single_tenant(&repo); 434 let backend = Arc::new( 435 GitRegistryBackend::open_existing(config) 436 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_REGISTRY_ERROR] Failed to open registry: {e}")))?, 437 ); 438 439 let keychain = get_platform_keychain_with_config(&env_config) 440 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_KEYCHAIN_ERROR] Keychain error: {e}")))?; 441 442 let alias = resolve_key_alias(identity_key_alias, keychain.as_ref())?; 443 444 let keychain: Arc<dyn KeyStorage + Send + Sync> = Arc::from(keychain); 445 let identity_storage = Arc::new(RegistryIdentityStorage::new(&repo)); 446 let attestation_storage = Arc::new(RegistryAttestationStorage::new(&repo)); 447 448 let parsed_caps: Vec<Capability> = capabilities 449 .iter() 450 .map(|c| { 451 Capability::parse(c) 452 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_INVALID_INPUT] Invalid capability '{c}': {e}"))) 453 }) 454 .collect::<PyResult<Vec<_>>>()?; 455 456 let link_config = DeviceLinkConfig { 457 identity_key_alias: alias, 458 device_key_alias: None, 459 device_did: None, 460 capabilities: parsed_caps, 461 expires_in_days, 462 note: None, 463 payload: None, 464 }; 465 466 let ctx = AuthsContext::builder() 467 .registry(backend) 468 .key_storage(keychain) 469 .clock(clock.clone()) 470 .identity_storage(identity_storage) 471 .attestation_sink(attestation_storage.clone()) 472 .attestation_source(attestation_storage) 473 .passphrase_provider(provider) 474 .build(); 475 476 py.allow_threads(|| { 477 let result = link_device(link_config, &ctx, clock.as_ref()) 478 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_DEVICE_ERROR] Device linking failed: {e}")))?; 479 Ok(( 480 result.device_did.to_string(), 481 result.attestation_id.to_string(), 482 )) 483 }) 484 } 485 486 /// Revoke a device from an identity. 487 /// 488 /// Args: 489 /// * `device_did`: The DID of the device to revoke. 490 /// * `identity_key_alias`: Alias of the identity key in the keychain. 491 /// * `passphrase`: Optional passphrase for the keychain. 492 /// * `repo_path`: Path to the auths repository. 493 /// * `note`: Optional revocation note. 494 /// 495 /// Usage: 496 /// ```ignore 497 /// revoke_device_ffi(py, "did:key:z6Mk...", "my-id", None, "~/.auths", None)?; 498 /// ``` 499 #[pyfunction] 500 #[pyo3(signature = (device_did, identity_key_alias, repo_path, passphrase=None, note=None))] 501 pub fn revoke_device_from_identity( 502 py: Python<'_>, 503 device_did: &str, 504 identity_key_alias: &str, 505 repo_path: &str, 506 passphrase: Option<String>, 507 note: Option<String>, 508 ) -> PyResult<()> { 509 let passphrase_str = resolve_passphrase(passphrase); 510 let env_config = make_keychain_config(&passphrase_str); 511 let provider = Arc::new(PrefilledPassphraseProvider::new(&passphrase_str)); 512 let clock = Arc::new(SystemClock); 513 514 let repo = PathBuf::from(shellexpand::tilde(repo_path).as_ref()); 515 let config = RegistryConfig::single_tenant(&repo); 516 let backend = Arc::new( 517 GitRegistryBackend::open_existing(config) 518 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_REGISTRY_ERROR] Failed to open registry: {e}")))?, 519 ); 520 521 let keychain = get_platform_keychain_with_config(&env_config) 522 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_KEYCHAIN_ERROR] Keychain error: {e}")))?; 523 524 let alias = resolve_key_alias(identity_key_alias, keychain.as_ref())?; 525 526 let keychain: Arc<dyn KeyStorage + Send + Sync> = Arc::from(keychain); 527 let identity_storage = Arc::new(RegistryIdentityStorage::new(&repo)); 528 let attestation_storage = Arc::new(RegistryAttestationStorage::new(&repo)); 529 530 let ctx = AuthsContext::builder() 531 .registry(backend) 532 .key_storage(keychain) 533 .clock(clock.clone()) 534 .identity_storage(identity_storage) 535 .attestation_sink(attestation_storage.clone()) 536 .attestation_source(attestation_storage) 537 .passphrase_provider(provider) 538 .build(); 539 540 py.allow_threads(|| { 541 revoke_device(device_did, &alias, &ctx, note, clock.as_ref()) 542 .map_err(|e| PyRuntimeError::new_err(format!("[AUTHS_DEVICE_ERROR] Device revocation failed: {e}")))?; 543 Ok(()) 544 }) 545 }