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