1use 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#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum AgentStatus {
20 Running {
22 key_count: usize,
24 },
25 ConnectionFailed,
27 NotRunning,
29}
30
31mod proto {
33 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 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
46pub 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 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 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 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
98pub 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 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 let key_data = match pubkey.len() {
139 32 => {
140 #[allow(clippy::unwrap_used)] 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 let signature = sign_request_raw(&mut stream, &key_data, data)?;
158
159 debug!("Successfully signed via agent");
160 Ok(signature)
161}
162
163pub 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 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
232pub 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
269pub 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
319fn request_identities_raw(stream: &mut UnixStream) -> Result<Vec<Identity>, AgentError> {
323 let msg = [proto::SSH_AGENTC_REQUEST_IDENTITIES];
325 send_message(stream, &msg)?;
326
327 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
344fn 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 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 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 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 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 if let Some(pubkey) = parse_ssh_pubkey_blob(blob) {
387 identities.push(Identity { pubkey, comment });
388 }
389 }
390
391 Ok(identities)
392}
393
394fn parse_ssh_pubkey_blob(blob: &[u8]) -> Option<KeyData> {
396 if blob.len() < 4 {
397 return None;
398 }
399
400 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 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
445fn sign_request_raw(
447 stream: &mut UnixStream,
448 pubkey: &KeyData,
449 data: &[u8],
450) -> Result<Vec<u8>, AgentError> {
451 let pubkey_blob = encode_pubkey_blob(pubkey)?;
453
454 let mut msg = Vec::new();
456 msg.push(proto::SSH_AGENTC_SIGN_REQUEST);
457
458 msg.extend_from_slice(&(pubkey_blob.len() as u32).to_be_bytes());
460 msg.extend_from_slice(&pubkey_blob);
461
462 msg.extend_from_slice(&(data.len() as u32).to_be_bytes());
464 msg.extend_from_slice(data);
465
466 msg.extend_from_slice(&0u32.to_be_bytes());
468
469 send_message(stream, &msg)?;
470
471 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
490fn 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 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 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
524fn 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
541fn add_identity_raw(
543 stream: &mut UnixStream,
544 private_key: &SshPrivateKey,
545) -> Result<(), AgentError> {
546 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 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 msg.extend_from_slice(&32u32.to_be_bytes());
559 msg.extend_from_slice(&kp.public.0);
560
561 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 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 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 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 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 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 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 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
628fn 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
637fn 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 assert_eq!(&blob[0..4], &11u32.to_be_bytes()); assert_eq!(&blob[4..15], b"ssh-ed25519");
674 assert_eq!(&blob[15..19], &32u32.to_be_bytes()); assert_eq!(&blob[19..51], &[0x42; 32]); }
677
678 #[test]
679 fn test_parse_ssh_pubkey_blob() {
680 let mut blob = Vec::new();
682 blob.extend_from_slice(&11u32.to_be_bytes()); blob.extend_from_slice(b"ssh-ed25519");
684 blob.extend_from_slice(&32u32.to_be_bytes()); blob.extend_from_slice(&[0x42; 32]); 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}