use std::io::Cursor;
#[cfg(any(feature = "shamir-sharing", all(feature = "remote-factor", unix)))]
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
#[cfg(any(feature = "shamir-sharing", all(feature = "remote-factor", unix)))]
use std::process::Stdio;
use rand_core::OsRng;
use ssh_key::{Algorithm, LineEnding, PrivateKey};
use sshenv_vault::recipient::fingerprint_from_line;
fn cargo_bin() -> PathBuf {
if let Some(path) = option_env!("CARGO_BIN_EXE_sshenv") {
return PathBuf::from(path);
}
let dir = env!("CARGO_MANIFEST_DIR");
let target = PathBuf::from(dir).join("../../target");
let debug = target.join("debug").join("sshenv");
if debug.exists() {
return debug;
}
target.join("release").join("sshenv")
}
fn write_pubkey_file(dir: &std::path::Path) -> (PathBuf, String) {
write_named_keypair(dir, "id_test")
}
fn write_named_keypair(dir: &std::path::Path, name: &str) -> (PathBuf, String) {
let priv_key = PrivateKey::random(&mut OsRng, Algorithm::Ed25519).expect("gen key");
let pub_line = priv_key.public_key().to_openssh().expect("pub");
let priv_pem = priv_key
.to_openssh(LineEnding::LF)
.expect("priv pem")
.to_string();
let priv_path = dir.join(name);
std::fs::write(&priv_path, &priv_pem).unwrap();
std::fs::write(priv_path.with_extension("pub"), format!("{pub_line}\n")).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&priv_path, std::fs::Permissions::from_mode(0o600)).unwrap();
}
(priv_path, pub_line)
}
fn init_vault_with_profile(
bin: &std::path::Path,
home: &std::path::Path,
vault_path: &std::path::Path,
profile: &str,
) {
std::fs::create_dir_all(home.join(".ssh")).unwrap();
let _key = write_named_keypair(&home.join(".ssh"), "id_ed25519");
let init_out = Command::new(bin)
.arg("--vault")
.arg(vault_path)
.arg("init")
.arg("--recipient-key")
.arg(home.join(".ssh").join("id_ed25519.pub"))
.env("HOME", home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run init");
assert!(
init_out.status.success(),
"init failed: {}",
String::from_utf8_lossy(&init_out.stderr)
);
let set_out = Command::new(bin)
.arg("--vault")
.arg(vault_path)
.arg("set")
.arg(profile)
.arg("DUMMY")
.arg("--value")
.arg("value")
.env("HOME", home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run set");
assert!(
set_out.status.success(),
"set failed: {}",
String::from_utf8_lossy(&set_out.stderr)
);
}
fn migrate_vault_to_v2(
bin: &std::path::Path,
home: &std::path::Path,
vault_path: &std::path::Path,
) {
let migrate_out = Command::new(bin)
.arg("--vault")
.arg(vault_path)
.arg("migrate-vault")
.arg("--to")
.arg("v2")
.arg("--recipient-key")
.arg(home.join(".ssh").join("id_ed25519.pub"))
.env("HOME", home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run migrate-vault");
assert!(
migrate_out.status.success(),
"migrate-vault failed: {}",
String::from_utf8_lossy(&migrate_out.stderr)
);
}
#[test]
fn age_roundtrip_using_generated_key() {
let dir = tempfile::tempdir().unwrap();
let (priv_path, pub_line) = write_pubkey_file(dir.path());
let (mut vault, key) = sshenv_vault::Vault::create(&pub_line).expect("create");
vault.profiles.set("p", "VAR", "value".into());
let vault_path = dir.path().join("vault");
vault.save(&vault_path, &key).expect("save");
let raw = std::fs::read(&priv_path).unwrap();
let id = age::ssh::Identity::from_buffer(Cursor::new(&raw), None).expect("parse");
let identities: Vec<Box<dyn age::Identity>> = vec![Box::new(id)];
let ct = sshenv_vault::Vault::load_ciphertext(&vault_path).expect("load ct");
let (unlocked, _k) = sshenv_vault::Vault::unlock(ct, &identities).expect("unlock");
assert_eq!(
unlocked.profiles.get("p").unwrap().get("VAR").unwrap(),
"value"
);
}
#[test]
fn binary_help_runs() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!(
"skipping: {} does not exist; run `cargo build` first",
bin.display()
);
return;
}
let output = Command::new(bin)
.arg("--help")
.output()
.expect("run --help");
assert!(
output.status.success(),
"sshenv --help exited non-zero: {}\nstderr: {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("sshenv"));
}
#[test]
fn binary_security_planning_commands_emit_json() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let passphrase_cache_expected = if cfg!(target_os = "macos") {
"macos-keychain"
} else if cfg!(target_os = "windows") {
"windows-dpapi"
} else {
"unavailable"
};
let cases: &[(&[&str], &str, &str)] = &[
(
&["security", "passphrase-cache", "plan", "--json"],
"enabled",
passphrase_cache_expected,
),
(
&["security", "device", "plan", "--backend", "tpm", "--json"],
"backend",
"Tpm",
),
(
&[
"security",
"rollback",
"plan",
"--backend",
"remote-checkpoint",
"--json",
],
"backend",
"RemoteCheckpoint",
),
(
&[
"security",
"remote",
"plan",
"--backend",
"cloud-kms",
"--command",
"kms-adapter",
"--json",
],
"enable_command_example",
"kms-adapter",
),
];
for (args, key, expected) in cases {
let output = Command::new(&bin)
.args(*args)
.output()
.expect("run planning command");
assert!(
output.status.success(),
"{:?} failed: {}",
args,
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(
json.get(*key).is_some(),
"missing {key} in {}",
String::from_utf8_lossy(&output.stdout)
);
assert!(
String::from_utf8_lossy(&output.stdout).contains(expected),
"stdout did not contain {expected}: {}",
String::from_utf8_lossy(&output.stdout)
);
}
}
#[cfg(unix)]
#[test]
fn binary_hardware_command_adapter_discovers_and_enrolls() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let (_priv_path, pub_line) = write_named_keypair(dir.path(), "id_ed25519");
let command_path = dir.path().join("hardware-adapter.sh");
std::fs::write(
&command_path,
format!(
r#"#!/bin/sh
input=$(cat)
case "$input" in
*'"operation":"list"'*|*'"operation": "list"'*)
printf '[{{"id":"slot-9c","label":"test token","kind":"yubi-key-piv","public_descriptor":"{pub_line}"}}]\n'
;;
*'"operation":"recipient"'*|*'"operation": "recipient"'*)
printf '{{"id":"slot-9c","label":"test token","kind":"yubi-key-piv","public_descriptor":"{pub_line}"}}\n'
;;
*) echo 'unknown operation' >&2; exit 1 ;;
esac
"#
),
)
.unwrap();
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&command_path, std::fs::Permissions::from_mode(0o755)).unwrap();
}
let discover_out = Command::new(&bin)
.args([
"security",
"hardware",
"discover",
"--kind",
"yubi-key-piv",
"--command",
])
.arg(&command_path)
.arg("--json")
.output()
.expect("run hardware discover");
assert!(
discover_out.status.success(),
"hardware discover failed: {}",
String::from_utf8_lossy(&discover_out.stderr)
);
let discover_json: serde_json::Value = serde_json::from_slice(&discover_out.stdout).unwrap();
assert_eq!(discover_json["recipients"][0]["id"], "slot-9c");
assert_eq!(discover_json["recipients"][0]["valid"], true);
assert!(
discover_json["recipients"][0]["add_recipient_example"]
.as_str()
.unwrap()
.contains("add-recipient --hardware")
);
let enroll_out = Command::new(&bin)
.args([
"security",
"hardware",
"enroll",
"--kind",
"yubi-key-piv",
"--id",
"slot-9c",
"--command",
])
.arg(&command_path)
.arg("--json")
.output()
.expect("run hardware enroll");
assert!(
enroll_out.status.success(),
"hardware enroll failed: {}",
String::from_utf8_lossy(&enroll_out.stderr)
);
let enroll_json: serde_json::Value = serde_json::from_slice(&enroll_out.stdout).unwrap();
assert_eq!(enroll_json["id"], "slot-9c");
assert_eq!(enroll_json["valid"], true);
assert!(
enroll_json["fingerprint"]
.as_str()
.unwrap()
.starts_with("SHA256:")
);
}
#[test]
fn binary_hardware_validate_recipient_reports_fingerprint() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let (_priv_path, pub_line) = write_named_keypair(dir.path(), "id_ed25519");
let expected_fingerprint = fingerprint_from_line(&pub_line).unwrap();
let output = Command::new(&bin)
.args(["security", "hardware", "validate-recipient"])
.arg(&pub_line)
.arg("--json")
.output()
.expect("run hardware validate-recipient");
assert!(
output.status.success(),
"validate-recipient failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["valid"], true);
assert_eq!(json["descriptor_kind"], "SshRecipient");
assert_eq!(json["hardware_recipient"], false);
assert_eq!(json["fingerprint"], expected_fingerprint);
}
#[cfg(feature = "age-plugin-recipient")]
#[test]
fn binary_hardware_status_reports_age_plugin_identity_files() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let identity_file = dir.path().join("plugin-identities.txt");
std::fs::create_dir_all(&home).unwrap();
std::fs::write(
&identity_file,
"# comment\nAGE-PLUGIN-FOOBAR-1QVHULF\nnot-an-identity\n",
)
.unwrap();
let output = Command::new(&bin)
.args(["security", "hardware", "status", "--json"])
.env("HOME", &home)
.env("SSHENV_AGE_PLUGIN_IDENTITIES", &identity_file)
.output()
.expect("run hardware status");
assert!(
output.status.success(),
"hardware status failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["age_plugin_recipient_feature"], true);
assert_eq!(
json["age_plugin_identity_files"].as_array().unwrap().len(),
1
);
assert_eq!(json["age_plugin_identity_files"][0]["identity_count"], 1);
assert_eq!(json["age_plugin_identity_files"][0]["invalid_lines"], 1);
assert_eq!(json["age_plugin_plugins"]["foobar"], 1);
}
#[cfg(feature = "shamir-sharing")]
#[test]
fn binary_recovery_split_validate_and_combine_roundtrip() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let metadata_path = dir.path().join("recovery.json");
std::fs::write(
&metadata_path,
r#"{
"id": "ops-break-glass",
"label": "ops break glass",
"threshold": 2,
"shares": [
{ "id": "share-a", "label": "A", "holder": "alice", "public_identifier": "alice-pub" },
{ "id": "share-b", "label": "B", "holder": "bob", "public_identifier": "bob-pub" },
{ "id": "share-c", "label": "C", "holder": "carol", "public_identifier": "carol-pub" }
],
"shamir": { "threshold": 2, "share_count": 3 }
}"#,
)
.unwrap();
let mut child = Command::new(&bin)
.args(["security", "recovery", "split"])
.arg("--metadata")
.arg(&metadata_path)
.args(["--secret-hex-stdin", "--json"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("spawn recovery split");
child
.stdin
.as_mut()
.unwrap()
.write_all(b"00112233445566778899aabbccddeeff")
.unwrap();
let split_out = child.wait_with_output().expect("wait recovery split");
assert!(
split_out.status.success(),
"split failed: {}",
String::from_utf8_lossy(&split_out.stderr)
);
let split_json: serde_json::Value = serde_json::from_slice(&split_out.stdout).unwrap();
let shares = split_json["shares"].as_array().unwrap();
assert_eq!(shares.len(), 3);
assert_eq!(split_json["metadata_verified"], true);
let share_a = dir.path().join("share-a.txt");
let share_b = dir.path().join("share-b.txt");
std::fs::write(&share_a, shares[0].as_str().unwrap()).unwrap();
std::fs::write(&share_b, shares[1].as_str().unwrap()).unwrap();
let validate_out = Command::new(&bin)
.args(["security", "recovery", "validate-share"])
.arg(&share_a)
.arg("--metadata")
.arg(&metadata_path)
.arg("--json")
.output()
.expect("run validate-share");
assert!(
validate_out.status.success(),
"validate-share failed: {}",
String::from_utf8_lossy(&validate_out.stderr)
);
let validate_json: serde_json::Value = serde_json::from_slice(&validate_out.stdout).unwrap();
assert_eq!(validate_json["valid"], true);
assert_eq!(validate_json["metadata_verified"], true);
assert_eq!(validate_json["set_id"], "ops-break-glass");
let combine_out = Command::new(&bin)
.args(["security", "recovery", "combine", "--share-file"])
.arg(&share_a)
.arg("--share-file")
.arg(&share_b)
.arg("--metadata")
.arg(&metadata_path)
.arg("--json")
.output()
.expect("run recovery combine");
assert!(
combine_out.status.success(),
"combine failed: {}",
String::from_utf8_lossy(&combine_out.stderr)
);
let combine_json: serde_json::Value = serde_json::from_slice(&combine_out.stdout).unwrap();
assert_eq!(combine_json["metadata_verified"], true);
assert_eq!(
combine_json["recovered_secret_hex"],
"00112233445566778899aabbccddeeff"
);
}
#[cfg(feature = "shamir-sharing")]
#[test]
fn binary_recovery_split_vault_key_and_recover_recipient_roundtrip() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let recovery_home = dir.path().join("recovery-home");
let vault_path = dir.path().join("vault");
let recovered_vault = dir.path().join("recovered-vault");
let metadata_path = dir.path().join("recovery.json");
init_vault_with_profile(&bin, &home, &vault_path, "myprofile");
migrate_vault_to_v2(&bin, &home, &vault_path);
std::fs::write(
&metadata_path,
r#"{
"id": "ops-break-glass",
"label": "ops break glass",
"threshold": 2,
"shares": [
{ "id": "share-a", "label": "A", "holder": "alice", "public_identifier": "alice-pub" },
{ "id": "share-b", "label": "B", "holder": "bob", "public_identifier": "bob-pub" },
{ "id": "share-c", "label": "C", "holder": "carol", "public_identifier": "carol-pub" }
],
"shamir": { "threshold": 2, "share_count": 3 }
}"#,
)
.unwrap();
let split_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.args(["security", "recovery", "split-vault-key"])
.arg("--metadata")
.arg(&metadata_path)
.arg("--json")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run split-vault-key");
assert!(
split_out.status.success(),
"split-vault-key failed: {}",
String::from_utf8_lossy(&split_out.stderr)
);
let split_json: serde_json::Value = serde_json::from_slice(&split_out.stdout).unwrap();
let shares = split_json["shares"].as_array().unwrap();
assert_eq!(shares.len(), 3);
let share_a = dir.path().join("vault-share-a.txt");
let share_b = dir.path().join("vault-share-b.txt");
std::fs::write(&share_a, shares[0].as_str().unwrap()).unwrap();
std::fs::write(&share_b, shares[1].as_str().unwrap()).unwrap();
let share_files = std::env::join_paths([share_a.as_path(), share_b.as_path()]).unwrap();
let no_key_home = dir.path().join("no-key-home");
std::fs::create_dir_all(no_key_home.join(".ssh")).unwrap();
let recovery_unlock_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &no_key_home)
.env("SSHENV_RECOVERY_SHARE_FILES", &share_files)
.env("SSHENV_RECOVERY_METADATA", &metadata_path)
.env_remove("SSHENV_VAULT")
.output()
.expect("show original vault with recovery shares");
assert!(
recovery_unlock_out.status.success(),
"show with recovery shares failed: {}",
String::from_utf8_lossy(&recovery_unlock_out.stderr)
);
let recovery_stdout = String::from_utf8_lossy(&recovery_unlock_out.stdout);
assert!(
recovery_stdout.contains("DUMMY=value"),
"show output: {recovery_stdout}"
);
std::fs::create_dir_all(recovery_home.join(".ssh")).unwrap();
let (_recovery_key, recovery_pubkey) =
write_named_keypair(&recovery_home.join(".ssh"), "id_ed25519");
let recover_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.args(["security", "recovery", "recover-recipient"])
.arg("--share-file")
.arg(&share_a)
.arg("--share-file")
.arg(&share_b)
.arg("--metadata")
.arg(&metadata_path)
.arg("--recipient-key")
.arg(&recovery_pubkey)
.arg("--output")
.arg(&recovered_vault)
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run recover-recipient");
assert!(
recover_out.status.success(),
"recover-recipient failed: {}",
String::from_utf8_lossy(&recover_out.stderr)
);
assert!(recovered_vault.exists());
let show_out = Command::new(&bin)
.arg("--vault")
.arg(&recovered_vault)
.arg("show")
.arg("myprofile")
.env("HOME", &recovery_home)
.env_remove("SSHENV_VAULT")
.output()
.expect("show recovered vault with recovery recipient");
assert!(
show_out.status.success(),
"show with recovery recipient failed: {}",
String::from_utf8_lossy(&show_out.stderr)
);
let stdout = String::from_utf8_lossy(&show_out.stdout);
assert!(stdout.contains("DUMMY=value"), "show output: {stdout}");
}
#[cfg(feature = "recovery-shares")]
#[test]
fn binary_recovery_metadata_import_list_remove_roundtrip() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let vault_path = dir.path().join("vault");
let metadata_path = dir.path().join("recovery.json");
init_vault_with_profile(&bin, &home, &vault_path, "myprofile");
migrate_vault_to_v2(&bin, &home, &vault_path);
std::fs::write(
&metadata_path,
r#"{
"id": "ops-break-glass",
"label": "ops break glass",
"threshold": 2,
"shares": [
{ "id": "share-a", "label": "A", "holder": "alice", "public_identifier": "alice-pub" },
{ "id": "share-b", "label": "B", "holder": "bob", "public_identifier": "bob-pub" },
{ "id": "share-c", "label": "C", "holder": "carol", "public_identifier": "carol-pub" }
],
"shamir": { "threshold": 2, "share_count": 3 }
}"#,
)
.unwrap();
let import_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.args(["security", "recovery", "import"])
.arg(&metadata_path)
.arg("--json")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run recovery import");
assert!(
import_out.status.success(),
"recovery import failed: {}",
String::from_utf8_lossy(&import_out.stderr)
);
let import_json: serde_json::Value = serde_json::from_slice(&import_out.stdout).unwrap();
assert_eq!(import_json["imported"], "ops-break-glass");
assert_eq!(import_json["replaced"], false);
let list_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.args(["security", "recovery", "list", "--json"])
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run recovery list");
assert!(
list_out.status.success(),
"recovery list failed: {}",
String::from_utf8_lossy(&list_out.stderr)
);
let list_json: serde_json::Value = serde_json::from_slice(&list_out.stdout).unwrap();
assert_eq!(list_json.as_array().unwrap().len(), 1);
assert_eq!(list_json[0]["id"], "ops-break-glass");
let plan_out = Command::new(&bin)
.args(["security", "recovery", "plan"])
.arg(&metadata_path)
.args([
"--share-id",
"share-a",
"--share-id",
"not-a-share",
"--json",
])
.output()
.expect("run recovery plan");
assert!(
plan_out.status.success(),
"recovery plan failed: {}",
String::from_utf8_lossy(&plan_out.stderr)
);
let plan_json: serde_json::Value = serde_json::from_slice(&plan_out.stdout).unwrap();
assert_eq!(
plan_json["provided_share_ids"],
serde_json::json!(["share-a"])
);
assert_eq!(
plan_json["ignored_share_ids"],
serde_json::json!(["not-a-share"])
);
assert_eq!(plan_json["missing_share_count"], 1);
let apply_team_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.args([
"security",
"profile-policy",
"apply-all",
"--preset",
"team",
"--no-backup",
])
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy apply-all team");
assert!(
apply_team_out.status.success(),
"profile-policy apply-all team failed: {}",
String::from_utf8_lossy(&apply_team_out.stderr)
);
let team_status_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.args([
"security",
"profile-policy",
"status",
"myprofile",
"--json",
])
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy status after team apply");
assert!(
team_status_out.status.success(),
"profile-policy status failed: {}",
String::from_utf8_lossy(&team_status_out.stderr)
);
let team_status_json: serde_json::Value =
serde_json::from_slice(&team_status_out.stdout).unwrap();
assert_eq!(team_status_json["preset"], "Team");
assert!(
!team_status_json["warnings"]
.as_array()
.unwrap()
.iter()
.any(|warning| {
warning
.as_str()
.is_some_and(|message| message.contains("expects at least one recovery-share"))
}),
"{team_status_json}"
);
let global_team_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.args(["security", "preset", "team"])
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run security preset team");
assert!(
global_team_out.status.success(),
"security preset team failed: {}",
String::from_utf8_lossy(&global_team_out.stderr)
);
let remove_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.args(["security", "recovery", "remove", "ops-break-glass"])
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run recovery remove");
assert!(
remove_out.status.success(),
"recovery remove failed: {}",
String::from_utf8_lossy(&remove_out.stderr)
);
}
#[cfg(feature = "remote-factor")]
#[test]
fn binary_remote_request_template_validates_roundtrip() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let metadata_path = dir.path().join("remote.json");
let request_path = dir.path().join("request.json");
std::fs::write(
&metadata_path,
r#"{
"id": "prod-kms",
"backend": "cloud-kms",
"label": "prod",
"params": { "key": "alias/sshenv-prod" }
}"#,
)
.unwrap();
let validate_metadata_out = Command::new(&bin)
.args(["security", "remote", "validate"])
.arg(&metadata_path)
.arg("--json")
.output()
.expect("run remote validate");
assert!(
validate_metadata_out.status.success(),
"remote validate failed: {}",
String::from_utf8_lossy(&validate_metadata_out.stderr)
);
let template_out = Command::new(&bin)
.args(["security", "remote", "request-template"])
.arg(&metadata_path)
.args([
"--vault-id",
"vault-prod",
"--generation",
"7",
"--expires-unix",
"4102444800",
"--request-id",
"req-1",
"--encryption-context",
"sshenv:prod",
])
.output()
.expect("run remote request-template");
assert!(
template_out.status.success(),
"request-template failed: {}",
String::from_utf8_lossy(&template_out.stderr)
);
std::fs::write(&request_path, &template_out.stdout).unwrap();
let validate_request_out = Command::new(&bin)
.args(["security", "remote", "validate-request"])
.arg(&metadata_path)
.arg(&request_path)
.args([
"--expected-vault-id",
"vault-prod",
"--expected-generation",
"7",
"--expected-request-id",
"req-1",
"--json",
])
.output()
.expect("run remote validate-request");
assert!(
validate_request_out.status.success(),
"validate-request failed: {}",
String::from_utf8_lossy(&validate_request_out.stderr)
);
let request_json: serde_json::Value =
serde_json::from_slice(&validate_request_out.stdout).unwrap();
assert_eq!(request_json["valid"], true);
assert_eq!(request_json["factor_id"], "prod-kms");
assert_eq!(
request_json["checked_expectations"],
serde_json::json!(["vault-id", "generation", "request-id"])
);
}
#[cfg(all(feature = "remote-factor", unix))]
#[test]
fn binary_remote_command_adapter_wraps_and_unwraps() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let command_path = dir.path().join("remote-factor.sh");
std::fs::write(
&command_path,
r#"#!/bin/sh
input=$(cat)
case "$input" in
*'"operation":"wrap"'*|*'"operation": "wrap"'*) printf '{"wrapped_key":[222,173,190,239],"audit_id":"audit-1"}\n' ;;
*'"operation":"unwrap"'*|*'"operation": "unwrap"'*) printf '{"payload-key-hex":"00112233445566778899aabbccddeeff"}\n' ;;
*) echo 'unknown operation' >&2; exit 1 ;;
esac
"#,
)
.unwrap();
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&command_path, std::fs::Permissions::from_mode(0o755)).unwrap();
}
let metadata_path = dir.path().join("remote-command.json");
std::fs::write(
&metadata_path,
format!(
r#"{{
"id": "command-remote",
"backend": "self-hosted",
"label": "command remote",
"params": {{ "command": "{}" }}
}}"#,
command_path.display()
),
)
.unwrap();
let request_path = dir.path().join("request.json");
std::fs::write(
&request_path,
r#"{
"factor_id": "command-remote",
"context": {
"vault-id": "vault",
"request-id": "req-1",
"generation": "1",
"expires-unix": "4102444800",
"client-id": "client"
}
}"#,
)
.unwrap();
let mut wrap = Command::new(&bin)
.args(["security", "remote", "command-wrap"])
.arg(&metadata_path)
.arg(&request_path)
.arg("--payload-key-hex-stdin")
.arg("--json")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("spawn command-wrap");
wrap.stdin
.as_mut()
.unwrap()
.write_all(b"00112233445566778899aabbccddeeff")
.unwrap();
let wrap_out = wrap.wait_with_output().expect("wait command-wrap");
assert!(
wrap_out.status.success(),
"command-wrap failed: {}",
String::from_utf8_lossy(&wrap_out.stderr)
);
let wrap_json: serde_json::Value = serde_json::from_slice(&wrap_out.stdout).unwrap();
assert_eq!(wrap_json["wrapped_key_hex"], "deadbeef");
assert_eq!(wrap_json["audit_id"], "audit-1");
let mut unwrap = Command::new(&bin)
.args(["security", "remote", "command-unwrap"])
.arg(&metadata_path)
.arg(&request_path)
.arg("--wrapped-key-hex-stdin")
.arg("--json")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("spawn command-unwrap");
unwrap
.stdin
.as_mut()
.unwrap()
.write_all(b"deadbeef")
.unwrap();
let unwrap_out = unwrap.wait_with_output().expect("wait command-unwrap");
assert!(
unwrap_out.status.success(),
"command-unwrap failed: {}",
String::from_utf8_lossy(&unwrap_out.stderr)
);
let unwrap_json: serde_json::Value = serde_json::from_slice(&unwrap_out.stdout).unwrap();
assert_eq!(
unwrap_json["payload_key_hex"],
"00112233445566778899aabbccddeeff"
);
let cloud_metadata_path = dir.path().join("remote-command-cloud.json");
std::fs::write(
&cloud_metadata_path,
format!(
r#"{{
"id": "command-kms",
"backend": "cloud-kms",
"label": "command kms",
"params": {{ "key": "alias/sshenv", "command": "{}" }}
}}"#,
command_path.display()
),
)
.unwrap();
let cloud_request_path = dir.path().join("request-cloud.json");
std::fs::write(
&cloud_request_path,
r#"{
"factor_id": "command-kms",
"context": {
"vault-id": "vault",
"request-id": "req-2",
"generation": "1",
"expires-unix": "4102444800",
"encryption-context": "sshenv:vault"
}
}"#,
)
.unwrap();
let mut cloud_wrap = Command::new(&bin)
.args(["security", "remote", "command-wrap"])
.arg(&cloud_metadata_path)
.arg(&cloud_request_path)
.arg("--payload-key-hex-stdin")
.arg("--json")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("spawn cloud command-wrap");
cloud_wrap
.stdin
.as_mut()
.unwrap()
.write_all(b"00112233445566778899aabbccddeeff")
.unwrap();
let cloud_wrap_out = cloud_wrap
.wait_with_output()
.expect("wait cloud command-wrap");
assert!(
cloud_wrap_out.status.success(),
"cloud command-wrap failed: {}",
String::from_utf8_lossy(&cloud_wrap_out.stderr)
);
let cloud_wrap_json: serde_json::Value =
serde_json::from_slice(&cloud_wrap_out.stdout).unwrap();
assert_eq!(cloud_wrap_json["wrapped_key_hex"], "deadbeef");
let oidc_metadata_path = dir.path().join("remote-command-oidc.json");
std::fs::write(
&oidc_metadata_path,
format!(
r#"{{
"id": "command-oidc",
"backend": "oidc-approval",
"label": "command oidc approval",
"params": {{ "command": "{}" }}
}}"#,
command_path.display()
),
)
.unwrap();
let oidc_request_path = dir.path().join("request-oidc.json");
std::fs::write(
&oidc_request_path,
r#"{
"factor_id": "command-oidc",
"context": {
"vault-id": "vault",
"request-id": "req-3",
"generation": "1",
"expires-unix": "4102444800",
"subject": "user@example.com",
"audience": "sshenv"
}
}"#,
)
.unwrap();
let mut oidc_wrap = Command::new(&bin)
.args(["security", "remote", "command-wrap"])
.arg(&oidc_metadata_path)
.arg(&oidc_request_path)
.arg("--payload-key-hex-stdin")
.arg("--json")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("spawn oidc command-wrap");
oidc_wrap
.stdin
.as_mut()
.unwrap()
.write_all(b"00112233445566778899aabbccddeeff")
.unwrap();
let oidc_wrap_out = oidc_wrap
.wait_with_output()
.expect("wait oidc command-wrap");
assert!(
oidc_wrap_out.status.success(),
"oidc command-wrap failed: {}",
String::from_utf8_lossy(&oidc_wrap_out.stderr)
);
let oidc_wrap_json: serde_json::Value = serde_json::from_slice(&oidc_wrap_out.stdout).unwrap();
assert_eq!(oidc_wrap_json["wrapped_key_hex"], "deadbeef");
}
#[cfg(all(feature = "remote-factor", unix))]
#[test]
fn binary_remote_enable_command_factor_unlocks_with_request_env() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let vault_path = dir.path().join("vault");
init_vault_with_profile(&bin, &home, &vault_path, "myprofile");
migrate_vault_to_v2(&bin, &home, &vault_path);
let state_path = dir.path().join("remote-factor-key.hex");
let command_path = dir.path().join("remote-factor-stateful.sh");
std::fs::write(
&command_path,
format!(
r#"#!/bin/sh
state='{}'
input=$(cat)
case "$input" in
*'"operation":"wrap"'*|*'"operation": "wrap"'*)
key=$(printf '%s' "$input" | sed -n 's/.*"payload-key-hex":"\([0-9a-f]*\)".*/\1/p')
printf '%s' "$key" > "$state"
printf '{{"wrapped_key":[1,2,3],"audit_id":"enable-audit"}}\n'
;;
*'"operation":"unwrap"'*|*'"operation": "unwrap"'*)
key=$(cat "$state")
printf '{{"payload-key-hex":"%s"}}\n' "$key"
;;
*) echo 'unknown operation' >&2; exit 1 ;;
esac
"#,
state_path.display()
),
)
.unwrap();
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&command_path, std::fs::Permissions::from_mode(0o755)).unwrap();
}
let metadata_path = dir.path().join("remote-command.json");
std::fs::write(
&metadata_path,
format!(
r#"{{
"id": "command-remote",
"backend": "self-hosted",
"label": "command remote",
"params": {{ "command": "{}" }}
}}"#,
command_path.display()
),
)
.unwrap();
let request_path = dir.path().join("request.json");
std::fs::write(
&request_path,
r#"{
"factor_id": "command-remote",
"context": {
"vault-id": "vault",
"request-id": "req-1",
"generation": "2",
"expires-unix": "4102444800",
"client-id": "client"
}
}"#,
)
.unwrap();
let enable_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.args(["security", "remote", "enable-command"])
.arg(&metadata_path)
.arg(&request_path)
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run remote enable-command");
assert!(
enable_out.status.success(),
"enable-command failed: {}",
String::from_utf8_lossy(&enable_out.stderr)
);
let show_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env("SSHENV_REMOTE_REQUEST", &request_path)
.env_remove("SSHENV_VAULT")
.output()
.expect("show remote-factor vault");
assert!(
show_out.status.success(),
"show with remote request failed: {}",
String::from_utf8_lossy(&show_out.stderr)
);
let stdout = String::from_utf8_lossy(&show_out.stdout);
assert!(stdout.contains("DUMMY=value"), "show output: {stdout}");
let stale_request_path = dir.path().join("stale-request.json");
std::fs::write(
&stale_request_path,
r#"{
"factor_id": "command-remote",
"context": {
"vault-id": "vault",
"request-id": "req-stale",
"generation": "1",
"expires-unix": "4102444800",
"client-id": "client"
}
}"#,
)
.unwrap();
let stale_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env("SSHENV_REMOTE_REQUEST", &stale_request_path)
.env_remove("SSHENV_VAULT")
.output()
.expect("show remote-factor vault with stale request");
assert!(!stale_out.status.success());
assert!(
String::from_utf8_lossy(&stale_out.stderr).contains("does not match vault generation"),
"stderr: {}",
String::from_utf8_lossy(&stale_out.stderr)
);
}
#[cfg(feature = "remote-factor")]
#[test]
fn binary_remote_validate_request_rejects_expired_context() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let metadata_path = dir.path().join("remote.json");
let request_path = dir.path().join("request.json");
std::fs::write(
&metadata_path,
r#"{
"id": "prod-kms",
"backend": "cloud-kms",
"label": "prod",
"params": { "key": "alias/sshenv-prod" }
}"#,
)
.unwrap();
std::fs::write(
&request_path,
r#"{
"factor_id": "prod-kms",
"context": {
"vault-id": "vault-prod",
"request-id": "req-expired",
"generation": "7",
"expires-unix": "1",
"encryption-context": "sshenv:prod"
}
}"#,
)
.unwrap();
let output = Command::new(&bin)
.args(["security", "remote", "validate-request"])
.arg(&metadata_path)
.arg(&request_path)
.arg("--json")
.output()
.expect("run remote validate-request");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("expired"), "{stderr}");
}
#[cfg(feature = "remote-factor")]
#[test]
fn binary_remote_metadata_import_list_remove_roundtrip() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let vault_path = dir.path().join("vault");
let metadata_path = dir.path().join("remote.json");
init_vault_with_profile(&bin, &home, &vault_path, "myprofile");
migrate_vault_to_v2(&bin, &home, &vault_path);
std::fs::write(
&metadata_path,
r#"{
"id": "prod-kms",
"backend": "cloud-kms",
"label": "prod",
"params": { "key": "alias/sshenv-prod" }
}"#,
)
.unwrap();
let import_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.args(["security", "remote", "import"])
.arg(&metadata_path)
.arg("--json")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run remote import");
assert!(
import_out.status.success(),
"remote import failed: {}",
String::from_utf8_lossy(&import_out.stderr)
);
let import_json: serde_json::Value = serde_json::from_slice(&import_out.stdout).unwrap();
assert_eq!(import_json["imported"], "prod-kms");
assert_eq!(import_json["replaced"], false);
let list_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.args(["security", "remote", "list", "--json"])
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run remote list");
assert!(
list_out.status.success(),
"remote list failed: {}",
String::from_utf8_lossy(&list_out.stderr)
);
let list_json: serde_json::Value = serde_json::from_slice(&list_out.stdout).unwrap();
assert_eq!(list_json.as_array().unwrap().len(), 1);
assert_eq!(list_json[0]["id"], "prod-kms");
let remove_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.args(["security", "remote", "remove", "prod-kms"])
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run remote remove");
assert!(
remove_out.status.success(),
"remote remove failed: {}",
String::from_utf8_lossy(&remove_out.stderr)
);
}
#[test]
fn binary_init_non_tty_without_key_errors_cleanly() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!(
"skipping: {} does not exist; run `cargo build` first",
bin.display()
);
return;
}
let dir = tempfile::tempdir().unwrap();
let vault_path = dir.path().join("vault");
let output = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("init")
.env("HOME", dir.path())
.env_remove("SSHENV_VAULT")
.env_remove("RUST_BACKTRACE")
.env_remove("RUST_LIB_BACKTRACE")
.output()
.expect("run init");
assert!(
!output.status.success(),
"init should fail non-interactively; stdout={} stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("--recipient-key"),
"stderr missing --recipient-key hint: {stderr}"
);
assert!(
!stderr.contains("Stack backtrace"),
"anyhow backtrace leaked to user: {stderr}"
);
assert!(
!stderr.contains("std::backtrace::Backtrace"),
"anyhow backtrace leaked to user: {stderr}"
);
assert!(!vault_path.exists(), "vault file should not exist");
}
#[test]
fn binary_init_with_explicit_key_works_non_tty() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let (_priv_path, _pub_line) = write_pubkey_file(dir.path());
let pub_path = dir.path().join("id_test.pub");
let vault_path = dir.path().join("vault");
let output = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("init")
.arg("--recipient-key")
.arg(&pub_path)
.env("HOME", dir.path())
.output()
.expect("run init");
assert!(
output.status.success(),
"init failed: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
assert!(vault_path.exists(), "vault file should exist");
}
#[test]
fn binary_set_does_not_prompt_for_non_matching_encrypted_key() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
std::fs::create_dir_all(home.join(".ssh")).unwrap();
let (_auth_priv, _auth_pub) = write_named_keypair(&home.join(".ssh"), "id_ed25519");
let (_other_priv, _other_pub) = write_named_keypair(&home.join(".ssh"), "id_other");
let vault_path = dir.path().join("vault");
let init_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("init")
.arg("--recipient-key")
.arg(home.join(".ssh").join("id_ed25519.pub"))
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run init");
assert!(
init_out.status.success(),
"init failed: {}",
String::from_utf8_lossy(&init_out.stderr)
);
let set_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("set")
.arg("myprofile")
.arg("MYVAR")
.arg("--value")
.arg("myvalue")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run set");
assert!(
set_out.status.success(),
"set failed: stdout={} stderr={}",
String::from_utf8_lossy(&set_out.stdout),
String::from_utf8_lossy(&set_out.stderr),
);
let stderr = String::from_utf8_lossy(&set_out.stderr);
assert!(
!stderr.contains("passphrase"),
"non-matching key should not have been prompted: {stderr}"
);
assert!(
!stderr.contains("Stack backtrace"),
"backtrace leaked to user: {stderr}"
);
}
#[test]
fn binary_migrate_vault_to_v2_preserves_secret_access() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let vault_path = dir.path().join("vault");
init_vault_with_profile(&bin, &home, &vault_path, "myprofile");
let migrate_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("migrate-vault")
.arg("--to")
.arg("v2")
.arg("--recipient-key")
.arg(home.join(".ssh").join("id_ed25519.pub"))
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run migrate-vault");
assert!(
migrate_out.status.success(),
"migrate-vault failed: {}",
String::from_utf8_lossy(&migrate_out.stderr)
);
let recipients_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("list-recipients")
.arg("--verbose")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run list-recipients");
assert!(recipients_out.status.success());
assert!(
String::from_utf8_lossy(&recipients_out.stdout).contains("ssh-ed25519"),
"v2 recipient metadata should include the public key line"
);
let show_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run show");
assert!(
show_out.status.success(),
"show failed after migrate: {}",
String::from_utf8_lossy(&show_out.stderr)
);
assert!(String::from_utf8_lossy(&show_out.stdout).contains("DUMMY=value"));
}
#[cfg(not(feature = "device-seal"))]
#[test]
fn binary_profile_policy_apply_all_paranoid_requires_device_seal_backend() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let vault_path = dir.path().join("vault");
init_vault_with_profile(&bin, &home, &vault_path, "myprofile");
let apply_all_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("apply-all")
.arg("--preset")
.arg("paranoid")
.arg("--passphrase")
.arg("bulk-passphrase")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy apply-all paranoid without device seal");
assert!(!apply_all_out.status.success());
let stderr = String::from_utf8_lossy(&apply_all_out.stderr);
assert!(
stderr.contains("requires an available device-seal backend"),
"{stderr}"
);
}
#[cfg(all(feature = "device-seal-file", not(feature = "macos-keychain")))]
#[test]
fn binary_security_device_authorize_list_remove_roundtrips() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let vault_path = dir.path().join("vault");
init_vault_with_profile(&bin, &home, &vault_path, "myprofile");
let migrate_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("migrate-vault")
.arg("--to")
.arg("v2")
.arg("--recipient-key")
.arg(home.join(".ssh").join("id_ed25519.pub"))
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run migrate-vault");
assert!(migrate_out.status.success());
let authorize_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("device")
.arg("authorize")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run security device authorize");
assert!(
authorize_out.status.success(),
"device authorize failed: {}",
String::from_utf8_lossy(&authorize_out.stderr)
);
let list_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("device")
.arg("list")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run security device list");
assert!(list_out.status.success());
let list_stdout = String::from_utf8_lossy(&list_out.stdout);
assert!(
list_stdout.contains("vault: device-seal:default"),
"{list_stdout}"
);
let remove_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("device")
.arg("remove")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run security device remove");
assert!(
remove_out.status.success(),
"device remove failed: {}",
String::from_utf8_lossy(&remove_out.stderr)
);
let list_after_remove_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("device")
.arg("list")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run security device list after remove");
assert!(list_after_remove_out.status.success());
let list_after_remove_stdout = String::from_utf8_lossy(&list_after_remove_out.stdout);
assert!(
list_after_remove_stdout.contains("(no device-seal factors configured)"),
"{list_after_remove_stdout}"
);
}
#[cfg(all(feature = "device-seal-file", not(feature = "macos-keychain")))]
#[test]
fn binary_profile_policy_apply_all_recommended_binds_device_seal() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let vault_path = dir.path().join("vault");
init_vault_with_profile(&bin, &home, &vault_path, "myprofile");
let set_other_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("set")
.arg("otherprofile")
.arg("DUMMY")
.arg("--value")
.arg("other")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run set other profile");
assert!(
set_other_out.status.success(),
"set other profile failed: {}",
String::from_utf8_lossy(&set_other_out.stderr)
);
let apply_all_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("apply-all")
.arg("--preset")
.arg("recommended")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy apply-all recommended");
assert!(
apply_all_out.status.success(),
"profile-policy apply-all recommended failed: {}",
String::from_utf8_lossy(&apply_all_out.stderr)
);
for profile in ["myprofile", "otherprofile"] {
let status_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("status")
.arg(profile)
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy status");
assert!(status_out.status.success());
let stdout = String::from_utf8_lossy(&status_out.stdout);
assert!(stdout.contains("preset: Recommended"), "{stdout}");
assert!(
stdout.contains("requirement device-seal: profile-specific cryptographic binding"),
"{stdout}"
);
let show_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg(profile)
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run show with device seal");
assert!(
show_out.status.success(),
"show {profile} with device seal failed: {}",
String::from_utf8_lossy(&show_out.stderr)
);
}
std::fs::remove_file(home.join(".sshenv").join("device-seal")).unwrap();
let missing_seal_show_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run show without device seal");
assert!(!missing_seal_show_out.status.success());
}
#[cfg(all(feature = "device-seal-file", not(feature = "macos-keychain")))]
#[test]
fn binary_profile_policy_apply_all_paranoid_binds_passphrase_and_device_seal() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let vault_path = dir.path().join("vault");
init_vault_with_profile(&bin, &home, &vault_path, "myprofile");
let set_other_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("set")
.arg("otherprofile")
.arg("DUMMY")
.arg("--value")
.arg("other")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run set other profile");
assert!(
set_other_out.status.success(),
"set other profile failed: {}",
String::from_utf8_lossy(&set_other_out.stderr)
);
let apply_all_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("apply-all")
.arg("--preset")
.arg("paranoid")
.arg("--passphrase")
.arg("bulk-passphrase")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy apply-all paranoid");
assert!(
apply_all_out.status.success(),
"profile-policy apply-all paranoid failed: {}",
String::from_utf8_lossy(&apply_all_out.stderr)
);
for profile in ["myprofile", "otherprofile"] {
let status_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("status")
.arg(profile)
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy status");
assert!(status_out.status.success());
let stdout = String::from_utf8_lossy(&status_out.stdout);
assert!(stdout.contains("preset: Paranoid"), "{stdout}");
assert!(
stdout.contains("requirement passphrase: profile-specific cryptographic binding"),
"{stdout}"
);
assert!(
stdout.contains("requirement device-seal: profile-specific cryptographic binding"),
"{stdout}"
);
let show_without_passphrase_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg(profile)
.env("HOME", &home)
.env_remove("SSHENV_PROFILE_PASSPHRASE")
.env_remove("SSHENV_VAULT")
.output()
.expect("run show without passphrase");
assert!(!show_without_passphrase_out.status.success());
let show_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg(profile)
.env("HOME", &home)
.env("SSHENV_PROFILE_PASSPHRASE", "bulk-passphrase")
.env_remove("SSHENV_VAULT")
.output()
.expect("run show with passphrase and device seal");
assert!(
show_out.status.success(),
"show {profile} with passphrase and device seal failed: {}",
String::from_utf8_lossy(&show_out.stderr)
);
}
std::fs::remove_file(home.join(".sshenv").join("device-seal")).unwrap();
let missing_seal_show_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env("SSHENV_PROFILE_PASSPHRASE", "bulk-passphrase")
.env_remove("SSHENV_VAULT")
.output()
.expect("run show without device seal");
assert!(!missing_seal_show_out.status.success());
}
#[test]
fn binary_profile_policy_migrate_preserves_secret_access() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let vault_path = dir.path().join("vault");
init_vault_with_profile(&bin, &home, &vault_path, "myprofile");
let set_other_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("set")
.arg("otherprofile")
.arg("DUMMY")
.arg("--value")
.arg("other")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("set second profile");
assert!(set_other_out.status.success());
let migrate_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("migrate-vault")
.arg("--to")
.arg("v2")
.arg("--recipient-key")
.arg(home.join(".ssh").join("id_ed25519.pub"))
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run migrate-vault");
assert!(migrate_out.status.success());
let profile_migrate_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("migrate")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy migrate");
assert!(
profile_migrate_out.status.success(),
"profile-policy migrate failed: {}",
String::from_utf8_lossy(&profile_migrate_out.stderr)
);
let rotate_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("rotate-key")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy rotate-key");
assert!(
rotate_out.status.success(),
"profile-policy rotate-key failed: {}",
String::from_utf8_lossy(&rotate_out.stderr)
);
let require_without_value_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("require-passphrase")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy require-passphrase without value");
assert!(!require_without_value_out.status.success());
assert!(
String::from_utf8_lossy(&require_without_value_out.stderr)
.contains("failed to read passphrase"),
"missing prompt error: {}",
String::from_utf8_lossy(&require_without_value_out.stderr)
);
let require_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("require-passphrase")
.arg("myprofile")
.arg("--passphrase")
.arg("test-passphrase")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy require-passphrase");
assert!(
require_out.status.success(),
"profile-policy require-passphrase failed: {}",
String::from_utf8_lossy(&require_out.stderr)
);
let status_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("status")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy status");
assert!(
status_out.status.success(),
"profile-policy status failed: {}",
String::from_utf8_lossy(&status_out.stderr)
);
let status_stdout = String::from_utf8_lossy(&status_out.stdout);
assert!(
status_stdout.contains("profile factor metadata: passphrase=yes"),
"missing passphrase status: {status_stdout}"
);
assert!(
status_stdout.contains("requirement passphrase: profile-specific cryptographic binding"),
"missing binding status: {status_stdout}"
);
let change_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("change-passphrase")
.arg("myprofile")
.arg("--old-passphrase")
.arg("test-passphrase")
.arg("--new-passphrase")
.arg("new-test-passphrase")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy change-passphrase");
assert!(
change_out.status.success(),
"profile-policy change-passphrase failed: {}",
String::from_utf8_lossy(&change_out.stderr)
);
let old_show_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env("SSHENV_PROFILE_PASSPHRASE", "test-passphrase")
.env_remove("SSHENV_VAULT")
.output()
.expect("run show with old profile passphrase");
assert!(!old_show_out.status.success());
let show_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env("SSHENV_PROFILE_PASSPHRASE", "new-test-passphrase")
.env_remove("SSHENV_VAULT")
.output()
.expect("run show after profile-policy migrate");
assert!(show_out.status.success());
assert!(String::from_utf8_lossy(&show_out.stdout).contains("DUMMY=value"));
let disable_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("disable-passphrase")
.arg("myprofile")
.arg("--passphrase")
.arg("new-test-passphrase")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy disable-passphrase");
assert!(
disable_out.status.success(),
"profile-policy disable-passphrase failed: {}",
String::from_utf8_lossy(&disable_out.stderr)
);
let show_without_passphrase_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_PROFILE_PASSPHRASE")
.env_remove("SSHENV_VAULT")
.output()
.expect("run show after profile-policy disable-passphrase");
assert!(show_without_passphrase_out.status.success());
assert!(String::from_utf8_lossy(&show_without_passphrase_out.stdout).contains("DUMMY=value"));
let apply_all_standard_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("apply-all")
.arg("--preset")
.arg("standard")
.env("HOME", &home)
.env_remove("SSHENV_PROFILE_PASSPHRASE")
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy apply-all standard");
assert!(
apply_all_standard_out.status.success(),
"profile-policy apply-all standard failed: {}",
String::from_utf8_lossy(&apply_all_standard_out.stderr)
);
let other_standard_status_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("status")
.arg("otherprofile")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run other profile status after apply-all standard");
assert!(
other_standard_status_out.status.success(),
"other status failed: {}",
String::from_utf8_lossy(&other_standard_status_out.stderr)
);
let other_standard_status_stdout = String::from_utf8_lossy(&other_standard_status_out.stdout);
assert!(
other_standard_status_stdout.contains("preset: Standard"),
"{other_standard_status_stdout}"
);
let other_show_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("otherprofile")
.env("HOME", &home)
.env_remove("SSHENV_PROFILE_PASSPHRASE")
.env_remove("SSHENV_VAULT")
.output()
.expect("run show other profile after apply-all standard");
assert!(other_show_out.status.success());
assert!(String::from_utf8_lossy(&other_show_out.stdout).contains("DUMMY=other"));
let apply_all_portable_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("apply-all")
.arg("--preset")
.arg("portable")
.arg("--passphrase")
.arg("bulk-passphrase")
.env("HOME", &home)
.env_remove("SSHENV_PROFILE_PASSPHRASE")
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy apply-all portable");
assert!(
apply_all_portable_out.status.success(),
"profile-policy apply-all portable failed: {}",
String::from_utf8_lossy(&apply_all_portable_out.stderr)
);
let other_show_without_bulk_passphrase = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("otherprofile")
.env("HOME", &home)
.env_remove("SSHENV_PROFILE_PASSPHRASE")
.env_remove("SSHENV_VAULT")
.output()
.expect("run show other profile without bulk passphrase");
assert!(!other_show_without_bulk_passphrase.status.success());
for profile in ["myprofile", "otherprofile"] {
let bulk_show_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg(profile)
.env("HOME", &home)
.env("SSHENV_PROFILE_PASSPHRASE", "bulk-passphrase")
.env_remove("SSHENV_VAULT")
.output()
.expect("run show with bulk passphrase");
assert!(
bulk_show_out.status.success(),
"show {profile} with bulk passphrase failed: {}",
String::from_utf8_lossy(&bulk_show_out.stderr)
);
}
let apply_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("apply")
.arg("myprofile")
.arg("--preset")
.arg("portable")
.arg("--passphrase")
.arg("apply-passphrase")
.env("HOME", &home)
.env("SSHENV_PROFILE_PASSPHRASE", "bulk-passphrase")
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy apply portable");
assert!(
apply_out.status.success(),
"profile-policy apply portable failed: {}",
String::from_utf8_lossy(&apply_out.stderr)
);
let apply_status_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("status")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy status after apply");
assert!(apply_status_out.status.success());
let apply_status_stdout = String::from_utf8_lossy(&apply_status_out.stdout);
assert!(
apply_status_stdout.contains("preset: Portable"),
"missing applied preset: {apply_status_stdout}"
);
assert!(
apply_status_stdout
.contains("requirement passphrase: profile-specific cryptographic binding"),
"missing applied binding: {apply_status_stdout}"
);
let apply_show_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env("SSHENV_PROFILE_PASSPHRASE", "apply-passphrase")
.env_remove("SSHENV_VAULT")
.output()
.expect("run show after profile-policy apply");
assert!(
apply_show_out.status.success(),
"show after profile-policy apply failed: {}",
String::from_utf8_lossy(&apply_show_out.stderr)
);
assert!(String::from_utf8_lossy(&apply_show_out.stdout).contains("DUMMY=value"));
let apply_standard_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("apply")
.arg("myprofile")
.arg("--preset")
.arg("standard")
.env("HOME", &home)
.env("SSHENV_PROFILE_PASSPHRASE", "apply-passphrase")
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy apply standard");
assert!(
apply_standard_out.status.success(),
"profile-policy apply standard failed: {}",
String::from_utf8_lossy(&apply_standard_out.stderr)
);
let standard_show_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_PROFILE_PASSPHRASE")
.env_remove("SSHENV_VAULT")
.output()
.expect("run show after profile-policy apply standard");
assert!(standard_show_out.status.success());
assert!(String::from_utf8_lossy(&standard_show_out.stdout).contains("DUMMY=value"));
}
#[test]
fn binary_profile_policy_apply_migrates_v1_to_enforced_v2() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let vault_path = dir.path().join("vault");
init_vault_with_profile(&bin, &home, &vault_path, "myprofile");
let set_second_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("set")
.arg("otherprofile")
.arg("DUMMY")
.arg("--value")
.arg("value2")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("set second profile");
assert!(set_second_out.status.success());
let apply_all_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("apply-all")
.arg("--preset")
.arg("portable")
.arg("--dry-run")
.arg("--json")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy apply-all dry-run json");
assert!(apply_all_out.status.success());
let apply_all_json = String::from_utf8_lossy(&apply_all_out.stdout);
assert!(
apply_all_json.contains("\"profiles_total\": 2"),
"{apply_all_json}"
);
assert!(
apply_all_json.contains("\"profile\": \"myprofile\"")
&& apply_all_json.contains("\"profile\": \"otherprofile\""),
"{apply_all_json}"
);
assert!(
apply_all_json.contains("\"requires_passphrase_count\": 2"),
"{apply_all_json}"
);
assert!(
apply_all_json.contains("\"requires_recipient_key_count\": 2"),
"{apply_all_json}"
);
let apply_all_status_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("status")
.arg("otherprofile")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run status after apply-all dry-run");
assert!(apply_all_status_out.status.success());
let apply_all_status_stdout = String::from_utf8_lossy(&apply_all_status_out.stdout);
assert!(
apply_all_status_stdout.contains("policy metadata: absent"),
"{apply_all_status_stdout}"
);
let dry_run_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("apply")
.arg("myprofile")
.arg("--preset")
.arg("portable")
.arg("--dry-run")
.arg("--json")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy apply dry-run json");
assert!(dry_run_out.status.success());
let dry_run_json = String::from_utf8_lossy(&dry_run_out.stdout);
assert!(
dry_run_json.contains("\"target_preset\": \"Portable\""),
"{dry_run_json}"
);
assert!(
dry_run_json.contains("\"requires_passphrase\": true"),
"{dry_run_json}"
);
assert!(
dry_run_json.contains("\"requires_recipient_key\": true"),
"{dry_run_json}"
);
assert!(dry_run_json.contains("\"migrate-to-v2\""), "{dry_run_json}");
let dry_run_status_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("status")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy status after apply dry-run");
assert!(dry_run_status_out.status.success());
let dry_run_status_stdout = String::from_utf8_lossy(&dry_run_status_out.stdout);
assert!(
dry_run_status_stdout.contains("policy metadata: absent"),
"{dry_run_status_stdout}"
);
let missing_passphrase_apply_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("apply")
.arg("myprofile")
.arg("--preset")
.arg("portable")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy apply without passphrase");
assert!(!missing_passphrase_apply_out.status.success());
assert!(
String::from_utf8_lossy(&missing_passphrase_apply_out.stderr)
.contains("profile policy apply requires --passphrase <value> in non-interactive mode")
);
let strict_inputs_apply_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("apply")
.arg("myprofile")
.arg("--preset")
.arg("portable")
.arg("--passphrase")
.arg("profile-passphrase")
.arg("--strict-inputs")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy apply strict-inputs without recipient key");
assert!(!strict_inputs_apply_out.status.success());
assert!(
String::from_utf8_lossy(&strict_inputs_apply_out.stderr).contains(
"profile policy apply requires --recipient-key <path-or-public-key-line> in strict-inputs mode"
)
);
let failed_apply_status_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("status")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy status after failed apply");
assert!(failed_apply_status_out.status.success());
let failed_apply_status_stdout = String::from_utf8_lossy(&failed_apply_status_out.stdout);
assert!(
failed_apply_status_stdout.contains("policy metadata: absent"),
"{failed_apply_status_stdout}"
);
let apply_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("apply")
.arg("myprofile")
.arg("--preset")
.arg("portable")
.arg("--recipient-key")
.arg(home.join(".ssh").join("id_ed25519.pub"))
.arg("--passphrase")
.arg("profile-passphrase")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy apply portable from v1");
assert!(
apply_out.status.success(),
"profile-policy apply failed: {}",
String::from_utf8_lossy(&apply_out.stderr)
);
let status_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("status")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy status");
assert!(status_out.status.success());
let status_stdout = String::from_utf8_lossy(&status_out.stdout);
assert!(
status_stdout.contains("profile-key mode: enabled"),
"missing profile-key mode: {status_stdout}"
);
assert!(
status_stdout.contains("preset: Portable"),
"missing preset: {status_stdout}"
);
assert!(
status_stdout.contains("requirement passphrase: profile-specific cryptographic binding"),
"missing passphrase binding: {status_stdout}"
);
let show_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env("SSHENV_PROFILE_PASSPHRASE", "profile-passphrase")
.env_remove("SSHENV_VAULT")
.output()
.expect("run show after profile-policy apply");
assert!(show_out.status.success());
assert!(String::from_utf8_lossy(&show_out.stdout).contains("DUMMY=value"));
}
#[test]
fn binary_profile_policy_repair_enforces_advisory_portable() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let vault_path = dir.path().join("vault");
init_vault_with_profile(&bin, &home, &vault_path, "myprofile");
let migrate_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("migrate-vault")
.arg("--to")
.arg("v2")
.arg("--recipient-key")
.arg(home.join(".ssh").join("id_ed25519.pub"))
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run migrate-vault");
assert!(migrate_out.status.success());
let set_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("set")
.arg("myprofile")
.arg("--preset")
.arg("portable")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy set portable");
assert!(
set_out.status.success(),
"profile-policy set failed: {}",
String::from_utf8_lossy(&set_out.stderr)
);
let advisory_status_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("status")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run advisory profile-policy status");
assert!(advisory_status_out.status.success());
let advisory_stdout = String::from_utf8_lossy(&advisory_status_out.stdout);
assert!(
advisory_stdout.contains("preset Portable expects profile-specific passphrase binding"),
"{advisory_stdout}"
);
assert!(
advisory_stdout.contains(
"repair: sshenv security profile-policy repair myprofile --passphrase <value>"
),
"{advisory_stdout}"
);
let advisory_json_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("status")
.arg("myprofile")
.arg("--json")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run advisory profile-policy status json");
assert!(advisory_json_out.status.success());
let advisory_json = String::from_utf8_lossy(&advisory_json_out.stdout);
assert!(
advisory_json.contains("\"repair_recommended\": true"),
"{advisory_json}"
);
assert!(
advisory_json.contains("\"severity\": \"warning\""),
"{advisory_json}"
);
assert!(
advisory_json.contains("\"code\": \"missing-preset-binding\""),
"{advisory_json}"
);
let check_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("check")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run advisory profile-policy check");
assert!(check_out.status.success());
let check_stdout = String::from_utf8_lossy(&check_out.stdout);
assert!(
check_stdout.contains("profiles checked: 1"),
"{check_stdout}"
);
assert!(check_stdout.contains("warnings: 1"), "{check_stdout}");
assert!(
check_stdout.contains(
"repair: sshenv security profile-policy repair myprofile --passphrase <value>"
),
"{check_stdout}"
);
let strict_check_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("check")
.arg("--strict")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run strict advisory profile-policy check");
assert!(!strict_check_out.status.success());
assert!(
String::from_utf8_lossy(&strict_check_out.stderr)
.contains("profile policy check failed with 1 warning(s) in strict mode")
);
let check_json_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("check")
.arg("--json")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run advisory profile-policy check json");
assert!(check_json_out.status.success());
let check_json = String::from_utf8_lossy(&check_json_out.stdout);
assert!(
check_json.contains("\"profiles_checked\": 1"),
"{check_json}"
);
assert!(check_json.contains("\"warnings\": 1"), "{check_json}");
assert!(
check_json.contains("\"repairable_profiles\": [\n \"myprofile\"\n ]"),
"{check_json}"
);
assert!(check_json.contains("\"repairable\": true"), "{check_json}");
assert!(
check_json.contains("\"bound profile payload to passphrase\""),
"{check_json}"
);
assert!(
check_json.contains("\"requires_passphrase\": true"),
"{check_json}"
);
let dry_run_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("repair")
.arg("myprofile")
.arg("--dry-run")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy repair dry-run");
assert!(dry_run_out.status.success());
let dry_run_stdout = String::from_utf8_lossy(&dry_run_out.stdout);
assert!(
dry_run_stdout.contains("profile policy repair plan"),
"{dry_run_stdout}"
);
assert!(
dry_run_stdout.contains("bound profile payload to passphrase"),
"{dry_run_stdout}"
);
assert!(
dry_run_stdout.contains("requires passphrase: yes"),
"{dry_run_stdout}"
);
let dry_run_json_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("repair")
.arg("myprofile")
.arg("--dry-run")
.arg("--json")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy repair dry-run json");
assert!(dry_run_json_out.status.success());
let dry_run_json = String::from_utf8_lossy(&dry_run_json_out.stdout);
assert!(
dry_run_json.contains("\"repairable\": true"),
"{dry_run_json}"
);
assert!(
dry_run_json.contains("\"bind-passphrase\""),
"{dry_run_json}"
);
assert!(
dry_run_json.contains("\"requires_passphrase\": true"),
"{dry_run_json}"
);
let missing_passphrase_repair_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("repair")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy repair without passphrase");
assert!(!missing_passphrase_repair_out.status.success());
assert!(
String::from_utf8_lossy(&missing_passphrase_repair_out.stderr).contains(
"profile policy repair requires --passphrase <value> in non-interactive mode"
)
);
let repair_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("repair")
.arg("myprofile")
.arg("--passphrase")
.arg("repair-passphrase")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy repair");
assert!(
repair_out.status.success(),
"profile-policy repair failed: {}",
String::from_utf8_lossy(&repair_out.stderr)
);
let repair_stderr = String::from_utf8_lossy(&repair_out.stderr);
assert!(
repair_stderr.contains("bound profile payload to passphrase"),
"{repair_stderr}"
);
assert!(
repair_stderr.contains("rotated profile data key"),
"{repair_stderr}"
);
let status_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("status")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy status after repair");
assert!(status_out.status.success());
let stdout = String::from_utf8_lossy(&status_out.stdout);
assert!(stdout.contains("profile-key mode: enabled"), "{stdout}");
assert!(stdout.contains("preset: Portable"), "{stdout}");
assert!(
stdout.contains("requirement passphrase: profile-specific cryptographic binding"),
"{stdout}"
);
assert!(stdout.contains("warnings: none"), "{stdout}");
let clean_check_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("check")
.arg("--strict")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run strict profile-policy check after repair");
assert!(
clean_check_out.status.success(),
"strict check failed after repair: {}",
String::from_utf8_lossy(&clean_check_out.stderr)
);
let clean_check_stdout = String::from_utf8_lossy(&clean_check_out.stdout);
assert!(
clean_check_stdout.contains("warnings: 0"),
"{clean_check_stdout}"
);
assert!(
clean_check_stdout.contains("errors: 0"),
"{clean_check_stdout}"
);
let show_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env("SSHENV_PROFILE_PASSPHRASE", "repair-passphrase")
.env_remove("SSHENV_VAULT")
.output()
.expect("run show after profile-policy repair");
assert!(show_out.status.success());
assert!(String::from_utf8_lossy(&show_out.stdout).contains("DUMMY=value"));
}
#[test]
fn binary_profile_policy_metadata_roundtrips() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let vault_path = dir.path().join("vault");
init_vault_with_profile(&bin, &home, &vault_path, "myprofile");
let migrate_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("migrate-vault")
.arg("--to")
.arg("v2")
.arg("--recipient-key")
.arg(home.join(".ssh").join("id_ed25519.pub"))
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run migrate-vault");
assert!(migrate_out.status.success());
let set_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("set")
.arg("myprofile")
.arg("--preset")
.arg("paranoid")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy set");
assert!(
set_out.status.success(),
"profile-policy set failed: {}",
String::from_utf8_lossy(&set_out.stderr)
);
let list_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("list")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy list");
assert!(list_out.status.success());
let stdout = String::from_utf8_lossy(&list_out.stdout);
assert!(stdout.contains("myprofile"), "missing profile: {stdout}");
assert!(stdout.contains("Paranoid"), "missing preset: {stdout}");
assert!(
stdout.contains("unmet"),
"missing unmet posture note: {stdout}"
);
let show_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run show with advisory policy");
assert!(show_out.status.success());
let stderr = String::from_utf8_lossy(&show_out.stderr);
assert!(
stderr.contains("advisory policy"),
"missing advisory warning: {stderr}"
);
}
#[test]
fn binary_rollback_protection_rejects_older_v2_generation() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let vault_path = dir.path().join("vault");
init_vault_with_profile(&bin, &home, &vault_path, "myprofile");
migrate_vault_to_v2(&bin, &home, &vault_path);
let old_vault = dir.path().join("old-vault");
std::fs::copy(&vault_path, &old_vault).unwrap();
let set_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("set")
.arg("myprofile")
.arg("OTHER")
.arg("--value")
.arg("newer")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run set on migrated vault");
assert!(set_out.status.success());
std::fs::copy(&old_vault, &vault_path).unwrap();
let status_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("rollback")
.arg("status")
.arg("--json")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run rollback status after rollback");
assert!(
status_out.status.success(),
"rollback status failed: {}",
String::from_utf8_lossy(&status_out.stderr)
);
let status_json: serde_json::Value = serde_json::from_slice(&status_out.stdout).unwrap();
assert_eq!(status_json["vault_generation"], 1);
assert_eq!(status_json["local_baseline_generation"], 2);
assert!(
status_json["baseline_status"]
.as_str()
.unwrap()
.contains("rollback suspected"),
"{status_json}"
);
let checkpoint_path = dir.path().join("checkpoint.json");
std::fs::write(
&checkpoint_path,
r#"{
"backend": "remote-checkpoint",
"vault-id": "test-vault",
"generation": 2,
"created-unix": 4102444800,
"signer": null,
"signature": null
}"#,
)
.unwrap();
let checkpoint_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.args(["security", "rollback", "validate-checkpoint"])
.arg(&checkpoint_path)
.arg("--json")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run rollback validate-checkpoint");
assert!(
checkpoint_out.status.success(),
"validate-checkpoint failed: {}",
String::from_utf8_lossy(&checkpoint_out.stderr)
);
let checkpoint_json: serde_json::Value =
serde_json::from_slice(&checkpoint_out.stdout).unwrap();
assert_eq!(checkpoint_json["valid"], true);
assert_eq!(checkpoint_json["checkpoint_generation"], 2);
assert_eq!(checkpoint_json["vault_generation"], 1);
assert_eq!(checkpoint_json["would_reject_current_vault"], true);
let show_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.env_remove("RUST_BACKTRACE")
.env_remove("RUST_LIB_BACKTRACE")
.output()
.expect("run show after rollback");
assert!(!show_out.status.success());
let stderr = String::from_utf8_lossy(&show_out.stderr);
assert!(
stderr.contains("rollback"),
"rollback error should mention rollback: {stderr}"
);
}
#[test]
fn binary_security_preset_recommended_migrates_to_v2() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let vault_path = dir.path().join("vault");
init_vault_with_profile(&bin, &home, &vault_path, "myprofile");
let preset_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("preset")
.arg("recommended")
.arg("--recipient-key")
.arg(home.join(".ssh").join("id_ed25519.pub"))
.env("HOME", &home)
.env("SSHENV_DEVICE_SEAL_BACKEND", "local-file")
.env_remove("SSHENV_VAULT")
.output()
.expect("run security preset recommended");
assert!(
preset_out.status.success(),
"security preset recommended failed: {}",
String::from_utf8_lossy(&preset_out.stderr)
);
let recipients_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("list-recipients")
.arg("--verbose")
.env("HOME", &home)
.env("SSHENV_DEVICE_SEAL_BACKEND", "local-file")
.env_remove("SSHENV_VAULT")
.output()
.expect("run list-recipients");
assert!(recipients_out.status.success());
assert!(String::from_utf8_lossy(&recipients_out.stdout).contains("ssh-ed25519"));
}
#[test]
fn binary_remove_recipient_without_rotate_sets_rotation_reminder() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let vault_path = dir.path().join("vault");
std::fs::create_dir_all(home.join(".ssh")).unwrap();
let (_first_priv, first_pub) = write_named_keypair(&home.join(".ssh"), "id_ed25519");
let (_second_priv, _second_pub) = write_named_keypair(&home.join(".ssh"), "id_other");
let first_fingerprint = fingerprint_from_line(&first_pub).expect("first fingerprint");
let init_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("init")
.arg("--recipient-key")
.arg(home.join(".ssh").join("id_ed25519.pub"))
.env("HOME", &home)
.env("SSHENV_CONFIG", home.join(".sshenv").join("config.toml"))
.env_remove("SSHENV_VAULT")
.output()
.expect("run init");
assert!(init_out.status.success());
let add_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("add-recipient")
.arg("--key")
.arg(home.join(".ssh").join("id_other.pub"))
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run add-recipient");
assert!(add_out.status.success());
let remove_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("remove-recipient")
.arg("--fingerprint")
.arg(&first_fingerprint)
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run remove-recipient");
assert!(
remove_out.status.success(),
"remove-recipient failed: {}",
String::from_utf8_lossy(&remove_out.stderr)
);
let remove_stderr = String::from_utf8_lossy(&remove_out.stderr);
assert!(
remove_stderr.contains("rotate the vault data key"),
"{remove_stderr}"
);
let status_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("status")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run security status");
assert!(status_out.status.success());
let status_stdout = String::from_utf8_lossy(&status_out.stdout);
assert!(
status_stdout.contains("data-key rotation: recommended"),
"{status_stdout}"
);
let doctor_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("doctor")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run doctor");
assert!(!doctor_out.status.success());
let doctor_stdout = String::from_utf8_lossy(&doctor_out.stdout);
assert!(
doctor_stdout.contains("data-key rotation: recommended"),
"{doctor_stdout}"
);
let rotate_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("rotate-key")
.arg("--recipient-key")
.arg(home.join(".ssh").join("id_other.pub"))
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run rotate-key");
assert!(
rotate_out.status.success(),
"rotate-key failed: {}",
String::from_utf8_lossy(&rotate_out.stderr)
);
let status_after_rotate_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("status")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run security status after rotate");
assert!(status_after_rotate_out.status.success());
let status_after_rotate_stdout = String::from_utf8_lossy(&status_after_rotate_out.stdout);
assert!(
status_after_rotate_stdout.contains("data-key rotation: no local reminder"),
"{status_after_rotate_stdout}"
);
}
#[test]
fn binary_ssh_hardening_config_denies_unencrypted_authorized_key() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let vault_path = dir.path().join("vault");
init_vault_with_profile(&bin, &home, &vault_path, "myprofile");
let config_path = home.join(".sshenv").join("config.toml");
std::fs::create_dir_all(config_path.parent().unwrap()).unwrap();
std::fs::write(
&config_path,
"[security]\nunencrypted_ssh_keys = \"deny\"\n",
)
.unwrap();
let denied_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run show with deny config");
assert!(!denied_out.status.success());
let denied_stderr = String::from_utf8_lossy(&denied_out.stderr);
assert!(
denied_stderr.contains("authorized unencrypted SSH private key(s) denied by config"),
"{denied_stderr}"
);
std::fs::write(
&config_path,
"[security]\nunencrypted_ssh_keys = \"warn\"\n",
)
.unwrap();
let warned_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run show with warn config");
assert!(
warned_out.status.success(),
"show with warn config failed: {}",
String::from_utf8_lossy(&warned_out.stderr)
);
let warned_stderr = String::from_utf8_lossy(&warned_out.stderr);
assert!(
warned_stderr.contains("warning: authorized SSH private key is unencrypted"),
"{warned_stderr}"
);
std::fs::write(
&config_path,
"[security]\nunencrypted_ssh_keys = \"allow\"\n",
)
.unwrap();
let allowed_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run show with allow config");
assert!(
allowed_out.status.success(),
"show with allow config failed: {}",
String::from_utf8_lossy(&allowed_out.stderr)
);
let allowed_stderr = String::from_utf8_lossy(&allowed_out.stderr);
assert!(
!allowed_stderr.contains("authorized SSH private key is unencrypted"),
"{allowed_stderr}"
);
}
#[test]
fn binary_profile_policy_apply_all_no_backup_suppresses_default_backup() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let vault_path = dir.path().join("vault");
init_vault_with_profile(&bin, &home, &vault_path, "myprofile");
let apply_all_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("apply-all")
.arg("--preset")
.arg("standard")
.arg("--recipient-key")
.arg(home.join(".ssh").join("id_ed25519.pub"))
.arg("--no-backup")
.env("HOME", &home)
.env_remove("SSHENV_PROFILE_PASSPHRASE")
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy apply-all --no-backup");
assert!(
apply_all_out.status.success(),
"profile-policy apply-all --no-backup failed: {}",
String::from_utf8_lossy(&apply_all_out.stderr)
);
let stderr = String::from_utf8_lossy(&apply_all_out.stderr);
assert!(!stderr.contains("Backup written to "), "{stderr}");
}
#[test]
fn binary_profile_policy_repair_all_enforces_advisory_portable() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let vault_path = dir.path().join("vault");
init_vault_with_profile(&bin, &home, &vault_path, "myprofile");
let set_other_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("set")
.arg("otherprofile")
.arg("DUMMY")
.arg("--value")
.arg("other")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run set other profile");
assert!(set_other_out.status.success());
let migrate_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("migrate-vault")
.arg("--to")
.arg("v2")
.arg("--recipient-key")
.arg(home.join(".ssh").join("id_ed25519.pub"))
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run migrate-vault");
assert!(migrate_out.status.success());
for profile in ["myprofile", "otherprofile"] {
let set_policy_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("set")
.arg(profile)
.arg("--preset")
.arg("portable")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy set portable");
assert!(
set_policy_out.status.success(),
"profile-policy set {profile} failed: {}",
String::from_utf8_lossy(&set_policy_out.stderr)
);
}
let dry_run_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("repair-all")
.arg("--dry-run")
.arg("--json")
.env("HOME", &home)
.env_remove("SSHENV_PROFILE_PASSPHRASE")
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy repair-all dry-run json");
assert!(dry_run_out.status.success());
let dry_run_json = String::from_utf8_lossy(&dry_run_out.stdout);
assert!(
dry_run_json.contains("\"profiles_total\": 2"),
"{dry_run_json}"
);
assert!(
dry_run_json.contains("\"requires_passphrase_count\": 2"),
"{dry_run_json}"
);
let missing_passphrase_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("repair-all")
.env("HOME", &home)
.env_remove("SSHENV_PROFILE_PASSPHRASE")
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy repair-all without passphrase");
assert!(!missing_passphrase_out.status.success());
let stderr = String::from_utf8_lossy(&missing_passphrase_out.stderr);
assert!(
stderr.contains(
"profile policy repair-all requires --passphrase <value> in non-interactive mode"
),
"{stderr}"
);
let repair_all_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("repair-all")
.arg("--passphrase")
.arg("bulk-passphrase")
.env("HOME", &home)
.env_remove("SSHENV_PROFILE_PASSPHRASE")
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy repair-all");
assert!(
repair_all_out.status.success(),
"profile-policy repair-all failed: {}",
String::from_utf8_lossy(&repair_all_out.stderr)
);
let repair_stderr = String::from_utf8_lossy(&repair_all_out.stderr);
let backup_path = repair_stderr
.lines()
.find_map(|line| line.strip_prefix("Backup written to "))
.map(PathBuf::from)
.expect("backup path in repair-all stderr");
assert!(backup_path.exists(), "missing backup at {backup_path:?}");
let backups_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("backups")
.env("HOME", &home)
.env_remove("SSHENV_PROFILE_PASSPHRASE")
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy backups");
assert!(backups_out.status.success());
let backups_stdout = String::from_utf8_lossy(&backups_out.stdout);
let backup_file_name = backup_path
.file_name()
.expect("backup file name")
.to_string_lossy();
assert!(
backups_stdout.contains("kind: bulk-backup"),
"{backups_stdout}"
);
assert!(
backups_stdout.contains(backup_file_name.as_ref()),
"{backups_stdout}"
);
let verify_backup_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("verify-backup")
.arg(&backup_path)
.arg("--json")
.env("HOME", &home)
.env_remove("SSHENV_PROFILE_PASSPHRASE")
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy verify-backup json");
assert!(
verify_backup_out.status.success(),
"profile-policy verify-backup failed: {}",
String::from_utf8_lossy(&verify_backup_out.stderr)
);
let verify_json = String::from_utf8_lossy(&verify_backup_out.stdout);
assert!(verify_json.contains("\"readable\": true"), "{verify_json}");
assert!(
verify_json.contains("\"unlockable\": true"),
"{verify_json}"
);
assert!(
verify_json.contains("\"profiles_checked\":"),
"{verify_json}"
);
let corrupt_backup = dir.path().join("corrupt-backup");
std::fs::write(&corrupt_backup, b"not a vault").unwrap();
let corrupt_verify_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("verify-backup")
.arg(&corrupt_backup)
.arg("--json")
.env("HOME", &home)
.env_remove("SSHENV_PROFILE_PASSPHRASE")
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy verify-backup corrupt json");
assert!(!corrupt_verify_out.status.success());
let corrupt_verify_json = String::from_utf8_lossy(&corrupt_verify_out.stdout);
assert!(
corrupt_verify_json.contains("\"readable\": false"),
"{corrupt_verify_json}"
);
assert!(
corrupt_verify_json.contains("\"unlockable\": false"),
"{corrupt_verify_json}"
);
let backups_json_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("backups")
.arg("--json")
.env("HOME", &home)
.env_remove("SSHENV_PROFILE_PASSPHRASE")
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy backups json");
assert!(backups_json_out.status.success());
let backups_json = String::from_utf8_lossy(&backups_json_out.stdout);
assert!(
backups_json.contains("\"kind\": \"bulk-backup\""),
"{backups_json}"
);
assert!(backups_json.contains("\"version\":"), "{backups_json}");
assert!(backups_json.contains("\"generation\":"), "{backups_json}");
assert!(
backups_json.contains(backup_file_name.as_ref()),
"{backups_json}"
);
let backup_show_out = Command::new(&bin)
.arg("--vault")
.arg(&backup_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_PROFILE_PASSPHRASE")
.env_remove("SSHENV_VAULT")
.output()
.expect("run show against backup vault");
assert!(
backup_show_out.status.success(),
"show against backup failed: {}",
String::from_utf8_lossy(&backup_show_out.stderr)
);
for profile in ["myprofile", "otherprofile"] {
let show_without_passphrase_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg(profile)
.env("HOME", &home)
.env_remove("SSHENV_PROFILE_PASSPHRASE")
.env_remove("SSHENV_VAULT")
.output()
.expect("run show without repaired passphrase");
assert!(!show_without_passphrase_out.status.success());
let show_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg(profile)
.env("HOME", &home)
.env("SSHENV_PROFILE_PASSPHRASE", "bulk-passphrase")
.env_remove("SSHENV_VAULT")
.output()
.expect("run show with repaired passphrase");
assert!(
show_out.status.success(),
"show {profile} with repaired passphrase failed: {}",
String::from_utf8_lossy(&show_out.stderr)
);
}
let restore_dry_run_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("restore-backup")
.arg(&backup_path)
.arg("--dry-run")
.arg("--json")
.env("HOME", &home)
.env_remove("SSHENV_PROFILE_PASSPHRASE")
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy restore-backup dry-run");
assert!(
restore_dry_run_out.status.success(),
"profile-policy restore-backup dry-run failed: {}",
String::from_utf8_lossy(&restore_dry_run_out.stderr)
);
let restore_dry_run_json = String::from_utf8_lossy(&restore_dry_run_out.stdout);
assert!(
restore_dry_run_json.contains("\"would_restore\": true"),
"{restore_dry_run_json}"
);
assert!(
restore_dry_run_json.contains("\"generation_rollback\": true"),
"{restore_dry_run_json}"
);
let dry_run_preserved_current_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_PROFILE_PASSPHRASE")
.env_remove("SSHENV_VAULT")
.output()
.expect("run show after restore-backup dry-run");
assert!(!dry_run_preserved_current_out.status.success());
let restore_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("restore-backup")
.arg(&backup_path)
.env("HOME", &home)
.env_remove("SSHENV_PROFILE_PASSPHRASE")
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy restore-backup");
assert!(
restore_out.status.success(),
"profile-policy restore-backup failed: {}",
String::from_utf8_lossy(&restore_out.stderr)
);
let restore_stderr = String::from_utf8_lossy(&restore_out.stderr);
let pre_restore_backup_path = restore_stderr
.lines()
.find_map(|line| line.strip_prefix("Pre-restore backup written to "))
.map(PathBuf::from)
.expect("pre-restore backup path in stderr");
assert!(
pre_restore_backup_path.exists(),
"missing pre-restore backup at {pre_restore_backup_path:?}"
);
assert!(
restore_stderr.contains("Restored vault from ")
&& restore_stderr.contains(
backup_path
.file_name()
.expect("backup file name")
.to_string_lossy()
.as_ref()
),
"{restore_stderr}"
);
let restored_show_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_PROFILE_PASSPHRASE")
.env_remove("SSHENV_VAULT")
.output()
.expect("run show after restore-backup");
assert!(
restored_show_out.status.success(),
"show after restore-backup failed: {}",
String::from_utf8_lossy(&restored_show_out.stderr)
);
let pre_restore_show_out = Command::new(&bin)
.arg("--vault")
.arg(&pre_restore_backup_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env("SSHENV_PROFILE_PASSPHRASE", "bulk-passphrase")
.env_remove("SSHENV_VAULT")
.output()
.expect("run show against pre-restore backup");
assert!(
pre_restore_show_out.status.success(),
"show against pre-restore backup failed: {}",
String::from_utf8_lossy(&pre_restore_show_out.stderr)
);
let prune_dry_run_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("prune-backups")
.arg("--keep")
.arg("1")
.arg("--dry-run")
.arg("--json")
.env("HOME", &home)
.env_remove("SSHENV_PROFILE_PASSPHRASE")
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy prune-backups dry-run");
assert!(prune_dry_run_out.status.success());
let prune_json = String::from_utf8_lossy(&prune_dry_run_out.stdout);
assert!(prune_json.contains("\"dry_run\": true"), "{prune_json}");
assert!(prune_json.contains("\"pruned\": ["), "{prune_json}");
assert!(backup_path.exists());
assert!(pre_restore_backup_path.exists());
let prune_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("profile-policy")
.arg("prune-backups")
.arg("--keep")
.arg("1")
.arg("--confirm")
.env("HOME", &home)
.env_remove("SSHENV_PROFILE_PASSPHRASE")
.env_remove("SSHENV_VAULT")
.output()
.expect("run profile-policy prune-backups confirm");
assert!(
prune_out.status.success(),
"profile-policy prune-backups failed: {}",
String::from_utf8_lossy(&prune_out.stderr)
);
let remaining_backups = [backup_path.exists(), pre_restore_backup_path.exists()]
.into_iter()
.filter(|exists| *exists)
.count();
assert_eq!(remaining_backups, 1);
}
#[test]
fn binary_passphrase_factor_requires_passphrase_after_enable() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let vault_path = dir.path().join("vault");
init_vault_with_profile(&bin, &home, &vault_path, "myprofile");
let migrate_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("migrate-vault")
.arg("--to")
.arg("v2")
.arg("--recipient-key")
.arg(home.join(".ssh").join("id_ed25519.pub"))
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run migrate-vault");
assert!(migrate_out.status.success());
let enable_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("enable-passphrase")
.arg("--passphrase")
.arg("correct horse battery staple")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run security enable-passphrase");
assert!(
enable_out.status.success(),
"enable-passphrase failed: {}",
String::from_utf8_lossy(&enable_out.stderr)
);
let missing_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.env_remove("SSHENV_PASSPHRASE")
.output()
.expect("run show without passphrase");
assert!(!missing_out.status.success());
let show_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env("SSHENV_PASSPHRASE", "correct horse battery staple")
.env_remove("SSHENV_VAULT")
.output()
.expect("run show with passphrase");
assert!(
show_out.status.success(),
"show failed with passphrase: {}",
String::from_utf8_lossy(&show_out.stderr)
);
assert!(String::from_utf8_lossy(&show_out.stdout).contains("DUMMY=value"));
let change_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("change-passphrase")
.arg("--old-passphrase")
.arg("correct horse battery staple")
.arg("--new-passphrase")
.arg("new horse battery staple")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run security change-passphrase");
assert!(
change_out.status.success(),
"change-passphrase failed: {}",
String::from_utf8_lossy(&change_out.stderr)
);
let old_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env("SSHENV_PASSPHRASE", "correct horse battery staple")
.env_remove("SSHENV_VAULT")
.output()
.expect("run show with old passphrase");
assert!(!old_out.status.success());
let disable_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("security")
.arg("disable-passphrase")
.arg("--passphrase")
.arg("new horse battery staple")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run security disable-passphrase");
assert!(
disable_out.status.success(),
"disable-passphrase failed: {}",
String::from_utf8_lossy(&disable_out.stderr)
);
let disabled_show_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.env_remove("SSHENV_PASSPHRASE")
.output()
.expect("run show after disabling passphrase");
assert!(
disabled_show_out.status.success(),
"show failed after disabling passphrase: {}",
String::from_utf8_lossy(&disabled_show_out.stderr)
);
}
#[test]
fn binary_rotate_key_preserves_secret_access() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let vault_path = dir.path().join("vault");
init_vault_with_profile(&bin, &home, &vault_path, "myprofile");
let rotate_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("rotate-key")
.arg("--recipient-key")
.arg(home.join(".ssh").join("id_ed25519.pub"))
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run rotate-key");
assert!(
rotate_out.status.success(),
"rotate-key failed: {}",
String::from_utf8_lossy(&rotate_out.stderr)
);
let show_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("show")
.arg("myprofile")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run show");
assert!(
show_out.status.success(),
"show failed after rotate: {}",
String::from_utf8_lossy(&show_out.stderr)
);
assert!(
String::from_utf8_lossy(&show_out.stdout).contains("DUMMY=value"),
"rotated vault did not preserve profile contents"
);
}
#[test]
fn binary_unlock_no_matching_key_errors_helpfully() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home_a = dir.path().join("home_a");
let home_b = dir.path().join("home_b");
std::fs::create_dir_all(home_a.join(".ssh")).unwrap();
std::fs::create_dir_all(home_b.join(".ssh")).unwrap();
let (_a_priv, _a_pub) = write_named_keypair(&home_a.join(".ssh"), "id_ed25519");
let vault_path = dir.path().join("vault");
let init_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("init")
.arg("--recipient-key")
.arg(home_a.join(".ssh").join("id_ed25519.pub"))
.env("HOME", &home_a)
.env_remove("SSHENV_VAULT")
.output()
.expect("init");
assert!(init_out.status.success());
let (_b_priv, _b_pub) = write_named_keypair(&home_b.join(".ssh"), "id_ed25519");
let out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("list")
.env("HOME", &home_b)
.env_remove("SSHENV_VAULT")
.env_remove("RUST_BACKTRACE")
.env_remove("RUST_LIB_BACKTRACE")
.output()
.expect("list");
assert!(!out.status.success(), "list should fail");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no SSH private key authorized"),
"stderr missing 'no SSH private key authorized': {stderr}"
);
assert!(
stderr.contains("Vault recipients:"),
"stderr missing vault recipient list: {stderr}"
);
assert!(
stderr.contains("Local keys checked:"),
"stderr missing local key diagnostics: {stderr}"
);
assert!(
!stderr.contains("Stack backtrace"),
"backtrace leaked: {stderr}"
);
}
#[cfg(unix)]
#[test]
fn binary_shim_does_not_self_invoke() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
std::fs::create_dir_all(home.join(".ssh")).unwrap();
let _k = write_named_keypair(&home.join(".ssh"), "id_ed25519");
let vault_path = home.join(".sshenv").join("vault");
let init_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("init")
.arg("--recipient-key")
.arg(home.join(".ssh").join("id_ed25519.pub"))
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run init");
assert!(
init_out.status.success(),
"init failed: {}",
String::from_utf8_lossy(&init_out.stderr)
);
let set_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("set")
.arg("shim-test")
.arg("MYVAR")
.arg("--value")
.arg("hello-from-sshenv")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run set");
assert!(
set_out.status.success(),
"set failed: {}",
String::from_utf8_lossy(&set_out.stderr)
);
let real_bin_dir = home.join("real_bin");
std::fs::create_dir_all(&real_bin_dir).unwrap();
let real_cmd = real_bin_dir.join("test-cmd");
std::fs::write(
&real_cmd,
"#!/bin/sh\nprintf 'env-check:%s\\n' \"$MYVAR\"\n",
)
.unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&real_cmd, std::fs::Permissions::from_mode(0o755)).unwrap();
}
let shim_dir = home.join(".sshenv").join("bin");
let bindings_path = home.join(".sshenv").join("bindings.toml");
let bind_out = Command::new(&bin)
.arg("shims")
.arg("bind")
.arg("shim-test")
.arg("--command")
.arg("test-cmd")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.env("SSHENV_BINDINGS", &bindings_path)
.env("SSHENV_SHIM_DIR", &shim_dir)
.output()
.expect("run shims bind");
assert!(
bind_out.status.success(),
"shims bind failed: {}",
String::from_utf8_lossy(&bind_out.stderr)
);
let shim_path = shim_dir.join("test-cmd");
assert!(
shim_path.exists(),
"shim not created at {}",
shim_path.display()
);
let sshenv_bin_dir = bin.parent().expect("sshenv bin has a parent dir");
let path_value = format!(
"{}:{}:{}",
shim_dir.display(),
sshenv_bin_dir.display(),
real_bin_dir.display()
);
let run_out = Command::new(&shim_path)
.env("HOME", &home)
.env("PATH", &path_value)
.env("SSHENV_VAULT", &vault_path)
.env("SSHENV_BINDINGS", &bindings_path)
.env("SSHENV_SHIM_DIR", &shim_dir)
.output()
.expect("run shim");
assert!(
run_out.status.success(),
"shim invocation failed: stdout={} stderr={}",
String::from_utf8_lossy(&run_out.stdout),
String::from_utf8_lossy(&run_out.stderr),
);
let stdout = String::from_utf8_lossy(&run_out.stdout);
assert!(
stdout.contains("env-check:hello-from-sshenv"),
"expected env var to be injected into real binary; got stdout: {stdout}"
);
}
#[test]
fn binary_rename_profile_moves_vars_and_updates_shim_bindings() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
std::fs::create_dir_all(home.join(".ssh")).unwrap();
let _k = write_named_keypair(&home.join(".ssh"), "id_ed25519");
let vault_path = home.join(".sshenv").join("vault");
let bindings_path = home.join(".sshenv").join("bindings.toml");
let shim_dir = home.join(".sshenv").join("bin");
let init_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("init")
.arg("--recipient-key")
.arg(home.join(".ssh").join("id_ed25519.pub"))
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run init");
assert!(
init_out.status.success(),
"init failed: {}",
String::from_utf8_lossy(&init_out.stderr)
);
let set_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("set")
.arg("pi-bedrock")
.arg("AWS_BEARER_TOKEN_BEDROCK")
.arg("--value")
.arg("secret")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run set");
assert!(
set_out.status.success(),
"set failed: {}",
String::from_utf8_lossy(&set_out.stderr)
);
let bind_out = Command::new(&bin)
.arg("shims")
.arg("bind")
.arg("pi-bedrock")
.arg("--command")
.arg("pi-bedrock")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.env("SSHENV_BINDINGS", &bindings_path)
.env("SSHENV_SHIM_DIR", &shim_dir)
.output()
.expect("run shims bind");
assert!(
bind_out.status.success(),
"bind failed: {}",
String::from_utf8_lossy(&bind_out.stderr)
);
let rename_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("rename-profile")
.arg("pi-bedrock")
.arg("bedrock")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.env("SSHENV_BINDINGS", &bindings_path)
.env("SSHENV_SHIM_DIR", &shim_dir)
.output()
.expect("run rename-profile");
assert!(
rename_out.status.success(),
"rename-profile failed: stdout={} stderr={}",
String::from_utf8_lossy(&rename_out.stdout),
String::from_utf8_lossy(&rename_out.stderr),
);
let list_profiles = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("list")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run list");
assert!(list_profiles.status.success());
let stdout = String::from_utf8_lossy(&list_profiles.stdout);
assert!(stdout.contains("bedrock"), "missing new profile: {stdout}");
assert!(
!stdout.contains("pi-bedrock"),
"old profile still listed: {stdout}"
);
let list_new_profile = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("list")
.arg("bedrock")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run list bedrock");
assert!(list_new_profile.status.success());
let stdout = String::from_utf8_lossy(&list_new_profile.stdout);
assert!(
stdout.contains("AWS_BEARER_TOKEN_BEDROCK"),
"missing renamed profile var: {stdout}"
);
let list_old_profile = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("list")
.arg("pi-bedrock")
.env("HOME", &home)
.env_remove("SSHENV_VAULT")
.output()
.expect("run list pi-bedrock");
assert!(!list_old_profile.status.success());
let bindings = sshenv_shims::load_bindings(&bindings_path).unwrap();
let binding = bindings.find_by_command("pi-bedrock").unwrap();
assert_eq!(binding.profile, "bedrock");
let shim =
std::fs::read_to_string(shim_dir.join(sshenv_shims::shim_file_name("pi-bedrock"))).unwrap();
assert!(shim.contains("profile: bedrock"));
#[cfg(windows)]
assert!(shim.contains("sshenv run \"bedrock\" -- \"pi-bedrock\" %*"));
#[cfg(not(windows))]
assert!(shim.contains("exec sshenv run \"bedrock\" -- \"pi-bedrock\" \"$@\""));
}
#[test]
fn binary_shims_rename_updates_binding_and_regenerates_shims() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let bindings_path = home.join(".sshenv").join("bindings.toml");
let shim_dir = home.join(".sshenv").join("bin");
let bind_out = Command::new(&bin)
.arg("shims")
.arg("bind")
.arg("bedrock")
.arg("--command")
.arg("pi-bedrock")
.env("HOME", &home)
.env("SSHENV_BINDINGS", &bindings_path)
.env("SSHENV_SHIM_DIR", &shim_dir)
.output()
.expect("run shims bind");
assert!(
bind_out.status.success(),
"bind failed: {}",
String::from_utf8_lossy(&bind_out.stderr)
);
assert!(
shim_dir
.join(sshenv_shims::shim_file_name("pi-bedrock"))
.exists()
);
let rename_out = Command::new(&bin)
.arg("shims")
.arg("rename")
.arg("--command")
.arg("pi-bedrock")
.arg("--to")
.arg("bedrock")
.env("HOME", &home)
.env("SSHENV_BINDINGS", &bindings_path)
.env("SSHENV_SHIM_DIR", &shim_dir)
.output()
.expect("run shims rename");
assert!(
rename_out.status.success(),
"shims rename failed: stdout={} stderr={}",
String::from_utf8_lossy(&rename_out.stdout),
String::from_utf8_lossy(&rename_out.stderr),
);
let bindings = sshenv_shims::load_bindings(&bindings_path).unwrap();
assert!(bindings.find_by_command("pi-bedrock").is_none());
assert_eq!(
bindings.find_by_command("bedrock").unwrap().profile,
"bedrock"
);
assert!(
!shim_dir
.join(sshenv_shims::shim_file_name("pi-bedrock"))
.exists()
);
let shim =
std::fs::read_to_string(shim_dir.join(sshenv_shims::shim_file_name("bedrock"))).unwrap();
assert!(shim.contains("profile: bedrock"));
assert!(shim.contains("command: bedrock"));
#[cfg(windows)]
assert!(shim.contains("sshenv run \"bedrock\" -- \"bedrock\" %*"));
#[cfg(not(windows))]
assert!(shim.contains("exec sshenv run \"bedrock\" -- \"bedrock\" \"$@\""));
}
#[cfg(any(unix, windows))]
fn wait_for_stdout_contains(command: &mut Command, needle: &str) -> bool {
for _ in 0..40 {
let output = command.output().expect("run polling command");
let stdout = String::from_utf8_lossy(&output.stdout);
if output.status.success() && stdout.contains(needle) {
return true;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
false
}
#[cfg(any(unix, windows))]
fn sleep_command_args() -> Vec<&'static str> {
#[cfg(windows)]
{
vec![
"powershell.exe",
"-NoProfile",
"-Command",
"Start-Sleep -Seconds 30",
]
}
#[cfg(unix)]
{
vec!["/bin/sleep", "30"]
}
}
#[cfg(any(unix, windows))]
#[test]
fn binary_sessions_list_and_kill_tracked_run() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let vault_path = home.join(".sshenv").join("vault");
let sessions_path = dir.path().join("sessions.toml");
init_vault_with_profile(&bin, &home, &vault_path, "tracked");
let mut child = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("run")
.arg("tracked")
.arg("--")
.args(sleep_command_args())
.env("HOME", &home)
.env("SSHENV_SESSIONS", &sessions_path)
.spawn()
.expect("spawn tracked run");
let pid_text = child.id().to_string();
let mut list_cmd = Command::new(&bin);
list_cmd
.arg("--vault")
.arg(&vault_path)
.arg("sessions")
.arg("list")
.arg("--profile")
.arg("tracked")
.env("HOME", &home)
.env("SSHENV_SESSIONS", &sessions_path);
assert!(
wait_for_stdout_contains(&mut list_cmd, &pid_text),
"tracked session did not appear in sessions list"
);
let kill_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("sessions")
.arg("kill")
.arg("tracked")
.arg("--signal")
.arg("kill")
.env("HOME", &home)
.env("SSHENV_SESSIONS", &sessions_path)
.output()
.expect("run sessions kill");
assert!(
kill_out.status.success(),
"sessions kill failed: stdout={} stderr={}",
String::from_utf8_lossy(&kill_out.stdout),
String::from_utf8_lossy(&kill_out.stderr)
);
for _ in 0..40 {
if child.try_wait().unwrap().is_some() {
return;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
child.kill().ok();
panic!("tracked child was not killed by sessions kill");
}
#[cfg(any(unix, windows))]
#[test]
fn binary_run_incognito_is_not_tracked() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let vault_path = home.join(".sshenv").join("vault");
let sessions_path = dir.path().join("sessions.toml");
init_vault_with_profile(&bin, &home, &vault_path, "hidden");
let mut child = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("run")
.arg("--incognito")
.arg("hidden")
.arg("--")
.args(sleep_command_args())
.env("HOME", &home)
.env("SSHENV_SESSIONS", &sessions_path)
.spawn()
.expect("spawn incognito run");
std::thread::sleep(std::time::Duration::from_millis(200));
let list_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("sessions")
.arg("list")
.arg("--profile")
.arg("hidden")
.env("HOME", &home)
.env("SSHENV_SESSIONS", &sessions_path)
.output()
.expect("run sessions list");
child.kill().ok();
child.wait().ok();
assert!(
list_out.status.success(),
"sessions list failed: stdout={} stderr={}",
String::from_utf8_lossy(&list_out.stdout),
String::from_utf8_lossy(&list_out.stderr)
);
let stdout = String::from_utf8_lossy(&list_out.stdout);
assert!(
!stdout.contains("hidden"),
"incognito run should not be listed: {stdout}"
);
}
#[cfg(any(unix, windows))]
#[test]
fn binary_sessions_kill_all_targets_current_vault() {
let bin = cargo_bin();
if !bin.exists() {
eprintln!("skipping: {} does not exist", bin.display());
return;
}
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("home");
let vault_path = home.join(".sshenv").join("vault");
let sessions_path = dir.path().join("sessions.toml");
init_vault_with_profile(&bin, &home, &vault_path, "one");
let set_two = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("set")
.arg("two")
.arg("DUMMY")
.arg("--value")
.arg("value")
.env("HOME", &home)
.output()
.expect("set second profile");
assert!(
set_two.status.success(),
"set second profile failed: {}",
String::from_utf8_lossy(&set_two.stderr)
);
let mut child_one = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("run")
.arg("one")
.arg("--")
.args(sleep_command_args())
.env("HOME", &home)
.env("SSHENV_SESSIONS", &sessions_path)
.spawn()
.expect("spawn profile one run");
let mut child_two = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("run")
.arg("two")
.arg("--")
.args(sleep_command_args())
.env("HOME", &home)
.env("SSHENV_SESSIONS", &sessions_path)
.spawn()
.expect("spawn profile two run");
let pid_one = child_one.id().to_string();
let pid_two = child_two.id().to_string();
let mut both_listed = false;
for _ in 0..40 {
let list_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("sessions")
.arg("list")
.env("HOME", &home)
.env("SSHENV_SESSIONS", &sessions_path)
.output()
.expect("run sessions list");
let stdout = String::from_utf8_lossy(&list_out.stdout);
if list_out.status.success() && stdout.contains(&pid_one) && stdout.contains(&pid_two) {
both_listed = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
assert!(
both_listed,
"both tracked sessions did not appear in sessions list"
);
let kill_out = Command::new(&bin)
.arg("--vault")
.arg(&vault_path)
.arg("sessions")
.arg("kill")
.arg("--all")
.arg("--signal")
.arg("kill")
.env("HOME", &home)
.env("SSHENV_SESSIONS", &sessions_path)
.output()
.expect("run sessions kill --all");
assert!(
kill_out.status.success(),
"sessions kill --all failed: stdout={} stderr={}",
String::from_utf8_lossy(&kill_out.stdout),
String::from_utf8_lossy(&kill_out.stderr)
);
let mut one_exited = false;
let mut two_exited = false;
for _ in 0..40 {
one_exited |= child_one.try_wait().unwrap().is_some();
two_exited |= child_two.try_wait().unwrap().is_some();
if one_exited && two_exited {
return;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
child_one.kill().ok();
child_two.kill().ok();
panic!("kill --all did not terminate both tracked children");
}