add delta sync and fail fetch process

This commit is contained in:
2026-02-27 18:02:01 +08:00
parent 68cbd3c500
commit 13516c4f73
24 changed files with 4815 additions and 89 deletions

View 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(&notif_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, &notif_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"
);
}

View 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}");
}

View File

@@ -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"
);
}

View 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}"),
}
}

View File

@@ -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

View 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}");
}

View 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}"
);
}