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