Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 96 additions & 2 deletions tests/harness/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::{
ffi::{OsStr, OsString},
fs,
io::{self, Write},
path::PathBuf,
time::{Duration, Instant},
};

Expand All @@ -12,6 +15,79 @@ const CRATE_MAIN_BIN: &str = env!(concat!("CARGO_BIN_EXE_", env!("CARGO_PKG_NAME
const AGENT_TIMEOUT: Duration = Duration::from_secs(2);
const AGENT_POLL: Duration = Duration::from_micros(100);
const SIGTERM: std::ffi::c_int = 15;
const SSH_KEY_FILE_MODE: u32 = 0o400;

#[derive(Debug)]
pub struct SshPkiKeygen {
pub ca_key: Vec<u8>,
pub serial: usize,
}

impl SshPkiKeygen {
const CA_KEY_FILENAME: &str = "ca_key";
const USER_KEY_FILENAME: &str = "user_key";

pub fn new() -> io::Result<Self> {
let temp_dir = tempfile::tempdir()?;
cmd!("ssh-keygen", "-q", "-N", "", "-f", Self::CA_KEY_FILENAME)
.dir(temp_dir.path())
.run()
.map_err(|e| map_binary_notfound_error("ssh-keygen", e))?;

let ca_key = fs::read(temp_dir.path().join(Self::CA_KEY_FILENAME))?;

Ok(Self { ca_key, serial: 0 })
}

fn make_ca_dir(&self) -> io::Result<(tempfile::TempDir, PathBuf)> {
let temp_dir = tempfile::tempdir()?;
let ca_key_path = temp_dir.path().join(Self::CA_KEY_FILENAME);
fs::write(&ca_key_path, &self.ca_key)?;
#[cfg(unix)]
fs::set_permissions(&ca_key_path, fs::Permissions::from_mode(SSH_KEY_FILE_MODE))?;
Ok((temp_dir, ca_key_path))
}

pub fn sign(&mut self, key: impl AsRef<[u8]>) -> io::Result<Vec<u8>> {
let key = key.as_ref();

let (ca_dir, ca_key_path) = self.make_ca_dir()?;

let user_key_path = ca_dir.path().join(Self::USER_KEY_FILENAME);
fs::write(&user_key_path, key)?;

self.serial += 1;
#[rustfmt::skip]
cmd!(
"ssh-keygen",
"-q",
"-s", &ca_key_path,
"-I", "test_user",
"-n", "test_user",
"-V", "+1h",
"-z", self.serial.to_string(),
&user_key_path
)
.dir(ca_dir.path())
.run()
.map_err(|e| map_binary_notfound_error("ssh-keygen", e))?;

let user_cert_path = {
// Get the filename part of the key path
let mut user_cert_filename = user_key_path
.file_name()
.map(|s| s.to_os_string())
.unwrap_or_default();
user_cert_filename.push("-cert.pub");
// Clone the key path, but replace the filename with the `-cert.pub`-appended filename
let mut user_cert_path = user_key_path.clone();
user_cert_path.set_file_name(user_cert_filename);
user_cert_path
};

fs::read(&user_cert_path)
}
}

pub enum SshAgentType {
OpenSsh,
Expand Down Expand Up @@ -106,11 +182,29 @@ impl SshAgentInstance {
.map_err(|e| map_binary_notfound_error(CRATE_MAIN_BIN, e))
}

pub fn add(&self, key: &str) -> io::Result<()> {
pub fn add(&self, key: impl AsRef<[u8]>) -> io::Result<()> {
// Add an ssh-key from stdin
cmd!("ssh-add", "-q", "--", "-")
.env("SSH_AUTH_SOCK", &self.sock_path)
.stdin_bytes(key)
.stdin_bytes(key.as_ref())
.run()
.map_err(|e| map_binary_notfound_error("ssh-add", e))?;

Ok(())
}

pub fn add_cert(&self, key: impl AsRef<[u8]>, cert: impl AsRef<[u8]>) -> io::Result<()> {
// Add a key and certificate pair from temporary files
let temp_dir = tempfile::tempdir()?;
let key_file = temp_dir.path().join("key");
fs::write(&key_file, key.as_ref())?;
#[cfg(unix)]
fs::set_permissions(&key_file, fs::Permissions::from_mode(SSH_KEY_FILE_MODE))?;
let cert_file = temp_dir.path().join("key-cert.pub");
fs::write(&cert_file, cert.as_ref())?;

cmd!("ssh-add", "-q", "--", &key_file)
.env("SSH_AUTH_SOCK", &self.sock_path)
.run()
.map_err(|e| map_binary_notfound_error("ssh-add", e))?;

Expand Down
40 changes: 39 additions & 1 deletion tests/ssh-agent-integration.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{ffi::OsString, io};

use harness::SshAgentInstance;
use harness::{SshAgentInstance, SshPkiKeygen};

mod harness;
mod keys;
Expand All @@ -18,6 +18,19 @@ fn make_openssh_agent_with_keys() -> io::Result<SshAgentInstance> {
Ok(agent)
}

fn make_openssh_agent_with_certs() -> io::Result<SshAgentInstance> {
let agent = SshAgentInstance::new_openssh()?;
println!("{:#?}", agent);

let mut pki = SshPkiKeygen::new()?;
for key in keys::PRIVATE {
let cert = pki.sign(key)?;
agent.add_cert(key, cert)?;
}

Ok(agent)
}

fn assert_all_keys_in_agent(agent: &SshAgentInstance) -> TestResult {
let keys_in_agent = agent.list()?;
for key in keys::PUBLIC {
Expand All @@ -36,6 +49,15 @@ fn add_keys_to_openssh_agent() -> TestResult {
Ok(())
}

#[test]
fn add_certs_to_openssh_agent() -> TestResult {
let agent = make_openssh_agent_with_certs()?;

assert_all_keys_in_agent(&agent)?;

Ok(())
}

#[test]
fn empty_mux_agent() -> TestResult {
let agent = SshAgentInstance::new_mux("", None::<OsString>)?;
Expand Down Expand Up @@ -85,3 +107,19 @@ fn mux_with_three_agents() -> TestResult {

Ok(())
}

#[test]
fn mux_with_one_agent_with_certs() -> TestResult {
let openssh_agent = make_openssh_agent_with_certs()?;
let mux_agent = SshAgentInstance::new_mux(
&format!(
r##"agent_sock_paths = ["{}"]"##,
openssh_agent.sock_path.display()
),
None::<OsString>,
)?;

assert_all_keys_in_agent(&mux_agent)?;

Ok(())
}
Loading