use super::*;
use crate::key::base64;
fn load_cert_blob(name: &str) -> Vec<u8> {
let path = concat_fixture(name);
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {path}: {e}"));
let b64 = text
.split_whitespace()
.nth(1)
.unwrap_or_else(|| panic!("fixture {path} has no base64 field"));
base64::decode(b64.as_bytes()).expect("decode cert base64")
}
fn load_pub_blob(name: &str) -> Vec<u8> {
let path = concat_fixture(name);
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {path}: {e}"));
let b64 = text.split_whitespace().nth(1).expect("base64 field");
base64::decode(b64.as_bytes()).expect("decode pub base64")
}
fn concat_fixture(name: &str) -> String {
format!(
"{}/tests/fixtures/cert/{}",
env!("CARGO_MANIFEST_DIR"),
name
)
}
const CA_ALGOS: &[&str] = &[
"ssh-ed25519",
"ecdsa-sha2-nistp256",
"ecdsa-sha2-nistp384",
"ecdsa-sha2-nistp521",
"rsa-sha2-512",
"rsa-sha2-256",
];
const NOW: u64 = 1_700_000_000;
#[test]
fn parse_ed25519_user_cert() {
let blob = load_cert_blob("u_ed25519-cert.pub");
let cert = Certificate::parse(&blob).unwrap();
assert_eq!(cert.key_type, "ssh-ed25519-cert-v01@openssh.com");
assert_eq!(cert.embedded_algorithm(), "ssh-ed25519");
assert_eq!(cert.cert_type, CertType::User);
assert_eq!(cert.serial, 1);
assert_eq!(cert.key_id, "alice");
assert_eq!(cert.valid_principals, vec!["alice", "bob"]);
assert_eq!(cert.embedded_pubkey_blob(), load_pub_blob("u_ed25519.pub"));
}
#[test]
fn parse_ecdsa_user_cert() {
for (file, algo) in [
("u_ecdsa-cert.pub", "ecdsa-sha2-nistp256"),
("u_ecdsa384-cert.pub", "ecdsa-sha2-nistp384"),
("u_ecdsa521-cert.pub", "ecdsa-sha2-nistp521"),
] {
let blob = load_cert_blob(file);
let cert = Certificate::parse(&blob).unwrap();
assert_eq!(cert.embedded_algorithm(), algo, "{file}");
assert_eq!(cert.cert_type, CertType::User);
cert.verify_ca_signature(CA_ALGOS).unwrap();
}
}
#[test]
fn parse_rsa_user_cert() {
let blob = load_cert_blob("u_rsa-cert.pub");
let cert = Certificate::parse(&blob).unwrap();
assert_eq!(cert.key_type, "ssh-rsa-cert-v01@openssh.com");
assert_eq!(cert.embedded_algorithm(), "ssh-rsa");
assert_eq!(cert.ca_algorithm().unwrap(), "rsa-sha2-512");
assert_eq!(cert.cert_type, CertType::User);
assert_eq!(cert.embedded_pubkey_blob(), load_pub_blob("u_rsa.pub"));
cert.verify_ca_signature(CA_ALGOS).unwrap();
}
#[test]
fn parse_host_cert() {
let blob = load_cert_blob("h_ed25519-cert.pub");
let cert = Certificate::parse(&blob).unwrap();
assert_eq!(cert.cert_type, CertType::Host);
assert_eq!(
cert.valid_principals,
vec!["host.example.com", "host2.example.com"]
);
cert.verify_ca_signature(CA_ALGOS).unwrap();
cert.check_type(CertType::Host).unwrap();
cert.check_principal("host.example.com").unwrap();
assert!(cert.check_principal("evil.example.com").is_err());
}
#[test]
fn good_ca_signature_verifies() {
let blob = load_cert_blob("u_ed25519-cert.pub");
let cert = Certificate::parse(&blob).unwrap();
cert.verify_ca_signature(CA_ALGOS).unwrap();
}
#[test]
fn tampered_cert_fails_ca_signature() {
let mut blob = load_cert_blob("u_ed25519-cert.pub");
let cert = Certificate::parse(&blob).unwrap();
let idx = cert.signed_len / 2;
blob[idx] ^= 0xff;
if let Ok(c2) = Certificate::parse(&blob) {
assert!(matches!(
c2.verify_ca_signature(CA_ALGOS),
Err(Error::CertBadCaSignature)
));
}
}
#[test]
fn wrong_ca_algo_set_rejected() {
let blob = load_cert_blob("u_ed25519-cert.pub");
let cert = Certificate::parse(&blob).unwrap();
assert!(matches!(
cert.verify_ca_signature(&["rsa-sha2-512"]),
Err(Error::CertBadCaSignature)
));
assert!(matches!(
cert.verify_ca_signature(&[]),
Err(Error::CertBadCaSignature)
));
}
#[test]
fn wrong_ca_key_rejected() {
let blob = load_cert_blob("u_ecdsa-cert.pub");
let cert = Certificate::parse(&blob).unwrap();
let mut raw = cert.raw.clone();
raw[cert.signed_len - 1] ^= 0xff;
if let Ok(c2) = Certificate::parse(&raw) {
assert!(c2.verify_ca_signature(CA_ALGOS).is_err());
}
}
#[test]
fn expired_cert_rejected() {
let blob = load_cert_blob("u_ed25519_expired-cert.pub");
let cert = Certificate::parse(&blob).unwrap();
cert.verify_ca_signature(CA_ALGOS).unwrap(); assert!(matches!(cert.check_validity(NOW), Err(Error::CertExpired)));
}
#[test]
fn not_yet_valid_cert_rejected() {
let blob = load_cert_blob("u_ed25519_notyet-cert.pub");
let cert = Certificate::parse(&blob).unwrap();
assert!(matches!(
cert.check_validity(NOW),
Err(Error::CertNotYetValid)
));
}
#[test]
fn principal_mismatch_rejected() {
let blob = load_cert_blob("u_ecdsa-cert.pub"); let cert = Certificate::parse(&blob).unwrap();
cert.check_principal("bob").unwrap();
assert!(matches!(
cert.check_principal("alice"),
Err(Error::CertPrincipalMismatch)
));
}
#[test]
fn host_principal_match_is_case_insensitive() {
let mut cert = Certificate::parse(&load_cert_blob("h_ed25519-cert.pub")).unwrap();
assert_eq!(cert.cert_type, CertType::Host);
assert_eq!(
cert.valid_principals,
vec!["host.example.com", "host2.example.com"]
);
cert.check_principal("Host.Example.Com").unwrap();
cert.check_principal("HOST2.EXAMPLE.COM").unwrap();
assert!(matches!(
cert.check_principal("evil.example.com"),
Err(Error::CertPrincipalMismatch)
));
cert.cert_type = CertType::User;
assert!(matches!(
cert.check_principal("Host.Example.Com"),
Err(Error::CertPrincipalMismatch)
));
cert.check_principal("host.example.com").unwrap();
}
#[test]
fn user_principal_match_is_case_sensitive() {
let cert = Certificate::parse(&load_cert_blob("u_ed25519-cert.pub")).unwrap();
assert_eq!(cert.cert_type, CertType::User);
assert_eq!(cert.valid_principals, vec!["alice", "bob"]);
cert.check_principal("alice").unwrap();
assert!(matches!(
cert.check_principal("Alice"),
Err(Error::CertPrincipalMismatch)
));
assert!(matches!(
cert.check_principal("BOB"),
Err(Error::CertPrincipalMismatch)
));
}
#[test]
fn type_mismatch_rejected() {
let user = Certificate::parse(&load_cert_blob("u_ed25519-cert.pub")).unwrap();
assert!(matches!(
user.check_type(CertType::Host),
Err(Error::CertTypeMismatch)
));
let host = Certificate::parse(&load_cert_blob("h_ed25519-cert.pub")).unwrap();
assert!(matches!(
host.check_type(CertType::User),
Err(Error::CertTypeMismatch)
));
}
#[test]
fn critical_options_parsed_and_known() {
let blob = load_cert_blob("u_ed25519_crit-cert.pub");
let cert = Certificate::parse(&blob).unwrap();
assert!(cert.unknown_critical_options().is_empty());
cert.require_known_critical_options().unwrap();
assert_eq!(
cert.critical_option("force-command").unwrap(),
encode_str(b"/usr/bin/uptime").as_slice()
);
}
#[test]
fn unknown_critical_option_rejected() {
let mut cert = Certificate::parse(&load_cert_blob("u_ed25519-cert.pub")).unwrap();
cert.critical_options
.push(("totally-unknown-option".to_string(), Vec::new()));
assert_eq!(
cert.unknown_critical_options(),
vec!["totally-unknown-option"]
);
assert!(matches!(
cert.require_known_critical_options(),
Err(Error::CertUnknownCriticalOption)
));
}
#[test]
fn oversized_blob_rejected() {
let big = vec![0u8; MAX_CERT_BLOB + 1];
assert!(Certificate::parse(&big).is_err());
}
#[test]
fn trailing_data_rejected() {
let mut blob = load_cert_blob("u_ed25519-cert.pub");
blob.push(0x00); assert!(matches!(Certificate::parse(&blob), Err(Error::Format(_))));
}
#[test]
fn truncated_blob_rejected() {
let blob = load_cert_blob("u_ed25519-cert.pub");
assert!(Certificate::parse(&blob[..blob.len() / 2]).is_err());
}
#[test]
fn embedded_verifier_matches_underlying_key() {
use crate::key::PrivateKey;
for (cert_file, priv_file) in [
("u_ed25519-cert.pub", "u_ed25519"),
("u_ecdsa-cert.pub", "u_ecdsa"),
("u_rsa-cert.pub", "u_rsa"),
] {
let cert = Certificate::parse(&load_cert_blob(cert_file)).unwrap();
let pem = std::fs::read_to_string(concat_fixture(priv_file)).unwrap();
let sk = PrivateKey::parse_openssh_pem(&pem, None).unwrap();
let signer = sk.into_host_key().unwrap();
let msg = b"exchange-hash stand-in";
let sig = signer.sign(msg).unwrap();
let verifier = cert.embedded_verifier(&sig).unwrap();
verifier.verify(msg, &sig).unwrap();
assert!(verifier.verify(b"different", &sig).is_err());
}
}
fn build_rsa_cert_blob(n_bits: usize) -> Vec<u8> {
build_rsa_cert_blob_with_e(n_bits, &[0x01u8, 0x00, 0x01]) }
fn build_rsa_cert_blob_with_e(n_bits: usize, e_mpint: &[u8]) -> Vec<u8> {
assert_eq!(n_bits % 8, 0);
let n_len = n_bits / 8;
let mut n_mag = alloc::vec![0xa5u8; n_len];
n_mag[0] = 0x80;
let mut n_mpint = alloc::vec![0x00u8];
n_mpint.extend_from_slice(&n_mag);
let mut w = Writer::new();
w.write_string(b"ssh-rsa-cert-v01@openssh.com");
w.write_string(&[0u8; 16]); w.write_string(e_mpint); w.write_string(&n_mpint); w.write_u64(1); w.write_u32(1); w.write_string(b"weak-rsa"); w.write_string(b""); w.write_u64(0); w.write_u64(u64::MAX); w.write_string(b""); w.write_string(b""); w.write_string(b""); w.write_string(b"\x00\x00\x00\x07ssh-rsa"); w.write_string(b"\x00\x00\x00\x07ssh-rsasig"); w.into_vec()
}
#[test]
fn rsa_cert_with_weak_modulus_rejected_at_parse() {
let blob = build_rsa_cert_blob(1024);
match Certificate::parse(&blob) {
Err(Error::Format(msg)) => {
assert!(
msg.contains("2048"),
"expected 2048-bit floor error, got {msg:?}"
)
}
Err(other) => panic!("expected Format(2048), got {other:?}"),
Ok(_) => panic!("expected 1024-bit embedded RSA key to be rejected at parse"),
}
}
#[test]
fn rsa_cert_with_2048_modulus_parses() {
let blob = build_rsa_cert_blob(2048);
let cert = Certificate::parse(&blob).expect("2048-bit embedded RSA must parse");
assert_eq!(cert.embedded_algorithm(), "ssh-rsa");
}
#[test]
fn rsa_cert_with_oversized_exponent_rejected_at_parse() {
let mut e_mpint = alloc::vec![0xffu8; 256];
e_mpint[0] = 0x7f;
let blob = build_rsa_cert_blob_with_e(2048, &e_mpint);
match Certificate::parse(&blob) {
Err(Error::Format(msg)) => assert!(msg.contains("exponent"), "got {msg:?}"),
Err(other) => panic!("expected Format(exponent), got {other:?}"),
Ok(_) => panic!("expected oversized embedded exponent to be rejected at parse"),
}
}
#[test]
fn rsa_ca_key_with_oversized_exponent_rejected_at_verify() {
let mut n_mag = alloc::vec![0xa5u8; 256];
n_mag[0] = 0x80;
let mut e_mag = alloc::vec![0xffu8; 256];
e_mag[0] = 0x7f;
let mut ca = Writer::new();
ca.write_string(b"ssh-rsa");
crate::format::write_mpint(&mut ca, &e_mag);
crate::format::write_mpint(&mut ca, &n_mag);
let ca_blob = ca.into_vec();
let mut cert = Certificate::parse(&load_cert_blob("u_ed25519-cert.pub")).unwrap();
cert.signature_key_blob = ca_blob;
let mut sig = Writer::new();
sig.write_string(b"rsa-sha2-512");
sig.write_string(&[0u8; 256]);
cert.signature = sig.into_vec();
assert!(matches!(
cert.verify_ca_signature(CA_ALGOS),
Err(Error::CertBadCaSignature)
));
}
#[test]
fn malformed_critical_option_payload_rejected() {
let base = Certificate::parse(&load_cert_blob("u_ed25519-cert.pub")).unwrap();
for bad in [
alloc::vec![0xffu8, 0xff, 0xff, 0xff], alloc::vec![0x00u8, 0x00, 0x00, 0x02, b'a'], {
let mut v = encode_str(b"/bin/true");
v.push(0x00); v
},
alloc::vec![0x00u8, 0x00, 0x00, 0x01, 0xff], Vec::new(), ] {
let mut cert = base.clone();
cert.critical_options = alloc::vec![("force-command".to_string(), bad.clone())];
assert!(
cert.require_known_critical_options().is_err(),
"force-command payload {bad:?} must be rejected"
);
let mut cert = base.clone();
cert.critical_options = alloc::vec![("source-address".to_string(), bad.clone())];
assert!(
cert.require_known_critical_options().is_err(),
"source-address payload {bad:?} must be rejected"
);
}
let mut cert = base.clone();
cert.critical_options = alloc::vec![
("force-command".to_string(), encode_str(b"/usr/bin/uptime")),
("source-address".to_string(), encode_str(b"10.0.0.0/8")),
];
cert.require_known_critical_options().unwrap();
}
fn encode_str(s: &[u8]) -> Vec<u8> {
let mut w = Writer::new();
w.write_string(s);
w.into_vec()
}
#[test]
fn openssh_rsa_cert_line_parses() {
let text = std::fs::read_to_string(concat_fixture("u_rsa-cert.pub")).unwrap();
assert!(text.starts_with("ssh-rsa-cert-v01@openssh.com "));
let cert = Certificate::parse_openssh_line(&text).unwrap();
assert_eq!(cert.key_type, "ssh-rsa-cert-v01@openssh.com");
let renamed = text.replacen(
"ssh-rsa-cert-v01@openssh.com",
"rsa-sha2-512-cert-v01@openssh.com",
1,
);
Certificate::parse_openssh_line(&renamed).unwrap();
}
#[test]
fn builder_reproduces_openssh_cert() {
for name in [
"u_ed25519-cert.pub",
"u_ed25519_crit-cert.pub",
"h_ed25519-cert.pub",
"u_rsa-cert.pub",
"u_ecdsa-cert.pub",
] {
let orig = Certificate::parse(&load_cert_blob(name)).unwrap();
let mut b = CertificateBuilder::new(
orig.embedded_pubkey_blob.clone(),
orig.cert_type,
orig.nonce.clone(),
);
b.serial = orig.serial;
b.key_id = orig.key_id.clone();
b.valid_principals = orig.valid_principals.clone();
b.valid_after = orig.valid_after;
b.valid_before = orig.valid_before;
b.critical_options = orig.critical_options.clone();
b.extensions = orig.extensions.iter().rev().cloned().collect();
let rebuilt = b
.sign(&orig.signature_key_blob, |data| {
assert_eq!(data, &orig.raw[..orig.signed_len]);
Ok(orig.signature.clone())
})
.unwrap();
assert_eq!(rebuilt.raw, orig.raw, "{name}");
}
}
#[test]
fn builder_signs_verifiable_cert() {
use crate::hostkey::{Ed25519HostKey, HostKey};
let ca = Ed25519HostKey::from_seed([7u8; 32]);
let user = Ed25519HostKey::from_seed([8u8; 32]);
let mut b = CertificateBuilder::new(user.public_blob(), CertType::User, alloc::vec![1u8; 32]);
b.key_id = "alice".into();
b.valid_principals = alloc::vec!["alice".into(), "root".into()];
b.critical_options = alloc::vec![("force-command".into(), encode_option_string("/bin/true"))];
b.extensions = DEFAULT_USER_EXTENSIONS
.iter()
.map(|e| (e.to_string(), Vec::new()))
.collect();
let cert = b.sign(&ca.public_blob(), |d| ca.sign(d)).unwrap();
cert.verify_ca_signature(CA_ALGOS).unwrap();
cert.check_principal("root").unwrap();
assert_eq!(
cert.critical_option_string("force-command").as_deref(),
Some("/bin/true")
);
assert!(cert.has_extension("permit-pty"));
let line = cert.to_openssh_line("alice@host");
let back = Certificate::parse_openssh_line(&line).unwrap();
assert_eq!(back.raw, cert.raw);
b.extensions.push(("permit-pty".into(), Vec::new()));
assert!(b.sign(&ca.public_blob(), |d| ca.sign(d)).is_err());
}
#[test]
fn cert_names_for_key_types() {
assert_eq!(
cert_name_for_key_type("ssh-rsa"),
Some("rsa-sha2-512-cert-v01@openssh.com")
);
assert_eq!(
cert_name_for_key_type("rsa-sha2-256"),
Some("rsa-sha2-256-cert-v01@openssh.com")
);
assert_eq!(
cert_name_for_key_type("ssh-ed25519"),
Some("ssh-ed25519-cert-v01@openssh.com")
);
assert_eq!(
cert_name_for_key_type("ecdsa-sha2-nistp384"),
Some("ecdsa-sha2-nistp384-cert-v01@openssh.com")
);
assert_eq!(cert_name_for_key_type("ssh-dss"), None);
}
#[test]
fn host_principals_are_patterns_and_must_be_present() {
let mut cert = Certificate::parse(&load_cert_blob("h_ed25519-cert.pub")).unwrap();
cert.valid_principals = vec!["*.example.com".into(), "loc?lhost".into()];
cert.check_principal("host.example.com").unwrap();
cert.check_principal("LOCALHOST").unwrap();
assert!(cert.check_principal("example.org").is_err());
cert.valid_principals.clear();
assert!(matches!(
cert.check_principal("host.example.com"),
Err(Error::CertPrincipalMismatch)
));
cert.cert_type = CertType::User;
assert!(cert.check_principal("anyone").is_err());
}