Skip to main content

auths_core/storage/
keychain.rs

1//! Keychain abstraction.
2
3use 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/// Service name used for all platform keychains.
32/// Used inside cfg-gated blocks (macOS, iOS, Linux, Windows, Android).
33#[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
46// Re-exported from auths-verifier (the leaf dependency shared by all crates).
47pub use auths_verifier::IdentityDID;
48
49/// The role a stored key serves within its identity.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
51#[serde(rename_all = "snake_case")]
52pub enum KeyRole {
53    /// The identity's current active signing key.
54    Primary,
55    /// A pre-committed rotation key (not yet active).
56    NextRotation,
57    /// A key delegated to an autonomous agent.
58    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/// Validated alias for a stored key.
84///
85/// Invariants: non-empty and contains no null bytes.
86///
87/// Usage:
88/// ```ignore
89/// let alias = KeyAlias::new("my-signing-key")?;
90/// keychain.store_key(&alias, &did, &encrypted)?;
91/// ```
92#[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    /// Creates a validated `KeyAlias`.
101    ///
102    /// Rejects empty strings and strings containing null bytes.
103    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    /// Wraps a string without validation (for trusted internal paths).
119    pub fn new_unchecked<S: Into<String>>(s: S) -> Self {
120        Self(s.into())
121    }
122
123    /// Returns the alias as a string slice.
124    pub fn as_str(&self) -> &str {
125        &self.0
126    }
127
128    /// Consumes self and returns the inner `String`.
129    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
183/// Migrate a legacy single-key alias `{alias}` to the multi-key form
184/// `{alias}--0`.
185///
186/// Idempotent: safe to call whether or not the migration has already
187/// happened. Reads the legacy slot, writes to the new slot, deletes the
188/// legacy slot. Returns the new alias form.
189///
190/// Behavior matrix:
191/// - Both legacy and `--0` present: no-op, returns `{alias}--0` (assumes
192///   `--0` is authoritative since it's the new convention).
193/// - Only legacy present: copies to `--0`, deletes legacy, returns `--0`.
194/// - Only `--0` present: no-op, returns `--0`.
195/// - Neither present: returns `--0` anyway; the caller's next `load_key`
196///   will surface the absence.
197pub 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        // Already migrated (or a fresh multi-key identity was created without
205        // ever touching the legacy alias). In either case, new form wins.
206        return Ok(new_alias);
207    }
208
209    let (did, role, data) = match keychain.load_key(old_alias) {
210        Ok(v) => v,
211        Err(_) => {
212            // Legacy absent too; nothing to migrate. Caller hits absence on
213            // their next load_key call.
214            return Ok(new_alias);
215        }
216    };
217
218    keychain.store_key(&new_alias, &did, role, &data)?;
219    // Best-effort legacy cleanup; failure here isn't fatal.
220    let _ = keychain.delete_key(old_alias);
221    Ok(new_alias)
222}
223
224/// Platform-agnostic interface for storing and loading private keys securely.
225///
226/// All implementors must be Send + Sync for thread-safe access.
227pub trait KeyStorage: Send + Sync {
228    /// Stores encrypted key data associated with an alias AND an identity DID.
229    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    /// Loads the encrypted key data AND the associated identity DID for a given alias.
238    fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError>;
239
240    /// Deletes a key by its alias.
241    fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError>;
242
243    /// Lists all aliases stored by this backend for the specific service.
244    fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError>;
245
246    /// Lists aliases associated ONLY with the given identity DID.
247    fn list_aliases_for_identity(
248        &self,
249        identity_did: &IdentityDID,
250    ) -> Result<Vec<KeyAlias>, AgentError>;
251
252    /// List aliases for an identity filtered by role.
253    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    /// Retrieves the identity DID associated with a given alias.
271    fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError>;
272
273    /// Returns the name of the storage backend.
274    fn backend_name(&self) -> &'static str;
275
276    /// Returns true if this backend manages keys in hardware (SE, HSM).
277    ///
278    /// Hardware backends generate keys internally, don't need passphrases,
279    /// and store opaque handles instead of encrypted PKCS8.
280    fn is_hardware_backend(&self) -> bool {
281        false
282    }
283
284    /// Re-associates an existing stored key with a different identity DID
285    /// without touching the key material.
286    ///
287    /// Needed by KERI inception: the key must exist (so its public key can be
288    /// read) before the identity prefix it belongs to can be derived. The
289    /// default implementation round-trips the stored material through
290    /// `store_key`, which is correct for software backends. Hardware backends
291    /// MUST override this — their `store_key` generates a fresh key, which
292    /// would orphan the original.
293    ///
294    /// Args:
295    /// * `alias`: Alias of the already-stored key.
296    /// * `identity_did`: The identity to associate the key with.
297    ///
298    /// Usage:
299    /// ```ignore
300    /// keychain.rebind_identity(&alias, &controller_did)?;
301    /// ```
302    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    /// Returns the raw public key bytes of a stored key without a passphrase.
318    ///
319    /// Hardware backends derive this from their opaque handle; software
320    /// backends would need a passphrase to decrypt the stored PKCS8, so the
321    /// default errors — use `extract_public_key_bytes` for those.
322    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    /// Signs a message with a stored key without a passphrase.
331    ///
332    /// Hardware backends sign inside the hardware (may trigger a biometric
333    /// prompt); software backends need a passphrase, so the default errors —
334    /// use `SecureSigner::sign_with_alias` for those.
335    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
344/// Decrypt a stored key and return its public key bytes and curve type.
345///
346/// Loads the encrypted key for `alias`, calls `passphrase_provider` to obtain
347/// the decryption passphrase, decrypts the PKCS8 blob, and returns the raw
348/// public key along with its curve type.
349///
350/// Args:
351/// * `keychain`: The key storage backend holding the encrypted key.
352/// * `alias`: Keychain alias of the stored key.
353/// * `passphrase_provider`: Provider to obtain the decryption passphrase.
354///
355/// Usage:
356/// ```ignore
357/// let (pk, curve) = extract_public_key_bytes(keychain, "my-key", &provider)?;
358/// ```
359pub 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        // Hardware backends store opaque handles — get public key from hardware
366        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
392/// Sign a message using the key for `alias`, handling both software and hardware backends.
393///
394/// - Software: prompts for passphrase, decrypts PKCS8, signs with `TypedSeed`
395/// - Hardware (Secure Enclave): loads handle, signs via biometric (Touch ID)
396///
397/// Returns `(signature, public_key, curve)`. Callers never need to know the backend type.
398///
399/// Args:
400/// * `keychain`: The key storage backend.
401/// * `alias`: The key alias to sign with.
402/// * `passphrase_provider`: Provider for passphrase prompts (ignored on hardware backends).
403/// * `message`: The raw bytes to sign.
404pub 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
447/// Return a boxed `KeyStorage` implementation driven by the supplied `EnvironmentConfig`.
448///
449/// Uses `config.keychain.backend` to select the backend and `config.keychain.file_path`
450/// / `config.keychain.passphrase` for the encrypted-file backend. Falls back to the
451/// platform default when no override is specified.
452///
453/// Args:
454/// * `config` - The environment configuration carrying keychain settings and home path.
455///
456/// Usage:
457/// ```ignore
458/// let env = EnvironmentConfig::from_env();
459/// let keychain = get_platform_keychain_with_config(&env)?;
460/// ```
461pub 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
470/// Return a boxed KeyStorage implementation for the current platform.
471///
472/// Reads keychain configuration from environment variables via
473/// `EnvironmentConfig::from_env()`. Prefer `get_platform_keychain_with_config`
474/// for new code to keep env-var reads at the process boundary.
475///
476/// # Environment Variable Override
477///
478/// Set `AUTHS_KEYCHAIN_BACKEND` to override the platform default:
479/// - `"file"` - Use encrypted file storage at `~/.auths/keys.enc`
480/// - `"memory"` - Use in-memory storage (for testing only)
481///
482/// Invalid values will log a warning and use the platform default.
483///
484/// # Errors
485/// Returns `AgentError` if the platform keychain fails to initialize.
486pub fn get_platform_keychain() -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
487    get_platform_keychain_with_config(&EnvironmentConfig::from_env())
488}
489
490/// Get the platform-default keychain backend.
491#[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        // Try Secure Enclave first (fingerprint signing), fall back to macOS Keychain
503        #[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        // Try Secret Service first, fall back to encrypted file storage
528        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        // No Secret Service feature, check for file fallback
550        #[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    // Fallback for unsupported platforms or missing features
570    #[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
579/// Get a keychain backend by name (for environment variable override).
580fn 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
629/// Construct an `EncryptedFileStorage` from the provided config.
630///
631/// Uses `config.keychain.file_path` when set; otherwise resolves the default
632/// path from `config.auths_home` (or `~/.auths/keys.enc`).
633/// Sets the password from `config.keychain.passphrase` when present.
634fn 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/// Creates a PKCS#11-backed [`SecureSigner`](crate::signing::SecureSigner) from the
653/// environment config, if the backend is set to `"pkcs11"` or `"hsm"`.
654///
655/// Returns `None` if the keychain backend is not PKCS#11.
656///
657/// Args:
658/// * `config`: Environment configuration.
659///
660/// Usage:
661/// ```ignore
662/// if let Some(signer) = get_pkcs11_signer(&env)? {
663///     signer.sign_with_alias(&alias, &provider, message)?;
664/// }
665/// ```
666#[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        // INVARIANT: test-only literal with valid did:keri: prefix
840        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}