From 164c0bd35557cdcdba48ee8698c1a82d19ce36d0 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 17 Oct 2025 14:02:41 +0200 Subject: [PATCH] Fix --- .../ssh_agent/examples/integration.rs | 174 ++++++++++++++++++ .../ssh_agent/src/protocol/requests.rs | 3 +- .../ssh_agent/src/protocol/types.rs | 22 ++- 3 files changed, 189 insertions(+), 10 deletions(-) create mode 100644 apps/desktop/desktop_native/ssh_agent/examples/integration.rs diff --git a/apps/desktop/desktop_native/ssh_agent/examples/integration.rs b/apps/desktop/desktop_native/ssh_agent/examples/integration.rs new file mode 100644 index 00000000000..ebd1df194cc --- /dev/null +++ b/apps/desktop/desktop_native/ssh_agent/examples/integration.rs @@ -0,0 +1,174 @@ +use std::{fs, process::Command, sync::Arc}; + +use ssh_agent::{agent::{ui_requester::{UiRequestMessage, UiRequester}, BitwardenDesktopAgent}, memory::UnlockedSshItem, protocol::types::{KeyPair, PrivateKey}, transport::unix_listener_stream::UnixListenerStream}; +use tokio::{sync::{broadcast, mpsc, Mutex}, task}; +use tracing::info; + +const TEST_RUN_DIR: &str = "/home/quexten/test_run/"; + +#[tokio::main] +async fn main() { + // set up tracing to stdout + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .with_thread_ids(true) + .with_thread_names(true) + .init(); + + fs::remove_dir_all(TEST_RUN_DIR).unwrap_or(()); + // Prepare test run directory + fs::create_dir_all(TEST_RUN_DIR).unwrap(); + + let config = format!("Port 2222 +HostKey {}/ssh_host_rsa_key +HostKey {}/ssh_host_ecdsa_key +HostKey {}/ssh_host_ed25519_key +AuthorizedKeysFile {}/authorized_keys +", TEST_RUN_DIR, TEST_RUN_DIR, TEST_RUN_DIR, TEST_RUN_DIR); + fs::write(format!("{}/sshd_config", TEST_RUN_DIR), config).unwrap(); + + let keys = make_keys(); + + // Start ssh server + std::thread::spawn(move || { + Command::new("/usr/bin/sshd") + .args(&["-f", &format!("{}/sshd_config", TEST_RUN_DIR), "-D", "-e"]) + .status() + .expect("failed to execute process"); + }); + + let ui_requester = mock_channels(); + let desktop_agent = BitwardenDesktopAgent::new(ui_requester); + desktop_agent.set_keys(keys); + + task::spawn(async move { + println!("Starting SSH Agent V2 socket..."); + info!(target: "ssh-agent", "Listening on {}", format!("{}/ssh-agent.sock", TEST_RUN_DIR)); + UnixListenerStream::listen(format!("{}/ssh-agent.sock", TEST_RUN_DIR), desktop_agent) + .await + .unwrap(); + }); + + // run ssh-add -L + Command::new("ssh-add") + .env("SSH_AUTH_SOCK", format!("{}/ssh-agent.sock", TEST_RUN_DIR)) + .args(&["-L"]) + .status() + .expect("failed to execute process"); + + // run ssh + Command::new("ssh") + .env("SSH_AUTH_SOCK", format!("{}/ssh-agent.sock", TEST_RUN_DIR)) + .args(&[ + "-o", + "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", + "-p", + "2222", + "localhost", + "echo", + "Hello, world!", + ]) + .status() + .expect("failed to execute process"); + + // Cleanup + fs::remove_dir_all(TEST_RUN_DIR).unwrap(); + std::process::exit(0); +} + +fn make_keys() -> Vec { + Command::new("ssh-keygen") + .args(&[ + "-f", + &format!("{}/ssh_host_rsa_key", TEST_RUN_DIR), + "-N", + "", + "-t", + "rsa", + ]) + .status() + .expect("failed to execute process"); + Command::new("ssh-keygen") + .args(&[ + "-f", + &format!("{}/ssh_host_ecdsa_key", TEST_RUN_DIR), + "-N", + "", + "-t", + "ecdsa", + ]) + .status() + .expect("failed to execute process"); + Command::new("ssh-keygen") + .args(&[ + "-f", + &format!("{}/ssh_host_ed25519_key", TEST_RUN_DIR), + "-N", + "", + "-t", + "ed25519", + ]) + .status() + .expect("failed to execute process"); + // // Make user key + Command::new("ssh-keygen") + .args(&[ + "-f", + &format!("{}/id_ed25519", TEST_RUN_DIR), + "-N", + "", + "-t", + "ed25519", + ]) + .status() + .expect("failed to execute process"); + Command::new("ssh-keygen") + .args(&[ + "-f", + &format!("{}/ssh_rsa", TEST_RUN_DIR), + "-N", + "", + "-t", + "rsa", + ]) + .status() + .expect("failed to execute process"); + let pubkey1 = fs::read_to_string(format!("{}/id_ed25519.pub", TEST_RUN_DIR)).unwrap(); + let pubkey2 = fs::read_to_string(format!("{}/ssh_rsa.pub", TEST_RUN_DIR)).unwrap(); + fs::write( + format!("{}/authorized_keys", TEST_RUN_DIR), + format!("{}{}", pubkey1, pubkey2), + ) + .unwrap(); + let privkey1 = fs::read_to_string(format!("{}/id_ed25519", TEST_RUN_DIR)).unwrap(); + let key1 = KeyPair::new(PrivateKey::try_from(privkey1).unwrap(), "ed25519-key".to_string()); + let privkey2 = fs::read_to_string(format!("{}/ssh_rsa", TEST_RUN_DIR)).unwrap(); + let key2 = KeyPair::new(PrivateKey::try_from(privkey2).unwrap(), "rsa-key".to_string()); + let unlocked_items = vec![ + UnlockedSshItem::new(key1, "cipher1".to_string()), + UnlockedSshItem::new(key2, "cipher2".to_string()), + ]; + unlocked_items +} + +fn mock_channels() -> UiRequester { + let (show_ui_request_tx, mut show_ui_request_rx) = mpsc::channel::(10); + + // Create mock broadcast channel for responses + let (response_tx, response_rx) = broadcast::channel::<(u32, bool)>(10); + let get_ui_response_rx = Arc::new(Mutex::new(response_rx)); + + // Spawn a task to automatically send back "true" responses + let response_tx_clone = response_tx.clone(); + let _ = task::spawn(async move { + while let Some(req) = show_ui_request_rx.recv().await { + info!("Mock UI requester received request: {:?}", req); + let _ = response_tx_clone.send((req.id(), true)); + } + info!("Mock UI requester task ending"); + }); + + UiRequester::new(show_ui_request_tx, get_ui_response_rx) +} \ No newline at end of file diff --git a/apps/desktop/desktop_native/ssh_agent/src/protocol/requests.rs b/apps/desktop/desktop_native/ssh_agent/src/protocol/requests.rs index cfebf19538e..2f0bda89646 100644 --- a/apps/desktop/desktop_native/ssh_agent/src/protocol/requests.rs +++ b/apps/desktop/desktop_native/ssh_agent/src/protocol/requests.rs @@ -82,7 +82,6 @@ impl TryFrom<&[u8]> for Request { // Only support session bind for now let extension_request: SessionBindRequest = contents.as_slice().try_into()?; if !extension_request.verify_signature() { - info!("Invalid session bind signature"); return Err(anyhow::anyhow!("Invalid session bind signature")); } Ok(Request::SessionBind(extension_request)) @@ -162,7 +161,7 @@ impl TryFrom<&[u8]> for SshSignRequest { .map_err(|e| anyhow::anyhow!("Failed to read flags from sign request: {e}"))?; Ok(SshSignRequest { - public_key: public_key_blob.try_into()?, + public_key: PublicKey::from_blob(public_key_blob), payload_to_sign: data.clone(), parsed_sign_request: data.as_slice().try_into()?, flags, diff --git a/apps/desktop/desktop_native/ssh_agent/src/protocol/types.rs b/apps/desktop/desktop_native/ssh_agent/src/protocol/types.rs index ce01be2a623..296cf1602ec 100644 --- a/apps/desktop/desktop_native/ssh_agent/src/protocol/types.rs +++ b/apps/desktop/desktop_native/ssh_agent/src/protocol/types.rs @@ -269,12 +269,14 @@ impl PrivateKey { PrivateKey::Ecdsa(key) => ssh_key::private::PrivateKey::from(key.to_owned()), }; - private_key + let pubkey_bytes = BASE64_STANDARD.encode(private_key .public_key() .to_bytes() - .map(PublicKey::try_from) - .expect("Key is always valid") - .expect("Key is always valid") + .expect("Converting to public key bytes should always be possible")); + let alg_str = private_key.algorithm(); + + PublicKey::try_from(format!("{} {}", alg_str.as_str(), BASE64_STANDARD.encode(&pubkey_bytes))) + .expect("Parsing public key should always be possible") } pub(crate) fn sign( @@ -345,10 +347,7 @@ impl PublicKey { &self, writer: &mut impl ssh_encoding::Writer, ) -> Result<(), ssh_encoding::Error> { - let mut buf = Vec::new(); - self.alg().as_bytes().encode(&mut buf)?; - self.blob().encode(&mut buf)?; - buf.encode(writer)?; + self.blob().encode(writer)?; Ok(()) } fn try_read_from(mut bytes: &[u8]) -> Result { @@ -356,6 +355,13 @@ impl PublicKey { let blob = read_bytes(&mut bytes)?; Ok(PublicKey { alg, blob }) } + + pub fn from_blob(blob: Vec) -> Self { + // Parse the blob to extract the algorithm + let mut bytes = &blob[..]; + let alg = String::from_utf8_lossy(read_bytes(&mut bytes).unwrap().as_slice()).to_string(); + PublicKey { alg, blob } + } } impl Display for PublicKey {