use crate::data_model::common::X509NameDer; use x509_parser::prelude::FromDer; fn canonicalize(name: &X509NameDer) -> Option { let (rem, parsed) = x509_parser::x509::X509Name::from_der(name.as_raw()).ok()?; if !rem.is_empty() { return None; } Some(parsed.to_string()) } /// Compare two X.509 distinguished names using a tolerant semantic comparison. /// /// RPKI repositories in the wild sometimes encode the same name using different /// ASN.1 string types (e.g., PrintableString vs UTF8String) while remaining /// semantically equivalent. RFC 5280 path validation requires name matching, but /// DER byte equality is too strict for interoperability. pub fn x509_names_equivalent(a: &X509NameDer, b: &X509NameDer) -> bool { let Some(ca) = canonicalize(a) else { return a == b; }; let Some(cb) = canonicalize(b) else { return a == b; }; ca == cb } #[cfg(test)] mod tests { use super::*; #[test] fn x509_names_equivalent_falls_back_to_der_equality_when_parse_fails() { // Invalid tag (not a SEQUENCE) makes x509-parser fail and forces DER equality fallback. let a = X509NameDer(vec![0x01, 0x00]); let b = X509NameDer(vec![0x01, 0x00]); assert!(x509_names_equivalent(&a, &b)); } #[test] fn x509_names_equivalent_compares_semantic_names_when_parse_succeeds() { let cert_der = std::fs::read( "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer", ) .expect("read certificate fixture"); let (_rem, cert) = x509_parser::parse_x509_certificate(&cert_der).expect("parse certificate fixture"); let subject = X509NameDer(cert.tbs_certificate.subject.as_raw().to_vec()); let issuer = X509NameDer(cert.tbs_certificate.issuer.as_raw().to_vec()); assert!(x509_names_equivalent(&subject, &subject)); assert!(!x509_names_equivalent(&subject, &issuer)); } #[test] fn x509_names_equivalent_falls_back_when_name_has_trailing_bytes() { // Use a real name DER and append a trailing byte so parsing yields leftover `rem`. let cert_der = std::fs::read( "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer", ) .expect("read certificate fixture"); let (_rem, cert) = x509_parser::parse_x509_certificate(&cert_der).expect("parse certificate fixture"); let mut name = cert.tbs_certificate.subject.as_raw().to_vec(); name.push(0x00); let a = X509NameDer(name.clone()); let b = X509NameDer(name); assert!(x509_names_equivalent(&a, &b)); } #[test] fn x509_names_equivalent_falls_back_when_one_side_fails_to_parse() { let cert_der = std::fs::read( "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer", ) .expect("read certificate fixture"); let (_rem, cert) = x509_parser::parse_x509_certificate(&cert_der).expect("parse certificate fixture"); let good = X509NameDer(cert.tbs_certificate.subject.as_raw().to_vec()); let bad = X509NameDer(vec![0x01, 0x00]); assert!(!x509_names_equivalent(&good, &bad)); } }