1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-06 11:43:51 +00:00
Files
browser/apps/desktop/desktop_native/ssh_agent/tests/integration.rs
Bernd Schoolmann 4a9186c5fc Cargo format
2025-10-17 15:10:42 +02:00

209 lines
6.3 KiB
Rust

#[cfg(target_os = "linux")]
use std::{fs, process::Command, sync::Arc};
#[cfg(target_os = "linux")]
use ssh_agent::{
agent::{
ui_requester::{UiRequestMessage, UiRequester},
BitwardenDesktopAgent,
},
memory::UnlockedSshItem,
protocol::types::{KeyPair, PrivateKey},
transport::unix_listener_stream::UnixListenerStream,
};
#[cfg(target_os = "linux")]
use tokio::{
sync::{broadcast, mpsc, Mutex},
task,
};
#[cfg(target_os = "linux")]
use tracing::info;
#[cfg(target_os = "linux")]
#[tokio::test]
#[test_log::test]
async fn ssh_agent_auth() {
let dir = homedir::my_home().unwrap().unwrap();
let dir = dir.join(".cache");
let dir = dir.join("ssh_agent_integration_test");
let dir = dir.to_string_lossy().into_owned();
fs::remove_dir_all(&dir).unwrap_or(());
// Prepare test run directory
fs::create_dir_all(&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
",
dir, dir, dir, dir
);
fs::write(format!("{}/sshd_config", dir), config).unwrap();
let keys = make_keys(&dir);
// Start ssh server
let dir_clone = dir.clone();
std::thread::spawn(move || {
Command::new("/usr/bin/sshd")
.args(["-f", &format!("{}/sshd_config", &dir_clone), "-D", "-e"])
.status()
.expect("failed to execute process");
});
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
let ui_requester = mock_channels();
let desktop_agent = BitwardenDesktopAgent::new(ui_requester);
desktop_agent.set_keys(keys);
let dir_clone = dir.clone();
task::spawn(async move {
info!(target: "ssh-agent", "Listening on {}", format!("{}/ssh-agent.sock", dir_clone));
UnixListenerStream::listen(format!("{}/ssh-agent.sock", dir_clone), desktop_agent)
.await
.unwrap();
});
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
// Test listing keys
let dir_clone = dir.clone();
let jh1 = std::thread::spawn(move || {
Command::new("ssh-add")
.env("SSH_AUTH_SOCK", format!("{}/ssh-agent.sock", dir_clone))
.args(["-L"])
.status()
.expect("failed to execute process");
});
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
jh1.join().unwrap();
// Test ssh connection
let dir_clone = dir.clone();
let jh1 = std::thread::spawn(move || {
Command::new("ssh")
.env("SSH_AUTH_SOCK", format!("{}/ssh-agent.sock", dir_clone))
.args([
"-o",
"StrictHostKeyChecking=no",
"-o",
"UserKnownHostsFile=/dev/null",
"-p",
"2222",
"localhost",
"echo",
"Success",
])
.status()
.expect("failed to execute process");
});
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
jh1.join().unwrap();
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
// Cleanup
fs::remove_dir_all(dir).unwrap();
std::process::exit(0);
}
#[cfg(target_os = "linux")]
fn make_keys(dir: &str) -> Vec<UnlockedSshItem> {
Command::new("ssh-keygen")
.args([
"-f",
&format!("{}/ssh_host_rsa_key", dir),
"-N",
"",
"-t",
"rsa",
])
.status()
.expect("failed to execute process");
Command::new("ssh-keygen")
.args([
"-f",
&format!("{}/ssh_host_ecdsa_key", dir),
"-N",
"",
"-t",
"ecdsa",
])
.status()
.expect("failed to execute process");
Command::new("ssh-keygen")
.args([
"-f",
&format!("{}/ssh_host_ed25519_key", dir),
"-N",
"",
"-t",
"ed25519",
])
.status()
.expect("failed to execute process");
// // Make user key
Command::new("ssh-keygen")
.args([
"-f",
&format!("{}/id_ed25519", dir),
"-N",
"",
"-t",
"ed25519",
])
.status()
.expect("failed to execute process");
Command::new("ssh-keygen")
.args(["-f", &format!("{}/ssh_rsa", dir), "-N", "", "-t", "rsa"])
.status()
.expect("failed to execute process");
let pubkey1 =
fs::read_to_string(format!("{}/id_ed25519.pub", dir)).expect("failed to read public key");
let pubkey2 =
fs::read_to_string(format!("{}/ssh_rsa.pub", dir)).expect("failed to read public key");
fs::write(
format!("{}/authorized_keys", dir),
format!("{}{}", pubkey1, pubkey2),
)
.expect("failed to write authorized_keys");
let privkey1 = fs::read_to_string(format!("{}/id_ed25519", dir)).expect("key is valid");
let key1 = KeyPair::new(
PrivateKey::try_from(privkey1).expect("key is valid"),
"ed25519-key".to_string(),
);
let privkey2 = fs::read_to_string(format!("{}/ssh_rsa", dir)).expect("key is valid");
let key2 = KeyPair::new(
PrivateKey::try_from(privkey2).expect("key is valid"),
"rsa-key".to_string(),
);
let unlocked_items = vec![
UnlockedSshItem::new(key1, "cipher1".to_string()),
UnlockedSshItem::new(key2, "cipher2".to_string()),
];
unlocked_items
}
#[cfg(target_os = "linux")]
fn mock_channels() -> UiRequester {
let (show_ui_request_tx, mut show_ui_request_rx) = mpsc::channel::<UiRequestMessage>(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 _handle = 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)
}