use ssh_key::{Certificate, PrivateKey, PublicKey, certificate::CertType};
use uselesskey_core::Factory;
use uselesskey_ssh::{
SshCertFactoryExt, SshCertSpec, SshCertType, SshFactoryExt, SshSpec, SshValidity,
};
use uselesskey_test_support::{TestResult, ensure, ensure_eq, require_ok};
#[test]
fn round_trip_parse_openssh_keys() {
let fx = Factory::deterministic_from_str("ssh-roundtrip-seed");
for spec in [SshSpec::ed25519(), SshSpec::rsa()] {
let key = fx.ssh_key("deploy", spec);
let parsed_private = PrivateKey::from_openssh(key.private_key_openssh())
.expect("private key fixture must parse");
let parsed_public = PublicKey::from_openssh(key.authorized_key_line())
.expect("authorized_keys line must parse");
assert_eq!(
parsed_private
.public_key()
.to_openssh()
.expect("public key encoding must succeed"),
parsed_public
.to_openssh()
.expect("public key encoding must succeed")
);
}
}
#[test]
fn authorized_keys_lines_are_deterministic() {
let fx_a = Factory::deterministic_from_str("ssh-authz-seed");
let fx_b = Factory::deterministic_from_str("ssh-authz-seed");
let a = fx_a.ssh_key("host-a", SshSpec::ed25519());
let b = fx_b.ssh_key("host-a", SshSpec::ed25519());
let c = fx_a.ssh_key("host-b", SshSpec::ed25519());
assert_eq!(a.authorized_key_line(), b.authorized_key_line());
assert_ne!(a.authorized_key_line(), c.authorized_key_line());
}
#[test]
fn cert_principals_and_validity_match_spec() {
let fx = Factory::deterministic_from_str("ssh-cert-seed");
let spec = SshCertSpec {
principals: vec!["deploy".to_string(), "ci".to_string()],
validity: SshValidity::new(1_700_000_000, 1_800_000_000),
cert_type: SshCertType::User,
critical_options: vec![("force-command".to_string(), "/usr/bin/deploy".to_string())],
extensions: vec![("permit-pty".to_string(), "".to_string())],
};
let cert_fx = fx.ssh_cert("deploy-cert", spec.clone());
let parsed = Certificate::from_openssh(cert_fx.certificate_openssh())
.expect("certificate fixture must parse");
assert_eq!(parsed.valid_principals(), spec.principals.as_slice());
assert_eq!(parsed.valid_after(), spec.validity.valid_after);
assert_eq!(parsed.valid_before(), spec.validity.valid_before);
assert_eq!(parsed.cert_type(), CertType::User);
let force_command = parsed
.critical_options()
.get("force-command")
.map(String::as_str);
assert_eq!(force_command, Some("/usr/bin/deploy"));
let permit_pty = parsed.extensions().get("permit-pty").map(String::as_str);
assert_eq!(permit_pty, Some(""));
}
#[test]
fn host_cert_decodes_with_host_cert_type() -> TestResult<()> {
let fx = Factory::deterministic_from_str("ssh-host-cert-seed");
let spec = SshCertSpec::host(
["host1.internal", "host2.internal"],
SshValidity::new(1_700_000_000, 1_700_001_000),
);
let cert_fx = fx.ssh_cert("host-cert", spec.clone());
let parsed = require_ok(
Certificate::from_openssh(cert_fx.certificate_openssh()),
"host certificate fixture must parse",
)?;
ensure_eq!(parsed.cert_type(), CertType::Host);
ensure_eq!(
parsed.valid_principals(),
&["host1.internal".to_string(), "host2.internal".to_string()][..]
);
ensure_eq!(parsed.valid_after(), spec.validity.valid_after);
ensure_eq!(parsed.valid_before(), spec.validity.valid_before);
Ok(())
}
#[test]
fn empty_principals_yields_all_principals_valid() -> TestResult<()> {
let fx = Factory::deterministic_from_str("ssh-empty-principals-seed");
let spec = SshCertSpec {
principals: Vec::new(),
validity: SshValidity::new(1_700_000_000, 1_700_000_300),
cert_type: SshCertType::User,
critical_options: Vec::new(),
extensions: Vec::new(),
};
let cert_fx = fx.ssh_cert("anyone", spec);
let parsed = require_ok(
Certificate::from_openssh(cert_fx.certificate_openssh()),
"empty-principals certificate fixture must parse",
)?;
ensure!(
parsed.valid_principals().is_empty(),
"an empty principals list must encode 'all principals valid', got {:?}",
parsed.valid_principals()
);
Ok(())
}
#[test]
fn key_pair_accessors_report_label_and_spec() -> TestResult<()> {
let fx = Factory::deterministic_from_str("ssh-key-accessors-seed");
let key = fx.ssh_key("my-deploy", SshSpec::ed25519());
ensure_eq!(key.label(), "my-deploy");
ensure_eq!(key.spec(), SshSpec::Ed25519);
let rsa_key = fx.ssh_key("my-rsa", SshSpec::rsa());
ensure_eq!(rsa_key.label(), "my-rsa");
ensure_eq!(rsa_key.spec(), SshSpec::Rsa);
Ok(())
}
#[test]
fn cert_fixture_accessors_report_label_and_spec() -> TestResult<()> {
let fx = Factory::deterministic_from_str("ssh-cert-accessors-seed");
let spec = SshCertSpec::user(["alice"], SshValidity::new(1_700_000_000, 1_700_000_600));
let cert_fx = fx.ssh_cert("alice-cert", spec.clone());
ensure_eq!(cert_fx.label(), "alice-cert");
ensure_eq!(cert_fx.spec(), &spec);
Ok(())
}
#[test]
fn ssh_spec_default_is_ed25519() -> TestResult<()> {
ensure_eq!(SshSpec::default(), SshSpec::Ed25519);
ensure_eq!(SshCertType::default(), SshCertType::User);
Ok(())
}
#[test]
fn ssh_cert_type_stable_byte_distinguishes_variants() -> TestResult<()> {
ensure!(SshCertType::User.stable_byte() != SshCertType::Host.stable_byte());
Ok(())
}
#[test]
fn key_pair_debug_omits_key_material() -> TestResult<()> {
let fx = Factory::deterministic_from_str("ssh-debug-seed");
let key = fx.ssh_key("debug-host", SshSpec::ed25519());
let dbg = format!("{key:?}");
ensure!(dbg.contains("SshKeyPair"));
ensure!(dbg.contains("debug-host"));
ensure!(
!dbg.contains("BEGIN OPENSSH PRIVATE KEY"),
"Debug output must not leak private key material: {dbg}"
);
ensure!(
!dbg.contains("ssh-ed25519 "),
"Debug output must not leak the public key body: {dbg}"
);
Ok(())
}
#[test]
fn cert_fixture_debug_omits_key_material() -> TestResult<()> {
let fx = Factory::deterministic_from_str("ssh-cert-debug-seed");
let cert = fx.ssh_cert(
"debug-cert",
SshCertSpec::user(["alice"], SshValidity::new(1, 2)),
);
let dbg = format!("{cert:?}");
ensure!(dbg.contains("SshCertFixture"));
ensure!(dbg.contains("debug-cert"));
ensure!(
!dbg.contains("BEGIN OPENSSH PRIVATE KEY"),
"Debug output must not leak private key material: {dbg}"
);
ensure!(
!dbg.contains("ssh-ed25519-cert-v01"),
"Debug output must not leak the certificate body: {dbg}"
);
Ok(())
}
#[test]
fn cert_spec_stable_bytes_change_with_critical_options_and_extensions() -> TestResult<()> {
let base = SshCertSpec::user(["alice"], SshValidity::new(1, 2));
let with_option = SshCertSpec {
critical_options: vec![("force-command".to_string(), "/bin/echo".to_string())],
..base.clone()
};
let with_extension = SshCertSpec {
extensions: vec![("permit-pty".to_string(), String::new())],
..base.clone()
};
ensure!(base.stable_bytes() != with_option.stable_bytes());
ensure!(base.stable_bytes() != with_extension.stable_bytes());
ensure!(with_option.stable_bytes() != with_extension.stable_bytes());
Ok(())
}
#[test]
fn cert_spec_stable_bytes_change_with_validity_and_cert_type() -> TestResult<()> {
let user = SshCertSpec::user(["alice"], SshValidity::new(1, 2));
let later = SshCertSpec::user(["alice"], SshValidity::new(10, 20));
let host = SshCertSpec::host(["alice"], SshValidity::new(1, 2));
ensure!(user.stable_bytes() != later.stable_bytes());
ensure!(user.stable_bytes() != host.stable_bytes());
Ok(())
}