Skip to main content

auths_core/api/
runtime.rs

1//! Application-level runtime API for managing the identity agent and keys.
2//!
3//! Provides functions to interact with core components: secure key storage (`KeyStorage`),
4//! cryptographic operations, the in-memory agent (`AgentCore`), and the agent listener.
5//! Uses `AgentHandle` for lifecycle management of agent instances.
6//! Also includes functions for interacting with the platform's SSH agent (on macOS).
7
8use crate::agent::AgentCore;
9use crate::agent::AgentHandle;
10#[cfg(unix)]
11use crate::agent::AgentSession;
12use crate::crypto::provider_bridge;
13use crate::crypto::signer::extract_seed_from_key_bytes;
14use crate::crypto::signer::{decrypt_keypair, encrypt_keypair};
15use crate::error::AgentError;
16use crate::signing::PassphraseProvider;
17use crate::storage::keychain::{KeyAlias, KeyStorage};
18use log::{debug, error, info, warn};
19#[cfg(target_os = "macos")]
20use pkcs8::PrivateKeyInfo;
21#[cfg(target_os = "macos")]
22use pkcs8::der::Decode;
23#[cfg(target_os = "macos")]
24use pkcs8::der::asn1::OctetString;
25use serde::Serialize;
26#[cfg(unix)]
27use ssh_agent_lib;
28#[cfg(unix)]
29use ssh_agent_lib::agent::listen;
30#[cfg(target_os = "macos")]
31use ssh_key::Fingerprint;
32use ssh_key::private::{Ed25519Keypair as SshEdKeypair, KeypairData};
33use ssh_key::{
34    self, LineEnding, PrivateKey as SshPrivateKey, PublicKey as SshPublicKey,
35    public::Ed25519PublicKey as SshEd25519PublicKey,
36};
37#[cfg(unix)]
38use std::io;
39#[cfg(unix)]
40use std::sync::Arc;
41#[cfg(unix)]
42use tokio::net::UnixListener;
43use zeroize::Zeroizing;
44
45#[cfg(target_os = "macos")]
46use anyhow::Context;
47#[cfg(target_os = "macos")]
48use std::io::Write;
49
50#[cfg(target_os = "macos")]
51use {
52    anyhow::Result as AnyhowResult, // Use anyhow result for ssh-add interactions
53    std::fs::{self, Permissions},
54    std::os::unix::fs::PermissionsExt,
55    std::process::{Command, Output},
56    tempfile::Builder as TempFileBuilder,
57};
58
59// --- Public Structs ---
60
61/// Represents the result of trying to load a single key into the agent core.
62#[derive(Serialize, Debug, Clone)]
63pub struct KeyLoadStatus {
64    /// Key alias.
65    pub alias: KeyAlias,
66    /// Whether the key was successfully loaded.
67    pub loaded: bool,
68    /// Load error message, if any.
69    pub error: Option<String>,
70}
71
72/// Represents the outcome of attempting to register a key with the system SSH agent.
73#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
74pub enum RegistrationOutcome {
75    /// Key was successfully added to the agent.
76    Added,
77    /// Key already exists in the agent.
78    AlreadyExists,
79    /// The SSH agent process was not found.
80    AgentNotFound,
81    /// The agent command failed.
82    CommandFailed,
83    /// The key type is not supported by this agent.
84    UnsupportedKeyType,
85    /// Key format conversion failed.
86    ConversionFailed,
87    /// An I/O error occurred.
88    IoError,
89    /// An unexpected internal error occurred.
90    InternalError,
91}
92
93/// Represents the status of registering a single key with the system SSH agent.
94#[derive(Serialize, Debug, Clone)]
95pub struct KeyRegistrationStatus {
96    /// Key fingerprint.
97    pub fingerprint: String,
98    /// Registration outcome.
99    pub status: RegistrationOutcome,
100    /// Additional message, if any.
101    pub message: Option<String>,
102}
103
104// --- Public API Functions ---
105
106/// Clears all unlocked keys from the specified agent handle.
107///
108/// This effectively locks the agent until keys are reloaded.
109///
110/// # Arguments
111/// * `handle` - The agent handle to clear keys from
112///
113/// # Example
114/// ```rust,ignore
115/// use auths_core::AgentHandle;
116/// use auths_core::api::clear_agent_keys_with_handle;
117///
118/// let handle = AgentHandle::new(socket_path);
119/// clear_agent_keys_with_handle(&handle)?;
120/// ```
121pub fn clear_agent_keys_with_handle(handle: &AgentHandle) -> Result<(), AgentError> {
122    info!("Clearing all keys from agent handle.");
123    let mut agent_guard = handle.lock()?;
124    agent_guard.clear_keys();
125    debug!("Agent keys cleared.");
126    Ok(())
127}
128
129/// Loads specific keys (by alias) from secure storage into the specified agent handle.
130///
131/// Requires the correct passphrase for each key, obtained via the `passphrase_provider`.
132/// Replaces any keys currently loaded in the agent. Stores decrypted PKCS#8 bytes securely
133/// in memory using `zeroize`.
134///
135/// # Arguments
136/// * `handle` - The agent handle to load keys into
137/// * `aliases`: A list of key aliases to load from secure storage.
138/// * `passphrase_provider`: A component responsible for securely obtaining passphrases.
139/// * `keychain`: The key storage backend to load keys from.
140///
141/// # Returns
142/// A `Result` containing a list of `KeyLoadStatus` structs, indicating the outcome
143/// for each requested alias, or an `AgentError` if a fatal error occurs.
144pub fn load_keys_into_agent_with_handle(
145    handle: &AgentHandle,
146    aliases: Vec<String>,
147    passphrase_provider: &dyn PassphraseProvider,
148    keychain: &(dyn KeyStorage + Send + Sync),
149) -> Result<Vec<KeyLoadStatus>, AgentError> {
150    info!(
151        "Attempting to load keys into agent handle for aliases: {:?}",
152        aliases
153    );
154    if aliases.is_empty() {
155        warn!("load_keys_into_agent_with_handle called with empty alias list. Clearing agent.");
156        clear_agent_keys_with_handle(handle)?;
157        return Ok(vec![]);
158    }
159
160    let mut load_statuses = Vec::new();
161    let mut temp_unlocked_core = AgentCore::default();
162
163    for alias in aliases {
164        debug!("Processing alias for agent load: {}", alias);
165        let key_alias = KeyAlias::new_unchecked(&alias);
166        let mut status = KeyLoadStatus {
167            alias: key_alias.clone(),
168            loaded: false,
169            error: None,
170        };
171
172        let load_result = || -> Result<Zeroizing<Vec<u8>>, AgentError> {
173            let (_controller_did, encrypted_pkcs8) = keychain.load_key(&key_alias)?;
174            let prompt = format!(
175                "Enter passphrase to unlock key '{}' for agent session:",
176                key_alias
177            );
178            let passphrase = passphrase_provider.get_passphrase(&prompt)?;
179            let pkcs8_bytes = decrypt_keypair(&encrypted_pkcs8, &passphrase)?;
180            let _ = extract_seed_from_key_bytes(&pkcs8_bytes).map_err(|e| {
181                AgentError::KeyDeserializationError(format!(
182                    "Failed to parse key for alias '{}' after decryption: {}",
183                    key_alias, e
184                ))
185            })?;
186            Ok(pkcs8_bytes)
187        }();
188
189        match load_result {
190            Ok(pkcs8_bytes) => {
191                info!("Successfully unlocked key for alias '{}'", key_alias);
192                match temp_unlocked_core.register_key(pkcs8_bytes) {
193                    Ok(()) => status.loaded = true,
194                    Err(e) => {
195                        error!(
196                            "Failed to register key '{}' in agent core state after successful unlock/parse: {}",
197                            key_alias, e
198                        );
199                        status.error = Some(format!(
200                            "Internal error: Failed to register key in agent core state: {}",
201                            e
202                        ));
203                    }
204                }
205            }
206            Err(e) => {
207                error!(
208                    "Failed to load/decrypt key for alias '{}': {}",
209                    key_alias, e
210                );
211                match e {
212                    AgentError::IncorrectPassphrase => {
213                        status.error = Some("Incorrect passphrase".to_string())
214                    }
215                    AgentError::KeyNotFound => status.error = Some("Key not found".to_string()),
216                    AgentError::UserInputCancelled => {
217                        status.error = Some("Operation cancelled by user".to_string())
218                    }
219                    AgentError::KeyDeserializationError(_) => {
220                        status.error = Some(format!("Failed to parse key after decryption: {}", e))
221                    }
222                    _ => status.error = Some(e.to_string()),
223                }
224            }
225        }
226        load_statuses.push(status);
227    }
228
229    // Atomically update the agent state
230    let mut agent_guard = handle.lock()?;
231    info!(
232        "Replacing agent core with {} unlocked keys ({} aliases attempted).",
233        temp_unlocked_core.key_count(),
234        load_statuses.len()
235    );
236    *agent_guard = temp_unlocked_core;
237
238    Ok(load_statuses)
239}
240
241/// Rotates the keypair for a given alias *in the secure storage only*.
242///
243/// This generates a new Ed25519 keypair, encrypts it with the `new_passphrase`,
244/// and overwrites the existing entry for `alias` in the platform's keychain or
245/// secure storage. The key remains associated with the *same Controller DID*
246/// as the original key.
247///
248/// **Warning:** This function does *not* update any corresponding identity
249/// representation in a Git repository (e.g., changing the Controller DID stored
250/// in an identity commit or creating a KERI rotation event). Using this function
251/// alone may lead to inconsistencies if the identity representation relies on the
252/// public key associated with the Controller DID. It also does not automatically
253/// update the key loaded in the running agent; `load_keys_into_agent` or restarting
254/// the agent may be required.
255///
256/// # Arguments
257/// * `alias`: The alias of the key entry in secure storage to rotate.
258/// * `new_passphrase`: The passphrase to encrypt the *new* private key with.
259///
260/// # Returns
261/// `Ok(())` on success, or an `AgentError` if the alias is not found, key generation
262/// fails, encryption fails, or storage fails.
263pub fn rotate_key(
264    alias: &str,
265    new_passphrase: &str,
266    keychain: &(dyn KeyStorage + Send + Sync),
267) -> Result<(), AgentError> {
268    info!(
269        "[API] Attempting secure storage key rotation for local alias: {}",
270        alias
271    );
272    if alias.trim().is_empty() {
273        return Err(AgentError::InvalidInput(
274            "Alias cannot be empty".to_string(),
275        ));
276    }
277    if new_passphrase.is_empty() {
278        return Err(AgentError::InvalidInput(
279            "New passphrase cannot be empty".to_string(),
280        ));
281    }
282
283    // 1. Verify the alias exists and retrieve its associated Controller DID
284    let key_alias = KeyAlias::new_unchecked(alias);
285    let existing_did = keychain.get_identity_for_alias(&key_alias)?;
286    info!(
287        "Found existing key for alias '{}', associated with Controller DID '{}'. Proceeding with rotation.",
288        alias, existing_did
289    );
290
291    // 2. Generate new keypair via CryptoProvider
292    let (seed, pubkey) = provider_bridge::generate_ed25519_keypair_sync()
293        .map_err(|e| AgentError::CryptoError(format!("Failed to generate new keypair: {}", e)))?;
294    // Build PKCS#8 v2 DER for storage compatibility
295    let new_pkcs8_bytes = auths_crypto::build_ed25519_pkcs8_v2(seed.as_bytes(), &pubkey);
296    debug!("Generated new keypair via CryptoProvider.");
297
298    // 3. Encrypt the new keypair with the new passphrase
299    let encrypted_new_key = encrypt_keypair(&new_pkcs8_bytes, new_passphrase)?;
300    debug!("Encrypted new keypair with provided passphrase.");
301
302    // 4. Overwrite the existing entry in secure storage with the new encrypted key,
303    //    keeping the original Controller DID association.
304    keychain.store_key(&key_alias, &existing_did, &encrypted_new_key)?;
305    info!(
306        "Successfully overwrote secure storage for alias '{}' with new encrypted key.",
307        alias
308    );
309
310    warn!(
311        "Secure storage key rotated for alias '{}'. This did NOT update any Git identity representation. The running agent may still hold the old decrypted key. Consider reloading keys into the agent.",
312        alias
313    );
314    Ok(())
315}
316
317/// Signs a message using a key currently loaded in the specified agent handle.
318///
319/// This retrieves the decrypted key material from the agent handle based on the
320/// provided public key bytes and performs the signing operation. It does *not*
321/// require a passphrase as the key is assumed to be already unlocked.
322///
323/// # Arguments
324/// * `handle` - The agent handle containing the loaded keys
325/// * `pubkey`: The public key bytes of the key to use for signing.
326/// * `data`: The data bytes to sign.
327///
328/// # Returns
329/// The raw signature bytes, or an `AgentError` if the key is not found in the
330/// agent core or if the signing operation fails internally.
331pub fn agent_sign_with_handle(
332    handle: &AgentHandle,
333    pubkey: &[u8],
334    data: &[u8],
335) -> Result<Vec<u8>, AgentError> {
336    debug!(
337        "Agent sign request for pubkey starting with: {:x?}...",
338        &pubkey[..core::cmp::min(pubkey.len(), 8)]
339    );
340
341    // Use the handle's sign method which includes lock check
342    handle.sign(pubkey, data)
343}
344
345/// Exports the decrypted private key in OpenSSH PEM format.
346///
347/// Retrieves the encrypted key from secure storage, decrypts it using the
348/// provided passphrase, and formats it as a standard OpenSSH PEM private key string.
349///
350/// # Arguments
351/// * `alias`: The alias of the key in secure storage.
352/// * `passphrase`: The passphrase to decrypt the key.
353///
354/// # Returns
355/// A `Zeroizing<String>` containing the PEM data on success, or an `AgentError`.
356pub fn export_key_openssh_pem(
357    alias: &str,
358    passphrase: &str,
359    keychain: &(dyn KeyStorage + Send + Sync),
360) -> Result<Zeroizing<String>, AgentError> {
361    info!("Exporting PEM for local alias: {}", alias);
362    if alias.trim().is_empty() {
363        return Err(AgentError::InvalidInput(
364            "Alias cannot be empty".to_string(),
365        ));
366    }
367    // 1. Load encrypted key data
368    let key_alias = KeyAlias::new_unchecked(alias);
369    let (_controller_did, encrypted_pkcs8) = keychain.load_key(&key_alias)?;
370
371    // 2. Decrypt key data
372    let pkcs8_bytes = decrypt_keypair(&encrypted_pkcs8, passphrase)?;
373
374    // 3. Extract seed via the consolidated SSH crypto module
375    let secure_seed = crate::crypto::ssh::extract_seed_from_pkcs8(&pkcs8_bytes).map_err(|e| {
376        AgentError::KeyDeserializationError(format!(
377            "Failed to extract Ed25519 seed for alias '{}': {}",
378            alias, e
379        ))
380    })?;
381
382    let ssh_ed_keypair = SshEdKeypair::from_seed(secure_seed.as_bytes());
383    let keypair_data = KeypairData::Ed25519(ssh_ed_keypair);
384    // Create the private key object (comment is typically empty for PEM)
385    let ssh_private_key = SshPrivateKey::new(keypair_data, "") // Empty comment
386        .map_err(|e| {
387            // Use CryptoError for ssh-key object creation failure
388            AgentError::CryptoError(format!(
389                "Failed to create ssh_key::PrivateKey for alias '{}': {}",
390                alias, e
391            ))
392        })?;
393
394    // 5. Format as OpenSSH PEM (uses LF line endings by default)
395    let pem = ssh_private_key.to_openssh(LineEnding::LF).map_err(|e| {
396        // Use CryptoError for formatting failure
397        AgentError::CryptoError(format!(
398            "Failed to encode OpenSSH PEM for alias '{}': {}",
399            alias, e
400        ))
401    })?;
402
403    debug!("Successfully generated PEM for alias '{}'", alias);
404    Ok(pem) // Returns Zeroizing<String>
405}
406
407/// Exports the public key in OpenSSH `.pub` format.
408///
409/// Retrieves the encrypted key from secure storage, decrypts it using the
410/// provided passphrase, derives the public key, and formats it as a standard
411/// OpenSSH `.pub` line (including the alias as a comment).
412///
413/// # Arguments
414/// * `alias`: The alias of the key in secure storage.
415/// * `passphrase`: The passphrase to decrypt the key.
416///
417/// # Returns
418/// A `String` containing the public key line on success, or an `AgentError`.
419pub fn export_key_openssh_pub(
420    alias: &str,
421    passphrase: &str,
422    keychain: &(dyn KeyStorage + Send + Sync),
423) -> Result<String, AgentError> {
424    info!("Exporting OpenSSH public key for local alias: {}", alias);
425    if alias.trim().is_empty() {
426        return Err(AgentError::InvalidInput(
427            "Alias cannot be empty".to_string(),
428        ));
429    }
430    // 1. Load encrypted key data
431    let key_alias = KeyAlias::new_unchecked(alias);
432    let (_controller_did, encrypted_pkcs8) = keychain.load_key(&key_alias)?;
433
434    // 2. Decrypt key data
435    let pkcs8_bytes = decrypt_keypair(&encrypted_pkcs8, passphrase)?;
436
437    // 3. Extract seed and derive public key via CryptoProvider
438    let (seed, pubkey_bytes) =
439        crate::crypto::signer::load_seed_and_pubkey(&pkcs8_bytes).map_err(|e| {
440            AgentError::CryptoError(format!(
441                "Failed to extract key for alias '{}': {}",
442                alias, e
443            ))
444        })?;
445    let _ = seed; // seed not needed for public key export
446    let ssh_ed25519_pubkey =
447        SshEd25519PublicKey::try_from(pubkey_bytes.as_slice()).map_err(|e| {
448            AgentError::CryptoError(format!(
449                "Failed to create Ed25519PublicKey from bytes: {}",
450                e
451            ))
452        })?;
453    let key_data = ssh_key::public::KeyData::Ed25519(ssh_ed25519_pubkey);
454
455    // 5. Create the ssh-key PublicKey object (comment is optional here)
456    let ssh_pub_key = SshPublicKey::new(key_data, ""); // Use empty comment for base formatting
457
458    // 6. Format the base public key string (type and key material)
459    let pubkey_base = ssh_pub_key.to_openssh().map_err(|e| {
460        // Use CryptoError for formatting failure
461        AgentError::CryptoError(format!("Failed to format OpenSSH pubkey base: {}", e))
462    })?;
463
464    // 7. Manually append the alias as the comment part of the .pub line
465    let formatted_pubkey = format!("{} {}", pubkey_base, alias);
466
467    debug!(
468        "Successfully generated OpenSSH public key string for alias '{}'",
469        alias
470    );
471    Ok(formatted_pubkey)
472}
473
474/// Returns the number of keys currently loaded in the specified agent handle.
475///
476/// # Arguments
477/// * `handle` - The agent handle to query
478///
479/// # Returns
480/// The number of keys currently loaded.
481pub fn get_agent_key_count_with_handle(handle: &AgentHandle) -> Result<usize, AgentError> {
482    handle.key_count()
483}
484
485/// Attempts to register all keys currently loaded in the specified agent handle
486/// with the system's running `ssh-agent` on macOS using the `ssh-add` command.
487///
488/// This iterates through the unlocked keys in the agent core, converts each to
489/// OpenSSH PEM format, writes it to a temporary file, and calls `ssh-add` on it.
490///
491/// # Arguments
492/// * `handle` - The agent handle containing the keys to register
493///
494/// # Returns
495/// A `Result` containing a list of `KeyRegistrationStatus` structs detailing the
496/// outcome for each key, or an `AgentError` if accessing the agent core state fails.
497#[cfg(target_os = "macos")]
498pub fn register_keys_with_macos_agent_with_handle(
499    handle: &AgentHandle,
500    ssh_agent_socket: Option<&std::path::Path>,
501) -> Result<Vec<KeyRegistrationStatus>, AgentError> {
502    info!("Attempting to register keys from agent handle with macOS system ssh-agent...");
503    if ssh_agent_socket.is_none() {
504        warn!("SSH_AUTH_SOCK not configured. System ssh-agent may not be running or configured.");
505    }
506
507    let keys_to_register: Vec<(Vec<u8>, Zeroizing<Vec<u8>>)> = {
508        let agent_guard = handle.lock()?;
509        agent_guard
510            .keys
511            .iter()
512            .map(|(pubkey, seed)| {
513                let pubkey_arr: [u8; 32] = pubkey.as_slice().try_into().unwrap_or([0u8; 32]);
514                let pkcs8 = auths_crypto::build_ed25519_pkcs8_v2(seed.as_bytes(), &pubkey_arr);
515                (pubkey.clone(), Zeroizing::new(pkcs8))
516            })
517            .collect()
518    };
519
520    register_keys_with_macos_agent_internal(keys_to_register)
521}
522
523/// Stub function for non-macOS platforms.
524#[cfg(not(target_os = "macos"))]
525pub fn register_keys_with_macos_agent_with_handle(
526    _handle: &AgentHandle,
527    _ssh_agent_socket: Option<&std::path::Path>,
528) -> Result<Vec<KeyRegistrationStatus>, AgentError> {
529    info!("Not on macOS, skipping system ssh-agent registration.");
530    Ok(vec![])
531}
532
533/// Internal helper that performs the actual macOS ssh-agent registration.
534#[cfg(target_os = "macos")]
535#[allow(clippy::too_many_lines)]
536fn register_keys_with_macos_agent_internal(
537    keys_to_register: Vec<(Vec<u8>, Zeroizing<Vec<u8>>)>,
538) -> Result<Vec<KeyRegistrationStatus>, AgentError> {
539    if keys_to_register.is_empty() {
540        info!("No keys to register with system agent.");
541        return Ok(vec![]);
542    }
543    info!(
544        "Found {} keys to attempt registration with system agent.",
545        keys_to_register.len()
546    );
547
548    let mut results = Vec::with_capacity(keys_to_register.len());
549
550    for (pubkey_bytes, pkcs8_bytes_zeroizing) in keys_to_register.into_iter() {
551        let fingerprint_str = (|| -> Result<String, AgentError> {
552            let pk = SshEd25519PublicKey::try_from(pubkey_bytes.as_slice()).map_err(|e| {
553                AgentError::KeyDeserializationError(format!(
554                    "Invalid pubkey bytes for fingerprint: {}",
555                    e
556                ))
557            })?;
558            let ssh_pub_key: SshPublicKey =
559                SshPublicKey::new(ssh_key::public::KeyData::Ed25519(pk), "");
560            let fp: Fingerprint = ssh_pub_key.fingerprint(Default::default());
561            Ok(fp.to_string())
562        })()
563        .unwrap_or_else(|e| {
564            warn!(
565                "Could not calculate fingerprint for key being registered: {}",
566                e
567            );
568            "unknown_fingerprint".to_string()
569        });
570
571        let mut status = KeyRegistrationStatus {
572            fingerprint: fingerprint_str.clone(),
573            status: RegistrationOutcome::InternalError,
574            message: None,
575        };
576
577        let result: AnyhowResult<Output> = (|| {
578            let pkcs8_bytes = pkcs8_bytes_zeroizing.as_ref();
579            let private_key_info = PrivateKeyInfo::from_der(pkcs8_bytes)
580                .context("Failed to parse PKCS#8 DER for ssh-add")?;
581            let seed_octet_string = OctetString::from_der(private_key_info.private_key)
582                .context("Failed to parse PKCS#8 seed OctetString for ssh-add")?;
583            let seed_bytes = seed_octet_string.as_bytes();
584            if seed_bytes.len() != 32 {
585                anyhow::bail!(
586                    "Invalid seed length {} in PKCS#8 for ssh-add",
587                    seed_bytes.len()
588                );
589            }
590            let seed_array: [u8; 32] = seed_bytes.try_into().expect("Length checked");
591            let ssh_ed_keypair = SshEdKeypair::from_seed(&seed_array);
592            let keypair_data = KeypairData::Ed25519(ssh_ed_keypair);
593            let ssh_private_key = SshPrivateKey::new(keypair_data, "")
594                .context("Failed to create ssh_key::PrivateKey for ssh-add")?;
595            let pem_zeroizing = ssh_private_key
596                .to_openssh(LineEnding::LF)
597                .context("Failed to encode OpenSSH PEM for ssh-add")?;
598            let pem_string = pem_zeroizing.to_string();
599
600            let mut temp_file_guard = TempFileBuilder::new()
601                .prefix("auths-key-")
602                .suffix(".pem")
603                .rand_bytes(5)
604                .tempfile()
605                .context("Failed to create temporary file for ssh-add")?;
606            if let Err(e) =
607                fs::set_permissions(temp_file_guard.path(), Permissions::from_mode(0o600))
608            {
609                warn!(
610                    "Failed to set 600 permissions on temp file {:?}: {}. Continuing...",
611                    temp_file_guard.path(),
612                    e
613                );
614            }
615            temp_file_guard
616                .write_all(pem_string.as_bytes())
617                .context("Failed to write PEM to temporary file")?;
618            temp_file_guard
619                .flush()
620                .context("Failed to flush temporary PEM file")?;
621            let temp_file_path = temp_file_guard.path().to_path_buf();
622
623            debug!(
624                "Attempting ssh-add for temporary key file: {:?}",
625                temp_file_path
626            );
627            let output = Command::new("ssh-add").arg(&temp_file_path).output()?;
628            debug!("ssh-add command finished for {:?}", temp_file_path);
629            Ok(output)
630        })();
631
632        match result {
633            Ok(output) => {
634                let stdout = String::from_utf8_lossy(&output.stdout)
635                    .trim()
636                    .to_lowercase();
637                let stderr = String::from_utf8_lossy(&output.stderr)
638                    .trim()
639                    .to_lowercase();
640                let full_output = format!("ssh-add stdout: '{}', stderr: '{}'", stdout, stderr);
641                status.message = Some(full_output.clone());
642
643                if output.status.success() {
644                    if stdout.contains("identity added") {
645                        info!(
646                            "ssh-add successful for {}: Identity added.",
647                            fingerprint_str
648                        );
649                        status.status = RegistrationOutcome::Added;
650                    } else if stderr.contains("already exists") || stdout.contains("already exists")
651                    {
652                        info!(
653                            "ssh-add skipped for {}: Identity already exists.",
654                            fingerprint_str
655                        );
656                        status.status = RegistrationOutcome::AlreadyExists;
657                    } else {
658                        warn!(
659                            "ssh-add command succeeded for {} but output was unexpected: {}",
660                            fingerprint_str, full_output
661                        );
662                        status.status = RegistrationOutcome::Added;
663                    }
664                } else {
665                    error!(
666                        "ssh-add command failed for {}: {}",
667                        fingerprint_str, full_output
668                    );
669                    if stderr.contains("could not open a connection")
670                        || stderr.contains("connection refused")
671                        || stderr.contains("communication with agent failed")
672                    {
673                        status.status = RegistrationOutcome::AgentNotFound;
674                    } else {
675                        status.status = RegistrationOutcome::CommandFailed;
676                    }
677                }
678            }
679            Err(e) => {
680                error!(
681                    "Error during registration process for {}: {:?}",
682                    fingerprint_str, e
683                );
684                if e.downcast_ref::<std::io::Error>().is_some() {
685                    status.status = RegistrationOutcome::IoError;
686                } else if e.downcast_ref::<ssh_key::Error>().is_some()
687                    || e.downcast_ref::<pkcs8::Error>().is_some()
688                    || e.downcast_ref::<pkcs8::der::Error>().is_some()
689                {
690                    status.status = RegistrationOutcome::ConversionFailed;
691                } else {
692                    status.status = RegistrationOutcome::InternalError;
693                }
694                status.message = Some(format!("Internal error during registration: {}", e));
695            }
696        }
697        results.push(status);
698    }
699
700    info!(
701        "Finished attempting macOS system agent registration for {} keys.",
702        results.len()
703    );
704    Ok(results)
705}
706
707/// Starts the SSH agent listener using the provided `AgentHandle`.
708///
709/// Binds to the socket path from the handle, cleans up any old socket file if present,
710/// and enters an asynchronous loop (`ssh_agent_lib::listen`) to accept and handle
711/// incoming agent connections using `AgentSession`.
712///
713/// Requires a `tokio` runtime context. Runs indefinitely on success.
714///
715/// # Arguments
716/// * `handle`: The agent handle containing the socket path and agent core.
717///
718/// # Returns
719/// - `Ok(())` if the listener starts successfully (runs indefinitely).
720/// - `Err(AgentError)` if binding/setup fails or the listener loop exits with an error.
721#[cfg(unix)]
722pub async fn start_agent_listener_with_handle(handle: Arc<AgentHandle>) -> Result<(), AgentError> {
723    let socket_path = handle.socket_path();
724    info!("Attempting to start agent listener at {:?}", socket_path);
725
726    // --- Ensure parent directory exists ---
727    if let Some(parent) = socket_path.parent()
728        && !parent.exists()
729    {
730        debug!("Creating parent directory for socket: {:?}", parent);
731        if let Err(e) = std::fs::create_dir_all(parent) {
732            error!("Failed to create parent directory {:?}: {}", parent, e);
733            return Err(AgentError::IO(e));
734        }
735    }
736
737    // --- Clean up existing socket file (if any) ---
738    match std::fs::remove_file(socket_path) {
739        Ok(()) => info!("Removed existing socket file at {:?}", socket_path),
740        Err(e) if e.kind() == io::ErrorKind::NotFound => {
741            debug!(
742                "No existing socket file found at {:?}, proceeding.",
743                socket_path
744            );
745        }
746        Err(e) => {
747            warn!(
748                "Failed to remove existing socket file at {:?}: {}. Binding might fail.",
749                socket_path, e
750            );
751        }
752    }
753
754    // --- Bind the listener ---
755    let listener = UnixListener::bind(socket_path).map_err(|e| {
756        error!("Failed to bind listener socket at {:?}: {}", socket_path, e);
757        AgentError::IO(e)
758    })?;
759
760    // --- Listener started successfully ---
761    let actual_path = socket_path
762        .canonicalize()
763        .unwrap_or_else(|_| socket_path.to_path_buf());
764    info!(
765        "🚀 Agent listener started successfully at {:?}",
766        actual_path
767    );
768    info!("   Set SSH_AUTH_SOCK={:?} to use this agent.", actual_path);
769
770    // Mark agent as running
771    handle.set_running(true);
772
773    // --- Create the agent session handler using the provided handle ---
774    let session = AgentSession::new(handle.clone());
775
776    // --- Start the main listener loop from ssh_agent_lib ---
777    let result = listen(listener, session).await;
778
779    // Mark agent as no longer running
780    handle.set_running(false);
781
782    if let Err(e) = result {
783        error!("SSH Agent listener failed: {:?}", e);
784        return Err(AgentError::IO(io::Error::other(format!(
785            "SSH Agent listener failed: {}",
786            e
787        ))));
788    }
789
790    warn!("Agent listener loop exited unexpectedly without error.");
791    Ok(())
792}
793
794/// Starts the SSH agent listener on the specified Unix domain socket path.
795///
796/// This is a convenience function that creates an `AgentHandle` internally.
797/// For more control over the agent lifecycle, use `start_agent_listener_with_handle`
798/// with your own `AgentHandle`.
799///
800/// Requires a `tokio` runtime context. Runs indefinitely on success.
801///
802/// # Arguments
803/// * `socket_path_str`: The filesystem path for the Unix domain socket.
804///
805/// # Returns
806/// - `Ok(())` if the listener starts successfully (runs indefinitely).
807/// - `Err(AgentError)` if binding/setup fails or the listener loop exits with an error.
808#[cfg(unix)]
809pub async fn start_agent_listener(socket_path_str: String) -> Result<(), AgentError> {
810    use std::path::PathBuf;
811    let handle = Arc::new(AgentHandle::new(PathBuf::from(&socket_path_str)));
812    start_agent_listener_with_handle(handle).await
813}