use std::path::Path; use sha2::Digest; use rpki::data_model::manifest::ManifestObject; use rpki::policy::{CaFailedFetchPolicy, Policy}; use rpki::storage::{ PackTime, RawByHashEntry, RocksStore, ValidatedCaInstanceResult, ValidatedManifestMeta, VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary, VcirInstanceGate, VcirRelatedArtifact, VcirSummary, }; use rpki::validation::manifest::{PublicationPointSource, process_manifest_publication_point}; fn issuer_ca_fixture() -> Vec { 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_validated_manifest_baseline( store: &RocksStore, manifest_rsync_uri: &str, manifest_bytes: &[u8], manifest_number_be: Vec, this_update: time::OffsetDateTime, next_update: time::OffsetDateTime, ) { let manifest_sha256 = hex::encode(sha2::Sha256::digest(manifest_bytes)); let mut manifest_raw = RawByHashEntry::from_bytes(manifest_sha256.clone(), manifest_bytes.to_vec()); manifest_raw .origin_uris .push(manifest_rsync_uri.to_string()); manifest_raw.object_type = Some("mft".to_string()); manifest_raw.encoding = Some("der".to_string()); store .put_raw_by_hash_entry(&manifest_raw) .expect("store VCIR manifest raw_by_hash"); let vcir = ValidatedCaInstanceResult { manifest_rsync_uri: manifest_rsync_uri.to_string(), parent_manifest_rsync_uri: None, tal_id: "test-tal".to_string(), ca_subject_name: "CN=test".to_string(), ca_ski: "aa".to_string(), issuer_ski: "aa".to_string(), last_successful_validation_time: PackTime::from_utc_offset_datetime(this_update), current_manifest_rsync_uri: manifest_rsync_uri.to_string(), current_crl_rsync_uri: format!("{manifest_rsync_uri}.crl"), validated_manifest_meta: ValidatedManifestMeta { validated_manifest_number: manifest_number_be, validated_manifest_this_update: PackTime::from_utc_offset_datetime(this_update), validated_manifest_next_update: PackTime::from_utc_offset_datetime(next_update), }, instance_gate: VcirInstanceGate { manifest_next_update: PackTime::from_utc_offset_datetime(next_update), current_crl_next_update: PackTime::from_utc_offset_datetime(next_update), self_ca_not_after: PackTime::from_utc_offset_datetime(next_update), instance_effective_until: PackTime::from_utc_offset_datetime(next_update), }, child_entries: Vec::new(), local_outputs: Vec::new(), related_artifacts: vec![VcirRelatedArtifact { artifact_role: VcirArtifactRole::Manifest, artifact_kind: VcirArtifactKind::Mft, uri: Some(manifest_rsync_uri.to_string()), sha256: manifest_sha256, object_type: Some("mft".to_string()), validation_status: VcirArtifactValidationStatus::Accepted, }], summary: VcirSummary { local_vrp_count: 0, local_aspa_count: 0, child_count: 0, accepted_object_count: 1, rejected_object_count: 0, }, audit_summary: VcirAuditSummary { failed_fetch_eligible: true, last_failed_fetch_reason: None, warning_count: 0, audit_flags: Vec::new(), }, }; store .put_vcir(&vcir) .expect("store validated manifest baseline"); } #[test] fn manifest_success_returns_validated_publication_point_data() { 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"); 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 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("process manifest publication point"); assert_eq!(out.source, PublicationPointSource::Fresh); assert!(out.warnings.is_empty()); assert_eq!(out.snapshot.manifest_rsync_uri, manifest_rsync_uri); assert_eq!( out.snapshot.publication_point_rsync_uri, publication_point_rsync_uri ); } #[test] fn manifest_hash_mismatch_reuses_current_instance_vcir_when_enabled() { 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"); 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()), validation_time, ) .expect("first run returns validated publication point"); assert_eq!(first.source, PublicationPointSource::Fresh); store_validated_manifest_baseline( &store, &manifest_rsync_uri, &manifest_bytes, manifest.manifest.manifest_number.bytes_be.clone(), manifest.manifest.this_update, manifest.manifest.next_update, ); let entries = manifest .manifest .parse_files() .expect("parse validated manifest fileList"); let victim = entries.first().expect("non-empty file list"); let victim_uri = format!("{publication_point_rsync_uri}{}", victim.file_name); let mut tampered = store .get_raw(&victim_uri) .expect("get victim raw") .expect("victim raw exists"); tampered[0] ^= 0xFF; store.put_raw(&victim_uri, &tampered).expect("tamper raw"); let second = process_manifest_publication_point( &store, &policy, &manifest_rsync_uri, &publication_point_rsync_uri, &issuer_ca_der, Some(issuer_ca_rsync_uri()), validation_time, ) .expect("second run reuses current-instance VCIR"); assert_eq!(second.source, PublicationPointSource::VcirCurrentInstance); assert!( second.warnings.iter().any(|w| w .message .contains("using latest validated result for current CA instance")), "expected current-instance VCIR reuse warning" ); } #[test] fn manifest_failed_fetch_stop_all_output() { 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"); 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 mut policy = Policy::default(); policy.ca_failed_fetch_policy = CaFailedFetchPolicy::ReuseCurrentInstanceVcir; let issuer_ca_der = issuer_ca_fixture(); 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 returns validated publication point"); let entries = manifest .manifest .parse_files() .expect("parse validated manifest fileList"); let victim = entries.first().expect("non-empty file list"); let victim_uri = format!("{publication_point_rsync_uri}{}", victim.file_name); let mut tampered = store .get_raw(&victim_uri) .expect("get victim raw") .expect("victim raw exists"); tampered[0] ^= 0xFF; store.put_raw(&victim_uri, &tampered).expect("tamper raw"); policy.ca_failed_fetch_policy = CaFailedFetchPolicy::StopAllOutput; 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, ) .expect_err("stop_all_output should not reuse current-instance VCIR"); let msg = err.to_string(); assert!(msg.contains("cache use is disabled")); } #[test] fn manifest_failed_fetch_rejects_stale_current_instance_vcir() { 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 ok_time = manifest.manifest.this_update + time::Duration::seconds(1); let stale_time = manifest.manifest.next_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"); 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 _ = process_manifest_publication_point( &store, &policy, &manifest_rsync_uri, &publication_point_rsync_uri, &issuer_ca_der, Some(issuer_ca_rsync_uri()), ok_time, ) .expect("first run returns validated publication point"); store_validated_manifest_baseline( &store, &manifest_rsync_uri, &manifest_bytes, manifest.manifest.manifest_number.bytes_be.clone(), manifest.manifest.this_update, manifest.manifest.next_update, ); store .delete_raw(&manifest_rsync_uri) .expect("delete manifest raw to force fallback"); let err = process_manifest_publication_point( &store, &policy, &manifest_rsync_uri, &publication_point_rsync_uri, &issuer_ca_der, Some(issuer_ca_rsync_uri()), stale_time, ) .expect_err("stale validation_time must reject current-instance VCIR reuse"); let msg = err.to_string(); assert!(msg.contains("instance_gate expired"), "{msg}"); } #[test] fn manifest_revalidation_with_unchanged_manifest_is_fresh() { 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 returns validated publication point"); assert_eq!(first.source, PublicationPointSource::Fresh); store_validated_manifest_baseline( &store, &manifest_rsync_uri, &manifest_bytes, first.snapshot.manifest_number_be.clone(), manifest.manifest.this_update, manifest.manifest.next_update, ); 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 accept revalidation of the same manifest"); assert_eq!(second.source, PublicationPointSource::Fresh); assert!(second.warnings.is_empty()); assert_eq!( second.snapshot.manifest_bytes, first.snapshot.manifest_bytes ); assert_eq!( second.snapshot.manifest_number_be, first.snapshot.manifest_number_be ); assert_eq!(second.snapshot.files, first.snapshot.files); } #[test] fn manifest_rollback_is_treated_as_failed_fetch_and_reuses_current_instance_vcir() { 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 returns validated publication point"); assert_eq!(first.source, PublicationPointSource::Fresh); // Simulate a previously validated manifest with a higher manifestNumber (rollback detection). let mut bumped = first.snapshot.clone(); // 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; } } store_validated_manifest_baseline( &store, &manifest_rsync_uri, &manifest_bytes, bumped.manifest_number_be.clone(), manifest.manifest.this_update, manifest.manifest.next_update, ); 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 reuse current-instance VCIR"); assert_eq!(second.source, PublicationPointSource::VcirCurrentInstance); assert!( second .warnings .iter() .any(|w| w.message.contains("manifestNumber not higher")), "expected warning mentioning manifestNumber monotonicity" ); }