1use crate::config::EnvironmentConfig;
4use crate::error::AgentError;
5use crate::paths::auths_home_with_config;
6use log::{info, warn};
7use std::sync::Arc;
8
9#[cfg(target_os = "ios")]
10use super::ios_keychain::IOSKeychain;
11
12#[cfg(target_os = "macos")]
13use super::macos_keychain::MacOSKeychain;
14
15#[cfg(all(target_os = "linux", feature = "keychain-linux-secretservice"))]
16use super::linux_secret_service::LinuxSecretServiceStorage;
17
18#[cfg(all(target_os = "windows", feature = "keychain-windows"))]
19use super::windows_credential::WindowsCredentialStorage;
20
21#[cfg(target_os = "android")]
22use super::android_keystore::AndroidKeystoreStorage;
23
24use super::encrypted_file::EncryptedFileStorage;
25use super::memory::MemoryKeychainHandle;
26use std::borrow::Borrow;
27use std::fmt;
28use std::ops::Deref;
29use zeroize::Zeroizing;
30
31#[cfg_attr(
34 not(any(
35 target_os = "macos",
36 target_os = "ios",
37 target_os = "android",
38 all(target_os = "linux", feature = "keychain-linux-secretservice"),
39 all(target_os = "windows", feature = "keychain-windows"),
40 test,
41 )),
42 allow(dead_code)
43)]
44const SERVICE_NAME: &str = "dev.auths.agent";
45
46pub use auths_verifier::IdentityDID;
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
51#[serde(rename_all = "snake_case")]
52pub enum KeyRole {
53 Primary,
55 NextRotation,
57 DelegatedAgent,
59}
60
61impl std::fmt::Display for KeyRole {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 match self {
64 KeyRole::Primary => write!(f, "primary"),
65 KeyRole::NextRotation => write!(f, "next_rotation"),
66 KeyRole::DelegatedAgent => write!(f, "delegated_agent"),
67 }
68 }
69}
70
71impl std::str::FromStr for KeyRole {
72 type Err = String;
73 fn from_str(s: &str) -> Result<Self, Self::Err> {
74 match s {
75 "primary" => Ok(KeyRole::Primary),
76 "next_rotation" => Ok(KeyRole::NextRotation),
77 "delegated_agent" => Ok(KeyRole::DelegatedAgent),
78 other => Err(format!("unknown key role: {other}")),
79 }
80 }
81}
82
83#[derive(
93 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
94)]
95#[serde(transparent)]
96#[repr(transparent)]
97pub struct KeyAlias(String);
98
99impl KeyAlias {
100 pub fn new<S: Into<String>>(s: S) -> Result<Self, AgentError> {
104 let s = s.into();
105 if s.is_empty() {
106 return Err(AgentError::InvalidInput(
107 "key alias must not be empty".into(),
108 ));
109 }
110 if s.contains('\0') {
111 return Err(AgentError::InvalidInput(
112 "key alias must not contain null bytes".into(),
113 ));
114 }
115 Ok(Self(s))
116 }
117
118 pub fn new_unchecked<S: Into<String>>(s: S) -> Self {
120 Self(s.into())
121 }
122
123 pub fn as_str(&self) -> &str {
125 &self.0
126 }
127
128 pub fn into_inner(self) -> String {
130 self.0
131 }
132}
133
134impl fmt::Display for KeyAlias {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 f.write_str(&self.0)
137 }
138}
139
140impl Deref for KeyAlias {
141 type Target = str;
142 fn deref(&self) -> &Self::Target {
143 &self.0
144 }
145}
146
147impl AsRef<str> for KeyAlias {
148 fn as_ref(&self) -> &str {
149 &self.0
150 }
151}
152
153impl Borrow<str> for KeyAlias {
154 fn borrow(&self) -> &str {
155 &self.0
156 }
157}
158
159impl From<KeyAlias> for String {
160 fn from(alias: KeyAlias) -> String {
161 alias.0
162 }
163}
164
165impl PartialEq<str> for KeyAlias {
166 fn eq(&self, other: &str) -> bool {
167 self.0 == other
168 }
169}
170
171impl PartialEq<&str> for KeyAlias {
172 fn eq(&self, other: &&str) -> bool {
173 self.0 == *other
174 }
175}
176
177impl PartialEq<String> for KeyAlias {
178 fn eq(&self, other: &String) -> bool {
179 self.0 == *other
180 }
181}
182
183pub fn migrate_legacy_alias(
198 keychain: &(dyn KeyStorage + Send + Sync),
199 old_alias: &KeyAlias,
200) -> Result<KeyAlias, AgentError> {
201 let new_alias = KeyAlias::new_unchecked(format!("{}--0", old_alias));
202
203 if keychain.load_key(&new_alias).is_ok() {
204 return Ok(new_alias);
207 }
208
209 let (did, role, data) = match keychain.load_key(old_alias) {
210 Ok(v) => v,
211 Err(_) => {
212 return Ok(new_alias);
215 }
216 };
217
218 keychain.store_key(&new_alias, &did, role, &data)?;
219 let _ = keychain.delete_key(old_alias);
221 Ok(new_alias)
222}
223
224pub trait KeyStorage: Send + Sync {
228 fn store_key(
230 &self,
231 alias: &KeyAlias,
232 identity_did: &IdentityDID,
233 role: KeyRole,
234 encrypted_key_data: &[u8],
235 ) -> Result<(), AgentError>;
236
237 fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError>;
239
240 fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError>;
242
243 fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError>;
245
246 fn list_aliases_for_identity(
248 &self,
249 identity_did: &IdentityDID,
250 ) -> Result<Vec<KeyAlias>, AgentError>;
251
252 fn list_aliases_for_identity_with_role(
254 &self,
255 identity_did: &IdentityDID,
256 role: KeyRole,
257 ) -> Result<Vec<KeyAlias>, AgentError> {
258 let all = self.list_aliases_for_identity(identity_did)?;
259 let mut filtered = Vec::new();
260 for alias in all {
261 if let Ok((_, r, _)) = self.load_key(&alias)
262 && r == role
263 {
264 filtered.push(alias);
265 }
266 }
267 Ok(filtered)
268 }
269
270 fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError>;
272
273 fn backend_name(&self) -> &'static str;
275
276 fn is_hardware_backend(&self) -> bool {
281 false
282 }
283
284 fn rebind_identity(
303 &self,
304 alias: &KeyAlias,
305 identity_did: &IdentityDID,
306 ) -> Result<(), AgentError> {
307 if self.is_hardware_backend() {
308 return Err(AgentError::StorageError(format!(
309 "hardware backend '{}' must override rebind_identity",
310 self.backend_name()
311 )));
312 }
313 let (_, role, material) = self.load_key(alias)?;
314 self.store_key(alias, identity_did, role, &material)
315 }
316
317 fn export_public_key(&self, alias: &KeyAlias) -> Result<Vec<u8>, AgentError> {
323 let _ = alias;
324 Err(AgentError::StorageError(format!(
325 "backend '{}' does not support passphrase-less public key export",
326 self.backend_name()
327 )))
328 }
329
330 fn sign_raw(&self, alias: &KeyAlias, message: &[u8]) -> Result<Vec<u8>, AgentError> {
336 let _ = (alias, message);
337 Err(AgentError::StorageError(format!(
338 "backend '{}' does not support passphrase-less signing",
339 self.backend_name()
340 )))
341 }
342}
343
344pub fn extract_public_key_bytes(
360 keychain: &dyn KeyStorage,
361 alias: &KeyAlias,
362 passphrase_provider: &dyn crate::signing::PassphraseProvider,
363) -> Result<(Vec<u8>, auths_crypto::CurveType), AgentError> {
364 if keychain.is_hardware_backend() {
365 let (_, _role, _handle) = keychain.load_key(alias)?;
367 #[cfg(all(target_os = "macos", feature = "keychain-secure-enclave"))]
368 {
369 let pubkey = super::secure_enclave::public_key_from_handle(&_handle)?;
370 return Ok((pubkey, auths_crypto::CurveType::P256));
371 }
372 #[cfg(not(all(target_os = "macos", feature = "keychain-secure-enclave")))]
373 {
374 return Err(AgentError::BackendUnavailable {
375 backend: keychain.backend_name(),
376 reason: "hardware backend not available on this platform".into(),
377 });
378 }
379 }
380
381 use crate::crypto::signer::{decrypt_keypair, load_seed_and_pubkey};
382
383 let (_, _role, encrypted) = keychain.load_key(alias)?;
384 let passphrase = passphrase_provider
385 .get_passphrase(&format!("Enter passphrase for key '{alias}':"))
386 .map_err(|e| AgentError::SigningFailed(e.to_string()))?;
387 let pkcs8 = decrypt_keypair(&encrypted, &passphrase)?;
388 let (_, pubkey, curve) = load_seed_and_pubkey(&pkcs8)?;
389 Ok((pubkey.to_vec(), curve))
390}
391
392pub fn sign_with_key(
405 keychain: &dyn KeyStorage,
406 alias: &KeyAlias,
407 passphrase_provider: &dyn crate::signing::PassphraseProvider,
408 message: &[u8],
409) -> Result<(Vec<u8>, Vec<u8>, auths_crypto::CurveType), AgentError> {
410 if keychain.is_hardware_backend() {
411 let (_, _role, _handle) = keychain.load_key(alias)?;
412 #[cfg(all(target_os = "macos", feature = "keychain-secure-enclave"))]
413 {
414 let sig = super::secure_enclave::sign_with_handle(&_handle, message)?;
415 let pubkey = super::secure_enclave::public_key_from_handle(&_handle)?;
416 return Ok((sig, pubkey, auths_crypto::CurveType::P256));
417 }
418 #[cfg(not(all(target_os = "macos", feature = "keychain-secure-enclave")))]
419 {
420 return Err(AgentError::BackendUnavailable {
421 backend: keychain.backend_name(),
422 reason: "hardware signing not available on this platform".into(),
423 });
424 }
425 }
426
427 use crate::crypto::signer::{decrypt_keypair, load_seed_and_pubkey};
428
429 let (_, _role, encrypted) = keychain.load_key(alias)?;
430 let passphrase = passphrase_provider
431 .get_passphrase(&format!("Enter passphrase for key '{}' to sign:", alias))
432 .map_err(|e| AgentError::SigningFailed(e.to_string()))?;
433 let pkcs8 = decrypt_keypair(&encrypted, &passphrase)?;
434 let (seed, pubkey, curve) = load_seed_and_pubkey(&pkcs8)?;
435
436 let typed_seed = match curve {
437 auths_crypto::CurveType::Ed25519 => auths_crypto::TypedSeed::Ed25519(*seed.as_bytes()),
438 auths_crypto::CurveType::P256 => auths_crypto::TypedSeed::P256(*seed.as_bytes()),
439 };
440
441 let sig = auths_crypto::typed_sign(&typed_seed, message)
442 .map_err(|e| AgentError::SigningFailed(e.to_string()))?;
443
444 Ok((sig, pubkey, curve))
445}
446
447pub fn get_platform_keychain_with_config(
462 config: &EnvironmentConfig,
463) -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
464 if let Some(ref backend) = config.keychain.backend {
465 return get_backend_by_name(backend, config);
466 }
467 get_platform_default(config)
468}
469
470pub fn get_platform_keychain() -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
487 get_platform_keychain_with_config(&EnvironmentConfig::from_env())
488}
489
490#[allow(unused_variables, unreachable_code)]
492fn get_platform_default(
493 config: &EnvironmentConfig,
494) -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
495 #[cfg(target_os = "ios")]
496 {
497 return Ok(Box::new(IOSKeychain::new(SERVICE_NAME)));
498 }
499
500 #[cfg(target_os = "macos")]
501 {
502 #[cfg(feature = "keychain-secure-enclave")]
504 {
505 if super::secure_enclave::is_available()
506 && let Ok(home) = auths_home_with_config(config)
507 {
508 match super::secure_enclave::SecureEnclaveKeyStorage::new(&home) {
509 Ok(storage) => {
510 log::info!("Using Secure Enclave (Touch ID signing)");
511 return Ok(Box::new(storage));
512 }
513 Err(e) => {
514 log::warn!(
515 "Secure Enclave available but init failed ({}), using macOS Keychain",
516 e
517 );
518 }
519 }
520 }
521 }
522 return Ok(Box::new(MacOSKeychain::new(SERVICE_NAME)));
523 }
524
525 #[cfg(all(target_os = "linux", feature = "keychain-linux-secretservice"))]
526 {
527 match LinuxSecretServiceStorage::new(SERVICE_NAME) {
529 Ok(storage) => return Ok(Box::new(storage)),
530 Err(e) => {
531 warn!("Secret Service unavailable ({}), trying file fallback", e);
532 #[cfg(feature = "keychain-file-fallback")]
533 {
534 return new_encrypted_file_storage(config).map(|s| {
535 let b: Box<dyn KeyStorage + Send + Sync> = Box::new(s);
536 b
537 });
538 }
539 #[cfg(not(feature = "keychain-file-fallback"))]
540 {
541 return Err(e);
542 }
543 }
544 }
545 }
546
547 #[cfg(all(target_os = "linux", not(feature = "keychain-linux-secretservice")))]
548 {
549 #[cfg(feature = "keychain-file-fallback")]
551 {
552 return new_encrypted_file_storage(config).map(|s| {
553 let b: Box<dyn KeyStorage + Send + Sync> = Box::new(s);
554 b
555 });
556 }
557 }
558
559 #[cfg(all(target_os = "windows", feature = "keychain-windows"))]
560 {
561 return Ok(Box::new(WindowsCredentialStorage::new(SERVICE_NAME)?));
562 }
563
564 #[cfg(target_os = "android")]
565 {
566 return Ok(Box::new(AndroidKeystoreStorage::new(SERVICE_NAME)?));
567 }
568
569 #[allow(unused_variables)]
571 let _ = config;
572 #[allow(unreachable_code)]
573 {
574 warn!("Using in-memory keychain (not recommended for production)");
575 Ok(Box::new(MemoryKeychainHandle))
576 }
577}
578
579fn get_backend_by_name(
581 name: &str,
582 config: &EnvironmentConfig,
583) -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
584 match name.to_lowercase().as_str() {
585 "memory" => {
586 info!("Using in-memory keychain (AUTHS_KEYCHAIN_BACKEND=memory)");
587 Ok(Box::new(MemoryKeychainHandle))
588 }
589 "file" => {
590 info!("Using encrypted file storage (AUTHS_KEYCHAIN_BACKEND=file)");
591 let storage = new_encrypted_file_storage(config)?;
592 Ok(Box::new(storage))
593 }
594 #[cfg(feature = "keychain-pkcs11")]
595 "hsm" | "pkcs11" => {
596 info!("Using PKCS#11 HSM backend (AUTHS_KEYCHAIN_BACKEND={name})");
597 let pkcs11_config =
598 config
599 .pkcs11
600 .as_ref()
601 .ok_or_else(|| AgentError::BackendInitFailed {
602 backend: "pkcs11",
603 error: "PKCS#11 configuration required (set AUTHS_PKCS11_LIBRARY)".into(),
604 })?;
605 let storage = super::pkcs11::Pkcs11KeyRef::new(pkcs11_config)?;
606 Ok(Box::new(storage))
607 }
608 #[cfg(all(target_os = "macos", feature = "keychain-secure-enclave"))]
609 "secure-enclave" => {
610 info!("Using Secure Enclave backend (AUTHS_KEYCHAIN_BACKEND=secure-enclave)");
611 let home =
612 auths_home_with_config(config).map_err(|e| AgentError::BackendInitFailed {
613 backend: "secure-enclave",
614 error: format!("failed to resolve auths home: {e}"),
615 })?;
616 let storage = super::secure_enclave::SecureEnclaveKeyStorage::new(&home)?;
617 Ok(Box::new(storage))
618 }
619 _ => {
620 warn!(
621 "Unknown keychain backend '{}', using platform default",
622 name
623 );
624 get_platform_default(config)
625 }
626 }
627}
628
629fn new_encrypted_file_storage(
635 config: &EnvironmentConfig,
636) -> Result<EncryptedFileStorage, AgentError> {
637 let storage = if let Some(ref path) = config.keychain.file_path {
638 EncryptedFileStorage::with_path(path.clone())?
639 } else {
640 let home =
641 auths_home_with_config(config).map_err(|e| AgentError::StorageError(e.to_string()))?;
642 EncryptedFileStorage::new(&home)?
643 };
644
645 if let Some(ref passphrase) = config.keychain.passphrase {
646 storage.set_password(Zeroizing::new(passphrase.clone()));
647 }
648
649 Ok(storage)
650}
651
652#[cfg(feature = "keychain-pkcs11")]
667pub fn get_pkcs11_signer(
668 config: &EnvironmentConfig,
669) -> Result<Option<Box<dyn crate::signing::SecureSigner>>, AgentError> {
670 let is_pkcs11 = config
671 .keychain
672 .backend
673 .as_deref()
674 .map(|b| matches!(b.to_lowercase().as_str(), "hsm" | "pkcs11"))
675 .unwrap_or(false);
676
677 if !is_pkcs11 {
678 return Ok(None);
679 }
680
681 let pkcs11_config = config
682 .pkcs11
683 .as_ref()
684 .ok_or_else(|| AgentError::BackendInitFailed {
685 backend: "pkcs11",
686 error: "PKCS#11 configuration required (set AUTHS_PKCS11_LIBRARY)".into(),
687 })?;
688
689 let signer = super::pkcs11::Pkcs11Signer::new(pkcs11_config)?;
690 Ok(Some(Box::new(signer)))
691}
692
693impl KeyStorage for Arc<dyn KeyStorage + Send + Sync> {
694 fn store_key(
695 &self,
696 alias: &KeyAlias,
697 identity_did: &IdentityDID,
698 role: KeyRole,
699 encrypted_key_data: &[u8],
700 ) -> Result<(), AgentError> {
701 self.as_ref()
702 .store_key(alias, identity_did, role, encrypted_key_data)
703 }
704 fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError> {
705 self.as_ref().load_key(alias)
706 }
707 fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError> {
708 self.as_ref().delete_key(alias)
709 }
710 fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError> {
711 self.as_ref().list_aliases()
712 }
713 fn list_aliases_for_identity(
714 &self,
715 identity_did: &IdentityDID,
716 ) -> Result<Vec<KeyAlias>, AgentError> {
717 self.as_ref().list_aliases_for_identity(identity_did)
718 }
719 fn list_aliases_for_identity_with_role(
720 &self,
721 identity_did: &IdentityDID,
722 role: KeyRole,
723 ) -> Result<Vec<KeyAlias>, AgentError> {
724 self.as_ref()
725 .list_aliases_for_identity_with_role(identity_did, role)
726 }
727 fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError> {
728 self.as_ref().get_identity_for_alias(alias)
729 }
730 fn backend_name(&self) -> &'static str {
731 self.as_ref().backend_name()
732 }
733 fn is_hardware_backend(&self) -> bool {
734 self.as_ref().is_hardware_backend()
735 }
736}
737
738impl KeyStorage for Box<dyn KeyStorage + Send + Sync> {
739 fn store_key(
740 &self,
741 alias: &KeyAlias,
742 identity_did: &IdentityDID,
743 role: KeyRole,
744 encrypted_key_data: &[u8],
745 ) -> Result<(), AgentError> {
746 self.as_ref()
747 .store_key(alias, identity_did, role, encrypted_key_data)
748 }
749 fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError> {
750 self.as_ref().load_key(alias)
751 }
752 fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError> {
753 self.as_ref().delete_key(alias)
754 }
755 fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError> {
756 self.as_ref().list_aliases()
757 }
758 fn list_aliases_for_identity(
759 &self,
760 identity_did: &IdentityDID,
761 ) -> Result<Vec<KeyAlias>, AgentError> {
762 self.as_ref().list_aliases_for_identity(identity_did)
763 }
764 fn list_aliases_for_identity_with_role(
765 &self,
766 identity_did: &IdentityDID,
767 role: KeyRole,
768 ) -> Result<Vec<KeyAlias>, AgentError> {
769 self.as_ref()
770 .list_aliases_for_identity_with_role(identity_did, role)
771 }
772 fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError> {
773 self.as_ref().get_identity_for_alias(alias)
774 }
775 fn backend_name(&self) -> &'static str {
776 self.as_ref().backend_name()
777 }
778 fn is_hardware_backend(&self) -> bool {
779 self.as_ref().is_hardware_backend()
780 }
781}
782
783#[cfg(test)]
784mod tests {
785 use super::*;
786
787 #[test]
788 fn test_service_name_constant() {
789 assert_eq!(SERVICE_NAME, "dev.auths.agent");
790 }
791
792 #[test]
793 fn test_get_backend_by_name_memory() {
794 let env = EnvironmentConfig::default();
795 let backend = get_backend_by_name("memory", &env).unwrap();
796 assert_eq!(backend.backend_name(), "Memory");
797 }
798
799 #[test]
800 fn test_get_backend_by_name_case_insensitive() {
801 let env = EnvironmentConfig::default();
802 let backend = get_backend_by_name("MEMORY", &env).unwrap();
803 assert_eq!(backend.backend_name(), "Memory");
804 }
805
806 #[test]
807 fn test_key_role_serde_roundtrip() {
808 let roles = [
809 KeyRole::Primary,
810 KeyRole::NextRotation,
811 KeyRole::DelegatedAgent,
812 ];
813 for role in &roles {
814 let json = serde_json::to_string(role).unwrap();
815 let parsed: KeyRole = serde_json::from_str(&json).unwrap();
816 assert_eq!(*role, parsed);
817 }
818 }
819
820 #[test]
821 fn test_key_role_display_and_parse() {
822 assert_eq!(KeyRole::Primary.to_string(), "primary");
823 assert_eq!(KeyRole::NextRotation.to_string(), "next_rotation");
824 assert_eq!(KeyRole::DelegatedAgent.to_string(), "delegated_agent");
825 assert_eq!("primary".parse::<KeyRole>().unwrap(), KeyRole::Primary);
826 assert_eq!(
827 "delegated_agent".parse::<KeyRole>().unwrap(),
828 KeyRole::DelegatedAgent
829 );
830 assert!("unknown".parse::<KeyRole>().is_err());
831 }
832
833 #[test]
834 fn test_list_aliases_with_role_filter() {
835 use super::super::memory::IsolatedKeychainHandle;
836
837 let keychain = IsolatedKeychainHandle::new();
838 #[allow(clippy::disallowed_methods)]
839 let did = IdentityDID::new_unchecked("did:keri:Etest".to_string());
841
842 keychain
843 .store_key(
844 &KeyAlias::new_unchecked("operator"),
845 &did,
846 KeyRole::Primary,
847 b"key1",
848 )
849 .unwrap();
850 keychain
851 .store_key(
852 &KeyAlias::new_unchecked("operator--next-0"),
853 &did,
854 KeyRole::NextRotation,
855 b"key2",
856 )
857 .unwrap();
858 keychain
859 .store_key(
860 &KeyAlias::new_unchecked("deploy-agent"),
861 &did,
862 KeyRole::DelegatedAgent,
863 b"key3",
864 )
865 .unwrap();
866
867 let primary = keychain
868 .list_aliases_for_identity_with_role(&did, KeyRole::Primary)
869 .unwrap();
870 assert_eq!(primary.len(), 1);
871 assert_eq!(primary[0].as_str(), "operator");
872
873 let agents = keychain
874 .list_aliases_for_identity_with_role(&did, KeyRole::DelegatedAgent)
875 .unwrap();
876 assert_eq!(agents.len(), 1);
877 assert_eq!(agents[0].as_str(), "deploy-agent");
878 }
879}