use std::collections::HashMap; use rpki::data_model::tal::Tal; use rpki::fetch::rsync::{RsyncFetchError, RsyncFetcher}; use rpki::policy::{Policy, SyncPreference}; use rpki::storage::RocksStore; use rpki::sync::rrdp::Fetcher; use rpki::validation::from_tal::{ FromTalError, discover_root_ca_instance_from_tal, discover_root_ca_instance_from_tal_and_ta_der, discover_root_ca_instance_from_tal_url, run_root_from_tal_url_once, }; use url::Url; struct MapFetcher { by_uri: HashMap>, } impl MapFetcher { fn new(by_uri: HashMap>) -> Self { Self { by_uri } } } impl Fetcher for MapFetcher { fn fetch(&self, uri: &str) -> Result, String> { self.by_uri .get(uri) .cloned() .ok_or_else(|| format!("missing mapping for {uri}")) } } struct EmptyRsync; impl RsyncFetcher for EmptyRsync { fn fetch_objects( &self, _rsync_base_uri: &str, ) -> Result)>, RsyncFetchError> { Ok(Vec::new()) } } fn apnic_tal_bytes() -> Vec { std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal").expect("read apnic TAL fixture") } fn apnic_ta_der() -> Vec { std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic TA fixture") } #[test] fn offline_discovery_from_apnic_tal_and_ta_der_fixture_works() { let tal_bytes = apnic_tal_bytes(); let ta_der = apnic_ta_der(); let d = discover_root_ca_instance_from_tal_and_ta_der(&tal_bytes, &ta_der, None) .expect("discover root from fixtures"); assert!(d.ca_instance.rsync_base_uri.starts_with("rsync://")); assert!(d.ca_instance.rsync_base_uri.ends_with('/')); assert!(d.ca_instance.manifest_rsync_uri.starts_with("rsync://")); assert!(d.ca_instance.manifest_rsync_uri.ends_with(".mft")); if let Some(n) = &d.ca_instance.rrdp_notification_uri { assert!(n.starts_with("https://")); } } #[test] fn discover_root_from_tal_url_works_with_mock_fetcher() { let tal_bytes = apnic_tal_bytes(); let tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL"); let ta_uri = tal.ta_uris[0].as_str().to_string(); let mut map = HashMap::new(); map.insert("https://example.test/apnic.tal".to_string(), tal_bytes); map.insert(ta_uri, apnic_ta_der()); let fetcher = MapFetcher::new(map); let d = discover_root_ca_instance_from_tal_url(&fetcher, "https://example.test/apnic.tal") .expect("discover"); assert!(d.ca_instance.rsync_base_uri.starts_with("rsync://")); } #[test] fn discover_root_tries_multiple_ta_uris_until_one_succeeds() { let tal_bytes = apnic_tal_bytes(); let mut tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL"); let good_uri = tal.ta_uris[0].clone(); tal.ta_uris .insert(0, Url::parse("https://example.invalid/bad.cer").unwrap()); let mut map = HashMap::new(); map.insert(good_uri.as_str().to_string(), apnic_ta_der()); let fetcher = MapFetcher::new(map); let d = discover_root_ca_instance_from_tal(&fetcher, tal, None).expect("discover"); assert_eq!(d.trust_anchor.resolved_ta_uri.as_ref(), Some(&good_uri)); } #[test] fn discover_root_errors_when_no_ta_uris_present() { let tal_bytes = apnic_tal_bytes(); let mut tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL"); tal.ta_uris.clear(); let fetcher = MapFetcher::new(HashMap::new()); let err = discover_root_ca_instance_from_tal(&fetcher, tal, None).unwrap_err(); assert!(matches!(err, FromTalError::NoTaUris)); } #[test] fn discover_root_errors_when_all_ta_fetches_fail() { let tal_bytes = apnic_tal_bytes(); let tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL"); let fetcher = MapFetcher::new(HashMap::new()); let err = discover_root_ca_instance_from_tal(&fetcher, tal, None).unwrap_err(); assert!(matches!(err, FromTalError::TaFetch(_))); } #[test] fn discover_root_errors_when_ta_does_not_bind_to_tal_spki() { let tal_bytes = apnic_tal_bytes(); let tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL"); // Use a different TA cert fixture to trigger SPKI mismatch. let wrong_ta = std::fs::read("tests/fixtures/ta/arin-ta.cer").expect("read arin ta"); let mut map = HashMap::new(); map.insert(tal.ta_uris[0].as_str().to_string(), wrong_ta); let fetcher = MapFetcher::new(map); let err = discover_root_ca_instance_from_tal(&fetcher, tal, None).unwrap_err(); assert!(matches!(err, FromTalError::TaFetch(_))); } #[test] fn discover_root_from_tal_url_errors_when_tal_fetch_fails() { let fetcher = MapFetcher::new(HashMap::new()); let err = discover_root_ca_instance_from_tal_url(&fetcher, "https://example.test/missing.tal") .unwrap_err(); assert!(matches!(err, FromTalError::TalFetch(_))); } #[test] fn run_root_from_tal_url_once_propagates_run_error_when_repo_is_empty() { let tal_bytes = apnic_tal_bytes(); let tal = Tal::decode_bytes(&tal_bytes).expect("decode TAL"); let ta_uri = tal.ta_uris[0].as_str().to_string(); let mut map = HashMap::new(); map.insert("https://example.test/apnic.tal".to_string(), tal_bytes); map.insert(ta_uri, apnic_ta_der()); let fetcher = MapFetcher::new(map); let temp = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(temp.path()).expect("open rocksdb"); let mut policy = Policy::default(); policy.sync_preference = SyncPreference::RsyncOnly; let err = run_root_from_tal_url_once( &store, &policy, "https://example.test/apnic.tal", &fetcher, &EmptyRsync, time::OffsetDateTime::now_utc(), ) .unwrap_err(); assert!(matches!(err, FromTalError::Run(_))); }