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> {
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 e_mpint = [0x01u8, 0x00, 0x01];
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");
}
fn encode_str(s: &[u8]) -> Vec<u8> {
let mut w = Writer::new();
w.write_string(s);
w.into_vec()
}