Skip to main content

auths_core/agent/
client.rs

1//! SSH agent client for communicating with a running agent.
2//!
3//! This module provides client functions to communicate with an SSH agent
4//! over a Unix domain socket, enabling passphrase-free signing operations.
5
6use crate::error::AgentError;
7use log::{debug, error, info, warn};
8use ssh_agent_lib::proto::Identity;
9use ssh_key::PrivateKey as SshPrivateKey;
10use ssh_key::private::{Ed25519Keypair as SshEd25519Keypair, KeypairData};
11use ssh_key::public::{Ed25519PublicKey, KeyData};
12use std::io::{Read, Write};
13use std::os::unix::net::UnixStream;
14use std::path::Path;
15use std::time::Duration;
16
17/// Status of the agent connection.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum AgentStatus {
20    /// Agent is running and responding.
21    Running {
22        /// Number of keys currently loaded in the agent.
23        key_count: usize,
24    },
25    /// Agent socket exists but connection failed.
26    ConnectionFailed,
27    /// Agent socket does not exist.
28    NotRunning,
29}
30
31/// SSH Agent protocol message types (from RFC draft-miller-ssh-agent).
32mod proto {
33    // Request message types
34    pub const SSH_AGENTC_REQUEST_IDENTITIES: u8 = 11;
35    pub const SSH_AGENTC_SIGN_REQUEST: u8 = 13;
36    pub const SSH_AGENTC_ADD_IDENTITY: u8 = 17;
37    pub const SSH_AGENTC_REMOVE_ALL_IDENTITIES: u8 = 19;
38
39    // Response message types
40    pub const SSH_AGENT_FAILURE: u8 = 5;
41    pub const SSH_AGENT_SUCCESS: u8 = 6;
42    pub const SSH_AGENT_IDENTITIES_ANSWER: u8 = 12;
43    pub const SSH_AGENT_SIGN_RESPONSE: u8 = 14;
44}
45
46/// Check the status of the SSH agent.
47///
48/// Connects to the agent socket and queries for loaded identities to verify
49/// the agent is running and responsive.
50///
51/// # Arguments
52/// * `socket_path` - Path to the agent's Unix domain socket.
53///
54/// # Returns
55/// * `AgentStatus::Running { key_count }` if agent is running with keys.
56/// * `AgentStatus::ConnectionFailed` if socket exists but connection failed.
57/// * `AgentStatus::NotRunning` if socket doesn't exist.
58pub fn check_agent_status<P: AsRef<Path>>(socket_path: P) -> AgentStatus {
59    let socket_path = socket_path.as_ref();
60
61    if !socket_path.exists() {
62        debug!("Agent socket does not exist: {:?}", socket_path);
63        return AgentStatus::NotRunning;
64    }
65
66    // Try to connect to the agent
67    let mut stream = match UnixStream::connect(socket_path) {
68        Ok(s) => s,
69        Err(e) => {
70            warn!("Failed to connect to agent socket {:?}: {}", socket_path, e);
71            return AgentStatus::ConnectionFailed;
72        }
73    };
74
75    // Set a timeout for the connection
76    if let Err(e) = stream.set_read_timeout(Some(Duration::from_secs(5))) {
77        warn!("Failed to set read timeout: {}", e);
78    }
79    if let Err(e) = stream.set_write_timeout(Some(Duration::from_secs(5))) {
80        warn!("Failed to set write timeout: {}", e);
81    }
82
83    // Send SSH_AGENTC_REQUEST_IDENTITIES
84    match request_identities_raw(&mut stream) {
85        Ok(identities) => {
86            info!("Agent is running with {} keys loaded", identities.len());
87            AgentStatus::Running {
88                key_count: identities.len(),
89            }
90        }
91        Err(e) => {
92            warn!("Failed to query agent identities: {}", e);
93            AgentStatus::ConnectionFailed
94        }
95    }
96}
97
98/// Sign data using a key loaded in the SSH agent.
99///
100/// This function communicates with the agent to sign data using the specified
101/// public key. The key must already be loaded in the agent.
102///
103/// # Arguments
104/// * `socket_path` - Path to the agent's Unix domain socket.
105/// * `pubkey` - The 32-byte Ed25519 public key bytes.
106/// * `data` - The data to sign.
107///
108/// # Returns
109/// * `Ok(Vec<u8>)` - The signature bytes on success.
110/// * `Err(AgentError)` - On failure.
111pub fn agent_sign<P: AsRef<Path>>(
112    socket_path: P,
113    pubkey: &[u8],
114    data: &[u8],
115) -> Result<Vec<u8>, AgentError> {
116    let socket_path = socket_path.as_ref();
117
118    debug!(
119        "Signing via agent at {:?} with pubkey {:?}...",
120        socket_path,
121        hex::encode(&pubkey[..4.min(pubkey.len())])
122    );
123
124    // Connect to the agent
125    let mut stream = UnixStream::connect(socket_path).map_err(|e| {
126        error!("Failed to connect to agent: {}", e);
127        AgentError::IO(e)
128    })?;
129
130    stream
131        .set_read_timeout(Some(Duration::from_secs(30)))
132        .map_err(AgentError::IO)?;
133    stream
134        .set_write_timeout(Some(Duration::from_secs(30)))
135        .map_err(AgentError::IO)?;
136
137    // Build the sign request — detect key type from length
138    let key_data = match pubkey.len() {
139        32 => {
140            #[allow(clippy::unwrap_used)] // INVARIANT: length checked
141            let pubkey_array: [u8; 32] = pubkey.try_into().unwrap();
142            KeyData::Ed25519(Ed25519PublicKey(pubkey_array))
143        }
144        33 | 65 => {
145            let ecdsa_pk = ssh_key::public::EcdsaPublicKey::from_sec1_bytes(pubkey)
146                .map_err(|e| AgentError::InvalidInput(format!("Invalid P-256 public key: {e}")))?;
147            KeyData::Ecdsa(ecdsa_pk)
148        }
149        n => {
150            return Err(AgentError::InvalidInput(format!(
151                "Unsupported public key length for agent signing: {n}"
152            )));
153        }
154    };
155
156    // Encode the sign request using the wire protocol
157    let signature = sign_request_raw(&mut stream, &key_data, data)?;
158
159    debug!("Successfully signed via agent");
160    Ok(signature)
161}
162
163/// Add an identity (private key) to the SSH agent.
164///
165/// This function loads a private key into the agent so it can be used for
166/// subsequent signing operations without requiring a passphrase.
167///
168/// # Arguments
169/// * `socket_path` - Path to the agent's Unix domain socket.
170/// * `pkcs8_bytes` - The PKCS#8 encoded Ed25519 private key bytes.
171///
172/// # Returns
173/// * `Ok(Vec<u8>)` - The public key bytes of the added identity.
174/// * `Err(AgentError)` - On failure.
175pub fn add_identity<P: AsRef<Path>>(
176    socket_path: P,
177    pkcs8_bytes: &[u8],
178) -> Result<Vec<u8>, AgentError> {
179    let socket_path = socket_path.as_ref();
180
181    debug!("Adding identity to agent at {:?}", socket_path);
182
183    // Parse the PKCS#8 to detect curve + extract seed+public
184    let parsed = auths_crypto::parse_key_material(pkcs8_bytes)
185        .map_err(|e| AgentError::KeyDeserializationError(e.to_string()))?;
186
187    let mut stream = UnixStream::connect(socket_path).map_err(|e| {
188        error!("Failed to connect to agent: {}", e);
189        AgentError::IO(e)
190    })?;
191
192    stream
193        .set_read_timeout(Some(Duration::from_secs(30)))
194        .map_err(AgentError::IO)?;
195    stream
196        .set_write_timeout(Some(Duration::from_secs(30)))
197        .map_err(AgentError::IO)?;
198
199    let (keypair_data, pubkey_bytes) = match parsed.seed.curve() {
200        auths_crypto::CurveType::Ed25519 => {
201            let ssh_keypair = SshEd25519Keypair::from_seed(parsed.seed.as_bytes());
202            let pubkey = ssh_keypair.public.0.to_vec();
203            (KeypairData::Ed25519(ssh_keypair), pubkey)
204        }
205        auths_crypto::CurveType::P256 => {
206            use p256::elliptic_curve::sec1::ToEncodedPoint;
207            use ssh_key::private::{EcdsaKeypair, EcdsaPrivateKey};
208
209            let secret = p256::SecretKey::from_slice(parsed.seed.as_bytes())
210                .map_err(|e| AgentError::CryptoError(format!("P-256 secret key parse: {e}")))?;
211            let public = secret.public_key();
212            let keypair = EcdsaKeypair::NistP256 {
213                public: public.to_encoded_point(false),
214                private: EcdsaPrivateKey::from(secret),
215            };
216            (KeypairData::Ecdsa(keypair), parsed.public_key.clone())
217        }
218    };
219
220    let private_key = SshPrivateKey::new(keypair_data, "auths-key")
221        .map_err(|e| AgentError::CryptoError(format!("Failed to create SSH key: {}", e)))?;
222
223    add_identity_raw(&mut stream, &private_key)?;
224
225    info!(
226        "Successfully added identity to agent: {:?}...",
227        hex::encode(&pubkey_bytes[..4.min(pubkey_bytes.len())])
228    );
229    Ok(pubkey_bytes)
230}
231
232/// List all identities (public keys) loaded in the agent.
233///
234/// # Arguments
235/// * `socket_path` - Path to the agent's Unix domain socket.
236///
237/// # Returns
238/// * `Ok(Vec<Vec<u8>>)` - List of public key bytes on success.
239/// * `Err(AgentError)` - On failure.
240pub fn list_identities<P: AsRef<Path>>(socket_path: P) -> Result<Vec<Vec<u8>>, AgentError> {
241    let socket_path = socket_path.as_ref();
242
243    let mut stream = UnixStream::connect(socket_path).map_err(|e| {
244        error!("Failed to connect to agent: {}", e);
245        AgentError::IO(e)
246    })?;
247
248    stream
249        .set_read_timeout(Some(Duration::from_secs(5)))
250        .map_err(AgentError::IO)?;
251    stream
252        .set_write_timeout(Some(Duration::from_secs(5)))
253        .map_err(AgentError::IO)?;
254
255    let identities = request_identities_raw(&mut stream)?;
256
257    let pubkeys: Vec<Vec<u8>> = identities
258        .into_iter()
259        .filter_map(|id| match id.pubkey {
260            KeyData::Ed25519(pk) => Some(pk.0.to_vec()),
261            KeyData::Ecdsa(pk) => Some(pk.as_ref().to_vec()),
262            _ => None,
263        })
264        .collect();
265
266    Ok(pubkeys)
267}
268
269/// Remove all identities (keys) from the SSH agent.
270///
271/// This clears all loaded keys from the agent's memory, effectively
272/// "locking" the agent so no signing operations can proceed until
273/// keys are re-added.
274///
275/// # Arguments
276/// * `socket_path` - Path to the agent's Unix domain socket.
277pub fn remove_all_identities<P: AsRef<Path>>(socket_path: P) -> Result<(), AgentError> {
278    let socket_path = socket_path.as_ref();
279
280    debug!("Removing all identities from agent at {:?}", socket_path);
281
282    let mut stream = UnixStream::connect(socket_path).map_err(|e| {
283        error!("Failed to connect to agent: {}", e);
284        AgentError::IO(e)
285    })?;
286
287    stream
288        .set_read_timeout(Some(Duration::from_secs(5)))
289        .map_err(AgentError::IO)?;
290    stream
291        .set_write_timeout(Some(Duration::from_secs(5)))
292        .map_err(AgentError::IO)?;
293
294    let msg = [proto::SSH_AGENTC_REMOVE_ALL_IDENTITIES];
295    send_message(&mut stream, &msg)?;
296
297    let response = read_message(&mut stream)?;
298    if response.is_empty() {
299        return Err(AgentError::Proto(
300            "Empty remove-all response from agent".to_string(),
301        ));
302    }
303
304    match response[0] {
305        proto::SSH_AGENT_SUCCESS => {
306            info!("All identities removed from agent");
307            Ok(())
308        }
309        proto::SSH_AGENT_FAILURE => Err(AgentError::Proto(
310            "Agent refused to remove identities".to_string(),
311        )),
312        other => Err(AgentError::Proto(format!(
313            "Unexpected remove-all response: {}",
314            other
315        ))),
316    }
317}
318
319// --- Internal protocol helpers ---
320
321/// Send SSH_AGENTC_REQUEST_IDENTITIES and parse response.
322fn request_identities_raw(stream: &mut UnixStream) -> Result<Vec<Identity>, AgentError> {
323    // Send request: length (4 bytes) + message type (1 byte)
324    let msg = [proto::SSH_AGENTC_REQUEST_IDENTITIES];
325    send_message(stream, &msg)?;
326
327    // Read response
328    let response = read_message(stream)?;
329
330    if response.is_empty() {
331        return Err(AgentError::Proto("Empty response from agent".to_string()));
332    }
333
334    match response[0] {
335        proto::SSH_AGENT_IDENTITIES_ANSWER => parse_identities_answer(&response[1..]),
336        proto::SSH_AGENT_FAILURE => Err(AgentError::Proto("Agent returned failure".to_string())),
337        other => Err(AgentError::Proto(format!(
338            "Unexpected response type: {}",
339            other
340        ))),
341    }
342}
343
344/// Parse SSH_AGENT_IDENTITIES_ANSWER message.
345fn parse_identities_answer(data: &[u8]) -> Result<Vec<Identity>, AgentError> {
346    if data.len() < 4 {
347        return Err(AgentError::Proto("Identities answer too short".to_string()));
348    }
349
350    let num_keys = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
351    let mut identities = Vec::with_capacity(num_keys);
352    let mut pos = 4;
353
354    for _ in 0..num_keys {
355        // Read key blob length
356        if pos + 4 > data.len() {
357            return Err(AgentError::Proto("Truncated key blob length".to_string()));
358        }
359        let blob_len =
360            u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
361        pos += 4;
362
363        // Read key blob
364        if pos + blob_len > data.len() {
365            return Err(AgentError::Proto("Truncated key blob".to_string()));
366        }
367        let blob = &data[pos..pos + blob_len];
368        pos += blob_len;
369
370        // Read comment length
371        if pos + 4 > data.len() {
372            return Err(AgentError::Proto("Truncated comment length".to_string()));
373        }
374        let comment_len =
375            u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
376        pos += 4;
377
378        // Read comment
379        if pos + comment_len > data.len() {
380            return Err(AgentError::Proto("Truncated comment".to_string()));
381        }
382        let comment = String::from_utf8_lossy(&data[pos..pos + comment_len]).to_string();
383        pos += comment_len;
384
385        // Parse the key blob
386        if let Some(pubkey) = parse_ssh_pubkey_blob(blob) {
387            identities.push(Identity { pubkey, comment });
388        }
389    }
390
391    Ok(identities)
392}
393
394/// Parse an SSH public key blob.
395fn parse_ssh_pubkey_blob(blob: &[u8]) -> Option<KeyData> {
396    if blob.len() < 4 {
397        return None;
398    }
399
400    // Read key type string length
401    let type_len = u32::from_be_bytes([blob[0], blob[1], blob[2], blob[3]]) as usize;
402    if blob.len() < 4 + type_len {
403        return None;
404    }
405
406    let key_type = std::str::from_utf8(&blob[4..4 + type_len]).ok()?;
407    let rest = &blob[4 + type_len..];
408
409    match key_type {
410        "ssh-ed25519" => {
411            if rest.len() < 4 {
412                return None;
413            }
414            let key_len = u32::from_be_bytes([rest[0], rest[1], rest[2], rest[3]]) as usize;
415            if rest.len() < 4 + key_len || key_len != 32 {
416                return None;
417            }
418            let key_bytes: [u8; 32] = rest[4..4 + 32].try_into().ok()?;
419            Some(KeyData::Ed25519(Ed25519PublicKey(key_bytes)))
420        }
421        "ecdsa-sha2-nistp256" => {
422            // ECDSA SSH format: curve-name-string + ec-point-string
423            if rest.len() < 4 {
424                return None;
425            }
426            let curve_len = u32::from_be_bytes([rest[0], rest[1], rest[2], rest[3]]) as usize;
427            let after_curve = 4 + curve_len;
428            if rest.len() < after_curve + 4 {
429                return None;
430            }
431            let point_len =
432                u32::from_be_bytes(rest[after_curve..after_curve + 4].try_into().ok()?) as usize;
433            let point_start = after_curve + 4;
434            if rest.len() < point_start + point_len {
435                return None;
436            }
437            let point = &rest[point_start..point_start + point_len];
438            let ecdsa_pk = ssh_key::public::EcdsaPublicKey::from_sec1_bytes(point).ok()?;
439            Some(KeyData::Ecdsa(ecdsa_pk))
440        }
441        _ => None,
442    }
443}
444
445/// Send a sign request and get the signature.
446fn sign_request_raw(
447    stream: &mut UnixStream,
448    pubkey: &KeyData,
449    data: &[u8],
450) -> Result<Vec<u8>, AgentError> {
451    // Encode the public key blob
452    let pubkey_blob = encode_pubkey_blob(pubkey)?;
453
454    // Build the sign request message
455    let mut msg = Vec::new();
456    msg.push(proto::SSH_AGENTC_SIGN_REQUEST);
457
458    // Key blob (length-prefixed)
459    msg.extend_from_slice(&(pubkey_blob.len() as u32).to_be_bytes());
460    msg.extend_from_slice(&pubkey_blob);
461
462    // Data to sign (length-prefixed)
463    msg.extend_from_slice(&(data.len() as u32).to_be_bytes());
464    msg.extend_from_slice(data);
465
466    // Flags (0 for default)
467    msg.extend_from_slice(&0u32.to_be_bytes());
468
469    send_message(stream, &msg)?;
470
471    // Read response
472    let response = read_message(stream)?;
473
474    if response.is_empty() {
475        return Err(AgentError::Proto("Empty sign response".to_string()));
476    }
477
478    match response[0] {
479        proto::SSH_AGENT_SIGN_RESPONSE => parse_sign_response(&response[1..]),
480        proto::SSH_AGENT_FAILURE => Err(AgentError::SigningFailed(
481            "Agent refused to sign".to_string(),
482        )),
483        other => Err(AgentError::Proto(format!(
484            "Unexpected sign response type: {}",
485            other
486        ))),
487    }
488}
489
490/// Parse SSH_AGENT_SIGN_RESPONSE message.
491fn parse_sign_response(data: &[u8]) -> Result<Vec<u8>, AgentError> {
492    if data.len() < 4 {
493        return Err(AgentError::Proto("Sign response too short".to_string()));
494    }
495
496    // Read signature blob length
497    let sig_len = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
498    if data.len() < 4 + sig_len {
499        return Err(AgentError::Proto("Truncated signature blob".to_string()));
500    }
501
502    let sig_blob = &data[4..4 + sig_len];
503
504    // Parse the signature blob (format: string type, string sig)
505    if sig_blob.len() < 4 {
506        return Err(AgentError::Proto("Signature blob too short".to_string()));
507    }
508
509    let type_len =
510        u32::from_be_bytes([sig_blob[0], sig_blob[1], sig_blob[2], sig_blob[3]]) as usize;
511    if sig_blob.len() < 4 + type_len + 4 {
512        return Err(AgentError::Proto("Truncated signature type".to_string()));
513    }
514
515    let rest = &sig_blob[4 + type_len..];
516    let sig_data_len = u32::from_be_bytes([rest[0], rest[1], rest[2], rest[3]]) as usize;
517    if rest.len() < 4 + sig_data_len {
518        return Err(AgentError::Proto("Truncated signature data".to_string()));
519    }
520
521    Ok(rest[4..4 + sig_data_len].to_vec())
522}
523
524/// Encode a public key as an SSH blob.
525fn encode_pubkey_blob(pubkey: &KeyData) -> Result<Vec<u8>, AgentError> {
526    match pubkey {
527        KeyData::Ed25519(pk) => Ok(crate::crypto::ssh::encode_ssh_pubkey(
528            &pk.0,
529            auths_crypto::CurveType::Ed25519,
530        )),
531        KeyData::Ecdsa(pk) => Ok(crate::crypto::ssh::encode_ssh_pubkey(
532            pk.as_ref(),
533            auths_crypto::CurveType::P256,
534        )),
535        _ => Err(AgentError::InvalidInput(
536            "Only Ed25519 and NistP256 keys are supported".to_string(),
537        )),
538    }
539}
540
541/// Send add identity request.
542fn add_identity_raw(
543    stream: &mut UnixStream,
544    private_key: &SshPrivateKey,
545) -> Result<(), AgentError> {
546    // Encode the add identity message
547    let mut msg = Vec::new();
548    msg.push(proto::SSH_AGENTC_ADD_IDENTITY);
549
550    match private_key.key_data() {
551        KeypairData::Ed25519(kp) => {
552            // Key type string
553            let key_type = b"ssh-ed25519";
554            msg.extend_from_slice(&(key_type.len() as u32).to_be_bytes());
555            msg.extend_from_slice(key_type);
556
557            // Public key (32 bytes, length-prefixed)
558            msg.extend_from_slice(&32u32.to_be_bytes());
559            msg.extend_from_slice(&kp.public.0);
560
561            // Private key (64 bytes = seed + public, length-prefixed)
562            let mut priv_bytes = Vec::with_capacity(64);
563            priv_bytes.extend_from_slice(&kp.private.to_bytes());
564            priv_bytes.extend_from_slice(&kp.public.0);
565            msg.extend_from_slice(&(priv_bytes.len() as u32).to_be_bytes());
566            msg.extend_from_slice(&priv_bytes);
567
568            // Comment
569            let comment = b"auths-key";
570            msg.extend_from_slice(&(comment.len() as u32).to_be_bytes());
571            msg.extend_from_slice(comment);
572        }
573        KeypairData::Ecdsa(ssh_key::private::EcdsaKeypair::NistP256 { public, private }) => {
574            // Key type string
575            let key_type = b"ecdsa-sha2-nistp256";
576            msg.extend_from_slice(&(key_type.len() as u32).to_be_bytes());
577            msg.extend_from_slice(key_type);
578
579            // Curve name
580            let curve_name = b"nistp256";
581            msg.extend_from_slice(&(curve_name.len() as u32).to_be_bytes());
582            msg.extend_from_slice(curve_name);
583
584            // Public key (SEC1 uncompressed encoding)
585            let public_bytes = public.as_bytes();
586            msg.extend_from_slice(&(public_bytes.len() as u32).to_be_bytes());
587            msg.extend_from_slice(public_bytes);
588
589            // Private scalar (mpint-encoded — RFC 5656 §3.1.2)
590            let scalar_bytes = private.as_slice();
591            let mpint = crate::crypto::ssh::encode_mpint_for_agent(scalar_bytes);
592            msg.extend_from_slice(&(mpint.len() as u32).to_be_bytes());
593            msg.extend_from_slice(&mpint);
594
595            // Comment
596            let comment = b"auths-key";
597            msg.extend_from_slice(&(comment.len() as u32).to_be_bytes());
598            msg.extend_from_slice(comment);
599        }
600        _ => {
601            return Err(AgentError::InvalidInput(
602                "Only Ed25519 and NistP256 keys are supported".to_string(),
603            ));
604        }
605    }
606
607    send_message(stream, &msg)?;
608
609    // Read response
610    let response = read_message(stream)?;
611
612    if response.is_empty() {
613        return Err(AgentError::Proto("Empty add identity response".to_string()));
614    }
615
616    match response[0] {
617        proto::SSH_AGENT_SUCCESS => Ok(()),
618        proto::SSH_AGENT_FAILURE => Err(AgentError::Proto(
619            "Agent refused to add identity".to_string(),
620        )),
621        other => Err(AgentError::Proto(format!(
622            "Unexpected add identity response: {}",
623            other
624        ))),
625    }
626}
627
628/// Send a length-prefixed message to the agent.
629fn send_message(stream: &mut UnixStream, msg: &[u8]) -> Result<(), AgentError> {
630    let len = (msg.len() as u32).to_be_bytes();
631    stream.write_all(&len).map_err(AgentError::IO)?;
632    stream.write_all(msg).map_err(AgentError::IO)?;
633    stream.flush().map_err(AgentError::IO)?;
634    Ok(())
635}
636
637/// Read a length-prefixed message from the agent.
638fn read_message(stream: &mut UnixStream) -> Result<Vec<u8>, AgentError> {
639    let mut len_buf = [0u8; 4];
640    stream.read_exact(&mut len_buf).map_err(AgentError::IO)?;
641    let len = u32::from_be_bytes(len_buf) as usize;
642
643    if len > 256 * 1024 {
644        return Err(AgentError::Proto(format!(
645            "Message too large: {} bytes",
646            len
647        )));
648    }
649
650    let mut msg = vec![0u8; len];
651    stream.read_exact(&mut msg).map_err(AgentError::IO)?;
652    Ok(msg)
653}
654
655#[cfg(test)]
656mod tests {
657    use super::*;
658
659    #[test]
660    fn test_check_agent_status_not_running() {
661        let status = check_agent_status("/nonexistent/path/to/socket.sock");
662        assert_eq!(status, AgentStatus::NotRunning);
663    }
664
665    #[test]
666    fn test_encode_pubkey_blob() {
667        let pubkey = Ed25519PublicKey([0x42; 32]);
668        let key_data = KeyData::Ed25519(pubkey);
669        let blob = encode_pubkey_blob(&key_data).unwrap();
670
671        // Verify format: 4-byte length + "ssh-ed25519" + 4-byte length + 32-byte key
672        assert_eq!(&blob[0..4], &11u32.to_be_bytes()); // "ssh-ed25519" length
673        assert_eq!(&blob[4..15], b"ssh-ed25519");
674        assert_eq!(&blob[15..19], &32u32.to_be_bytes()); // key length
675        assert_eq!(&blob[19..51], &[0x42; 32]); // key data
676    }
677
678    #[test]
679    fn test_parse_ssh_pubkey_blob() {
680        // Create a valid Ed25519 blob
681        let mut blob = Vec::new();
682        blob.extend_from_slice(&11u32.to_be_bytes()); // type length
683        blob.extend_from_slice(b"ssh-ed25519");
684        blob.extend_from_slice(&32u32.to_be_bytes()); // key length
685        blob.extend_from_slice(&[0x42; 32]); // key data
686
687        let result = parse_ssh_pubkey_blob(&blob);
688        assert!(result.is_some());
689
690        if let Some(KeyData::Ed25519(pk)) = result {
691            assert_eq!(pk.0, [0x42; 32]);
692        } else {
693            panic!("Expected Ed25519 key");
694        }
695    }
696
697    #[test]
698    fn test_parse_invalid_pkcs8() {
699        let result = auths_crypto::parse_key_material(&[0u8; 10]);
700        assert!(result.is_err());
701    }
702}