add delta sync and fail fetch process
This commit is contained in:
310
tests/test_apnic_rrdp_delta_live_20260226.rs
Normal file
310
tests/test_apnic_rrdp_delta_live_20260226.rs
Normal file
@@ -0,0 +1,310 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use rpki::fetch::http::{BlockingHttpFetcher, HttpFetcherConfig};
|
||||
use rpki::fetch::rsync::{RsyncFetchError, RsyncFetcher};
|
||||
use rpki::policy::{CaFailedFetchPolicy, Policy, SyncPreference};
|
||||
use rpki::storage::{FetchCachePpKey, RocksStore};
|
||||
use rpki::sync::rrdp::{Fetcher, parse_notification, sync_from_notification};
|
||||
use rpki::sync::repo::{RepoSyncSource, sync_publication_point};
|
||||
use rpki::validation::from_tal::discover_root_ca_instance_from_tal_url;
|
||||
use rpki::validation::manifest::{PublicationPointSource, process_manifest_publication_point};
|
||||
|
||||
const APNIC_TAL_URL: &str = "https://tal.apnic.net/tal-archive/apnic-rfc7730-https.tal";
|
||||
|
||||
fn persistent_db_dir() -> PathBuf {
|
||||
if let Ok(s) = std::env::var("RPKI_LIVE_DB_DIR") {
|
||||
return PathBuf::from(s);
|
||||
}
|
||||
PathBuf::from("target/live/apnic_rrdp_db")
|
||||
}
|
||||
|
||||
fn live_http_fetcher() -> BlockingHttpFetcher {
|
||||
let timeout_secs: u64 = std::env::var("RPKI_LIVE_HTTP_TIMEOUT_SECS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(15 * 60);
|
||||
BlockingHttpFetcher::new(HttpFetcherConfig {
|
||||
timeout: Duration::from_secs(timeout_secs),
|
||||
user_agent: "rpki-dev/0.1 (stage2 live rrdp delta test)".to_string(),
|
||||
})
|
||||
.expect("http fetcher")
|
||||
}
|
||||
|
||||
struct AlwaysFailRsyncFetcher;
|
||||
|
||||
impl RsyncFetcher for AlwaysFailRsyncFetcher {
|
||||
fn fetch_objects(&self, _rsync_base_uri: &str) -> Result<Vec<(String, Vec<u8>)>, RsyncFetchError> {
|
||||
Err(RsyncFetchError::Fetch("rsync disabled for this test".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CountingDenyUriFetcher {
|
||||
inner: BlockingHttpFetcher,
|
||||
deny_uri: String,
|
||||
counts: std::rc::Rc<RefCell<HashMap<String, u64>>>,
|
||||
}
|
||||
|
||||
impl CountingDenyUriFetcher {
|
||||
fn new(inner: BlockingHttpFetcher, deny_uri: String) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
deny_uri,
|
||||
counts: std::rc::Rc::new(RefCell::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
fn count(&self, uri: &str) -> u64 {
|
||||
*self.counts.borrow().get(uri).unwrap_or(&0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Fetcher for CountingDenyUriFetcher {
|
||||
fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
|
||||
*self.counts.borrow_mut().entry(uri.to_string()).or_insert(0) += 1;
|
||||
if uri == self.deny_uri {
|
||||
return Err(format!("snapshot fetch denied: {uri}"));
|
||||
}
|
||||
self.inner.fetch(uri)
|
||||
}
|
||||
}
|
||||
|
||||
fn live_policy() -> Policy {
|
||||
let mut p = Policy::default();
|
||||
p.sync_preference = SyncPreference::RrdpThenRsync;
|
||||
p.ca_failed_fetch_policy = CaFailedFetchPolicy::UseFetchCachePp;
|
||||
p
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "live network: APNIC RRDP snapshot bootstrap into persistent RocksDB"]
|
||||
fn apnic_live_bootstrap_snapshot_and_fetch_cache_pp_pack_to_persistent_db() {
|
||||
let http = live_http_fetcher();
|
||||
let rsync = AlwaysFailRsyncFetcher;
|
||||
|
||||
let db_dir = persistent_db_dir();
|
||||
std::fs::create_dir_all(&db_dir).expect("create db dir");
|
||||
let store = RocksStore::open(&db_dir).expect("open rocksdb");
|
||||
|
||||
let policy = live_policy();
|
||||
let validation_time = time::OffsetDateTime::now_utc();
|
||||
|
||||
let discovery = discover_root_ca_instance_from_tal_url(&http, APNIC_TAL_URL)
|
||||
.expect("discover root CA instance from APNIC TAL");
|
||||
let ca_instance = discovery.ca_instance;
|
||||
|
||||
let rrdp_notification_uri = ca_instance
|
||||
.rrdp_notification_uri
|
||||
.as_deref()
|
||||
.expect("APNIC root must have rrdpNotification");
|
||||
|
||||
let sync = sync_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
Some(rrdp_notification_uri),
|
||||
&ca_instance.rsync_base_uri,
|
||||
&http,
|
||||
&rsync,
|
||||
)
|
||||
.expect("repo sync");
|
||||
|
||||
assert_eq!(sync.source, RepoSyncSource::Rrdp);
|
||||
|
||||
// Build + persist a fetch_cache_pp pack for the root publication point so later runs can
|
||||
// validate behavior under failed fetch conditions (RFC 9286 §6.6).
|
||||
let ta_der = discovery.trust_anchor.ta_certificate.raw_der;
|
||||
let pp = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&ca_instance.manifest_rsync_uri,
|
||||
&ca_instance.publication_point_rsync_uri,
|
||||
&ta_der,
|
||||
None,
|
||||
validation_time,
|
||||
)
|
||||
.expect("process manifest publication point");
|
||||
|
||||
assert_eq!(pp.source, PublicationPointSource::Fresh);
|
||||
|
||||
let key = FetchCachePpKey::from_manifest_rsync_uri(&ca_instance.manifest_rsync_uri);
|
||||
let cached = store
|
||||
.get_fetch_cache_pp(&key)
|
||||
.expect("get fetch_cache_pp");
|
||||
assert!(cached.is_some(), "expected fetch_cache_pp to be stored");
|
||||
|
||||
eprintln!("OK: bootstrap complete; persistent db at: {}", db_dir.display());
|
||||
eprintln!("Next: run `cargo test --release -q --test test_apnic_rrdp_delta_live_20260226 -- --ignored` later to exercise delta sync.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "live network: waits for APNIC RRDP serial advance, then sync via deltas only (no snapshot) using persistent RocksDB"]
|
||||
fn apnic_live_delta_only_from_persistent_db() {
|
||||
let http = live_http_fetcher();
|
||||
|
||||
let db_dir = persistent_db_dir();
|
||||
let store = RocksStore::open(&db_dir).expect("open rocksdb (must have been bootstrapped)");
|
||||
let policy = live_policy();
|
||||
|
||||
let discovery = discover_root_ca_instance_from_tal_url(&http, APNIC_TAL_URL)
|
||||
.expect("discover root CA instance from APNIC TAL");
|
||||
let ca_instance = discovery.ca_instance;
|
||||
|
||||
let rrdp_notification_uri = ca_instance
|
||||
.rrdp_notification_uri
|
||||
.as_deref()
|
||||
.expect("APNIC root must have rrdpNotification");
|
||||
|
||||
let state_bytes = store
|
||||
.get_rrdp_state(rrdp_notification_uri)
|
||||
.expect("get rrdp_state")
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"missing rrdp_state for APNIC notification URI; run bootstrap test first. db_dir={}",
|
||||
db_dir.display()
|
||||
)
|
||||
});
|
||||
let state = rpki::sync::rrdp::RrdpState::decode(&state_bytes).expect("decode rrdp_state");
|
||||
let old_serial = state.serial;
|
||||
let old_session = state.session_id;
|
||||
|
||||
let max_wait_secs: u64 = std::env::var("RPKI_LIVE_MAX_WAIT_SECS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(30 * 60);
|
||||
let poll_secs: u64 = std::env::var("RPKI_LIVE_POLL_SECS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(60);
|
||||
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
if start.elapsed() > Duration::from_secs(max_wait_secs) {
|
||||
panic!(
|
||||
"timed out waiting for APNIC RRDP serial to advance for delta sync; old_session={} old_serial={} waited={}s",
|
||||
old_session,
|
||||
old_serial,
|
||||
max_wait_secs
|
||||
);
|
||||
}
|
||||
|
||||
let notif_xml = http
|
||||
.fetch(rrdp_notification_uri)
|
||||
.unwrap_or_else(|e| panic!("fetch notification failed: {e}"));
|
||||
let notif = parse_notification(¬if_xml).expect("parse notification");
|
||||
|
||||
if notif.session_id.to_string() != old_session {
|
||||
panic!(
|
||||
"RRDP session_id changed; this delta-only test assumes same snapshot baseline. old_session={} new_session={}",
|
||||
old_session,
|
||||
notif.session_id
|
||||
);
|
||||
}
|
||||
|
||||
if notif.serial <= old_serial {
|
||||
eprintln!(
|
||||
"waiting for serial advance: session={} old_serial={} current_serial={}",
|
||||
old_session, old_serial, notif.serial
|
||||
);
|
||||
std::thread::sleep(Duration::from_secs(poll_secs));
|
||||
continue;
|
||||
}
|
||||
|
||||
let want_first = old_serial + 1;
|
||||
let min_delta = notif.deltas.first().map(|d| d.serial).unwrap_or(u64::MAX);
|
||||
if notif.deltas.is_empty() || min_delta > want_first {
|
||||
panic!(
|
||||
"notification deltas do not cover required serial gap for delta-only sync; old_serial={} want_first={} min_delta={} current_serial={}. rerun bootstrap to refresh snapshot baseline.",
|
||||
old_serial,
|
||||
want_first,
|
||||
min_delta,
|
||||
notif.serial
|
||||
);
|
||||
}
|
||||
|
||||
// Deny snapshot fetch to ensure we truly test the delta path and keep the stored snapshot
|
||||
// baseline unchanged.
|
||||
let deny = notif.snapshot_uri.clone();
|
||||
let fetcher = CountingDenyUriFetcher::new(http.clone(), deny.clone());
|
||||
|
||||
match sync_from_notification(&store, rrdp_notification_uri, ¬if_xml, &fetcher) {
|
||||
Ok(written) => {
|
||||
assert!(
|
||||
written > 0,
|
||||
"expected delta sync to apply changes (written={written})"
|
||||
);
|
||||
assert_eq!(
|
||||
fetcher.count(&deny),
|
||||
0,
|
||||
"delta sync should not fetch snapshot"
|
||||
);
|
||||
eprintln!(
|
||||
"OK: delta sync applied: written={} old_serial={} new_serial={}",
|
||||
written, old_serial, notif.serial
|
||||
);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("delta sync attempt failed (will retry): {e}");
|
||||
std::thread::sleep(Duration::from_secs(poll_secs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep policy variable used, to avoid warnings if this test evolves.
|
||||
let _ = policy;
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "offline/synthetic: after bootstrap, force repo sync failure and assert fetch_cache_pp is used (RFC 9286 §6.6)"]
|
||||
fn apnic_root_repo_sync_failure_uses_fetch_cache_pp_pack() {
|
||||
let http = live_http_fetcher();
|
||||
let db_dir = persistent_db_dir();
|
||||
let store = RocksStore::open(&db_dir).expect("open rocksdb (must have been bootstrapped)");
|
||||
|
||||
let mut policy = live_policy();
|
||||
policy.sync_preference = SyncPreference::RrdpThenRsync;
|
||||
policy.ca_failed_fetch_policy = CaFailedFetchPolicy::UseFetchCachePp;
|
||||
|
||||
let validation_time = time::OffsetDateTime::now_utc();
|
||||
|
||||
let discovery = discover_root_ca_instance_from_tal_url(&http, APNIC_TAL_URL)
|
||||
.expect("discover root CA instance from APNIC TAL");
|
||||
let ca_instance = discovery.ca_instance;
|
||||
|
||||
// Ensure cache exists (created by bootstrap).
|
||||
let key = FetchCachePpKey::from_manifest_rsync_uri(&ca_instance.manifest_rsync_uri);
|
||||
let cached = store
|
||||
.get_fetch_cache_pp(&key)
|
||||
.expect("get fetch_cache_pp");
|
||||
assert!(
|
||||
cached.is_some(),
|
||||
"missing fetch_cache_pp; run bootstrap test first. db_dir={}",
|
||||
db_dir.display()
|
||||
);
|
||||
|
||||
// Simulate repo sync failure: skip calling sync_publication_point and directly drive manifest
|
||||
// processing with repo_sync_ok=false.
|
||||
let ta_der = discovery.trust_anchor.ta_certificate.raw_der;
|
||||
let pp = rpki::validation::manifest::process_manifest_publication_point_after_repo_sync(
|
||||
&store,
|
||||
&policy,
|
||||
&ca_instance.manifest_rsync_uri,
|
||||
&ca_instance.publication_point_rsync_uri,
|
||||
&ta_der,
|
||||
None,
|
||||
validation_time,
|
||||
false,
|
||||
Some("synthetic repo sync failure"),
|
||||
)
|
||||
.expect("must fall back to fetch_cache_pp");
|
||||
|
||||
assert_eq!(pp.source, PublicationPointSource::FetchCachePp);
|
||||
assert!(
|
||||
pp.warnings.iter().any(|w| w.message.contains("using fetch_cache_pp")),
|
||||
"expected cache-use warning"
|
||||
);
|
||||
}
|
||||
|
||||
125
tests/test_ca_instance_uris_coverage.rs
Normal file
125
tests/test_ca_instance_uris_coverage.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use rpki::data_model::oid::{OID_AD_CA_REPOSITORY, OID_AD_RPKI_MANIFEST, OID_AD_RPKI_NOTIFY};
|
||||
use rpki::data_model::rc::{
|
||||
AccessDescription, ResourceCertKind, ResourceCertificate, SubjectInfoAccess, SubjectInfoAccessCa,
|
||||
};
|
||||
use rpki::validation::ca_instance::{CaInstanceUrisError, ca_instance_uris_from_ca_certificate};
|
||||
|
||||
fn apnic_child_ca_fixture_der() -> Vec<u8> {
|
||||
std::fs::read(
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
||||
)
|
||||
.expect("read apnic fixture ca")
|
||||
}
|
||||
|
||||
fn set_sia_ca(cert: &mut ResourceCertificate, ads: Vec<AccessDescription>) {
|
||||
cert.tbs.extensions.subject_info_access =
|
||||
Some(SubjectInfoAccess::Ca(SubjectInfoAccessCa { access_descriptions: ads }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ca_instance_uris_success_and_error_branches() {
|
||||
let mut cert = ResourceCertificate::decode_der(&apnic_child_ca_fixture_der())
|
||||
.expect("decode apnic fixture ca");
|
||||
|
||||
// Success path.
|
||||
let uris = ca_instance_uris_from_ca_certificate(&cert).expect("uris");
|
||||
assert!(uris.rsync_base_uri.starts_with("rsync://"));
|
||||
assert!(uris.rsync_base_uri.ends_with('/'));
|
||||
assert!(uris.manifest_rsync_uri.starts_with("rsync://"));
|
||||
assert!(uris.manifest_rsync_uri.ends_with(".mft"));
|
||||
|
||||
// NotCa.
|
||||
cert.kind = ResourceCertKind::Ee;
|
||||
let err = ca_instance_uris_from_ca_certificate(&cert).unwrap_err();
|
||||
assert!(matches!(err, CaInstanceUrisError::NotCa));
|
||||
cert.kind = ResourceCertKind::Ca;
|
||||
|
||||
// MissingSia.
|
||||
cert.tbs.extensions.subject_info_access = None;
|
||||
let err = ca_instance_uris_from_ca_certificate(&cert).unwrap_err();
|
||||
assert!(matches!(err, CaInstanceUrisError::MissingSia));
|
||||
|
||||
// MissingCaRepository / MissingRpkiManifest.
|
||||
set_sia_ca(
|
||||
&mut cert,
|
||||
vec![AccessDescription {
|
||||
access_method_oid: OID_AD_RPKI_MANIFEST.to_string(),
|
||||
access_location: "rsync://example.test/repo/x.mft".to_string(),
|
||||
}],
|
||||
);
|
||||
let err = ca_instance_uris_from_ca_certificate(&cert).unwrap_err();
|
||||
assert!(matches!(err, CaInstanceUrisError::MissingCaRepository), "{err}");
|
||||
|
||||
set_sia_ca(
|
||||
&mut cert,
|
||||
vec![AccessDescription {
|
||||
access_method_oid: OID_AD_CA_REPOSITORY.to_string(),
|
||||
access_location: "rsync://example.test/repo/".to_string(),
|
||||
}],
|
||||
);
|
||||
let err = ca_instance_uris_from_ca_certificate(&cert).unwrap_err();
|
||||
assert!(matches!(err, CaInstanceUrisError::MissingRpkiManifest), "{err}");
|
||||
|
||||
// Scheme validation branches.
|
||||
set_sia_ca(
|
||||
&mut cert,
|
||||
vec![AccessDescription {
|
||||
access_method_oid: OID_AD_CA_REPOSITORY.to_string(),
|
||||
access_location: "http://example.test/repo/".to_string(),
|
||||
}],
|
||||
);
|
||||
let err = ca_instance_uris_from_ca_certificate(&cert).unwrap_err();
|
||||
assert!(matches!(err, CaInstanceUrisError::CaRepositoryNotRsync(_)), "{err}");
|
||||
|
||||
set_sia_ca(
|
||||
&mut cert,
|
||||
vec![AccessDescription {
|
||||
access_method_oid: OID_AD_CA_REPOSITORY.to_string(),
|
||||
access_location: "rsync://example.test/repo/".to_string(),
|
||||
},
|
||||
AccessDescription {
|
||||
access_method_oid: OID_AD_RPKI_MANIFEST.to_string(),
|
||||
access_location: "http://example.test/repo/x.mft".to_string(),
|
||||
}],
|
||||
);
|
||||
let err = ca_instance_uris_from_ca_certificate(&cert).unwrap_err();
|
||||
assert!(matches!(err, CaInstanceUrisError::RpkiManifestNotRsync(_)), "{err}");
|
||||
|
||||
set_sia_ca(
|
||||
&mut cert,
|
||||
vec![
|
||||
AccessDescription {
|
||||
access_method_oid: OID_AD_CA_REPOSITORY.to_string(),
|
||||
access_location: "rsync://example.test/repo/".to_string(),
|
||||
},
|
||||
AccessDescription {
|
||||
access_method_oid: OID_AD_RPKI_MANIFEST.to_string(),
|
||||
access_location: "rsync://example.test/repo/x.mft".to_string(),
|
||||
},
|
||||
AccessDescription {
|
||||
access_method_oid: OID_AD_RPKI_NOTIFY.to_string(),
|
||||
access_location: "rsync://example.test/repo/notification.xml".to_string(),
|
||||
},
|
||||
],
|
||||
);
|
||||
let err = ca_instance_uris_from_ca_certificate(&cert).unwrap_err();
|
||||
assert!(matches!(err, CaInstanceUrisError::RpkiNotifyNotHttps(_)), "{err}");
|
||||
|
||||
// ManifestNotUnderPublicationPoint.
|
||||
set_sia_ca(
|
||||
&mut cert,
|
||||
vec![
|
||||
AccessDescription {
|
||||
access_method_oid: OID_AD_CA_REPOSITORY.to_string(),
|
||||
access_location: "rsync://example.test/repo/".to_string(),
|
||||
},
|
||||
AccessDescription {
|
||||
access_method_oid: OID_AD_RPKI_MANIFEST.to_string(),
|
||||
access_location: "rsync://other.test/repo/x.mft".to_string(),
|
||||
},
|
||||
],
|
||||
);
|
||||
let err = ca_instance_uris_from_ca_certificate(&cert).unwrap_err();
|
||||
assert!(matches!(err, CaInstanceUrisError::ManifestNotUnderPublicationPoint { .. }), "{err}");
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ use std::path::Path;
|
||||
use rpki::data_model::manifest::ManifestObject;
|
||||
use rpki::policy::{CaFailedFetchPolicy, Policy};
|
||||
use rpki::storage::{FetchCachePpKey, FetchCachePpPack, RocksStore};
|
||||
use rpki::validation::manifest::process_manifest_publication_point;
|
||||
use rpki::validation::manifest::{
|
||||
PublicationPointSource, process_manifest_publication_point,
|
||||
process_manifest_publication_point_after_repo_sync,
|
||||
};
|
||||
|
||||
fn issuer_ca_fixture() -> Vec<u8> {
|
||||
std::fs::read(
|
||||
@@ -213,3 +216,62 @@ fn cached_pack_revalidation_rejects_hash_mismatch_against_manifest_filelist() {
|
||||
assert!(msg.contains("cached fetch_cache_pp file hash mismatch"), "{msg}");
|
||||
assert!(msg.contains("RFC 9286 §6.5"), "{msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repo_sync_failure_forces_fetch_cache_pp_even_if_raw_objects_are_present() {
|
||||
let (manifest_path, manifest_bytes, manifest) = load_cernet_manifest_fixture();
|
||||
let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);
|
||||
|
||||
let manifest_rsync_uri = fixture_to_rsync_uri(&manifest_path);
|
||||
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
store_raw_publication_point_files(
|
||||
&store,
|
||||
&manifest_path,
|
||||
&manifest_rsync_uri,
|
||||
&manifest_bytes,
|
||||
&manifest,
|
||||
&publication_point_rsync_uri,
|
||||
);
|
||||
|
||||
let mut policy = Policy::default();
|
||||
policy.ca_failed_fetch_policy = CaFailedFetchPolicy::UseFetchCachePp;
|
||||
let issuer_ca_der = issuer_ca_fixture();
|
||||
|
||||
// First run: fresh processing stores fetch_cache_pp.
|
||||
let _fresh = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.expect("fresh run stores fetch_cache_pp");
|
||||
|
||||
// Second run: simulate repo sync failure. Even though raw_objects still contain everything
|
||||
// needed for a fresh pack, failed fetch semantics require using cached objects only.
|
||||
let res = process_manifest_publication_point_after_repo_sync(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
false,
|
||||
Some("synthetic repo sync failure"),
|
||||
)
|
||||
.expect("must fall back to fetch_cache_pp");
|
||||
|
||||
assert_eq!(res.source, PublicationPointSource::FetchCachePp);
|
||||
assert!(
|
||||
res.warnings
|
||||
.iter()
|
||||
.any(|w| w.message.contains("using fetch_cache_pp")),
|
||||
"expected fetch_cache_pp warning"
|
||||
);
|
||||
}
|
||||
|
||||
70
tests/test_from_tal_discovery_cov.rs
Normal file
70
tests/test_from_tal_discovery_cov.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rpki::data_model::tal::Tal;
|
||||
use rpki::sync::rrdp::Fetcher;
|
||||
use rpki::validation::from_tal::{FromTalError, discover_root_ca_instance_from_tal_url};
|
||||
|
||||
#[derive(Default)]
|
||||
struct MapFetcher {
|
||||
map: HashMap<String, Result<Vec<u8>, String>>,
|
||||
}
|
||||
|
||||
impl Fetcher for MapFetcher {
|
||||
fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
|
||||
self.map
|
||||
.get(uri)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Err(format!("no fixture for uri: {uri}")))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_root_ca_instance_from_tal_url_succeeds_with_apnic_fixtures() {
|
||||
let tal_url = "https://example.test/apnic.tal";
|
||||
let tal_bytes =
|
||||
std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal").expect("read tal fixture");
|
||||
let tal = Tal::decode_bytes(&tal_bytes).expect("decode tal fixture");
|
||||
let ta_der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read ta fixture");
|
||||
|
||||
let mut fetcher = MapFetcher::default();
|
||||
fetcher
|
||||
.map
|
||||
.insert(tal_url.to_string(), Ok(tal_bytes.clone()));
|
||||
for u in &tal.ta_uris {
|
||||
fetcher.map.insert(u.as_str().to_string(), Ok(ta_der.clone()));
|
||||
}
|
||||
|
||||
let out = discover_root_ca_instance_from_tal_url(&fetcher, tal_url).expect("discover root");
|
||||
assert_eq!(out.tal_url.as_deref(), Some(tal_url));
|
||||
assert!(!out.ca_instance.rsync_base_uri.is_empty());
|
||||
assert!(!out.ca_instance.manifest_rsync_uri.is_empty());
|
||||
assert!(!out.ca_instance.publication_point_rsync_uri.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_root_ca_instance_from_tal_url_returns_ta_fetch_error_when_all_candidates_fail() {
|
||||
let tal_url = "https://example.test/apnic.tal";
|
||||
let tal_bytes =
|
||||
std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal").expect("read tal fixture");
|
||||
let tal = Tal::decode_bytes(&tal_bytes).expect("decode tal fixture");
|
||||
|
||||
let mut fetcher = MapFetcher::default();
|
||||
fetcher
|
||||
.map
|
||||
.insert(tal_url.to_string(), Ok(tal_bytes));
|
||||
for u in &tal.ta_uris {
|
||||
fetcher.map.insert(
|
||||
u.as_str().to_string(),
|
||||
Err("simulated TA fetch failure".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
let err = discover_root_ca_instance_from_tal_url(&fetcher, tal_url).unwrap_err();
|
||||
match err {
|
||||
FromTalError::TaFetch(s) => {
|
||||
assert!(s.contains("fetch"), "{s}");
|
||||
}
|
||||
other => panic!("unexpected error: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,7 +323,7 @@ fn manifest_fallback_pack_is_revalidated_and_rejected_if_stale() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_replay_is_treated_as_failed_fetch_and_uses_fetch_cache_pp() {
|
||||
fn manifest_revalidation_with_unchanged_manifest_is_fresh() {
|
||||
let manifest_path = Path::new(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
@@ -381,9 +381,100 @@ fn manifest_replay_is_treated_as_failed_fetch_and_uses_fetch_cache_pp() {
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
t2,
|
||||
)
|
||||
.expect("second run should treat replay as failed fetch and use cache");
|
||||
.expect("second run should accept revalidation of the same manifest");
|
||||
assert_eq!(second.source, PublicationPointSource::Fresh);
|
||||
assert!(second.warnings.is_empty());
|
||||
assert_eq!(second.pack.manifest_bytes, first.pack.manifest_bytes);
|
||||
assert_eq!(second.pack.manifest_number_be, first.pack.manifest_number_be);
|
||||
assert_eq!(second.pack.files, first.pack.files);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_rollback_is_treated_as_failed_fetch_and_uses_fetch_cache_pp() {
|
||||
let manifest_path = Path::new(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
|
||||
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
|
||||
|
||||
let t1 = manifest.manifest.this_update + time::Duration::seconds(1);
|
||||
let t2 = manifest.manifest.this_update + time::Duration::seconds(2);
|
||||
|
||||
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
|
||||
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
|
||||
store
|
||||
.put_raw(&manifest_rsync_uri, &manifest_bytes)
|
||||
.expect("store manifest");
|
||||
let entries = manifest
|
||||
.manifest
|
||||
.parse_files()
|
||||
.expect("parse validated manifest fileList");
|
||||
for entry in &entries {
|
||||
let file_path = manifest_path
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join(entry.file_name.as_str());
|
||||
let bytes = std::fs::read(&file_path)
|
||||
.unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}"));
|
||||
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
|
||||
store.put_raw(&rsync_uri, &bytes).expect("store file");
|
||||
}
|
||||
|
||||
let policy = Policy::default();
|
||||
let issuer_ca_der = issuer_ca_fixture();
|
||||
|
||||
let first = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
t1,
|
||||
)
|
||||
.expect("first run builds and stores fetch_cache_pp pack");
|
||||
assert_eq!(first.source, PublicationPointSource::Fresh);
|
||||
|
||||
// Simulate a previously validated manifest with a higher manifestNumber (rollback detection).
|
||||
let key = FetchCachePpKey::from_manifest_rsync_uri(&manifest_rsync_uri);
|
||||
let stored = store
|
||||
.get_fetch_cache_pp(&key)
|
||||
.expect("get fetch_cache_pp")
|
||||
.expect("fetch_cache_pp pack exists");
|
||||
let mut bumped = FetchCachePpPack::decode(&stored).expect("decode stored pack");
|
||||
// Deterministically bump the cached manifestNumber to be strictly greater than the current one.
|
||||
for i in (0..bumped.manifest_number_be.len()).rev() {
|
||||
let (v, carry) = bumped.manifest_number_be[i].overflowing_add(1);
|
||||
bumped.manifest_number_be[i] = v;
|
||||
if !carry {
|
||||
break;
|
||||
}
|
||||
if i == 0 {
|
||||
bumped.manifest_number_be.insert(0, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let bumped_bytes = bumped.encode().expect("encode bumped pack");
|
||||
store
|
||||
.put_fetch_cache_pp(&key, &bumped_bytes)
|
||||
.expect("store bumped pack");
|
||||
|
||||
let second = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
t2,
|
||||
)
|
||||
.expect("second run should treat rollback as failed fetch and use cache");
|
||||
assert_eq!(second.source, PublicationPointSource::FetchCachePp);
|
||||
assert_eq!(second.pack, first.pack);
|
||||
assert_eq!(second.pack, bumped);
|
||||
assert!(
|
||||
second
|
||||
.warnings
|
||||
|
||||
159
tests/test_manifest_processor_more_errors_cov.rs
Normal file
159
tests/test_manifest_processor_more_errors_cov.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
use std::path::Path;
|
||||
|
||||
use rpki::data_model::manifest::ManifestObject;
|
||||
use rpki::policy::Policy;
|
||||
use rpki::storage::RocksStore;
|
||||
use rpki::validation::manifest::{ManifestProcessError, PublicationPointSource, process_manifest_publication_point};
|
||||
|
||||
fn issuer_ca_fixture_der() -> Vec<u8> {
|
||||
std::fs::read(
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
||||
)
|
||||
.expect("read issuer ca fixture")
|
||||
}
|
||||
|
||||
fn issuer_ca_rsync_uri() -> &'static str {
|
||||
"rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer"
|
||||
}
|
||||
|
||||
fn fixture_to_rsync_uri(path: &Path) -> String {
|
||||
let rel = path
|
||||
.strip_prefix("tests/fixtures/repository")
|
||||
.expect("path under tests/fixtures/repository");
|
||||
let mut it = rel.components();
|
||||
let host = it
|
||||
.next()
|
||||
.expect("host component")
|
||||
.as_os_str()
|
||||
.to_string_lossy();
|
||||
let rest = it.as_path().to_string_lossy();
|
||||
format!("rsync://{host}/{rest}")
|
||||
}
|
||||
|
||||
fn fixture_dir_to_rsync_uri(dir: &Path) -> String {
|
||||
let mut s = fixture_to_rsync_uri(dir);
|
||||
if !s.ends_with('/') {
|
||||
s.push('/');
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_outside_publication_point_yields_no_usable_cache() {
|
||||
let manifest_path = Path::new(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
|
||||
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
|
||||
let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);
|
||||
|
||||
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
|
||||
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
|
||||
// Store manifest and its locked files so Fresh would otherwise succeed.
|
||||
store
|
||||
.put_raw(&manifest_rsync_uri, &manifest_bytes)
|
||||
.expect("store manifest");
|
||||
for entry in manifest
|
||||
.manifest
|
||||
.parse_files()
|
||||
.expect("parse fileList")
|
||||
.iter()
|
||||
{
|
||||
let file_path = manifest_path
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join(entry.file_name.as_str());
|
||||
let bytes = std::fs::read(&file_path).expect("read referenced file");
|
||||
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
|
||||
store.put_raw(&rsync_uri, &bytes).expect("store file");
|
||||
}
|
||||
|
||||
let policy = Policy::default();
|
||||
let issuer_ca_der = issuer_ca_fixture_der();
|
||||
|
||||
// Pass a publication point URI that does not include the manifest URI.
|
||||
let wrong_pp = "rsync://example.test/not-the-pp/";
|
||||
let err = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
wrong_pp,
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
// With no cached pack available for this wrong publication point, we get NoUsableCache.
|
||||
assert!(matches!(err, ManifestProcessError::NoUsableCache { .. }), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_outside_publication_point_detects_cached_pack_pp_mismatch() {
|
||||
let manifest_path = Path::new(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
|
||||
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
|
||||
let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);
|
||||
|
||||
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
|
||||
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
|
||||
store
|
||||
.put_raw(&manifest_rsync_uri, &manifest_bytes)
|
||||
.expect("store manifest");
|
||||
for entry in manifest
|
||||
.manifest
|
||||
.parse_files()
|
||||
.expect("parse fileList")
|
||||
.iter()
|
||||
{
|
||||
let file_path = manifest_path
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join(entry.file_name.as_str());
|
||||
let bytes = std::fs::read(&file_path).expect("read referenced file");
|
||||
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
|
||||
store.put_raw(&rsync_uri, &bytes).expect("store file");
|
||||
}
|
||||
|
||||
let policy = Policy::default();
|
||||
let issuer_ca_der = issuer_ca_fixture_der();
|
||||
|
||||
// First run creates and stores fetch_cache_pp pack (Fresh).
|
||||
let first = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.expect("first run ok");
|
||||
assert_eq!(first.source, PublicationPointSource::Fresh);
|
||||
|
||||
// Second run with wrong publication point: fresh fails outside PP; cache load also fails
|
||||
// because the cached pack's publication_point_rsync_uri doesn't match the expected one.
|
||||
let wrong_pp = "rsync://example.test/not-the-pp/";
|
||||
let err = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
wrong_pp,
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ManifestProcessError::NoUsableCache { .. }), "{err}");
|
||||
}
|
||||
|
||||
603
tests/test_manifest_processor_repo_sync_and_cached_pack_cov.rs
Normal file
603
tests/test_manifest_processor_repo_sync_and_cached_pack_cov.rs
Normal file
@@ -0,0 +1,603 @@
|
||||
use std::path::Path;
|
||||
|
||||
use rpki::data_model::manifest::ManifestObject;
|
||||
use rpki::policy::{CaFailedFetchPolicy, Policy};
|
||||
use rpki::storage::{FetchCachePpKey, FetchCachePpPack, RocksStore};
|
||||
use rpki::validation::manifest::{ManifestProcessError, PublicationPointSource, process_manifest_publication_point, process_manifest_publication_point_after_repo_sync};
|
||||
|
||||
fn issuer_ca_fixture_der() -> Vec<u8> {
|
||||
std::fs::read(
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
||||
)
|
||||
.expect("read issuer ca fixture")
|
||||
}
|
||||
|
||||
fn issuer_ca_rsync_uri() -> &'static str {
|
||||
"rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer"
|
||||
}
|
||||
|
||||
fn fixture_to_rsync_uri(path: &Path) -> String {
|
||||
let rel = path
|
||||
.strip_prefix("tests/fixtures/repository")
|
||||
.expect("path under tests/fixtures/repository");
|
||||
let mut it = rel.components();
|
||||
let host = it
|
||||
.next()
|
||||
.expect("host component")
|
||||
.as_os_str()
|
||||
.to_string_lossy();
|
||||
let rest = it.as_path().to_string_lossy();
|
||||
format!("rsync://{host}/{rest}")
|
||||
}
|
||||
|
||||
fn fixture_dir_to_rsync_uri(dir: &Path) -> String {
|
||||
let mut s = fixture_to_rsync_uri(dir);
|
||||
if !s.ends_with('/') {
|
||||
s.push('/');
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn store_manifest_and_locked_files(
|
||||
store: &RocksStore,
|
||||
manifest_path: &Path,
|
||||
manifest_rsync_uri: &str,
|
||||
publication_point_rsync_uri: &str,
|
||||
manifest: &ManifestObject,
|
||||
manifest_bytes: &[u8],
|
||||
) {
|
||||
store
|
||||
.put_raw(manifest_rsync_uri, manifest_bytes)
|
||||
.expect("store manifest");
|
||||
for entry in manifest
|
||||
.manifest
|
||||
.parse_files()
|
||||
.expect("parse validated manifest fileList")
|
||||
.iter()
|
||||
{
|
||||
let file_path = manifest_path
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join(entry.file_name.as_str());
|
||||
let bytes = std::fs::read(&file_path)
|
||||
.unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}"));
|
||||
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
|
||||
store.put_raw(&rsync_uri, &bytes).expect("store file");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repo_sync_failed_can_fall_back_to_fetch_cache_pp_when_present() {
|
||||
let manifest_path = Path::new(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
|
||||
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
|
||||
let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);
|
||||
|
||||
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
|
||||
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
store_manifest_and_locked_files(
|
||||
&store,
|
||||
manifest_path,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&manifest,
|
||||
&manifest_bytes,
|
||||
);
|
||||
|
||||
let issuer_ca_der = issuer_ca_fixture_der();
|
||||
|
||||
// First run: build and store a valid fetch_cache_pp pack (Fresh).
|
||||
let policy = Policy::default();
|
||||
let first = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.expect("first run ok");
|
||||
assert_eq!(first.source, PublicationPointSource::Fresh);
|
||||
|
||||
// Second run: simulate RRDP/rsync repo sync failure and ensure we still accept the cached pack.
|
||||
let second = process_manifest_publication_point_after_repo_sync(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
false,
|
||||
Some("repo sync failed in test"),
|
||||
)
|
||||
.expect("repo sync failure should fall back to fetch_cache_pp");
|
||||
assert_eq!(second.source, PublicationPointSource::FetchCachePp);
|
||||
assert_eq!(second.pack, first.pack);
|
||||
assert!(!second.warnings.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_pack_manifest_rsync_uri_mismatch_is_rejected_as_invalid_pack() {
|
||||
let manifest_path = Path::new(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
|
||||
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
|
||||
let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);
|
||||
|
||||
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
|
||||
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
store_manifest_and_locked_files(
|
||||
&store,
|
||||
manifest_path,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&manifest,
|
||||
&manifest_bytes,
|
||||
);
|
||||
|
||||
let issuer_ca_der = issuer_ca_fixture_der();
|
||||
let policy = Policy::default();
|
||||
|
||||
// First run stores a valid pack.
|
||||
let _ = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.expect("first run stores pack");
|
||||
|
||||
// Corrupt cached pack metadata: manifest_rsync_uri doesn't match the key.
|
||||
let key = FetchCachePpKey::from_manifest_rsync_uri(&manifest_rsync_uri);
|
||||
let cached_bytes = store
|
||||
.get_fetch_cache_pp(&key)
|
||||
.expect("get fetch_cache_pp")
|
||||
.expect("fetch_cache_pp exists");
|
||||
let mut pack = FetchCachePpPack::decode(&cached_bytes).expect("decode pack");
|
||||
pack.manifest_rsync_uri = "rsync://example.test/wrong.mft".to_string();
|
||||
store
|
||||
.put_fetch_cache_pp(&key, &pack.encode().expect("encode pack"))
|
||||
.expect("store corrupted pack");
|
||||
|
||||
// Force fresh failure and trigger cache load.
|
||||
let err = process_manifest_publication_point_after_repo_sync(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
false,
|
||||
Some("repo sync failed in test"),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, ManifestProcessError::NoUsableCache { .. }),
|
||||
"{err}"
|
||||
);
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("cached pack manifest_rsync_uri does not match key"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repo_sync_failed_stop_all_output_skips_cache() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
|
||||
let mut policy = Policy::default();
|
||||
policy.ca_failed_fetch_policy = CaFailedFetchPolicy::StopAllOutput;
|
||||
|
||||
let issuer_ca_der = issuer_ca_fixture_der();
|
||||
let err = process_manifest_publication_point_after_repo_sync(
|
||||
&store,
|
||||
&policy,
|
||||
"rsync://example.test/pp/manifest.mft",
|
||||
"rsync://example.test/pp/",
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
time::OffsetDateTime::now_utc(),
|
||||
false,
|
||||
Some("repo sync failed in test"),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
err.to_string().contains("repo sync failed"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_missing_locked_file_is_treated_as_failed_fetch() {
|
||||
let manifest_path = Path::new(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
|
||||
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
|
||||
let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);
|
||||
|
||||
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
|
||||
let pp_with_slash = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
|
||||
let publication_point_rsync_uri = pp_with_slash.trim_end_matches('/'); // exercise join/pp normalization branches
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
store
|
||||
.put_raw(&manifest_rsync_uri, &manifest_bytes)
|
||||
.expect("store manifest only (no locked files)");
|
||||
|
||||
let issuer_ca_der = issuer_ca_fixture_der();
|
||||
let policy = Policy::default();
|
||||
let err = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, ManifestProcessError::NoUsableCache { .. }),
|
||||
"{err}"
|
||||
);
|
||||
assert!(
|
||||
err.to_string().contains("file missing in raw_objects"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_number_increases_but_this_update_not_increasing_is_failed_fetch() {
|
||||
let manifest_path = Path::new(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
|
||||
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
|
||||
let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);
|
||||
|
||||
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
|
||||
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
store_manifest_and_locked_files(
|
||||
&store,
|
||||
manifest_path,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&manifest,
|
||||
&manifest_bytes,
|
||||
);
|
||||
|
||||
let issuer_ca_der = issuer_ca_fixture_der();
|
||||
let policy = Policy::default();
|
||||
|
||||
// Build and store a valid pack first.
|
||||
let _ = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.expect("first run stores pack");
|
||||
|
||||
// Replace the cached pack with an "older" manifestNumber but a newer thisUpdate to trigger
|
||||
// RFC 9286 §4.2.1 thisUpdate monotonicity failure on the fresh path.
|
||||
let key = FetchCachePpKey::from_manifest_rsync_uri(&manifest_rsync_uri);
|
||||
let cached_bytes = store
|
||||
.get_fetch_cache_pp(&key)
|
||||
.expect("get fetch_cache_pp")
|
||||
.expect("fetch_cache_pp exists");
|
||||
let mut old_pack = FetchCachePpPack::decode(&cached_bytes).expect("decode pack");
|
||||
old_pack.manifest_number_be = vec![0];
|
||||
old_pack.this_update = rpki::storage::PackTime::from_utc_offset_datetime(
|
||||
manifest.manifest.this_update + time::Duration::hours(24),
|
||||
);
|
||||
store
|
||||
.put_fetch_cache_pp(&key, &old_pack.encode().expect("encode pack"))
|
||||
.expect("store adjusted pack");
|
||||
|
||||
let out = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.expect("should fall back to fetch_cache_pp");
|
||||
assert_eq!(out.source, PublicationPointSource::FetchCachePp);
|
||||
assert!(
|
||||
out.warnings
|
||||
.iter()
|
||||
.any(|w| w.message.contains("thisUpdate not more recent")),
|
||||
"expected warning mentioning thisUpdate monotonicity"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_number_equal_but_bytes_differ_is_rejected_without_cache() {
|
||||
let manifest_path = Path::new(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
|
||||
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
|
||||
let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);
|
||||
|
||||
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
|
||||
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
store_manifest_and_locked_files(
|
||||
&store,
|
||||
manifest_path,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&manifest,
|
||||
&manifest_bytes,
|
||||
);
|
||||
|
||||
let issuer_ca_der = issuer_ca_fixture_der();
|
||||
let mut policy = Policy::default();
|
||||
policy.ca_failed_fetch_policy = CaFailedFetchPolicy::StopAllOutput;
|
||||
|
||||
// Store a cached pack that has the same manifestNumber but different manifest bytes.
|
||||
let _ = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.expect("first run stores pack");
|
||||
|
||||
let key = FetchCachePpKey::from_manifest_rsync_uri(&manifest_rsync_uri);
|
||||
let cached_bytes = store
|
||||
.get_fetch_cache_pp(&key)
|
||||
.expect("get fetch_cache_pp")
|
||||
.expect("fetch_cache_pp exists");
|
||||
let mut pack = FetchCachePpPack::decode(&cached_bytes).expect("decode pack");
|
||||
pack.manifest_bytes[0] ^= 0xFF;
|
||||
store
|
||||
.put_fetch_cache_pp(&key, &pack.encode().expect("encode pack"))
|
||||
.expect("store adjusted pack");
|
||||
|
||||
let err = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, ManifestProcessError::StopAllOutput(_)),
|
||||
"{err}"
|
||||
);
|
||||
assert!(
|
||||
err.to_string().contains("manifestNumber not higher"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_embedded_ee_cert_path_validation_fails_with_wrong_issuer_ca() {
|
||||
let manifest_path = Path::new(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
|
||||
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
|
||||
let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);
|
||||
|
||||
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
|
||||
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
store_manifest_and_locked_files(
|
||||
&store,
|
||||
manifest_path,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&manifest,
|
||||
&manifest_bytes,
|
||||
);
|
||||
|
||||
// Deliberately use a mismatched trust anchor as the "issuer CA" for this publication point.
|
||||
let wrong_issuer_ca_der =
|
||||
std::fs::read("tests/fixtures/ta/arin-ta.cer").expect("read wrong issuer fixture");
|
||||
|
||||
let mut policy = Policy::default();
|
||||
policy.ca_failed_fetch_policy = CaFailedFetchPolicy::StopAllOutput;
|
||||
let err = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&wrong_issuer_ca_der,
|
||||
None,
|
||||
validation_time,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
err.to_string().contains("path validation failed"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_pack_missing_file_is_rejected_during_revalidation() {
|
||||
let manifest_path = Path::new(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
|
||||
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
|
||||
let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);
|
||||
|
||||
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
|
||||
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
store_manifest_and_locked_files(
|
||||
&store,
|
||||
manifest_path,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&manifest,
|
||||
&manifest_bytes,
|
||||
);
|
||||
|
||||
let issuer_ca_der = issuer_ca_fixture_der();
|
||||
let policy = Policy::default();
|
||||
|
||||
// Store a valid pack first.
|
||||
let _ = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.expect("first run stores pack");
|
||||
|
||||
// Corrupt cached pack by removing one referenced file.
|
||||
let key = FetchCachePpKey::from_manifest_rsync_uri(&manifest_rsync_uri);
|
||||
let cached_bytes = store
|
||||
.get_fetch_cache_pp(&key)
|
||||
.expect("get fetch_cache_pp")
|
||||
.expect("fetch_cache_pp exists");
|
||||
let mut pack = FetchCachePpPack::decode(&cached_bytes).expect("decode pack");
|
||||
assert!(!pack.files.is_empty(), "fixture should lock some files");
|
||||
pack.files.pop();
|
||||
store
|
||||
.put_fetch_cache_pp(&key, &pack.encode().expect("encode pack"))
|
||||
.expect("store corrupted pack");
|
||||
|
||||
// Force the fresh path to fail and trigger cache revalidation.
|
||||
let err = process_manifest_publication_point_after_repo_sync(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
false,
|
||||
Some("repo sync failed in test"),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, ManifestProcessError::NoUsableCache { .. }),
|
||||
"{err}"
|
||||
);
|
||||
assert!(
|
||||
err.to_string().contains("cached fetch_cache_pp missing file"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_pack_hash_mismatch_is_rejected_during_revalidation() {
|
||||
let manifest_path = Path::new(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
);
|
||||
let manifest_bytes = std::fs::read(manifest_path).expect("read manifest fixture");
|
||||
let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture");
|
||||
let validation_time = manifest.manifest.this_update + time::Duration::seconds(1);
|
||||
|
||||
let manifest_rsync_uri = fixture_to_rsync_uri(manifest_path);
|
||||
let publication_point_rsync_uri = fixture_dir_to_rsync_uri(manifest_path.parent().unwrap());
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(temp.path()).expect("open rocksdb");
|
||||
store_manifest_and_locked_files(
|
||||
&store,
|
||||
manifest_path,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&manifest,
|
||||
&manifest_bytes,
|
||||
);
|
||||
|
||||
let issuer_ca_der = issuer_ca_fixture_der();
|
||||
let policy = Policy::default();
|
||||
|
||||
// Store a valid pack first.
|
||||
let _ = process_manifest_publication_point(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
)
|
||||
.expect("first run stores pack");
|
||||
|
||||
// Corrupt cached pack by changing one file's bytes+sha256 so internal validation passes,
|
||||
// but the manifest fileList binding check fails (RFC 9286 §6.5).
|
||||
let key = FetchCachePpKey::from_manifest_rsync_uri(&manifest_rsync_uri);
|
||||
let cached_bytes = store
|
||||
.get_fetch_cache_pp(&key)
|
||||
.expect("get fetch_cache_pp")
|
||||
.expect("fetch_cache_pp exists");
|
||||
let mut pack = FetchCachePpPack::decode(&cached_bytes).expect("decode pack");
|
||||
let victim = pack.files.first_mut().expect("non-empty file list");
|
||||
victim.bytes[0] ^= 0xFF;
|
||||
victim.sha256 = victim.compute_sha256();
|
||||
store
|
||||
.put_fetch_cache_pp(&key, &pack.encode().expect("encode pack"))
|
||||
.expect("store corrupted pack");
|
||||
|
||||
let err = process_manifest_publication_point_after_repo_sync(
|
||||
&store,
|
||||
&policy,
|
||||
&manifest_rsync_uri,
|
||||
&publication_point_rsync_uri,
|
||||
&issuer_ca_der,
|
||||
Some(issuer_ca_rsync_uri()),
|
||||
validation_time,
|
||||
false,
|
||||
Some("repo sync failed in test"),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, ManifestProcessError::NoUsableCache { .. }),
|
||||
"{err}"
|
||||
);
|
||||
assert!(
|
||||
err.to_string().contains("cached fetch_cache_pp file hash mismatch"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user