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