Skip to main content

auths_core/
signing.rs

1//! Signing abstractions and DID resolution.
2
3use auths_verifier::core::Ed25519PublicKey;
4
5use crate::crypto::provider_bridge;
6use crate::crypto::signer::{decrypt_keypair, extract_seed_from_key_bytes};
7use crate::error::AgentError;
8use crate::storage::keychain::{IdentityDID, KeyAlias, KeyStorage};
9
10use crate::config::PassphraseCachePolicy;
11use crate::storage::passphrase_cache::PassphraseCache;
12
13use std::collections::HashMap;
14use std::sync::{Arc, Mutex};
15use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
16use zeroize::Zeroizing;
17
18/// Type alias for passphrase callback functions.
19type PassphraseCallback = dyn Fn(&str) -> Result<Zeroizing<String>, AgentError> + Send + Sync;
20
21/// Error type for DID resolution.
22///
23/// Args:
24/// * Variants represent distinct failure modes during DID resolution.
25///
26/// Usage:
27/// ```ignore
28/// use auths_core::signing::DidResolverError;
29///
30/// let err = DidResolverError::UnsupportedMethod("web".to_string());
31/// assert!(err.to_string().contains("Unsupported"));
32/// ```
33#[derive(Debug, thiserror::Error)]
34#[non_exhaustive]
35pub enum DidResolverError {
36    /// The DID method is not supported.
37    #[error("Unsupported DID method: {0}")]
38    UnsupportedMethod(String),
39
40    /// The did:key identifier is invalid.
41    #[error("Invalid did:key format: {0}")]
42    InvalidDidKey(String),
43
44    /// The did:key format is malformed.
45    #[error("Invalid did:key format: {0}")]
46    InvalidDidKeyFormat(String),
47
48    /// Failed to decode the did:key.
49    #[error("did:key decoding failed: {0}")]
50    DidKeyDecodingFailed(String),
51
52    /// Unsupported multicodec prefix in did:key.
53    #[error("Invalid did:key multicodec prefix")]
54    InvalidDidKeyMulticodec,
55
56    /// DID resolution failed.
57    #[error("Resolution error: {0}")]
58    Resolution(String),
59
60    /// Repository access failed.
61    #[error("Repository error: {0}")]
62    Repository(String),
63}
64
65/// Result of DID resolution, parameterised by method.
66///
67/// Usage:
68/// ```ignore
69/// use auths_core::signing::ResolvedDid;
70/// use auths_verifier::core::Ed25519PublicKey;
71///
72/// let resolved = ResolvedDid::Key {
73///     did: "did:key:z6Mk...".to_string(),
74///     public_key: Ed25519PublicKey::from_bytes([1u8; 32]),
75/// };
76/// assert!(resolved.is_key());
77/// ```
78#[derive(Debug, Clone)]
79pub enum ResolvedDid {
80    /// Static did:key (no rotation possible).
81    Key {
82        /// The resolved DID string.
83        did: String,
84        /// The Ed25519 public key.
85        public_key: Ed25519PublicKey,
86    },
87    /// KERI-based identity with rotation capability.
88    Keri {
89        /// The resolved DID string.
90        did: String,
91        /// The Ed25519 public key.
92        public_key: Ed25519PublicKey,
93        /// Current KEL sequence number.
94        sequence: u64,
95        /// Whether key rotation is available.
96        can_rotate: bool,
97    },
98}
99
100impl ResolvedDid {
101    /// Returns the DID string.
102    pub fn did(&self) -> &str {
103        match self {
104            ResolvedDid::Key { did, .. } | ResolvedDid::Keri { did, .. } => did,
105        }
106    }
107
108    /// Returns the Ed25519 public key.
109    pub fn public_key(&self) -> &Ed25519PublicKey {
110        match self {
111            ResolvedDid::Key { public_key, .. } | ResolvedDid::Keri { public_key, .. } => {
112                public_key
113            }
114        }
115    }
116
117    /// Returns `true` if this is a `did:key` resolution.
118    pub fn is_key(&self) -> bool {
119        matches!(self, ResolvedDid::Key { .. })
120    }
121
122    /// Returns `true` if this is a `did:keri` resolution.
123    pub fn is_keri(&self) -> bool {
124        matches!(self, ResolvedDid::Keri { .. })
125    }
126}
127
128/// Resolves a Decentralized Identifier (DID) to its cryptographic material.
129///
130/// Implementations handle specific DID methods (did:key, did:keri) and return
131/// the resolved public key along with method-specific metadata. The resolver
132/// abstracts away the underlying storage and network access needed for resolution.
133///
134/// Args:
135/// * `did`: A DID string (e.g., `"did:keri:EABC..."` or `"did:key:z6Mk..."`).
136///
137/// Usage:
138/// ```ignore
139/// use auths_core::signing::DidResolver;
140///
141/// fn verify_attestation(resolver: &dyn DidResolver, issuer_did: &str) -> bool {
142///     match resolver.resolve(issuer_did) {
143///         Ok(resolved) => {
144///             let public_key = resolved.public_key();
145///             // use public_key for signature verification
146///             true
147///         }
148///         Err(_) => false,
149///     }
150/// }
151/// ```
152pub trait DidResolver: Send + Sync {
153    /// Resolve a DID to its public key and method.
154    fn resolve(&self, did: &str) -> Result<ResolvedDid, DidResolverError>;
155}
156
157/// A trait for components that can securely provide a passphrase when requested.
158///
159/// This allows the core signing logic to request a passphrase without knowing
160/// whether it's coming from a terminal prompt, a GUI dialog, or another source.
161/// Implementors should handle secure input and potential user cancellation.
162pub trait PassphraseProvider: Send + Sync {
163    /// Securely obtains a passphrase, potentially by prompting the user.
164    ///
165    /// Args:
166    /// * `prompt_message`: A message to display to the user indicating why the passphrase is needed.
167    ///
168    /// Usage:
169    /// ```ignore
170    /// let passphrase = provider.get_passphrase("Enter passphrase for key 'main':")?;
171    /// ```
172    fn get_passphrase(&self, prompt_message: &str) -> Result<Zeroizing<String>, AgentError>;
173
174    /// Notifies the provider that the passphrase returned for `prompt_message` was wrong.
175    ///
176    /// The default implementation is a no-op. Caching providers override this to
177    /// evict the stale entry so subsequent calls prompt the user again rather than
178    /// replaying a known-bad passphrase.
179    ///
180    /// Args:
181    /// * `prompt_message`: The prompt for which the bad passphrase was cached.
182    fn on_incorrect_passphrase(&self, _prompt_message: &str) {}
183}
184
185/// A trait for components that can perform signing operations using stored keys,
186/// identified by an alias, while securely handling decryption and passphrase input.
187pub trait SecureSigner: Send + Sync {
188    /// Requests a signature for the given message using the key identified by the alias.
189    ///
190    /// This method handles loading the encrypted key, obtaining the necessary passphrase
191    /// via the provided `PassphraseProvider`, decrypting the key, performing the signature,
192    /// and ensuring the decrypted key material is handled securely.
193    ///
194    /// # Arguments
195    /// * `alias`: The alias of the key to use for signing.
196    /// * `passphrase_provider`: An implementation of `PassphraseProvider` used to obtain the passphrase if needed.
197    /// * `message`: The message bytes to be signed.
198    ///
199    /// # Returns
200    /// * `Ok(Vec<u8>)`: The raw signature bytes.
201    /// * `Err(AgentError)`: If any step fails (key not found, incorrect passphrase, decryption error, signing error, etc.).
202    fn sign_with_alias(
203        &self,
204        alias: &KeyAlias,
205        passphrase_provider: &dyn PassphraseProvider,
206        message: &[u8],
207    ) -> Result<Vec<u8>, AgentError>;
208
209    /// Signs a message using the key associated with the given identity DID.
210    ///
211    /// This method resolves the identity DID to an alias by looking up keys
212    /// associated with that identity in storage, then delegates to `sign_with_alias`.
213    ///
214    /// # DID to Alias Resolution Strategy
215    /// The implementation uses the storage backend's `list_aliases_for_identity`
216    /// to find aliases associated with the given DID. The first matching alias
217    /// is used for signing.
218    ///
219    /// # Arguments
220    /// * `identity_did`: The identity DID (e.g., "did:keri:ABC...") to sign for.
221    /// * `passphrase_provider`: Used to obtain the passphrase for key decryption.
222    /// * `message`: The message bytes to be signed.
223    ///
224    /// # Returns
225    /// * `Ok(Vec<u8>)`: The raw signature bytes.
226    /// * `Err(AgentError)`: If no key is found for the identity, or if signing fails.
227    fn sign_for_identity(
228        &self,
229        identity_did: &IdentityDID,
230        passphrase_provider: &dyn PassphraseProvider,
231        message: &[u8],
232    ) -> Result<Vec<u8>, AgentError>;
233}
234
235/// Concrete implementation of `SecureSigner` that uses a `KeyStorage` backend.
236///
237/// It requires a `PassphraseProvider` to be passed into the signing method
238/// to handle user interaction for passphrase input securely.
239pub struct StorageSigner<S: KeyStorage> {
240    /// The storage backend implementation (e.g., IOSKeychain, MacOSKeychain).
241    storage: S,
242}
243
244impl<S: KeyStorage> StorageSigner<S> {
245    /// Creates a new `StorageSigner` with the given storage backend.
246    pub fn new(storage: S) -> Self {
247        Self { storage }
248    }
249
250    /// Returns a reference to the underlying storage backend.
251    pub fn inner(&self) -> &S {
252        &self.storage
253    }
254}
255
256impl<S: KeyStorage + Send + Sync + 'static> SecureSigner for StorageSigner<S> {
257    fn sign_with_alias(
258        &self,
259        alias: &KeyAlias,
260        passphrase_provider: &dyn PassphraseProvider,
261        message: &[u8],
262    ) -> Result<Vec<u8>, AgentError> {
263        let (_identity_did, _role, encrypted_data) = self.storage.load_key(alias)?;
264
265        const MAX_ATTEMPTS: u8 = 3;
266        let mut attempt = 0u8;
267        let key_bytes = loop {
268            let prompt = if attempt == 0 {
269                format!("Enter passphrase for key '{}' to sign:", alias)
270            } else {
271                format!(
272                    "Incorrect passphrase, try again ({}/{}):",
273                    attempt + 1,
274                    MAX_ATTEMPTS
275                )
276            };
277
278            let passphrase = passphrase_provider.get_passphrase(&prompt)?;
279
280            match decrypt_keypair(&encrypted_data, &passphrase) {
281                Ok(kb) => break kb,
282                Err(AgentError::IncorrectPassphrase) if attempt + 1 < MAX_ATTEMPTS => {
283                    passphrase_provider.on_incorrect_passphrase(&prompt);
284                    attempt += 1;
285                }
286                Err(e) => return Err(e),
287            }
288        };
289
290        let seed = extract_seed_from_key_bytes(&key_bytes)?;
291
292        provider_bridge::sign_ed25519_sync(&seed, message)
293            .map_err(|e| AgentError::CryptoError(format!("Ed25519 signing failed: {}", e)))
294    }
295
296    fn sign_for_identity(
297        &self,
298        identity_did: &IdentityDID,
299        passphrase_provider: &dyn PassphraseProvider,
300        message: &[u8],
301    ) -> Result<Vec<u8>, AgentError> {
302        // 1. Find aliases associated with this identity DID
303        let aliases = self.storage.list_aliases_for_identity(identity_did)?;
304
305        // 2. Get the first alias (primary key for this identity)
306        let alias = aliases.first().ok_or(AgentError::KeyNotFound)?;
307
308        // 3. Delegate to sign_with_alias
309        self.sign_with_alias(alias, passphrase_provider, message)
310    }
311}
312
313/// A `PassphraseProvider` that delegates to a callback function.
314///
315/// This is useful for GUI applications and FFI bindings where the passphrase
316/// input mechanism is provided externally.
317///
318/// # Examples
319///
320/// ```ignore
321/// use auths_core::signing::{CallbackPassphraseProvider, PassphraseProvider};
322///
323/// let provider = CallbackPassphraseProvider::new(|prompt| {
324///     // In a real GUI, this would show a dialog
325///     Ok("user-entered-passphrase".to_string())
326/// });
327/// ```
328pub struct CallbackPassphraseProvider {
329    callback: Box<PassphraseCallback>,
330}
331
332impl CallbackPassphraseProvider {
333    /// Creates a new `CallbackPassphraseProvider` with the given callback function.
334    ///
335    /// The callback receives the prompt message and should return the passphrase
336    /// entered by the user, or an error if passphrase acquisition failed.
337    pub fn new<F>(callback: F) -> Self
338    where
339        F: Fn(&str) -> Result<Zeroizing<String>, AgentError> + Send + Sync + 'static,
340    {
341        Self {
342            callback: Box::new(callback),
343        }
344    }
345}
346
347impl PassphraseProvider for CallbackPassphraseProvider {
348    fn get_passphrase(&self, prompt_message: &str) -> Result<Zeroizing<String>, AgentError> {
349        (self.callback)(prompt_message)
350    }
351}
352
353/// A `PassphraseProvider` that caches passphrases from an inner provider.
354///
355/// Cached values are stored in `Zeroizing<String>` for automatic zeroing on drop
356/// and expire after the configured TTL (time-to-live).
357///
358/// This is useful for agent sessions where prompting for every signing operation
359/// would be disruptive, but credentials shouldn't persist indefinitely.
360///
361/// # Security Considerations
362/// - Cached passphrases are wrapped in `Zeroizing<String>` for secure memory cleanup
363/// - TTL prevents stale credentials from persisting
364/// - Call `clear_cache()` on logout or lock events
365pub struct CachedPassphraseProvider {
366    inner: Arc<dyn PassphraseProvider + Send + Sync>,
367    cache: Mutex<HashMap<String, (Zeroizing<String>, Instant)>>,
368    ttl: Duration,
369}
370
371impl CachedPassphraseProvider {
372    /// Creates a new `CachedPassphraseProvider` wrapping the given provider.
373    ///
374    /// # Arguments
375    /// * `inner` - The underlying provider to fetch passphrases from on cache miss
376    /// * `ttl` - How long cached passphrases remain valid before expiring
377    pub fn new(inner: Arc<dyn PassphraseProvider + Send + Sync>, ttl: Duration) -> Self {
378        Self {
379            inner,
380            cache: Mutex::new(HashMap::new()),
381            ttl,
382        }
383    }
384
385    /// Clears all cached passphrases.
386    ///
387    /// Call this on logout, lock, or when the session ends to ensure
388    /// cached credentials don't persist in memory.
389    pub fn clear_cache(&self) {
390        self.cache.lock().unwrap_or_else(|e| e.into_inner()).clear();
391    }
392}
393
394impl PassphraseProvider for CachedPassphraseProvider {
395    fn get_passphrase(&self, prompt_message: &str) -> Result<Zeroizing<String>, AgentError> {
396        let mut cache = self
397            .cache
398            .lock()
399            .map_err(|e| AgentError::MutexError(e.to_string()))?;
400
401        // Check cache for unexpired entry
402        if let Some((passphrase, cached_at)) = cache.get(prompt_message) {
403            if cached_at.elapsed() < self.ttl {
404                // Clone the inner String and wrap in new Zeroizing
405                return Ok(passphrase.clone());
406            }
407            // Expired - remove the entry
408            cache.remove(prompt_message);
409        }
410
411        // Cache miss or expired - fetch from inner provider
412        drop(cache); // Release lock before calling inner to avoid deadlock
413        let passphrase = self.inner.get_passphrase(prompt_message)?;
414
415        // Store in cache - clone the passphrase since we return the original
416        let mut cache = self
417            .cache
418            .lock()
419            .map_err(|e| AgentError::MutexError(e.to_string()))?;
420        cache.insert(
421            prompt_message.to_string(),
422            (passphrase.clone(), Instant::now()),
423        );
424        Ok(passphrase)
425    }
426
427    fn on_incorrect_passphrase(&self, prompt_message: &str) {
428        self.cache
429            .lock()
430            .unwrap_or_else(|e| e.into_inner())
431            .remove(prompt_message);
432    }
433}
434
435/// A `PassphraseProvider` that wraps an inner provider with OS keychain caching.
436///
437/// On `get_passphrase()`, checks the OS keychain first via `PassphraseCache::load`.
438/// If a cached value exists and hasn't expired per the configured policy/TTL,
439/// returns it immediately. Otherwise delegates to the inner provider, then
440/// stores the result in the OS keychain for subsequent invocations.
441///
442/// Args:
443/// * `inner`: The underlying provider to prompt the user when cache misses.
444/// * `cache`: Platform keychain cache (macOS Security Framework, Linux Secret Service, etc.).
445/// * `alias`: Key alias used as the cache key in the OS keychain.
446/// * `policy`: The configured `PassphraseCachePolicy`.
447/// * `ttl_secs`: Optional TTL in seconds (for `Duration` policy).
448///
449/// Usage:
450/// ```ignore
451/// use auths_core::signing::{KeychainPassphraseProvider, PassphraseProvider};
452/// use auths_core::config::PassphraseCachePolicy;
453/// use auths_core::storage::passphrase_cache::get_passphrase_cache;
454///
455/// let inner = Arc::new(some_provider);
456/// let cache = get_passphrase_cache(true);
457/// let provider = KeychainPassphraseProvider::new(
458///     inner, cache, "main".to_string(),
459///     PassphraseCachePolicy::Duration, Some(3600),
460/// );
461/// let passphrase = provider.get_passphrase("Enter passphrase:")?;
462/// ```
463pub struct KeychainPassphraseProvider {
464    inner: Arc<dyn PassphraseProvider + Send + Sync>,
465    cache: Box<dyn PassphraseCache>,
466    alias: String,
467    policy: PassphraseCachePolicy,
468    ttl_secs: Option<i64>,
469}
470
471impl KeychainPassphraseProvider {
472    /// Creates a new `KeychainPassphraseProvider`.
473    ///
474    /// Args:
475    /// * `inner`: Fallback provider for cache misses.
476    /// * `cache`: OS keychain cache implementation.
477    /// * `alias`: Key alias used as the keychain entry identifier.
478    /// * `policy`: Caching policy controlling storage/expiry behavior.
479    /// * `ttl_secs`: TTL in seconds when `policy` is `Duration`.
480    pub fn new(
481        inner: Arc<dyn PassphraseProvider + Send + Sync>,
482        cache: Box<dyn PassphraseCache>,
483        alias: String,
484        policy: PassphraseCachePolicy,
485        ttl_secs: Option<i64>,
486    ) -> Self {
487        Self {
488            inner,
489            cache,
490            alias,
491            policy,
492            ttl_secs,
493        }
494    }
495
496    #[allow(clippy::disallowed_methods)] // Passphrase cache is a system boundary
497    fn is_expired(&self, stored_at_unix: i64) -> bool {
498        match self.policy {
499            PassphraseCachePolicy::Always => false,
500            PassphraseCachePolicy::Never => true,
501            PassphraseCachePolicy::Session => true,
502            PassphraseCachePolicy::Duration => {
503                let ttl = self.ttl_secs.unwrap_or(3600);
504                let now = SystemTime::now()
505                    .duration_since(UNIX_EPOCH)
506                    .unwrap_or_default()
507                    .as_secs() as i64;
508                now - stored_at_unix > ttl
509            }
510        }
511    }
512}
513
514impl PassphraseProvider for KeychainPassphraseProvider {
515    #[allow(clippy::disallowed_methods)] // Passphrase cache is a system boundary
516    fn get_passphrase(&self, prompt_message: &str) -> Result<Zeroizing<String>, AgentError> {
517        if self.policy != PassphraseCachePolicy::Never
518            && let Ok(Some((passphrase, stored_at))) = self.cache.load(&self.alias)
519        {
520            if !self.is_expired(stored_at) {
521                return Ok(passphrase);
522            }
523            let _ = self.cache.delete(&self.alias);
524        }
525
526        let passphrase = self.inner.get_passphrase(prompt_message)?;
527
528        if self.policy != PassphraseCachePolicy::Never
529            && self.policy != PassphraseCachePolicy::Session
530        {
531            let now = SystemTime::now()
532                .duration_since(UNIX_EPOCH)
533                .unwrap_or_default()
534                .as_secs() as i64;
535            let _ = self.cache.store(&self.alias, &passphrase, now);
536        }
537
538        Ok(passphrase)
539    }
540
541    fn on_incorrect_passphrase(&self, prompt_message: &str) {
542        let _ = self.cache.delete(&self.alias);
543        self.inner.on_incorrect_passphrase(prompt_message);
544    }
545}
546
547/// Provides a pre-collected passphrase for headless and automated environments.
548///
549/// Unlike [`CallbackPassphraseProvider`] which prompts interactively, this provider
550/// returns a passphrase that was collected or generated before construction.
551/// Intended for CI pipelines, Terraform providers, REST APIs, and integration tests.
552///
553/// Args:
554/// * `passphrase`: The passphrase to return on every `get_passphrase()` call.
555///
556/// Usage:
557/// ```ignore
558/// use auths_core::signing::{PrefilledPassphraseProvider, PassphraseProvider};
559///
560/// let provider = PrefilledPassphraseProvider::new("my-secret-passphrase");
561/// let passphrase = provider.get_passphrase("any prompt").unwrap();
562/// assert_eq!(*passphrase, "my-secret-passphrase");
563/// ```
564pub struct PrefilledPassphraseProvider {
565    passphrase: Zeroizing<String>,
566}
567
568impl PrefilledPassphraseProvider {
569    /// Creates a new `PrefilledPassphraseProvider` with the given passphrase.
570    ///
571    /// Args:
572    /// * `passphrase`: The passphrase string to store and return on every request.
573    ///
574    /// Usage:
575    /// ```ignore
576    /// let provider = PrefilledPassphraseProvider::new("hunter2");
577    /// ```
578    pub fn new(passphrase: &str) -> Self {
579        Self {
580            passphrase: Zeroizing::new(passphrase.to_string()),
581        }
582    }
583}
584
585impl PassphraseProvider for PrefilledPassphraseProvider {
586    fn get_passphrase(&self, _prompt_message: &str) -> Result<Zeroizing<String>, AgentError> {
587        Ok(self.passphrase.clone())
588    }
589}
590
591/// A passphrase provider that prompts exactly once regardless of how many
592/// distinct prompt messages are presented. Every call after the first is a
593/// cache hit. Designed for multi-key operations (e.g. device link) where the
594/// same passphrase protects all keys in the operation.
595pub struct UnifiedPassphraseProvider {
596    inner: Arc<dyn PassphraseProvider + Send + Sync>,
597    cached: Mutex<Option<Zeroizing<String>>>,
598}
599
600impl UnifiedPassphraseProvider {
601    /// Create a provider wrapping the given passphrase source.
602    pub fn new(inner: Arc<dyn PassphraseProvider + Send + Sync>) -> Self {
603        Self {
604            inner,
605            cached: Mutex::new(None),
606        }
607    }
608}
609
610impl PassphraseProvider for UnifiedPassphraseProvider {
611    fn get_passphrase(&self, prompt_message: &str) -> Result<Zeroizing<String>, AgentError> {
612        let mut guard = self
613            .cached
614            .lock()
615            .map_err(|e| AgentError::MutexError(e.to_string()))?;
616        if let Some(ref cached) = *guard {
617            return Ok(Zeroizing::new(cached.as_str().to_string()));
618        }
619        let passphrase = self.inner.get_passphrase(prompt_message)?;
620        *guard = Some(Zeroizing::new(passphrase.as_str().to_string()));
621        Ok(passphrase)
622    }
623}
624
625#[cfg(test)]
626mod tests {
627    use super::*;
628    use crate::crypto::signer::encrypt_keypair;
629    use ring::rand::SystemRandom;
630    use ring::signature::{ED25519, Ed25519KeyPair, KeyPair, UnparsedPublicKey};
631    use std::collections::HashMap;
632    use std::sync::Mutex;
633
634    use crate::storage::keychain::KeyRole;
635
636    /// Mock KeyStorage implementation for testing
637    struct MockKeyStorage {
638        #[allow(clippy::type_complexity)]
639        keys: Mutex<HashMap<String, (IdentityDID, KeyRole, Vec<u8>)>>,
640    }
641
642    impl MockKeyStorage {
643        fn new() -> Self {
644            Self {
645                keys: Mutex::new(HashMap::new()),
646            }
647        }
648    }
649
650    impl KeyStorage for MockKeyStorage {
651        fn store_key(
652            &self,
653            alias: &KeyAlias,
654            identity_did: &IdentityDID,
655            role: KeyRole,
656            encrypted_key_data: &[u8],
657        ) -> Result<(), AgentError> {
658            self.keys.lock().unwrap().insert(
659                alias.as_str().to_string(),
660                (identity_did.clone(), role, encrypted_key_data.to_vec()),
661            );
662            Ok(())
663        }
664
665        fn load_key(
666            &self,
667            alias: &KeyAlias,
668        ) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError> {
669            self.keys
670                .lock()
671                .unwrap()
672                .get(alias.as_str())
673                .cloned()
674                .ok_or(AgentError::KeyNotFound)
675        }
676
677        fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError> {
678            self.keys
679                .lock()
680                .unwrap()
681                .remove(alias.as_str())
682                .map(|_| ())
683                .ok_or(AgentError::KeyNotFound)
684        }
685
686        fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError> {
687            Ok(self
688                .keys
689                .lock()
690                .unwrap()
691                .keys()
692                .map(|s| KeyAlias::new_unchecked(s.clone()))
693                .collect())
694        }
695
696        fn list_aliases_for_identity(
697            &self,
698            identity_did: &IdentityDID,
699        ) -> Result<Vec<KeyAlias>, AgentError> {
700            Ok(self
701                .keys
702                .lock()
703                .unwrap()
704                .iter()
705                .filter(|(_, (did, _role, _))| did == identity_did)
706                .map(|(alias, _)| KeyAlias::new_unchecked(alias.clone()))
707                .collect())
708        }
709
710        fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError> {
711            self.keys
712                .lock()
713                .unwrap()
714                .get(alias.as_str())
715                .map(|(did, _role, _)| did.clone())
716                .ok_or(AgentError::KeyNotFound)
717        }
718
719        fn backend_name(&self) -> &'static str {
720            "MockKeyStorage"
721        }
722    }
723
724    /// Mock PassphraseProvider that returns a fixed passphrase
725    struct MockPassphraseProvider {
726        passphrase: String,
727    }
728
729    impl MockPassphraseProvider {
730        fn new(passphrase: &str) -> Self {
731            Self {
732                passphrase: passphrase.to_string(),
733            }
734        }
735    }
736
737    impl PassphraseProvider for MockPassphraseProvider {
738        fn get_passphrase(&self, _prompt_message: &str) -> Result<Zeroizing<String>, AgentError> {
739            Ok(Zeroizing::new(self.passphrase.clone()))
740        }
741    }
742
743    fn generate_test_keypair() -> (Vec<u8>, Vec<u8>) {
744        let rng = SystemRandom::new();
745        let pkcs8_doc = Ed25519KeyPair::generate_pkcs8(&rng).expect("Failed to generate PKCS#8");
746        let pkcs8_bytes = pkcs8_doc.as_ref().to_vec();
747        let keypair = Ed25519KeyPair::from_pkcs8(&pkcs8_bytes).expect("Failed to parse PKCS#8");
748        let pubkey_bytes = keypair.public_key().as_ref().to_vec();
749        (pkcs8_bytes, pubkey_bytes)
750    }
751
752    #[test]
753    fn test_sign_for_identity_success() {
754        let (pkcs8_bytes, pubkey_bytes) = generate_test_keypair();
755        let passphrase = "Test-P@ss12345";
756        let identity_did = IdentityDID::new("did:keri:ABC123");
757        let alias = KeyAlias::new_unchecked("test-key-alias");
758
759        // Encrypt the key
760        let encrypted = encrypt_keypair(&pkcs8_bytes, passphrase).expect("Failed to encrypt");
761
762        // Set up mock storage with the key
763        let storage = MockKeyStorage::new();
764        storage
765            .store_key(&alias, &identity_did, KeyRole::Primary, &encrypted)
766            .expect("Failed to store key");
767
768        // Create signer and mocks
769        let signer = StorageSigner::new(storage);
770        let passphrase_provider = MockPassphraseProvider::new(passphrase);
771
772        // Sign a message
773        let message = b"test message for sign_for_identity";
774        let signature = signer
775            .sign_for_identity(&identity_did, &passphrase_provider, message)
776            .expect("Signing failed");
777
778        // Verify the signature
779        let public_key = UnparsedPublicKey::new(&ED25519, &pubkey_bytes);
780        assert!(public_key.verify(message, &signature).is_ok());
781    }
782
783    #[test]
784    fn test_sign_for_identity_no_key_for_identity() {
785        let storage = MockKeyStorage::new();
786        let signer = StorageSigner::new(storage);
787        let passphrase_provider = MockPassphraseProvider::new("any-passphrase");
788
789        let identity_did = IdentityDID::new("did:keri:NONEXISTENT");
790        let message = b"test message";
791
792        let result = signer.sign_for_identity(&identity_did, &passphrase_provider, message);
793        assert!(matches!(result, Err(AgentError::KeyNotFound)));
794    }
795
796    #[test]
797    fn test_sign_for_identity_multiple_aliases() {
798        // Test that sign_for_identity works when multiple aliases exist for an identity
799        let (pkcs8_bytes, pubkey_bytes) = generate_test_keypair();
800        let passphrase = "Test-P@ss12345";
801        let identity_did = IdentityDID::new("did:keri:MULTI123");
802
803        let encrypted = encrypt_keypair(&pkcs8_bytes, passphrase).expect("Failed to encrypt");
804
805        let storage = MockKeyStorage::new();
806        // Store the same key under multiple aliases (first one should be used)
807        let alias = KeyAlias::new_unchecked("primary-alias");
808        storage
809            .store_key(&alias, &identity_did, KeyRole::Primary, &encrypted)
810            .expect("Failed to store key");
811
812        let signer = StorageSigner::new(storage);
813        let passphrase_provider = MockPassphraseProvider::new(passphrase);
814
815        let message = b"test message with multiple aliases";
816        let signature = signer
817            .sign_for_identity(&identity_did, &passphrase_provider, message)
818            .expect("Signing should succeed");
819
820        // Verify the signature
821        let public_key = UnparsedPublicKey::new(&ED25519, &pubkey_bytes);
822        assert!(public_key.verify(message, &signature).is_ok());
823    }
824
825    #[test]
826    fn test_callback_passphrase_provider() {
827        use std::sync::Arc;
828        use std::sync::atomic::{AtomicUsize, Ordering};
829
830        // Track how many times the callback is invoked
831        let call_count = Arc::new(AtomicUsize::new(0));
832        let call_count_clone = Arc::clone(&call_count);
833
834        let provider = CallbackPassphraseProvider::new(move |prompt| {
835            call_count_clone.fetch_add(1, Ordering::SeqCst);
836            assert!(prompt.contains("test-alias"));
837            Ok(Zeroizing::new("callback-passphrase".to_string()))
838        });
839
840        // Test successful passphrase retrieval
841        let result = provider.get_passphrase("Enter passphrase for test-alias:");
842        assert!(result.is_ok());
843        assert_eq!(*result.unwrap(), "callback-passphrase");
844        assert_eq!(call_count.load(Ordering::SeqCst), 1);
845
846        // Test multiple invocations
847        let result2 = provider.get_passphrase("Another prompt for test-alias");
848        assert!(result2.is_ok());
849        assert_eq!(call_count.load(Ordering::SeqCst), 2);
850    }
851
852    #[test]
853    fn test_callback_passphrase_provider_error() {
854        let provider =
855            CallbackPassphraseProvider::new(|_prompt| Err(AgentError::UserInputCancelled));
856
857        let result = provider.get_passphrase("Enter passphrase:");
858        assert!(matches!(result, Err(AgentError::UserInputCancelled)));
859    }
860
861    #[test]
862    fn test_cached_passphrase_provider_cache_hit() {
863        use std::sync::Arc;
864        use std::sync::atomic::{AtomicUsize, Ordering};
865        use std::time::Duration;
866
867        let call_count = Arc::new(AtomicUsize::new(0));
868        let call_count_clone = Arc::clone(&call_count);
869
870        let inner = Arc::new(CallbackPassphraseProvider::new(move |_prompt| {
871            call_count_clone.fetch_add(1, Ordering::SeqCst);
872            Ok(Zeroizing::new("cached-pass".to_string()))
873        }));
874
875        let cached = CachedPassphraseProvider::new(inner, Duration::from_secs(60));
876
877        // First call should invoke inner
878        let result1 = cached.get_passphrase("prompt1");
879        assert!(result1.is_ok());
880        assert_eq!(*result1.unwrap(), "cached-pass");
881        assert_eq!(call_count.load(Ordering::SeqCst), 1);
882
883        // Second call with same prompt should return cached value, not calling inner
884        let result2 = cached.get_passphrase("prompt1");
885        assert!(result2.is_ok());
886        assert_eq!(*result2.unwrap(), "cached-pass");
887        assert_eq!(call_count.load(Ordering::SeqCst), 1); // Still 1, cache hit
888    }
889
890    #[test]
891    fn test_cached_passphrase_provider_cache_miss() {
892        use std::sync::Arc;
893        use std::sync::atomic::{AtomicUsize, Ordering};
894        use std::time::Duration;
895
896        let call_count = Arc::new(AtomicUsize::new(0));
897        let call_count_clone = Arc::clone(&call_count);
898
899        let inner = Arc::new(CallbackPassphraseProvider::new(move |_prompt| {
900            call_count_clone.fetch_add(1, Ordering::SeqCst);
901            Ok(Zeroizing::new("pass".to_string()))
902        }));
903
904        let cached = CachedPassphraseProvider::new(inner, Duration::from_secs(60));
905
906        // Different prompts should each call inner
907        let _ = cached.get_passphrase("prompt1");
908        assert_eq!(call_count.load(Ordering::SeqCst), 1);
909
910        let _ = cached.get_passphrase("prompt2");
911        assert_eq!(call_count.load(Ordering::SeqCst), 2);
912
913        let _ = cached.get_passphrase("prompt3");
914        assert_eq!(call_count.load(Ordering::SeqCst), 3);
915    }
916
917    #[test]
918    fn test_cached_passphrase_provider_expiry() {
919        use std::sync::Arc;
920        use std::sync::atomic::{AtomicUsize, Ordering};
921        use std::time::Duration;
922
923        let call_count = Arc::new(AtomicUsize::new(0));
924        let call_count_clone = Arc::clone(&call_count);
925
926        let inner = Arc::new(CallbackPassphraseProvider::new(move |_prompt| {
927            call_count_clone.fetch_add(1, Ordering::SeqCst);
928            Ok(Zeroizing::new("pass".to_string()))
929        }));
930
931        // Very short TTL for testing expiry
932        let cached = CachedPassphraseProvider::new(inner, Duration::from_millis(10));
933
934        // First call
935        let _ = cached.get_passphrase("prompt");
936        assert_eq!(call_count.load(Ordering::SeqCst), 1);
937
938        // Wait for TTL to expire
939        std::thread::sleep(Duration::from_millis(20));
940
941        // This should re-fetch from inner since cache expired
942        let _ = cached.get_passphrase("prompt");
943        assert_eq!(call_count.load(Ordering::SeqCst), 2);
944    }
945
946    #[test]
947    fn test_cached_passphrase_provider_clear_cache() {
948        use std::sync::Arc;
949        use std::sync::atomic::{AtomicUsize, Ordering};
950        use std::time::Duration;
951
952        let call_count = Arc::new(AtomicUsize::new(0));
953        let call_count_clone = Arc::clone(&call_count);
954
955        let inner = Arc::new(CallbackPassphraseProvider::new(move |_prompt| {
956            call_count_clone.fetch_add(1, Ordering::SeqCst);
957            Ok(Zeroizing::new("pass".to_string()))
958        }));
959
960        let cached = CachedPassphraseProvider::new(inner, Duration::from_secs(60));
961
962        // First call
963        let _ = cached.get_passphrase("prompt");
964        assert_eq!(call_count.load(Ordering::SeqCst), 1);
965
966        // Second call should be cache hit
967        let _ = cached.get_passphrase("prompt");
968        assert_eq!(call_count.load(Ordering::SeqCst), 1);
969
970        // Clear cache
971        cached.clear_cache();
972
973        // Now should call inner again
974        let _ = cached.get_passphrase("prompt");
975        assert_eq!(call_count.load(Ordering::SeqCst), 2);
976    }
977
978    #[test]
979    fn test_prefilled_passphrase_provider_returns_stored_value() {
980        let provider = PrefilledPassphraseProvider::new("my-secret");
981        let result = provider.get_passphrase("any prompt").unwrap();
982        assert_eq!(*result, "my-secret");
983
984        let result2 = provider.get_passphrase("different prompt").unwrap();
985        assert_eq!(*result2, "my-secret");
986    }
987
988    #[test]
989    fn test_prefilled_passphrase_provider_empty_passphrase() {
990        let provider = PrefilledPassphraseProvider::new("");
991        let result = provider.get_passphrase("prompt").unwrap();
992        assert_eq!(*result, "");
993    }
994
995    #[test]
996    fn test_unified_passphrase_provider_prompts_once_for_multiple_keys() {
997        use std::sync::atomic::{AtomicUsize, Ordering};
998
999        let call_count = Arc::new(AtomicUsize::new(0));
1000        let count_clone = call_count.clone();
1001        let inner = CallbackPassphraseProvider::new(move |_prompt: &str| {
1002            count_clone.fetch_add(1, Ordering::SeqCst);
1003            Ok(Zeroizing::new("secret".to_string()))
1004        });
1005
1006        let provider = UnifiedPassphraseProvider::new(Arc::new(inner));
1007
1008        // Different prompt messages → should still only hit inner once
1009        let p1 = provider
1010            .get_passphrase("Enter passphrase for DEVICE key 'dev':")
1011            .unwrap();
1012        let p2 = provider
1013            .get_passphrase("Enter passphrase for IDENTITY key 'id':")
1014            .unwrap();
1015
1016        assert_eq!(*p1, "secret");
1017        assert_eq!(*p2, "secret");
1018        assert_eq!(call_count.load(Ordering::SeqCst), 1); // inner called exactly once
1019    }
1020}