use super::*;
#[cfg(unix)]
static PATH_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
fn cert_path_for_simple_alias() {
let path = cert_path_for("webserver").unwrap();
assert!(path.ends_with("certs/webserver-cert.pub"));
assert!(path.to_string_lossy().contains(".purple/certs/"));
}
#[test]
fn cert_path_for_alias_with_prefix() {
let path = cert_path_for("aws-prod-web01").unwrap();
assert!(path.ends_with("certs/aws-prod-web01-cert.pub"));
}
#[test]
fn sign_certificate_rejects_pubkey_path_with_equals() {
let dir = std::env::temp_dir().join(format!(
"purple_test_pubkey_eq_{:?}",
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let bad = dir.join("key=foo.pub");
std::fs::write(&bad, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI test@test\n").unwrap();
let result = sign_certificate("ssh/sign/test", &bad, "alias", None);
let err = result.unwrap_err().to_string();
assert!(
err.contains('=') && err.contains("Vault CLI"),
"expected explicit `=` rejection, got: {}",
err
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn sign_certificate_missing_pubkey() {
let result = sign_certificate(
"ssh/sign/test",
Path::new("/tmp/purple_nonexistent_key.pub"),
"test",
None,
);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("Public key not found"), "got: {}", err);
}
#[test]
fn sign_certificate_vault_not_configured() {
let tmpdir = std::env::temp_dir();
let fake_key = tmpdir.join("purple_test_vault_sign_key.pub");
std::fs::write(
&fake_key,
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI test@test\n",
)
.unwrap();
let result = sign_certificate("nonexistent/sign/role", &fake_key, "test-host", None);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("vault") || err.contains("Vault") || err.contains("Failed"),
"Error should mention vault: {}",
err
);
let _ = std::fs::remove_file(&fake_key);
}
#[test]
fn parse_valid_line_standard() {
let line = " Valid: from 2026-04-08T10:00:00 to 2026-04-09T10:00:00";
let (from, to) = parse_valid_line(line).unwrap();
assert!(from > 0);
assert!(to > from);
assert_eq!(to - from, 86400);
}
#[test]
fn parse_valid_line_no_match() {
assert!(parse_valid_line(" Type: ssh-ed25519-cert-v01@openssh.com").is_none());
}
#[test]
fn parse_valid_line_forever() {
let line = " Valid: from 2026-04-08T10:00:00 to forever";
assert!(parse_valid_line(line).is_none());
}
#[test]
fn parse_ssh_datetime_valid() {
let epoch = parse_ssh_datetime("2026-04-08T12:00:00").unwrap();
assert!(epoch > 1_700_000_000);
assert!(epoch < 2_000_000_000);
}
#[test]
fn parse_ssh_datetime_invalid() {
assert!(parse_ssh_datetime("not-a-date").is_none());
assert!(parse_ssh_datetime("2026-13-08T12:00:00").is_none());
}
#[test]
fn check_cert_validity_missing() {
let path = Path::new("/tmp/purple_test_nonexistent_cert.pub");
assert_eq!(check_cert_validity(path), CertStatus::Missing);
}
#[test]
fn needs_renewal_missing() {
assert!(needs_renewal(&CertStatus::Missing));
}
#[test]
fn needs_renewal_expired() {
assert!(needs_renewal(&CertStatus::Expired));
}
#[test]
fn needs_renewal_invalid() {
assert!(needs_renewal(&CertStatus::Invalid("bad".to_string())));
}
#[test]
fn needs_renewal_valid_plenty_of_time() {
assert!(!needs_renewal(&CertStatus::Valid {
expires_at: 0,
remaining_secs: 3600,
total_secs: 3600,
}));
}
#[test]
fn needs_renewal_valid_under_threshold() {
assert!(needs_renewal(&CertStatus::Valid {
expires_at: 0,
remaining_secs: 60,
total_secs: 3600,
}));
}
#[test]
fn needs_renewal_at_threshold_boundary() {
assert!(!needs_renewal(&CertStatus::Valid {
expires_at: 0,
remaining_secs: RENEWAL_THRESHOLD_SECS,
total_secs: 3600,
}));
assert!(needs_renewal(&CertStatus::Valid {
expires_at: 0,
remaining_secs: RENEWAL_THRESHOLD_SECS - 1,
total_secs: 3600,
}));
assert!(!needs_renewal(&CertStatus::Valid {
expires_at: 0,
remaining_secs: RENEWAL_THRESHOLD_SECS + 1,
total_secs: 3600,
}));
}
#[test]
fn needs_renewal_short_ttl_freshly_signed_not_renewed() {
let total = 120i64; assert!(!needs_renewal(&CertStatus::Valid {
expires_at: 0,
remaining_secs: total,
total_secs: total,
}));
assert!(!needs_renewal(&CertStatus::Valid {
expires_at: 0,
remaining_secs: 61,
total_secs: total,
}));
assert!(needs_renewal(&CertStatus::Valid {
expires_at: 0,
remaining_secs: 30,
total_secs: total,
}));
}
#[test]
fn needs_renewal_total_zero_uses_fixed_threshold() {
assert!(!needs_renewal(&CertStatus::Valid {
expires_at: 0,
remaining_secs: RENEWAL_THRESHOLD_SECS + 1,
total_secs: 0,
}));
assert!(needs_renewal(&CertStatus::Valid {
expires_at: 0,
remaining_secs: RENEWAL_THRESHOLD_SECS - 1,
total_secs: 0,
}));
}
#[test]
fn needs_renewal_total_one_uses_proportional_threshold() {
assert!(!needs_renewal(&CertStatus::Valid {
expires_at: 0,
remaining_secs: 1,
total_secs: 1,
}));
}
#[test]
fn needs_renewal_forever_cert_never_renews() {
assert!(!needs_renewal(&CertStatus::Valid {
expires_at: i64::MAX,
remaining_secs: i64::MAX,
total_secs: i64::MAX,
}));
}
#[test]
fn cert_error_backoff_is_shorter_than_normal_ttl() {
const _: () = assert!(CERT_ERROR_BACKOFF_SECS < CERT_STATUS_CACHE_TTL_SECS);
const _: () = assert!(CERT_ERROR_BACKOFF_SECS >= 5);
}
#[test]
fn needs_renewal_negative_remaining_is_expired() {
assert!(needs_renewal(&CertStatus::Valid {
expires_at: 0,
remaining_secs: -100,
total_secs: 3600,
}));
}
#[test]
fn needs_renewal_short_ttl_at_exact_threshold() {
let total = 200i64;
assert!(!needs_renewal(&CertStatus::Valid {
expires_at: 0,
remaining_secs: 100,
total_secs: total,
}));
assert!(needs_renewal(&CertStatus::Valid {
expires_at: 0,
remaining_secs: 99,
total_secs: total,
}));
}
#[test]
fn resolve_pubkey_from_identity_file() {
let path = resolve_pubkey_path("~/.ssh/id_rsa").unwrap();
let s = path.to_string_lossy();
assert!(s.ends_with("id_rsa.pub"), "got: {}", s);
assert!(!s.contains('~'), "tilde should be expanded: {}", s);
}
#[test]
fn resolve_pubkey_already_pub_no_double_suffix() {
let path = resolve_pubkey_path("~/.ssh/id_ed25519.pub").unwrap();
let s = path.to_string_lossy();
assert!(s.ends_with("id_ed25519.pub"), "got: {}", s);
assert!(!s.ends_with(".pub.pub"), "double .pub suffix: {}", s);
}
#[test]
fn resolve_pubkey_empty_falls_back() {
let path = resolve_pubkey_path("").unwrap();
let s = path.to_string_lossy();
assert!(s.ends_with("id_ed25519.pub"), "got: {}", s);
assert!(s.contains(".ssh/"), "should be in .ssh dir: {}", s);
}
#[test]
fn resolve_pubkey_absolute_path_inside_home() {
let home = dirs::home_dir().expect("home dir");
let abs = home.join(".ssh/deploy_key");
let path = resolve_pubkey_path(abs.to_str().unwrap()).unwrap();
let expected = home.join(".ssh/deploy_key.pub");
assert_eq!(path, expected);
}
#[test]
fn resolve_vault_role_host_override() {
let config = crate::providers::config::ProviderConfig::default();
let role = resolve_vault_role(Some("ssh/sign/admin"), Some("aws"), &config);
assert_eq!(role.as_deref(), Some("ssh/sign/admin"));
}
#[test]
fn is_valid_vault_addr_accepts_typical_urls() {
assert!(is_valid_vault_addr("http://127.0.0.1:8200"));
assert!(is_valid_vault_addr("https://vault.example.com:8200"));
assert!(is_valid_vault_addr("https://vault.internal/v1"));
}
#[test]
fn is_valid_vault_addr_rejects_empty_and_blank() {
assert!(!is_valid_vault_addr(""));
assert!(!is_valid_vault_addr(" "));
assert!(!is_valid_vault_addr("\t"));
}
#[test]
fn is_valid_vault_addr_rejects_whitespace_inside() {
assert!(!is_valid_vault_addr("http://host :8200"));
assert!(!is_valid_vault_addr("http://host\t:8200"));
}
#[test]
fn is_valid_vault_addr_rejects_control_chars() {
assert!(!is_valid_vault_addr("http://host\n8200"));
assert!(!is_valid_vault_addr("http://host\r8200"));
assert!(!is_valid_vault_addr("http://host\x00:8200"));
}
#[test]
fn is_valid_vault_addr_rejects_overlong() {
let long = "http://".to_string() + &"a".repeat(600);
assert!(!is_valid_vault_addr(&long));
}
#[test]
fn resolve_vault_addr_none_when_nothing_set() {
let config = crate::providers::config::ProviderConfig::default();
assert!(resolve_vault_addr(None, None, &config).is_none());
}
#[test]
fn resolve_vault_addr_uses_host_override() {
let config = crate::providers::config::ProviderConfig::default();
let addr = resolve_vault_addr(Some("http://127.0.0.1:8200"), Some("aws"), &config);
assert_eq!(addr.as_deref(), Some("http://127.0.0.1:8200"));
}
#[test]
fn resolve_vault_addr_falls_back_to_provider() {
let config = crate::providers::config::ProviderConfig::parse(
"[aws]\ntoken=abc\nvault_addr=https://vault.example:8200\n",
);
let addr = resolve_vault_addr(None, Some("aws"), &config);
assert_eq!(addr.as_deref(), Some("https://vault.example:8200"));
}
#[test]
fn resolve_vault_addr_host_beats_provider() {
let config = crate::providers::config::ProviderConfig::parse(
"[aws]\ntoken=abc\nvault_addr=https://provider:8200\n",
);
let addr = resolve_vault_addr(Some("http://host:8200"), Some("aws"), &config);
assert_eq!(addr.as_deref(), Some("http://host:8200"));
}
#[test]
fn resolve_vault_addr_empty_host_falls_through_to_provider() {
let config = crate::providers::config::ProviderConfig::parse(
"[aws]\ntoken=abc\nvault_addr=https://provider:8200\n",
);
let addr = resolve_vault_addr(Some(""), Some("aws"), &config);
assert_eq!(addr.as_deref(), Some("https://provider:8200"));
}
#[test]
fn resolve_vault_addr_whitespace_host_falls_through_to_provider() {
let config = crate::providers::config::ProviderConfig::parse(
"[aws]\ntoken=abc\nvault_addr=https://provider:8200\n",
);
let addr = resolve_vault_addr(Some(" "), Some("aws"), &config);
assert_eq!(addr.as_deref(), Some("https://provider:8200"));
}
#[test]
fn resolve_vault_addr_normalizes_bare_host_input() {
let config = crate::providers::config::ProviderConfig::default();
let addr = resolve_vault_addr(Some("192.168.1.100"), None, &config);
assert_eq!(addr.as_deref(), Some("https://192.168.1.100:8200"));
}
#[test]
fn resolve_vault_addr_normalizes_provider_bare_addr() {
let config = crate::providers::config::ProviderConfig::parse(
"[aws]\ntoken=abc\nvault_addr=vault.example\n",
);
let addr = resolve_vault_addr(None, Some("aws"), &config);
assert_eq!(addr.as_deref(), Some("https://vault.example:8200"));
}
#[test]
fn normalize_vault_addr_bare_ip() {
assert_eq!(
normalize_vault_addr("192.168.1.100"),
"https://192.168.1.100:8200"
);
}
#[test]
fn normalize_vault_addr_bare_hostname() {
assert_eq!(
normalize_vault_addr("vault.local"),
"https://vault.local:8200"
);
}
#[test]
fn normalize_vault_addr_ip_with_port() {
assert_eq!(
normalize_vault_addr("192.168.1.100:8200"),
"https://192.168.1.100:8200"
);
}
#[test]
fn normalize_vault_addr_ip_with_custom_port() {
assert_eq!(normalize_vault_addr("10.0.0.1:443"), "https://10.0.0.1:443");
}
#[test]
fn normalize_vault_addr_full_http_url() {
assert_eq!(
normalize_vault_addr("http://127.0.0.1:8200"),
"http://127.0.0.1:8200"
);
}
#[test]
fn normalize_vault_addr_full_https_url() {
assert_eq!(
normalize_vault_addr("https://vault.example.com:8200"),
"https://vault.example.com:8200"
);
}
#[test]
fn normalize_vault_addr_https_without_port() {
assert_eq!(
normalize_vault_addr("https://vault.example.com"),
"https://vault.example.com:443"
);
}
#[test]
fn normalize_vault_addr_trims_whitespace() {
assert_eq!(
normalize_vault_addr(" 10.0.0.1 "),
"https://10.0.0.1:8200"
);
}
#[test]
fn normalize_vault_addr_ipv6_bare() {
assert_eq!(normalize_vault_addr("[::1]"), "https://[::1]:8200");
}
#[test]
fn normalize_vault_addr_ipv6_with_port() {
assert_eq!(normalize_vault_addr("[::1]:8200"), "https://[::1]:8200");
}
#[test]
fn normalize_vault_addr_url_with_path_no_port() {
assert_eq!(
normalize_vault_addr("http://vault.host/v1"),
"http://vault.host:80/v1"
);
}
#[test]
fn normalize_vault_addr_trailing_slash() {
assert_eq!(
normalize_vault_addr("http://vault.host/"),
"http://vault.host:80/"
);
}
#[test]
fn normalize_vault_addr_uppercase_scheme() {
assert_eq!(
normalize_vault_addr("HTTP://vault.host"),
"HTTP://vault.host:80"
);
}
#[test]
fn normalize_vault_addr_unknown_scheme_passthrough() {
assert_eq!(normalize_vault_addr("ftp://vault.host"), "ftp://vault.host");
}
#[test]
fn normalize_vault_addr_ipv6_https_without_port() {
assert_eq!(normalize_vault_addr("https://[::1]"), "https://[::1]:443");
}
#[test]
fn normalize_vault_addr_https_custom_port() {
assert_eq!(
normalize_vault_addr("https://vault.host:9200"),
"https://vault.host:9200"
);
}
#[test]
fn resolve_vault_role_provider_fallback() {
let config = crate::providers::config::ProviderConfig::parse(
"[aws]\ntoken=abc\nvault_role=ssh/sign/engineer\n",
);
let role = resolve_vault_role(None, Some("aws"), &config);
assert_eq!(role.as_deref(), Some("ssh/sign/engineer"));
}
#[test]
fn resolve_vault_role_none_when_no_config() {
let config = crate::providers::config::ProviderConfig::default();
assert!(resolve_vault_role(None, None, &config).is_none());
}
#[test]
fn resolve_vault_role_none_when_provider_has_no_role() {
let config = crate::providers::config::ProviderConfig::parse("[aws]\ntoken=abc\n");
assert!(resolve_vault_role(None, Some("aws"), &config).is_none());
}
#[test]
fn resolve_vault_role_host_overrides_provider() {
let config = crate::providers::config::ProviderConfig::parse(
"[aws]\ntoken=abc\nvault_role=ssh/sign/default\n",
);
let role = resolve_vault_role(Some("ssh/sign/admin"), Some("aws"), &config);
assert_eq!(role.as_deref(), Some("ssh/sign/admin"));
}
#[test]
fn format_remaining_hours() {
assert_eq!(format_remaining(7200 + 900), "2h 15m");
}
#[test]
fn format_remaining_minutes_only() {
assert_eq!(format_remaining(300), "5m");
}
#[test]
fn format_remaining_expired() {
assert_eq!(format_remaining(0), "expired");
assert_eq!(format_remaining(-100), "expired");
}
#[test]
fn resolve_cert_path_uses_certificate_file_when_set() {
let path = resolve_cert_path("myhost", "~/.ssh/my-cert.pub").unwrap();
let s = path.to_string_lossy();
assert!(s.ends_with("my-cert.pub"), "got: {}", s);
assert!(!s.contains('~'), "tilde should be expanded: {}", s);
}
#[test]
fn resolve_cert_path_falls_back_to_default() {
let path = resolve_cert_path("myhost", "").unwrap();
assert!(
path.to_string_lossy()
.contains(".purple/certs/myhost-cert.pub"),
"got: {}",
path.display()
);
}
#[test]
fn resolve_cert_path_absolute() {
let path = resolve_cert_path("myhost", "/etc/ssh/certs/myhost.pub").unwrap();
assert_eq!(path, PathBuf::from("/etc/ssh/certs/myhost.pub"));
}
#[test]
fn cert_path_for_rejects_path_traversal() {
assert!(cert_path_for("../../tmp/evil").is_err());
assert!(cert_path_for("foo/bar").is_err());
assert!(cert_path_for("foo\\bar").is_err());
assert!(cert_path_for("host:22").is_err());
}
#[test]
fn cert_path_for_rejects_empty_alias() {
assert!(cert_path_for("").is_err());
}
#[test]
fn sign_certificate_rejects_role_starting_with_dash() {
let tmpdir = std::env::temp_dir();
let fake_key = tmpdir.join("purple_test_dash_role.pub");
std::fs::write(
&fake_key,
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI test@test\n",
)
.unwrap();
let result = sign_certificate("-format=json", &fake_key, "test", None);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Invalid Vault SSH role")
);
let _ = std::fs::remove_file(&fake_key);
}
#[test]
fn resolve_vault_role_empty_host_falls_through_to_provider() {
let config = crate::providers::config::ProviderConfig::parse(
"[aws]\ntoken=abc\nvault_role=ssh/sign/default\n",
);
let role = resolve_vault_role(Some(""), Some("aws"), &config);
assert_eq!(role.as_deref(), Some("ssh/sign/default"));
}
#[test]
fn ensure_cert_returns_error_without_vault() {
let tmpdir = std::env::temp_dir();
let fake_key = tmpdir.join("purple_test_ensure_cert_key.pub");
std::fs::write(
&fake_key,
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI test@test\n",
)
.unwrap();
let result = ensure_cert("ssh/sign/test", &fake_key, "ensure-test-host", "", None);
assert!(result.is_err());
let _ = std::fs::remove_file(&fake_key);
}
#[test]
fn parse_ssh_datetime_rejects_zero_month_and_day() {
assert!(parse_ssh_datetime("2026-00-08T12:00:00").is_none());
assert!(parse_ssh_datetime("2026-04-00T12:00:00").is_none());
}
#[test]
fn format_remaining_exactly_one_hour() {
assert_eq!(format_remaining(3600), "1h 0m");
}
#[test]
fn cert_path_rejects_nul_byte() {
assert!(cert_path_for("host\0name").is_err());
}
#[test]
fn is_valid_role_rejects_shell_metachars() {
for bad in [
"ssh/sign/role$x",
"ssh/sign/role;rm",
"ssh/sign/role|cat",
"ssh/sign/role`id`",
"ssh/sign/role&bg",
"ssh/sign/role x",
"ssh/sign/role\nx",
] {
assert!(!is_valid_role(bad), "should reject {:?}", bad);
}
}
#[test]
fn scrub_vault_stderr_redacts_all_marker_types() {
let raw = "error contacting server\n\
x-vault-token: abcdef\n\
Authorization: Bearer xyz\n\
Cookie: session=1\n\
SECRET=foo\n\
token expired perhaps\n\
harmless trailing line";
let out = scrub_vault_stderr(raw).to_ascii_lowercase();
assert!(!out.contains("token"));
assert!(!out.contains("x-vault-"));
assert!(!out.contains("authorization"));
assert!(!out.contains("cookie"));
assert!(!out.contains("secret"));
}
#[test]
fn scrub_vault_stderr_truncation_bound() {
let raw = "a".repeat(500);
let out = scrub_vault_stderr(&raw);
assert!(
out.chars().count() <= 203,
"len was {}",
out.chars().count()
);
assert!(out.ends_with("..."));
}
#[test]
fn scrub_vault_stderr_default_when_all_filtered() {
let raw = "token abc\nsecret def\nauthorization ghi";
let out = scrub_vault_stderr(raw);
assert_eq!(
out,
"Vault SSH signing failed. Check vault status and policy"
);
}
#[test]
fn is_valid_role_accepts_typical_paths() {
assert!(is_valid_role("ssh/sign/engineer"));
assert!(is_valid_role("ssh-ca/sign/admin_role"));
assert!(is_valid_role("a"));
assert!(is_valid_role(&"a".repeat(128)));
}
#[test]
fn is_valid_role_rejects_bad_input() {
assert!(!is_valid_role(""));
assert!(!is_valid_role("-format=json"));
assert!(!is_valid_role("ssh/sign/role with space"));
assert!(!is_valid_role("ssh/sign/role;rm"));
assert!(!is_valid_role("ssh/sign/rôle"));
assert!(!is_valid_role(&"a".repeat(129)));
}
#[test]
fn scrub_vault_stderr_drops_token_lines() {
let raw = "error occurred\nX-Vault-Token: abc123\nrole missing\n";
let out = scrub_vault_stderr(raw);
assert!(!out.to_lowercase().contains("token"));
assert!(out.contains("error occurred"));
assert!(out.contains("role missing"));
}
#[test]
fn scrub_vault_stderr_drops_secret_and_authorization() {
let raw = "line one\nsecret=foo\nAuthorization: Bearer x\nline four\n";
let out = scrub_vault_stderr(raw);
assert!(!out.to_lowercase().contains("secret"));
assert!(!out.to_lowercase().contains("authorization"));
assert!(out.contains("line one"));
assert!(out.contains("line four"));
}
#[test]
fn scrub_vault_stderr_empty_falls_back() {
let out = scrub_vault_stderr("");
assert!(out.contains("Vault SSH signing failed"));
}
#[test]
fn scrub_vault_stderr_only_filtered_falls_back() {
let out = scrub_vault_stderr("X-Vault-Token: abc\nSecret: xyz\n");
assert!(out.contains("Vault SSH signing failed"));
}
#[test]
fn scrub_vault_stderr_truncates_long_output() {
let raw = "x".repeat(500);
let out = scrub_vault_stderr(&raw);
assert!(out.ends_with("..."));
assert_eq!(out.chars().count(), 203);
}
#[test]
fn resolve_pubkey_rejects_path_outside_home() {
let path = resolve_pubkey_path("/etc/passwd").unwrap();
let s = path.to_string_lossy();
assert!(s.ends_with("id_ed25519.pub"), "got: {}", s);
assert!(s.contains(".ssh/"), "should be fallback: {}", s);
}
#[cfg(unix)]
fn unique_tmp_subdir(tag: &str) -> PathBuf {
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let dir = std::env::temp_dir().join(format!(
"purple_mock_vault_{}_{}_{}",
tag,
std::process::id(),
nanos
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[cfg(unix)]
fn with_mock_vault<F: FnOnce()>(tag: &str, stderr: &str, stdout: &str, exit_code: i32, f: F) {
use std::os::unix::fs::PermissionsExt;
let _guard = PATH_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let dir = unique_tmp_subdir(tag);
let script = dir.join("vault");
let escape = |s: &str| s.replace('\\', "\\\\").replace('"', "\\\"");
let body = format!(
"#!/bin/sh\nprintf '%s' \"{}\" >&2\nprintf '%s' \"{}\"\nexit {}\n",
escape(stderr),
escape(stdout),
exit_code
);
std::fs::write(&script, body).unwrap();
let mut perms = std::fs::metadata(&script).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&script, perms).unwrap();
let old_path = std::env::var("PATH").unwrap_or_default();
let new_path = format!("{}:{}", dir.display(), old_path);
unsafe { std::env::set_var("PATH", &new_path) };
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
unsafe { std::env::set_var("PATH", &old_path) };
let _ = std::fs::remove_dir_all(&dir);
if let Err(e) = result {
std::panic::resume_unwind(e);
}
}
#[cfg(unix)]
fn write_fake_pubkey(tag: &str) -> PathBuf {
let dir = unique_tmp_subdir(tag);
let p = dir.join("fake.pub");
std::fs::write(&p, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI test@test\n").unwrap();
p
}
#[cfg(unix)]
#[test]
fn sign_certificate_permission_denied_maps_to_friendly_error() {
let key = write_fake_pubkey("perm_denied");
let alias = "mock-perm-denied";
with_mock_vault(
"perm_denied",
"Error making API request.\npermission denied",
"",
1,
|| {
let result = sign_certificate("ssh/sign/role", &key, alias, None);
let err = result.unwrap_err().to_string();
assert!(err.contains("Vault SSH permission denied"), "got: {}", err);
},
);
let _ = std::fs::remove_file(&key);
}
#[cfg(unix)]
#[test]
fn sign_certificate_token_expired_maps_to_friendly_error() {
let key = write_fake_pubkey("tok_exp");
let alias = "mock-tok-exp";
with_mock_vault("tok_exp", "missing client token", "", 1, || {
let result = sign_certificate("ssh/sign/role", &key, alias, None);
let err = result.unwrap_err().to_string();
assert!(err.contains("token missing or expired"), "got: {}", err);
});
let _ = std::fs::remove_file(&key);
}
#[cfg(unix)]
#[test]
fn sign_certificate_scrubs_sensitive_stderr() {
let key = write_fake_pubkey("scrub");
let alias = "mock-scrub";
with_mock_vault(
"scrub",
"role not configured\nX-Vault-Token: hvs.ABCDEFG",
"",
1,
|| {
let result = sign_certificate("ssh/sign/role", &key, alias, None);
let err = result.unwrap_err().to_string();
assert!(!err.contains("hvs.ABCDEFG"), "leaked token: {}", err);
assert!(!err.contains("X-Vault-Token"), "leaked header: {}", err);
},
);
let _ = std::fs::remove_file(&key);
}
#[cfg(unix)]
#[test]
fn sign_certificate_empty_stdout_errors() {
let key = write_fake_pubkey("empty");
let alias = "mock-empty";
with_mock_vault("empty", "", "", 0, || {
let result = sign_certificate("ssh/sign/role", &key, alias, None);
let err = result.unwrap_err().to_string();
assert!(err.contains("empty certificate"), "got: {}", err);
});
let _ = std::fs::remove_file(&key);
}
#[cfg(unix)]
#[test]
fn sign_certificate_generic_failure_no_stderr() {
let key = write_fake_pubkey("generic");
let alias = "mock-generic";
with_mock_vault("generic", "", "", 1, || {
let result = sign_certificate("ssh/sign/role", &key, alias, None);
let err = result.unwrap_err().to_string();
assert!(err.contains("Vault SSH failed"), "got: {}", err);
});
let _ = std::fs::remove_file(&key);
}
#[cfg(unix)]
#[test]
fn sign_certificate_success_writes_cert() {
let key = write_fake_pubkey("success");
let alias = "mock-success-host";
let expected_cert = "ssh-ed25519-cert-v01@openssh.com AAAAFAKECERT test";
with_mock_vault("success", "", expected_cert, 0, || {
let result = sign_certificate("ssh/sign/role", &key, alias, None).unwrap();
assert!(result.cert_path.exists());
let content = std::fs::read_to_string(&result.cert_path).unwrap();
assert_eq!(content, expected_cert);
let _ = std::fs::remove_file(&result.cert_path);
});
let _ = std::fs::remove_file(&key);
}
#[cfg(unix)]
fn with_env_capturing_vault<F: FnOnce(&Path)>(tag: &str, f: F) {
use std::os::unix::fs::PermissionsExt;
let _guard = PATH_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let dir = unique_tmp_subdir(tag);
let capture = dir.join("captured_addr.txt");
let script = dir.join("vault");
let body = format!(
"#!/bin/sh\nprintf '%s' \"${{VAULT_ADDR-}}\" > {}\nprintf '%s' 'ssh-ed25519-cert-v01@openssh.com AAAAMOCKCERT mock'\nexit 0\n",
capture.display()
);
std::fs::write(&script, body).unwrap();
let mut perms = std::fs::metadata(&script).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&script, perms).unwrap();
let old_path = std::env::var("PATH").unwrap_or_default();
let old_vault_addr = std::env::var("VAULT_ADDR").ok();
let new_path = format!("{}:{}", dir.display(), old_path);
unsafe {
std::env::set_var("PATH", &new_path);
std::env::remove_var("VAULT_ADDR");
}
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(&capture)));
unsafe {
std::env::set_var("PATH", &old_path);
match old_vault_addr {
Some(v) => std::env::set_var("VAULT_ADDR", v),
None => std::env::remove_var("VAULT_ADDR"),
}
}
let _ = std::fs::remove_dir_all(&dir);
if let Err(e) = result {
std::panic::resume_unwind(e);
}
}
#[cfg(unix)]
#[test]
fn sign_certificate_sets_vault_addr_env_on_subprocess() {
let key = write_fake_pubkey("addr_set");
let alias = "mock-addr-set";
with_env_capturing_vault("addr_set", |capture| {
let res = sign_certificate(
"ssh/sign/role",
&key,
alias,
Some("http://override.example:8200"),
);
assert!(res.is_ok(), "sign failed: {:?}", res);
let captured = std::fs::read_to_string(capture).unwrap();
assert_eq!(
captured, "http://override.example:8200",
"subprocess did not receive the overridden VAULT_ADDR"
);
if let Ok(r) = res {
let _ = std::fs::remove_file(&r.cert_path);
}
});
let _ = std::fs::remove_file(&key);
}
#[cfg(unix)]
#[test]
fn sign_certificate_does_not_set_vault_addr_when_none() {
let key = write_fake_pubkey("addr_none");
let alias = "mock-addr-none";
with_env_capturing_vault("addr_none", |capture| {
let res = sign_certificate("ssh/sign/role", &key, alias, None);
assert!(res.is_ok(), "sign failed: {:?}", res);
let captured = std::fs::read_to_string(capture).unwrap();
assert!(
captured.is_empty(),
"subprocess saw unexpected VAULT_ADDR: {:?}",
captured
);
if let Ok(r) = res {
let _ = std::fs::remove_file(&r.cert_path);
}
});
let _ = std::fs::remove_file(&key);
}
#[cfg(unix)]
#[test]
fn sign_certificate_rejects_invalid_vault_addr() {
let key = write_fake_pubkey("addr_bad");
let alias = "mock-addr-bad";
let res = sign_certificate("ssh/sign/role", &key, alias, Some("http://has space:8200"));
assert!(res.is_err());
let msg = res.unwrap_err().to_string();
assert!(
msg.contains("Invalid VAULT_ADDR"),
"expected explicit rejection, got: {}",
msg
);
let _ = std::fs::remove_file(&key);
}
#[cfg(unix)]
#[test]
fn check_cert_validity_handles_forever() {
use std::os::unix::fs::PermissionsExt;
let _guard = PATH_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let dir = unique_tmp_subdir("forever");
let script = dir.join("ssh-keygen");
let body = "#!/bin/sh\nprintf '%s\\n' ' Type: ssh-ed25519-cert-v01@openssh.com'\nprintf '%s\\n' ' Valid: forever'\nexit 0\n";
std::fs::write(&script, body).unwrap();
let mut perms = std::fs::metadata(&script).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&script, perms).unwrap();
let cert = dir.join("cert.pub");
std::fs::write(&cert, "stub").unwrap();
let old_path = std::env::var("PATH").unwrap_or_default();
let new_path = format!("{}:{}", dir.display(), old_path);
unsafe { std::env::set_var("PATH", &new_path) };
let status = check_cert_validity(&cert);
unsafe { std::env::set_var("PATH", &old_path) };
let _ = std::fs::remove_dir_all(&dir);
match status {
CertStatus::Valid {
remaining_secs,
total_secs,
expires_at,
} => {
assert_eq!(remaining_secs, i64::MAX);
assert_eq!(total_secs, i64::MAX);
assert_eq!(expires_at, i64::MAX);
}
other => panic!("expected Valid(forever), got {:?}", other),
}
}
#[cfg(unix)]
#[test]
fn check_cert_validity_rejects_non_positive_window() {
use std::os::unix::fs::PermissionsExt;
let _guard = PATH_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let dir = unique_tmp_subdir("non_positive");
let script = dir.join("ssh-keygen");
let body = "#!/bin/sh\nprintf '%s\\n' ' Type: ssh-ed25519-cert-v01@openssh.com'\nprintf '%s\\n' ' Valid: from 2026-01-01T00:00:00 to 2026-01-01T00:00:00'\nexit 0\n";
std::fs::write(&script, body).unwrap();
let mut perms = std::fs::metadata(&script).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&script, perms).unwrap();
let cert = dir.join("cert.pub");
std::fs::write(&cert, "stub").unwrap();
let old_path = std::env::var("PATH").unwrap_or_default();
let new_path = format!("{}:{}", dir.display(), old_path);
unsafe { std::env::set_var("PATH", &new_path) };
let status = check_cert_validity(&cert);
unsafe { std::env::set_var("PATH", &old_path) };
let _ = std::fs::remove_dir_all(&dir);
match status {
CertStatus::Invalid(msg) => {
assert!(
msg.contains("non-positive"),
"expected non-positive window error, got: {}",
msg
);
}
other => panic!("expected Invalid, got {:?}", other),
}
}
#[test]
fn is_valid_role_rejects_spaces_and_shell_metacharacters() {
assert!(!is_valid_role(""));
assert!(!is_valid_role("bad role"));
assert!(!is_valid_role("role;rm"));
assert!(!is_valid_role("role$(x)"));
assert!(!is_valid_role("role|cat"));
assert!(!is_valid_role("role`id`"));
assert!(!is_valid_role("role&bg"));
assert!(!is_valid_role("role\nx"));
assert!(is_valid_role("ssh/engineer"));
}
#[test]
fn resolve_vault_role_host_overrides_provider_default() {
let config = crate::providers::config::ProviderConfig::parse(
"[aws]\ntoken=abc\nvault_role=ssh/sign/default\n",
);
let role = resolve_vault_role(Some("ssh/sign/override"), Some("aws"), &config);
assert_eq!(role.as_deref(), Some("ssh/sign/override"));
}
#[test]
fn resolve_vault_role_falls_back_to_provider_when_host_empty() {
let config = crate::providers::config::ProviderConfig::parse(
"[aws]\ntoken=abc\nvault_role=ssh/sign/default\n",
);
let role = resolve_vault_role(None, Some("aws"), &config);
assert_eq!(role.as_deref(), Some("ssh/sign/default"));
}
#[test]
fn resolve_vault_role_returns_none_when_neither_set() {
let config = crate::providers::config::ProviderConfig::default();
assert!(resolve_vault_role(None, Some("aws"), &config).is_none());
assert!(resolve_vault_role(None, None, &config).is_none());
}
#[cfg(unix)]
#[test]
fn check_cert_validity_invalid_file() {
use std::os::unix::fs::PermissionsExt;
let _guard = PATH_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let dir = unique_tmp_subdir("invalid_file");
let script = dir.join("ssh-keygen");
let body = "#!/bin/sh\necho 'is not a certificate' >&2\nexit 1\n";
std::fs::write(&script, body).unwrap();
let mut perms = std::fs::metadata(&script).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&script, perms).unwrap();
let cert = dir.join("cert.pub");
std::fs::write(&cert, "this is not a certificate\n").unwrap();
let old_path = std::env::var("PATH").unwrap_or_default();
let new_path = format!("{}:{}", dir.display(), old_path);
unsafe { std::env::set_var("PATH", &new_path) };
let status = check_cert_validity(&cert);
unsafe { std::env::set_var("PATH", &old_path) };
let _ = std::fs::remove_dir_all(&dir);
assert!(
matches!(status, CertStatus::Invalid(_)),
"Expected Invalid, got: {:?}",
status
);
}