1use 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#[derive(Serialize, Debug, Clone)]
82pub struct KeyLoadStatus {
83 pub alias: KeyAlias,
85 pub loaded: bool,
87 pub error: Option<String>,
89}
90
91#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
93pub enum RegistrationOutcome {
94 Added,
96 AlreadyExists,
98 AgentNotFound,
100 CommandFailed,
102 UnsupportedKeyType,
104 ConversionFailed,
106 IoError,
108 InternalError,
110}
111
112#[derive(Serialize, Debug, Clone)]
114pub struct KeyRegistrationStatus {
115 pub fingerprint: String,
117 pub status: RegistrationOutcome,
119 pub message: Option<String>,
121}
122
123pub 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
148pub 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 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
260pub 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 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 let (seed, pubkey) = provider_bridge::generate_ed25519_keypair_sync()
312 .map_err(|e| AgentError::CryptoError(format!("Failed to generate new keypair: {}", e)))?;
313 let new_pkcs8_bytes = auths_crypto::build_ed25519_pkcs8_v2(seed.as_bytes(), &pubkey);
315 debug!("Generated new keypair via CryptoProvider.");
316
317 let encrypted_new_key = encrypt_keypair(&new_pkcs8_bytes, new_passphrase)?;
319 debug!("Encrypted new keypair with provided passphrase.");
320
321 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
341pub 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 handle.sign(pubkey, data)
367}
368
369pub 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 let key_alias = KeyAlias::new_unchecked(alias);
393 let (_controller_did, _role, encrypted_pkcs8) = keychain.load_key(&key_alias)?;
394
395 let pkcs8_bytes = decrypt_keypair(&encrypted_pkcs8, passphrase)?;
397
398 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 let ssh_private_key = SshPrivateKey::new(keypair_data, "") .map_err(|e| {
412 AgentError::CryptoError(format!(
414 "Failed to create ssh_key::PrivateKey for alias '{}': {}",
415 alias, e
416 ))
417 })?;
418
419 let pem = ssh_private_key.to_openssh(LineEnding::LF).map_err(|e| {
421 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) }
431
432pub 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 let key_alias = KeyAlias::new_unchecked(alias);
457 let (_controller_did, _role, encrypted_pkcs8) = keychain.load_key(&key_alias)?;
458
459 let pkcs8_bytes = decrypt_keypair(&encrypted_pkcs8, passphrase)?;
461
462 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; 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 let ssh_pub_key = SshPublicKey::new(key_data, ""); let pubkey_base = ssh_pub_key.to_openssh().map_err(|e| {
485 AgentError::CryptoError(format!("Failed to format OpenSSH pubkey base: {}", e))
487 })?;
488
489 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
499pub fn get_agent_key_count_with_handle(handle: &AgentHandle) -> Result<usize, AgentError> {
507 handle.key_count()
508}
509
510#[cfg(target_os = "macos")]
529#[allow(clippy::disallowed_methods)]
530#[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#[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#[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 #[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#[cfg(unix)]
729#[allow(clippy::disallowed_methods)] pub 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 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 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 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 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 handle.set_running(true);
780
781 let session = AgentSession::new(handle.clone());
783
784 let result = listen(listener, session).await;
786
787 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#[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}