rpki/tests/test_from_tal_offline.rs
2026-02-09 19:35:54 +08:00

182 lines
5.9 KiB
Rust

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 rpki::validation::objects::IssuerCaCertificateResolver;
use url::Url;
struct MapFetcher {
by_uri: HashMap<String, Vec<u8>>,
}
impl MapFetcher {
fn new(by_uri: HashMap<String, Vec<u8>>) -> Self {
Self { by_uri }
}
}
impl Fetcher for MapFetcher {
fn fetch(&self, uri: &str) -> Result<Vec<u8>, 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<Vec<(String, Vec<u8>)>, RsyncFetchError> {
Ok(Vec::new())
}
}
struct NullResolver;
impl IssuerCaCertificateResolver for NullResolver {
fn resolve_by_subject_dn(&self, _subject_dn: &str) -> Option<Vec<u8>> {
None
}
}
fn apnic_tal_bytes() -> Vec<u8> {
std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal").expect("read apnic TAL fixture")
}
fn apnic_ta_der() -> Vec<u8> {
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,
&NullResolver,
time::OffsetDateTime::now_utc(),
)
.unwrap_err();
assert!(matches!(err, FromTalError::Run(_)));
}