/ packages / auths-python / src / identity.rs
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  }