#![expect(clippy::unwrap_used)]
use super::*;
use crate::tls::fingerprint::compute_fingerprint;
fn gen_self_signed_cert() -> Vec<u8> {
let params = rcgen::CertificateParams::new(vec!["localhost".to_owned()]).unwrap();
let cert = params
.self_signed(&rcgen::KeyPair::generate().unwrap())
.unwrap();
cert.der().to_vec()
}
#[test]
fn pinned_verifier_advertises_default_signature_schemes() {
use rustls::client::danger::ServerCertVerifier;
let der = gen_self_signed_cert();
let fp = compute_fingerprint(&der);
let verifier = PinnedCertVerifier::new(&fp, None, "localhost").unwrap();
let schemes = verifier.supported_verify_schemes();
assert!(
!schemes.is_empty(),
"verifier should advertise at least one signature scheme"
);
}
#[test]
fn pinned_verifier_accepts_matching_cert() {
let der = gen_self_signed_cert();
let fp = compute_fingerprint(&der);
let verifier = PinnedCertVerifier::new(&fp, None, "localhost").unwrap();
let cert = CertificateDer::from(der);
let server_name = ServerName::try_from("localhost").unwrap();
let result = verifier.verify_server_cert(&cert, &[], &server_name, &[], UnixTime::now());
assert!(
result.is_ok(),
"matching pin should be accepted: {result:?}"
);
}
#[test]
fn pinned_verifier_rejects_mismatched_cert() {
let der1 = gen_self_signed_cert();
let fp1 = compute_fingerprint(&der1);
let der2 = gen_self_signed_cert();
let verifier = PinnedCertVerifier::new(&fp1, None, "localhost").unwrap();
let cert = CertificateDer::from(der2);
let server_name = ServerName::try_from("localhost").unwrap();
let result = verifier.verify_server_cert(&cert, &[], &server_name, &[], UnixTime::now());
assert!(result.is_err(), "mismatched pin should be rejected");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("PIN_MISMATCH"),
"error should contain PIN_MISMATCH: {err_msg}"
);
}
#[test]
fn ca_cert_config_rejects_missing_file() {
let result = build_ca_cert_config(Path::new("/nonexistent/ca.pem"));
assert!(result.is_err(), "missing file should fail");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("failed to read"),
"error should mention 'failed to read': {err_msg}"
);
}
#[test]
fn build_pinned_config_succeeds() {
let der = gen_self_signed_cert();
let fp = compute_fingerprint(&der);
let result = build_pinned_config(&fp, None, "localhost");
assert!(
result.is_ok(),
"build_pinned_config should succeed: {result:?}"
);
}
#[test]
fn extract_issuer_dn_returns_fallback_for_garbage() {
let result = extract_issuer_dn(b"not a certificate");
assert!(
result.contains("raw DER"),
"garbage input should produce fallback: {result}"
);
}
#[test]
fn extract_issuer_dn_parses_rcgen_cert() {
let der = gen_self_signed_cert();
let issuer = extract_issuer_dn(&der);
assert!(
issuer.contains("CN="),
"should extract CN from issuer: {issuer}"
);
}
#[test]
fn pinned_verifier_accepts_matching_pin_regardless_of_issuer() {
let der = gen_self_signed_cert();
let fp = compute_fingerprint(&der);
let verifier = PinnedCertVerifier::new(&fp, None, "localhost").unwrap();
let cert = CertificateDer::from(der);
let server_name = ServerName::try_from("localhost").unwrap();
let result = verifier.verify_server_cert(&cert, &[], &server_name, &[], UnixTime::now());
assert!(
result.is_ok(),
"matching pin should always be accepted: {result:?}"
);
}
fn gen_cert_with_cn(cn: &str) -> Vec<u8> {
let mut params = rcgen::CertificateParams::new(vec![cn.to_owned()]).unwrap();
let mut dn = rcgen::DistinguishedName::new();
dn.push(rcgen::DnType::CommonName, cn);
params.distinguished_name = dn;
let cert = params
.self_signed(&rcgen::KeyPair::generate().unwrap())
.unwrap();
cert.der().to_vec()
}
#[test]
fn extract_issuer_der_returns_consistent_bytes() {
let der = gen_self_signed_cert();
let issuer1 = extract_issuer_der(&der);
let issuer2 = extract_issuer_der(&der);
assert_eq!(issuer1, issuer2, "should be deterministic");
assert!(issuer1.is_some(), "should extract from valid cert");
}
#[test]
fn extract_issuer_der_differs_for_different_issuers() {
let der1 = gen_cert_with_cn("CA One");
let der2 = gen_cert_with_cn("CA Two");
let issuer1 = extract_issuer_der(&der1).unwrap();
let issuer2 = extract_issuer_der(&der2).unwrap();
assert_ne!(issuer1, issuer2, "different CAs should have different DER");
}
#[test]
fn extract_issuer_der_returns_none_for_garbage() {
assert!(extract_issuer_der(b"not a certificate").is_none());
}
#[test]
fn pinned_verifier_detects_issuer_change_via_der() {
let der1 = gen_cert_with_cn("OriginalCA");
let fp1 = compute_fingerprint(&der1);
let issuer_der_bytes = extract_issuer_der(&der1).unwrap();
let issuer_der_b64 = base64::engine::general_purpose::STANDARD.encode(&issuer_der_bytes);
let verifier = PinnedCertVerifier::new(&fp1, Some(&issuer_der_b64), "localhost").unwrap();
let der2 = gen_cert_with_cn("EvilCA");
let cert2 = CertificateDer::from(der2);
let server_name = ServerName::try_from("localhost").unwrap();
let result = verifier.verify_server_cert(&cert2, &[], &server_name, &[], UnixTime::now());
assert!(result.is_err(), "issuer DER change should be rejected");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("ISSUER_CHANGED"),
"error should contain ISSUER_CHANGED: {err_msg}"
);
}
#[test]
fn pinned_verifier_allows_pin_mismatch_with_same_issuer_der() {
let der1 = gen_self_signed_cert();
let fp1 = compute_fingerprint(&der1);
let issuer_der_bytes = extract_issuer_der(&der1).unwrap();
let issuer_der_b64 = base64::engine::general_purpose::STANDARD.encode(&issuer_der_bytes);
let verifier = PinnedCertVerifier::new(&fp1, Some(&issuer_der_b64), "localhost").unwrap();
let der2 = gen_self_signed_cert();
let cert2 = CertificateDer::from(der2);
let server_name = ServerName::try_from("localhost").unwrap();
let result = verifier.verify_server_cert(&cert2, &[], &server_name, &[], UnixTime::now());
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("PIN_MISMATCH"),
"same issuer DER should produce PIN_MISMATCH: {err_msg}"
);
}
#[test]
fn pinned_verifier_rejects_invalid_base64_issuer_der() {
let der = gen_self_signed_cert();
let fp = compute_fingerprint(&der);
let result = PinnedCertVerifier::new(&fp, Some("!!!not-valid-base64!!!"), "localhost");
assert!(result.is_err(), "invalid base64 should be rejected");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("invalid base64 in tls_pin_issuer_der"),
"error should mention invalid base64: {err_msg}"
);
}
fn gen_self_signed_pem() -> String {
let params = rcgen::CertificateParams::new(vec!["localhost".to_owned()]).unwrap();
let cert = params
.self_signed(&rcgen::KeyPair::generate().unwrap())
.unwrap();
cert.pem()
}
#[test]
fn ca_cert_config_loads_valid_pem() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let pem = gen_self_signed_pem();
std::fs::write(tmp.path(), pem).unwrap();
let result = build_ca_cert_config(tmp.path());
assert!(
result.is_ok(),
"valid PEM should produce a config: {result:?}"
);
}
#[test]
fn ca_cert_config_rejects_empty_pem_file() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), "").unwrap();
let result = build_ca_cert_config(tmp.path());
assert!(result.is_err(), "empty PEM file should be rejected");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("no valid PEM certificates found"),
"error should mention missing certs: {err_msg}"
);
}
#[test]
fn ca_cert_config_rejects_malformed_pem() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(
tmp.path(),
"-----BEGIN CERTIFICATE-----\nnot valid base64 here !!!\n-----END CERTIFICATE-----\n",
)
.unwrap();
let result = build_ca_cert_config(tmp.path());
assert!(result.is_err(), "malformed PEM should be rejected");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("failed to parse PEM certificates")
|| err_msg.contains("no valid PEM certificates found"),
"error should mention parse failure or missing certs: {err_msg}"
);
}
#[test]
fn parse_der_length_short_form() {
let data = [0x05_u8, 0x01, 0x02, 0x03];
let (rest, len) = parse_der_length(&data).unwrap();
assert_eq!(len, 5);
assert_eq!(rest, &[0x01, 0x02, 0x03]);
}
#[test]
fn parse_der_length_long_form_two_bytes() {
let data = [0x82_u8, 0x01, 0x00, 0xaa];
let (rest, len) = parse_der_length(&data).unwrap();
assert_eq!(len, 256);
assert_eq!(rest, &[0xaa]);
}
#[test]
fn parse_der_length_long_form_one_byte() {
let data = [0x81_u8, 0x80, 0xaa];
let (rest, len) = parse_der_length(&data).unwrap();
assert_eq!(len, 128);
assert_eq!(rest, &[0xaa]);
}
#[test]
fn parse_der_length_rejects_indefinite_form() {
let data = [0x80_u8];
assert!(parse_der_length(&data).is_none());
}
#[test]
fn parse_der_length_long_form_four_bytes_with_exact_fit() {
let data = [0x84_u8, 0x00, 0x00, 0x01, 0x00];
let (rest, len) = parse_der_length(&data).unwrap();
assert_eq!(len, 256);
assert!(rest.is_empty());
}
#[test]
fn parse_der_length_rejects_too_many_length_bytes() {
let data = [0x85_u8, 0x00, 0x00, 0x00, 0x00, 0x01];
assert!(parse_der_length(&data).is_none());
}
#[test]
fn parse_der_length_rejects_truncated_long_form() {
let data = [0x82_u8, 0x01];
assert!(parse_der_length(&data).is_none());
}
#[test]
fn parse_der_sequence_rejects_wrong_tag() {
let data = [0x02_u8, 0x01, 0x05]; assert!(parse_der_sequence(&data).is_none());
}
#[test]
fn parse_der_sequence_rejects_truncated_content() {
let data = [0x30_u8, 0x05, 0x01, 0x02];
assert!(parse_der_sequence(&data).is_none());
}
#[test]
fn skip_der_element_rejects_empty() {
assert!(skip_der_element(&[]).is_none());
}
#[test]
fn skip_der_element_rejects_truncated() {
let data = [0x04_u8, 0x0a, 0x01, 0x02];
assert!(skip_der_element(&data).is_none());
}
#[test]
fn oid_short_name_maps_known_oids() {
assert_eq!(oid_short_name(&[0x55, 0x04, 0x03]), "CN");
assert_eq!(oid_short_name(&[0x55, 0x04, 0x06]), "C");
assert_eq!(oid_short_name(&[0x55, 0x04, 0x07]), "L");
assert_eq!(oid_short_name(&[0x55, 0x04, 0x08]), "ST");
assert_eq!(oid_short_name(&[0x55, 0x04, 0x0a]), "O");
assert_eq!(oid_short_name(&[0x55, 0x04, 0x0b]), "OU");
assert_eq!(oid_short_name(&[0x55, 0x04, 0xff]), "OID");
assert_eq!(oid_short_name(&[]), "OID");
}
#[test]
fn parse_attribute_type_and_value_rejects_non_oid() {
let data = [0x02_u8, 0x01, 0x05];
assert!(parse_attribute_type_and_value(&data).is_none());
}
#[test]
fn parse_attribute_type_and_value_falls_back_to_hex_for_non_utf8() {
let mut data = Vec::new();
data.extend_from_slice(&[0x06, 0x03, 0x55, 0x04, 0x03]);
data.extend_from_slice(&[0x04, 0x02, 0xff, 0xfe]);
let result = parse_attribute_type_and_value(&data).unwrap();
assert_eq!(result, "CN=fffe");
}
#[test]
fn extract_rdns_breaks_on_non_set_tag() {
let data = [0x30_u8, 0x00];
assert!(extract_rdns(&data).is_none());
}
#[test]
fn extract_rdns_returns_none_on_empty_input() {
assert!(extract_rdns(&[]).is_none());
}
#[test]
fn hex_encode_produces_lowercase_pairs() {
assert_eq!(hex::encode(&[0x00, 0x0f, 0xff, 0xab]), "000fffab");
assert_eq!(hex::encode(&[]), "");
}
#[test]
fn one_bit_flipped_pin_is_rejected() {
let der = gen_self_signed_cert();
let mut hash: [u8; 32] = Sha256::digest(&der).into();
hash[0] ^= 0x01;
let pin = format!(
"sha256//{}",
base64::engine::general_purpose::STANDARD.encode(hash)
);
let verifier = PinnedCertVerifier::new(&pin, None, "localhost").unwrap();
let cert = CertificateDer::from(der);
let server_name = ServerName::try_from("localhost").unwrap();
let result = verifier.verify_server_cert(&cert, &[], &server_name, &[], UnixTime::now());
assert!(result.is_err(), "one-bit-flipped pin must be rejected");
}
#[test]
fn pin_match_short_circuits_issuer_check() {
let der = gen_cert_with_cn("RealCA");
let fp = compute_fingerprint(&der);
let wrong_issuer_der = extract_issuer_der(&gen_cert_with_cn("OtherCA")).unwrap();
let wrong_issuer_b64 = base64::engine::general_purpose::STANDARD.encode(&wrong_issuer_der);
let verifier = PinnedCertVerifier::new(&fp, Some(&wrong_issuer_b64), "localhost").unwrap();
let cert = CertificateDer::from(der);
let server_name = ServerName::try_from("localhost").unwrap();
let result = verifier.verify_server_cert(&cert, &[], &server_name, &[], UnixTime::now());
assert!(
result.is_ok(),
"matching pin must short-circuit the issuer check: {result:?}"
);
}
#[test]
fn unparseable_leaf_degrades_to_reject() {
let der1 = gen_self_signed_cert();
let fp1 = compute_fingerprint(&der1);
let issuer_der = extract_issuer_der(&der1).unwrap();
let issuer_b64 = base64::engine::general_purpose::STANDARD.encode(&issuer_der);
let verifier = PinnedCertVerifier::new(&fp1, Some(&issuer_b64), "localhost").unwrap();
let cert = CertificateDer::from(b"not a certificate".to_vec());
let server_name = ServerName::try_from("localhost").unwrap();
let result = verifier.verify_server_cert(&cert, &[], &server_name, &[], UnixTime::now());
assert!(
result.is_err(),
"unparseable leaf must be rejected, never accepted"
);
}
proptest::proptest! {
#[test]
fn der_walkers_never_panic_on_arbitrary_bytes(data: Vec<u8>) {
let _ = extract_issuer_der(&data);
let _ = extract_issuer_dn(&data);
}
}