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(Debug, Clone, PartialEq, Eq, thiserror::Error)]
86#[error("stored key role {0:?} is not a recognized role")]
87pub struct UnknownKeyRole(String);
88
89impl KeyRole {
90 pub fn from_persisted(token: &str) -> Result<KeyRole, UnknownKeyRole> {
102 token.parse().map_err(|_| UnknownKeyRole(token.to_string()))
103 }
104}
105
106#[derive(
116 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
117)]
118#[serde(transparent)]
119#[repr(transparent)]
120pub struct KeyAlias(String);
121
122impl KeyAlias {
123 pub fn new<S: Into<String>>(s: S) -> Result<Self, AgentError> {
127 let s = s.into();
128 if s.is_empty() {
129 return Err(AgentError::InvalidInput(
130 "key alias must not be empty".into(),
131 ));
132 }
133 if s.contains('\0') {
134 return Err(AgentError::InvalidInput(
135 "key alias must not contain null bytes".into(),
136 ));
137 }
138 Ok(Self(s))
139 }
140
141 pub fn new_unchecked<S: Into<String>>(s: S) -> Self {
143 Self(s.into())
144 }
145
146 pub fn as_str(&self) -> &str {
148 &self.0
149 }
150
151 pub fn into_inner(self) -> String {
153 self.0
154 }
155}
156
157impl fmt::Display for KeyAlias {
158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159 f.write_str(&self.0)
160 }
161}
162
163impl Deref for KeyAlias {
164 type Target = str;
165 fn deref(&self) -> &Self::Target {
166 &self.0
167 }
168}
169
170impl AsRef<str> for KeyAlias {
171 fn as_ref(&self) -> &str {
172 &self.0
173 }
174}
175
176impl Borrow<str> for KeyAlias {
177 fn borrow(&self) -> &str {
178 &self.0
179 }
180}
181
182impl From<KeyAlias> for String {
183 fn from(alias: KeyAlias) -> String {
184 alias.0
185 }
186}
187
188impl PartialEq<str> for KeyAlias {
189 fn eq(&self, other: &str) -> bool {
190 self.0 == other
191 }
192}
193
194impl PartialEq<&str> for KeyAlias {
195 fn eq(&self, other: &&str) -> bool {
196 self.0 == *other
197 }
198}
199
200impl PartialEq<String> for KeyAlias {
201 fn eq(&self, other: &String) -> bool {
202 self.0 == *other
203 }
204}
205
206pub fn migrate_legacy_alias(
221 keychain: &(dyn KeyStorage + Send + Sync),
222 old_alias: &KeyAlias,
223) -> Result<KeyAlias, AgentError> {
224 let new_alias = KeyAlias::new_unchecked(format!("{}--0", old_alias));
225
226 if keychain.load_key(&new_alias).is_ok() {
227 migrate_next_key_commitments(keychain, old_alias)?;
231 return Ok(new_alias);
232 }
233
234 let (did, role, data) = match keychain.load_key(old_alias) {
235 Ok(v) => v,
236 Err(_) => {
237 return Ok(new_alias);
240 }
241 };
242
243 keychain.store_key(&new_alias, &did, role, &data)?;
244 let _ = keychain.delete_key(old_alias);
246
247 migrate_next_key_commitments(keychain, old_alias)?;
251 Ok(new_alias)
252}
253
254fn migrate_next_key_commitments(
258 keychain: &(dyn KeyStorage + Send + Sync),
259 old_alias: &KeyAlias,
260) -> Result<(), AgentError> {
261 let legacy_prefix = format!("{}--next-", old_alias);
262 let slot0_prefix = format!("{}--0--next-", old_alias);
263 for alias in keychain.list_aliases()? {
264 let Some(suffix) = alias.as_str().strip_prefix(legacy_prefix.as_str()) else {
265 continue;
266 };
267 let target = KeyAlias::new_unchecked(format!("{slot0_prefix}{suffix}"));
268 if keychain.load_key(&target).is_ok() {
269 continue;
270 }
271 let (did, role, data) = match keychain.load_key(&alias) {
272 Ok(v) => v,
273 Err(_) => continue,
274 };
275 keychain.store_key(&target, &did, role, &data)?;
276 let _ = keychain.delete_key(&alias);
277 }
278 Ok(())
279}
280
281pub trait KeyStorage: Send + Sync {
285 fn store_key(
287 &self,
288 alias: &KeyAlias,
289 identity_did: &IdentityDID,
290 role: KeyRole,
291 encrypted_key_data: &[u8],
292 ) -> Result<(), AgentError>;
293
294 fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError>;
296
297 fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError>;
299
300 fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError>;
302
303 fn list_aliases_for_identity(
305 &self,
306 identity_did: &IdentityDID,
307 ) -> Result<Vec<KeyAlias>, AgentError>;
308
309 fn list_aliases_for_identity_with_role(
311 &self,
312 identity_did: &IdentityDID,
313 role: KeyRole,
314 ) -> Result<Vec<KeyAlias>, AgentError> {
315 let all = self.list_aliases_for_identity(identity_did)?;
316 let mut filtered = Vec::new();
317 for alias in all {
318 if let Ok((_, r, _)) = self.load_key(&alias)
319 && r == role
320 {
321 filtered.push(alias);
322 }
323 }
324 Ok(filtered)
325 }
326
327 fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError>;
329
330 fn backend_name(&self) -> &'static str;
332
333 fn is_hardware_backend(&self) -> bool {
338 false
339 }
340
341 fn rebind_identity(
360 &self,
361 alias: &KeyAlias,
362 identity_did: &IdentityDID,
363 ) -> Result<(), AgentError> {
364 if self.is_hardware_backend() {
365 return Err(AgentError::StorageError(format!(
366 "hardware backend '{}' must override rebind_identity",
367 self.backend_name()
368 )));
369 }
370 let (_, role, material) = self.load_key(alias)?;
371 self.store_key(alias, identity_did, role, &material)
372 }
373
374 fn export_public_key(&self, alias: &KeyAlias) -> Result<Vec<u8>, AgentError> {
380 let _ = alias;
381 Err(AgentError::StorageError(format!(
382 "backend '{}' does not support passphrase-less public key export",
383 self.backend_name()
384 )))
385 }
386
387 fn sign_raw(&self, alias: &KeyAlias, message: &[u8]) -> Result<Vec<u8>, AgentError> {
393 let _ = (alias, message);
394 Err(AgentError::StorageError(format!(
395 "backend '{}' does not support passphrase-less signing",
396 self.backend_name()
397 )))
398 }
399}
400
401pub fn extract_public_key_bytes(
417 keychain: &dyn KeyStorage,
418 alias: &KeyAlias,
419 passphrase_provider: &dyn crate::signing::PassphraseProvider,
420) -> Result<(Vec<u8>, auths_crypto::CurveType), AgentError> {
421 if keychain.is_hardware_backend() {
422 let (_, _role, _handle) = keychain.load_key(alias)?;
424 #[cfg(all(target_os = "macos", feature = "keychain-secure-enclave"))]
425 {
426 let pubkey = super::secure_enclave::public_key_from_handle(&_handle)?;
427 return Ok((pubkey, auths_crypto::CurveType::P256));
428 }
429 #[cfg(not(all(target_os = "macos", feature = "keychain-secure-enclave")))]
430 {
431 return Err(AgentError::BackendUnavailable {
432 backend: keychain.backend_name(),
433 reason: "hardware backend not available on this platform".into(),
434 });
435 }
436 }
437
438 use crate::crypto::signer::{decrypt_keypair, load_seed_and_pubkey};
439
440 let (_, _role, encrypted) = keychain.load_key(alias)?;
441 let passphrase = passphrase_provider
442 .get_passphrase(&format!("Enter passphrase for key '{alias}':"))
443 .map_err(|e| AgentError::SigningFailed(e.to_string()))?;
444 let pkcs8 = decrypt_keypair(&encrypted, &passphrase)?;
445 let (_, pubkey, curve) = load_seed_and_pubkey(&pkcs8)?;
446 Ok((pubkey.to_vec(), curve))
447}
448
449pub fn sign_with_key(
462 keychain: &dyn KeyStorage,
463 alias: &KeyAlias,
464 passphrase_provider: &dyn crate::signing::PassphraseProvider,
465 message: &[u8],
466) -> Result<(Vec<u8>, Vec<u8>, auths_crypto::CurveType), AgentError> {
467 if keychain.is_hardware_backend() {
468 let (_, _role, _handle) = keychain.load_key(alias)?;
469 #[cfg(all(target_os = "macos", feature = "keychain-secure-enclave"))]
470 {
471 let sig = super::secure_enclave::sign_with_handle(&_handle, message)?;
472 let pubkey = super::secure_enclave::public_key_from_handle(&_handle)?;
473 return Ok((sig, pubkey, auths_crypto::CurveType::P256));
474 }
475 #[cfg(not(all(target_os = "macos", feature = "keychain-secure-enclave")))]
476 {
477 return Err(AgentError::BackendUnavailable {
478 backend: keychain.backend_name(),
479 reason: "hardware signing not available on this platform".into(),
480 });
481 }
482 }
483
484 use crate::crypto::signer::{decrypt_keypair, load_seed_and_pubkey};
485
486 let (_, _role, encrypted) = keychain.load_key(alias)?;
487 let passphrase = passphrase_provider
488 .get_passphrase(&format!("Enter passphrase for key '{}' to sign:", alias))
489 .map_err(|e| AgentError::SigningFailed(e.to_string()))?;
490 let pkcs8 = decrypt_keypair(&encrypted, &passphrase)?;
491 let (seed, pubkey, curve) = load_seed_and_pubkey(&pkcs8)?;
492
493 let typed_seed = match curve {
494 auths_crypto::CurveType::Ed25519 => auths_crypto::TypedSeed::Ed25519(*seed.as_bytes()),
495 auths_crypto::CurveType::P256 => auths_crypto::TypedSeed::P256(*seed.as_bytes()),
496 };
497
498 let sig = auths_crypto::typed_sign(&typed_seed, message)
499 .map_err(|e| AgentError::SigningFailed(e.to_string()))?;
500
501 Ok((sig, pubkey, curve))
502}
503
504pub fn get_platform_keychain_with_config(
519 config: &EnvironmentConfig,
520) -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
521 if let Some(ref backend) = config.keychain.backend {
522 return get_backend_by_name(backend, config);
523 }
524 get_platform_default(config)
525}
526
527pub fn get_platform_keychain() -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
544 get_platform_keychain_with_config(&EnvironmentConfig::from_env())
545}
546
547#[allow(unused_variables, unreachable_code)]
549fn get_platform_default(
550 config: &EnvironmentConfig,
551) -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
552 #[cfg(target_os = "ios")]
553 {
554 return Ok(Box::new(IOSKeychain::new(SERVICE_NAME)));
555 }
556
557 #[cfg(target_os = "macos")]
558 {
559 #[cfg(feature = "keychain-secure-enclave")]
561 {
562 if super::secure_enclave::is_available()
563 && let Ok(home) = auths_home_with_config(config)
564 {
565 match super::secure_enclave::SecureEnclaveKeyStorage::new(&home) {
566 Ok(storage) => {
567 log::info!("Using Secure Enclave (Touch ID signing)");
568 return Ok(Box::new(storage));
569 }
570 Err(e) => {
571 log::warn!(
572 "Secure Enclave available but init failed ({}), using macOS Keychain",
573 e
574 );
575 }
576 }
577 }
578 }
579 return Ok(Box::new(MacOSKeychain::new(SERVICE_NAME)));
580 }
581
582 #[cfg(all(target_os = "linux", feature = "keychain-linux-secretservice"))]
583 {
584 match LinuxSecretServiceStorage::new(SERVICE_NAME) {
586 Ok(storage) => return Ok(Box::new(storage)),
587 Err(e) => {
588 warn!("Secret Service unavailable ({}), trying file fallback", e);
589 #[cfg(feature = "keychain-file-fallback")]
590 {
591 return new_encrypted_file_storage(config).map(|s| {
592 let b: Box<dyn KeyStorage + Send + Sync> = Box::new(s);
593 b
594 });
595 }
596 #[cfg(not(feature = "keychain-file-fallback"))]
597 {
598 return Err(e);
599 }
600 }
601 }
602 }
603
604 #[cfg(all(target_os = "linux", not(feature = "keychain-linux-secretservice")))]
605 {
606 #[cfg(feature = "keychain-file-fallback")]
608 {
609 return new_encrypted_file_storage(config).map(|s| {
610 let b: Box<dyn KeyStorage + Send + Sync> = Box::new(s);
611 b
612 });
613 }
614 }
615
616 #[cfg(all(target_os = "windows", feature = "keychain-windows"))]
617 {
618 return Ok(Box::new(WindowsCredentialStorage::new(SERVICE_NAME)?));
619 }
620
621 #[cfg(target_os = "android")]
622 {
623 return Ok(Box::new(AndroidKeystoreStorage::new(SERVICE_NAME)?));
624 }
625
626 #[allow(unused_variables)]
628 let _ = config;
629 #[allow(unreachable_code)]
630 {
631 warn!("Using in-memory keychain (not recommended for production)");
632 Ok(Box::new(MemoryKeychainHandle))
633 }
634}
635
636fn get_backend_by_name(
638 name: &str,
639 config: &EnvironmentConfig,
640) -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
641 match name.to_lowercase().as_str() {
642 "memory" => {
643 info!("Using in-memory keychain (AUTHS_KEYCHAIN_BACKEND=memory)");
644 Ok(Box::new(MemoryKeychainHandle))
645 }
646 "file" => {
647 info!("Using encrypted file storage (AUTHS_KEYCHAIN_BACKEND=file)");
648 let storage = new_encrypted_file_storage(config)?;
649 Ok(Box::new(storage))
650 }
651 #[cfg(feature = "keychain-pkcs11")]
652 "hsm" | "pkcs11" => {
653 info!("Using PKCS#11 HSM backend (AUTHS_KEYCHAIN_BACKEND={name})");
654 let pkcs11_config =
655 config
656 .pkcs11
657 .as_ref()
658 .ok_or_else(|| AgentError::BackendInitFailed {
659 backend: "pkcs11",
660 error: "PKCS#11 configuration required (set AUTHS_PKCS11_LIBRARY)".into(),
661 })?;
662 let storage = super::pkcs11::Pkcs11KeyRef::new(pkcs11_config)?;
663 Ok(Box::new(storage))
664 }
665 #[cfg(all(target_os = "macos", feature = "keychain-secure-enclave"))]
666 "secure-enclave" => {
667 info!("Using Secure Enclave backend (AUTHS_KEYCHAIN_BACKEND=secure-enclave)");
668 let home =
669 auths_home_with_config(config).map_err(|e| AgentError::BackendInitFailed {
670 backend: "secure-enclave",
671 error: format!("failed to resolve auths home: {e}"),
672 })?;
673 let storage = super::secure_enclave::SecureEnclaveKeyStorage::new(&home)?;
674 Ok(Box::new(storage))
675 }
676 _ => {
677 warn!(
678 "Unknown keychain backend '{}', using platform default",
679 name
680 );
681 get_platform_default(config)
682 }
683 }
684}
685
686fn new_encrypted_file_storage(
692 config: &EnvironmentConfig,
693) -> Result<EncryptedFileStorage, AgentError> {
694 let storage = if let Some(ref path) = config.keychain.file_path {
695 EncryptedFileStorage::with_path(path.clone())?
696 } else {
697 let home =
698 auths_home_with_config(config).map_err(|e| AgentError::StorageError(e.to_string()))?;
699 EncryptedFileStorage::new(&home)?
700 };
701
702 if let Some(ref passphrase) = config.keychain.passphrase {
703 storage.set_password(Zeroizing::new(passphrase.clone()));
704 }
705
706 Ok(storage)
707}
708
709#[cfg(feature = "keychain-pkcs11")]
724pub fn get_pkcs11_signer(
725 config: &EnvironmentConfig,
726) -> Result<Option<Box<dyn crate::signing::SecureSigner>>, AgentError> {
727 let is_pkcs11 = config
728 .keychain
729 .backend
730 .as_deref()
731 .map(|b| matches!(b.to_lowercase().as_str(), "hsm" | "pkcs11"))
732 .unwrap_or(false);
733
734 if !is_pkcs11 {
735 return Ok(None);
736 }
737
738 let pkcs11_config = config
739 .pkcs11
740 .as_ref()
741 .ok_or_else(|| AgentError::BackendInitFailed {
742 backend: "pkcs11",
743 error: "PKCS#11 configuration required (set AUTHS_PKCS11_LIBRARY)".into(),
744 })?;
745
746 let signer = super::pkcs11::Pkcs11Signer::new(pkcs11_config)?;
747 Ok(Some(Box::new(signer)))
748}
749
750impl KeyStorage for Arc<dyn KeyStorage + Send + Sync> {
751 fn store_key(
752 &self,
753 alias: &KeyAlias,
754 identity_did: &IdentityDID,
755 role: KeyRole,
756 encrypted_key_data: &[u8],
757 ) -> Result<(), AgentError> {
758 self.as_ref()
759 .store_key(alias, identity_did, role, encrypted_key_data)
760 }
761 fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError> {
762 self.as_ref().load_key(alias)
763 }
764 fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError> {
765 self.as_ref().delete_key(alias)
766 }
767 fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError> {
768 self.as_ref().list_aliases()
769 }
770 fn list_aliases_for_identity(
771 &self,
772 identity_did: &IdentityDID,
773 ) -> Result<Vec<KeyAlias>, AgentError> {
774 self.as_ref().list_aliases_for_identity(identity_did)
775 }
776 fn list_aliases_for_identity_with_role(
777 &self,
778 identity_did: &IdentityDID,
779 role: KeyRole,
780 ) -> Result<Vec<KeyAlias>, AgentError> {
781 self.as_ref()
782 .list_aliases_for_identity_with_role(identity_did, role)
783 }
784 fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError> {
785 self.as_ref().get_identity_for_alias(alias)
786 }
787 fn backend_name(&self) -> &'static str {
788 self.as_ref().backend_name()
789 }
790 fn is_hardware_backend(&self) -> bool {
791 self.as_ref().is_hardware_backend()
792 }
793}
794
795impl KeyStorage for Box<dyn KeyStorage + Send + Sync> {
796 fn store_key(
797 &self,
798 alias: &KeyAlias,
799 identity_did: &IdentityDID,
800 role: KeyRole,
801 encrypted_key_data: &[u8],
802 ) -> Result<(), AgentError> {
803 self.as_ref()
804 .store_key(alias, identity_did, role, encrypted_key_data)
805 }
806 fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError> {
807 self.as_ref().load_key(alias)
808 }
809 fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError> {
810 self.as_ref().delete_key(alias)
811 }
812 fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError> {
813 self.as_ref().list_aliases()
814 }
815 fn list_aliases_for_identity(
816 &self,
817 identity_did: &IdentityDID,
818 ) -> Result<Vec<KeyAlias>, AgentError> {
819 self.as_ref().list_aliases_for_identity(identity_did)
820 }
821 fn list_aliases_for_identity_with_role(
822 &self,
823 identity_did: &IdentityDID,
824 role: KeyRole,
825 ) -> Result<Vec<KeyAlias>, AgentError> {
826 self.as_ref()
827 .list_aliases_for_identity_with_role(identity_did, role)
828 }
829 fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError> {
830 self.as_ref().get_identity_for_alias(alias)
831 }
832 fn backend_name(&self) -> &'static str {
833 self.as_ref().backend_name()
834 }
835 fn is_hardware_backend(&self) -> bool {
836 self.as_ref().is_hardware_backend()
837 }
838}
839
840#[cfg(test)]
841mod tests {
842 use super::*;
843
844 #[test]
845 fn test_service_name_constant() {
846 assert_eq!(SERVICE_NAME, "dev.auths.agent");
847 }
848
849 #[test]
850 fn test_get_backend_by_name_memory() {
851 let env = EnvironmentConfig::default();
852 let backend = get_backend_by_name("memory", &env).unwrap();
853 assert_eq!(backend.backend_name(), "Memory");
854 }
855
856 #[test]
857 fn test_get_backend_by_name_case_insensitive() {
858 let env = EnvironmentConfig::default();
859 let backend = get_backend_by_name("MEMORY", &env).unwrap();
860 assert_eq!(backend.backend_name(), "Memory");
861 }
862
863 #[test]
864 fn test_key_role_serde_roundtrip() {
865 let roles = [
866 KeyRole::Primary,
867 KeyRole::NextRotation,
868 KeyRole::DelegatedAgent,
869 ];
870 for role in &roles {
871 let json = serde_json::to_string(role).unwrap();
872 let parsed: KeyRole = serde_json::from_str(&json).unwrap();
873 assert_eq!(*role, parsed);
874 }
875 }
876
877 #[test]
878 fn test_key_role_display_and_parse() {
879 assert_eq!(KeyRole::Primary.to_string(), "primary");
880 assert_eq!(KeyRole::NextRotation.to_string(), "next_rotation");
881 assert_eq!(KeyRole::DelegatedAgent.to_string(), "delegated_agent");
882 assert_eq!("primary".parse::<KeyRole>().unwrap(), KeyRole::Primary);
883 assert_eq!(
884 "delegated_agent".parse::<KeyRole>().unwrap(),
885 KeyRole::DelegatedAgent
886 );
887 assert!("unknown".parse::<KeyRole>().is_err());
888 }
889
890 #[test]
891 fn a_corrupt_persisted_role_is_refused_not_elevated_to_primary() {
892 assert_eq!(KeyRole::from_persisted("primary"), Ok(KeyRole::Primary));
895 assert_eq!(
896 KeyRole::from_persisted("delegated_agent"),
897 Ok(KeyRole::DelegatedAgent)
898 );
899 assert!(KeyRole::from_persisted("bogus").is_err());
900 assert!(KeyRole::from_persisted("").is_err());
901 assert!(KeyRole::from_persisted("PRIMARY").is_err());
902 }
903
904 #[test]
905 fn migrate_legacy_alias_preserves_the_next_key_commitment() {
906 use super::super::memory::IsolatedKeychainHandle;
907
908 let kc = IsolatedKeychainHandle::new();
913 let did = IdentityDID::parse("did:keri:Etest").unwrap();
914 kc.store_key(
915 &KeyAlias::new_unchecked("main"),
916 &did,
917 KeyRole::Primary,
918 b"current",
919 )
920 .unwrap();
921 kc.store_key(
922 &KeyAlias::new_unchecked("main--next-2"),
923 &did,
924 KeyRole::NextRotation,
925 b"next",
926 )
927 .unwrap();
928
929 let migrated = migrate_legacy_alias(&kc, &KeyAlias::new_unchecked("main")).unwrap();
930 assert_eq!(migrated.as_str(), "main--0");
931
932 assert!(
933 kc.load_key(&KeyAlias::new_unchecked("main--0")).is_ok(),
934 "current key must migrate to slot 0"
935 );
936 assert!(
937 kc.load_key(&KeyAlias::new_unchecked("main--0--next-2"))
938 .is_ok(),
939 "the next-key commitment must migrate with the current key, or rotation is bricked"
940 );
941 assert!(kc.load_key(&KeyAlias::new_unchecked("main")).is_err());
942 assert!(
943 kc.load_key(&KeyAlias::new_unchecked("main--next-2"))
944 .is_err()
945 );
946 }
947
948 #[test]
949 fn migrate_legacy_alias_recovers_an_already_orphaned_next_key() {
950 use super::super::memory::IsolatedKeychainHandle;
951
952 let kc = IsolatedKeychainHandle::new();
956 let did = IdentityDID::parse("did:keri:Etest").unwrap();
957 kc.store_key(
958 &KeyAlias::new_unchecked("main--0"),
959 &did,
960 KeyRole::Primary,
961 b"current",
962 )
963 .unwrap();
964 kc.store_key(
965 &KeyAlias::new_unchecked("main--next-2"),
966 &did,
967 KeyRole::NextRotation,
968 b"next",
969 )
970 .unwrap();
971
972 migrate_legacy_alias(&kc, &KeyAlias::new_unchecked("main")).unwrap();
973
974 assert!(
975 kc.load_key(&KeyAlias::new_unchecked("main--0--next-2"))
976 .is_ok(),
977 "re-running migration must recover an orphaned next-key commitment"
978 );
979 assert!(
980 kc.load_key(&KeyAlias::new_unchecked("main--next-2"))
981 .is_err()
982 );
983 }
984
985 #[test]
986 fn test_list_aliases_with_role_filter() {
987 use super::super::memory::IsolatedKeychainHandle;
988
989 let keychain = IsolatedKeychainHandle::new();
990 let did = IdentityDID::parse("did:keri:Etest").unwrap();
991
992 keychain
993 .store_key(
994 &KeyAlias::new_unchecked("operator"),
995 &did,
996 KeyRole::Primary,
997 b"key1",
998 )
999 .unwrap();
1000 keychain
1001 .store_key(
1002 &KeyAlias::new_unchecked("operator--next-0"),
1003 &did,
1004 KeyRole::NextRotation,
1005 b"key2",
1006 )
1007 .unwrap();
1008 keychain
1009 .store_key(
1010 &KeyAlias::new_unchecked("deploy-agent"),
1011 &did,
1012 KeyRole::DelegatedAgent,
1013 b"key3",
1014 )
1015 .unwrap();
1016
1017 let primary = keychain
1018 .list_aliases_for_identity_with_role(&did, KeyRole::Primary)
1019 .unwrap();
1020 assert_eq!(primary.len(), 1);
1021 assert_eq!(primary[0].as_str(), "operator");
1022
1023 let agents = keychain
1024 .list_aliases_for_identity_with_role(&did, KeyRole::DelegatedAgent)
1025 .unwrap();
1026 assert_eq!(agents.len(), 1);
1027 assert_eq!(agents[0].as_str(), "deploy-agent");
1028 }
1029}