use super::*; use crate::data_model::oid::OID_AD_SIGNED_OBJECT; use crate::data_model::rc::{ AccessDescription, AsIdOrRange, AsIdentifierChoice, AsResourceSet, ResourceCertificate, }; use crate::data_model::roa::RoaAfi; use crate::fetch::rsync::LocalDirRsyncFetcher; use crate::fetch::rsync::{RsyncFetchError, RsyncFetcher}; use crate::storage::{ PackFile, PackTime, PublicationPointCacheProjection, PublicationPointCacheProjectionWriteAction, RawByHashEntry, RepositoryViewEntry, RepositoryViewState, RocksStore, ValidatedCaInstanceResult, ValidatedManifestMeta, VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary, VcirChildEntry, VcirInstanceGate, VcirLocalOutput, VcirLocalOutputPayload, VcirOutputType, VcirRelatedArtifact, VcirSourceObjectType, VcirSummary, }; use crate::sync::rrdp::Fetcher; use crate::validation::publication_point::PublicationPointSnapshot; use crate::validation::tree::{DiscoveredChildEntryProjection, PublicationPointRunner}; use std::process::Command; use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; fn sha256_32(input: &[u8]) -> [u8; 32] { sha256_hex_to_32(&sha256_hex(input)) } fn ipv4_addr(octets: [u8; 4]) -> [u8; 16] { let mut addr = [0u8; 16]; addr[..4].copy_from_slice(&octets); addr } #[test] fn publication_point_cache_policy_fingerprint_includes_resource_validation_mode() { let mut strict_policy = Policy::default(); strict_policy.resource_validation_mode = ResourceValidationMode::Rfc6487; let mut vrs_policy = Policy::default(); vrs_policy.resource_validation_mode = ResourceValidationMode::ValidationUpdate03; assert_ne!( publication_point_cache_policy_fingerprint(&strict_policy), publication_point_cache_policy_fingerprint(&vrs_policy) ); } #[test] fn router_asns_for_resource_mode_filters_vrs_and_rejects_strict_overclaim() { let issuer_as = AsResourceSet { asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![ AsIdOrRange::Range { min: 64500, max: 64510, }, ])), rdi: None, }; let strict = router_asns_for_resource_mode( &[64505, 64520], Some(&issuer_as), ResourceValidationMode::Rfc6487, ) .unwrap_err(); assert!(strict.contains("not a subset"), "{strict}"); let vrs = router_asns_for_resource_mode( &[64505, 64520], Some(&issuer_as), ResourceValidationMode::ValidationUpdate03, ) .expect("vrs filters router asns"); assert_eq!(vrs, vec![64505]); } struct NeverHttpFetcher; impl Fetcher for NeverHttpFetcher { fn fetch(&self, _uri: &str) -> Result, String> { Err("http fetch disabled in test".to_string()) } } struct FailingRsyncFetcher; impl RsyncFetcher for FailingRsyncFetcher { fn fetch_objects( &self, _rsync_base_uri: &str, ) -> Result)>, RsyncFetchError> { Err(RsyncFetchError::Fetch("rsync disabled in test".to_string())) } } fn sample_runner_with_ccr_accumulator<'a>( store: &'a RocksStore, policy: &'a Policy, ) -> Rpkiv1PublicationPointRunner<'a> { Rpkiv1PublicationPointRunner { store, policy, http_fetcher: &NeverHttpFetcher, rsync_fetcher: &FailingRsyncFetcher, validation_time: time::OffsetDateTime::now_utc(), timing: None, download_log: None, replay_archive_index: None, replay_delta_index: None, rrdp_dedup: false, rrdp_repo_cache: Mutex::new(HashMap::new()), rsync_dedup: false, rsync_repo_cache: Mutex::new(HashMap::new()), current_repo_index: None, repo_sync_runtime: None, parallel_phase2_config: None, parallel_roa_worker_pool: None, ccr_accumulator: Some(Mutex::new(CcrAccumulator::new(Vec::new()))), persist_vcir: true, enable_roa_validation_cache: false, enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, } } fn openssl_available() -> bool { Command::new("openssl") .arg("version") .output() .map(|o| o.status.success()) .unwrap_or(false) } struct Generated { issuer_ca_der: Vec, child_ca_der: Vec, issuer_crl_der: Vec, issuer_crl_der_next: Vec, } fn run(cmd: &mut Command) { let out = cmd.output().expect("run command"); if !out.status.success() { panic!( "command failed: {:?}\nstdout={}\nstderr={}", cmd, String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr) ); } } fn generate_chain_and_crl() -> Generated { assert!(openssl_available(), "openssl is required for this test"); let td = tempfile::tempdir().expect("tempdir"); let dir = td.path(); std::fs::create_dir_all(dir.join("newcerts")).expect("newcerts"); std::fs::write(dir.join("index.txt"), b"").expect("index"); std::fs::write(dir.join("serial"), b"1000\n").expect("serial"); std::fs::write(dir.join("crlnumber"), b"1000\n").expect("crlnumber"); let cnf = format!( r#" [ ca ] default_ca = CA_default [ CA_default ] dir = {dir} database = $dir/index.txt new_certs_dir = $dir/newcerts certificate = $dir/issuer.pem private_key = $dir/issuer.key serial = $dir/serial crlnumber = $dir/crlnumber default_md = sha256 default_days = 365 default_crl_days = 1 policy = policy_any x509_extensions = v3_issuer_ca crl_extensions = crl_ext unique_subject = no copy_extensions = none [ policy_any ] commonName = supplied [ req ] prompt = no distinguished_name = dn [ dn ] CN = Test Issuer CA [ v3_issuer_ca ] basicConstraints = critical,CA:true keyUsage = critical, keyCertSign, cRLSign subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always certificatePolicies = critical, 1.3.6.1.5.5.7.14.2 subjectInfoAccess = caRepository;URI:rsync://example.test/repo/issuer/, rpkiManifest;URI:rsync://example.test/repo/issuer/issuer.mft, rpkiNotify;URI:https://example.test/notification.xml sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/8 sbgp-autonomousSysNum = critical, AS:64496-64511 [ v3_child_ca ] basicConstraints = critical,CA:true keyUsage = critical, keyCertSign, cRLSign subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always crlDistributionPoints = URI:rsync://example.test/repo/issuer/issuer.crl authorityInfoAccess = caIssuers;URI:rsync://example.test/repo/issuer/issuer.cer certificatePolicies = critical, 1.3.6.1.5.5.7.14.2 subjectInfoAccess = caRepository;URI:rsync://example.test/repo/child/, rpkiManifest;URI:rsync://example.test/repo/child/child.mft, rpkiNotify;URI:https://example.test/notification.xml sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/16 sbgp-autonomousSysNum = critical, AS:64496 [ crl_ext ] authorityKeyIdentifier = keyid:always "#, dir = dir.display() ); std::fs::write(dir.join("openssl.cnf"), cnf.as_bytes()).expect("write cnf"); run(Command::new("openssl") .arg("genrsa") .arg("-out") .arg(dir.join("issuer.key")) .arg("2048")); run(Command::new("openssl") .arg("req") .arg("-new") .arg("-x509") .arg("-sha256") .arg("-days") .arg("365") .arg("-key") .arg(dir.join("issuer.key")) .arg("-config") .arg(dir.join("openssl.cnf")) .arg("-extensions") .arg("v3_issuer_ca") .arg("-out") .arg(dir.join("issuer.pem"))); run(Command::new("openssl") .arg("genrsa") .arg("-out") .arg(dir.join("child.key")) .arg("2048")); run(Command::new("openssl") .arg("req") .arg("-new") .arg("-key") .arg(dir.join("child.key")) .arg("-subj") .arg("/CN=Test Child CA") .arg("-out") .arg(dir.join("child.csr"))); run(Command::new("openssl") .arg("ca") .arg("-batch") .arg("-config") .arg(dir.join("openssl.cnf")) .arg("-in") .arg(dir.join("child.csr")) .arg("-extensions") .arg("v3_child_ca") .arg("-out") .arg(dir.join("child.pem"))); run(Command::new("openssl") .arg("x509") .arg("-in") .arg(dir.join("issuer.pem")) .arg("-outform") .arg("DER") .arg("-out") .arg(dir.join("issuer.cer"))); run(Command::new("openssl") .arg("x509") .arg("-in") .arg(dir.join("child.pem")) .arg("-outform") .arg("DER") .arg("-out") .arg(dir.join("child.cer"))); run(Command::new("openssl") .arg("ca") .arg("-gencrl") .arg("-config") .arg(dir.join("openssl.cnf")) .arg("-out") .arg(dir.join("issuer.crl.pem"))); run(Command::new("openssl") .arg("crl") .arg("-in") .arg(dir.join("issuer.crl.pem")) .arg("-outform") .arg("DER") .arg("-out") .arg(dir.join("issuer.crl"))); run(Command::new("openssl") .arg("ca") .arg("-gencrl") .arg("-config") .arg(dir.join("openssl.cnf")) .arg("-out") .arg(dir.join("issuer-next.crl.pem"))); run(Command::new("openssl") .arg("crl") .arg("-in") .arg(dir.join("issuer-next.crl.pem")) .arg("-outform") .arg("DER") .arg("-out") .arg(dir.join("issuer-next.crl"))); Generated { issuer_ca_der: std::fs::read(dir.join("issuer.cer")).expect("read issuer der"), child_ca_der: std::fs::read(dir.join("child.cer")).expect("read child der"), issuer_crl_der: std::fs::read(dir.join("issuer.crl")).expect("read crl der"), issuer_crl_der_next: std::fs::read(dir.join("issuer-next.crl")).expect("read next crl der"), } } struct GeneratedRouter { issuer_ca_der: Vec, router_der: Vec, issuer_crl_der: Vec, } fn generate_router_cert_with_variant(key_spec: &str, include_eku: bool) -> GeneratedRouter { assert!(openssl_available(), "openssl is required for this test"); let td = tempfile::tempdir().expect("tempdir"); let dir = td.path(); std::fs::create_dir_all(dir.join("newcerts")).expect("newcerts"); std::fs::write(dir.join("index.txt"), b"").expect("index"); std::fs::write(dir.join("serial"), b"1000\n").expect("serial"); std::fs::write(dir.join("crlnumber"), b"1000\n").expect("crlnumber"); let eku_line = if include_eku { "extendedKeyUsage = 1.3.6.1.5.5.7.3.30" } else { "" }; let cnf = format!( r#" [ ca ] default_ca = CA_default [ CA_default ] dir = {dir} database = $dir/index.txt new_certs_dir = $dir/newcerts certificate = $dir/issuer.pem private_key = $dir/issuer.key serial = $dir/serial crlnumber = $dir/crlnumber default_md = sha256 default_days = 365 default_crl_days = 1 policy = policy_any x509_extensions = v3_issuer_ca crl_extensions = crl_ext unique_subject = no copy_extensions = none [ policy_any ] commonName = supplied [ req ] prompt = no distinguished_name = dn [ dn ] CN = Test Issuer CA [ v3_issuer_ca ] basicConstraints = critical,CA:true keyUsage = critical, keyCertSign, cRLSign subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always certificatePolicies = critical, 1.3.6.1.5.5.7.14.2 subjectInfoAccess = caRepository;URI:rsync://example.test/repo/issuer/, rpkiManifest;URI:rsync://example.test/repo/issuer/issuer.mft, rpkiNotify;URI:https://example.test/notification.xml sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/8 sbgp-autonomousSysNum = critical, AS:64496-64511 [ v3_router ] keyUsage = critical, digitalSignature {eku_line} authorityKeyIdentifier = keyid:always crlDistributionPoints = URI:rsync://example.test/repo/issuer/issuer.crl authorityInfoAccess = caIssuers;URI:rsync://example.test/repo/issuer/issuer.cer certificatePolicies = critical, 1.3.6.1.5.5.7.14.2 sbgp-autonomousSysNum = critical, AS:64496 [ crl_ext ] authorityKeyIdentifier = keyid:always "#, dir = dir.display(), eku_line = eku_line, ); std::fs::write(dir.join("openssl.cnf"), cnf.as_bytes()).expect("write cnf"); run(Command::new("openssl") .arg("genrsa") .arg("-out") .arg(dir.join("issuer.key")) .arg("2048")); run(Command::new("openssl") .arg("req") .arg("-new") .arg("-x509") .arg("-sha256") .arg("-days") .arg("365") .arg("-key") .arg(dir.join("issuer.key")) .arg("-config") .arg(dir.join("openssl.cnf")) .arg("-extensions") .arg("v3_issuer_ca") .arg("-out") .arg(dir.join("issuer.pem"))); match key_spec { "ec-p256" => run(Command::new("openssl") .arg("ecparam") .arg("-name") .arg("prime256v1") .arg("-genkey") .arg("-noout") .arg("-out") .arg(dir.join("router.key"))), "ec-p384" => run(Command::new("openssl") .arg("ecparam") .arg("-name") .arg("secp384r1") .arg("-genkey") .arg("-noout") .arg("-out") .arg(dir.join("router.key"))), other => panic!("unsupported key_spec {other}"), } run(Command::new("openssl") .arg("req") .arg("-new") .arg("-key") .arg(dir.join("router.key")) .arg("-subj") .arg("/CN=ROUTER-0000FC10/serialNumber=01020304") .arg("-out") .arg(dir.join("router.csr"))); run(Command::new("openssl") .arg("ca") .arg("-batch") .arg("-config") .arg(dir.join("openssl.cnf")) .arg("-in") .arg(dir.join("router.csr")) .arg("-extensions") .arg("v3_router") .arg("-out") .arg(dir.join("router.pem"))); run(Command::new("openssl") .arg("x509") .arg("-in") .arg(dir.join("issuer.pem")) .arg("-outform") .arg("DER") .arg("-out") .arg(dir.join("issuer.cer"))); run(Command::new("openssl") .arg("x509") .arg("-in") .arg(dir.join("router.pem")) .arg("-outform") .arg("DER") .arg("-out") .arg(dir.join("router.cer"))); run(Command::new("openssl") .arg("ca") .arg("-gencrl") .arg("-config") .arg(dir.join("openssl.cnf")) .arg("-out") .arg(dir.join("issuer.crl.pem"))); run(Command::new("openssl") .arg("crl") .arg("-in") .arg(dir.join("issuer.crl.pem")) .arg("-outform") .arg("DER") .arg("-out") .arg(dir.join("issuer.crl"))); GeneratedRouter { issuer_ca_der: std::fs::read(dir.join("issuer.cer")).expect("read issuer der"), router_der: std::fs::read(dir.join("router.cer")).expect("read router der"), issuer_crl_der: std::fs::read(dir.join("issuer.crl")).expect("read crl der"), } } fn dummy_pack_with_files(files: Vec) -> PublicationPointSnapshot { let now = time::OffsetDateTime::now_utc(); PublicationPointSnapshot { format_version: PublicationPointSnapshot::FORMAT_VERSION_V1, manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_number_be: vec![1], this_update: PackTime::from_utc_offset_datetime(now), next_update: PackTime::from_utc_offset_datetime(now + time::Duration::hours(1)), verified_at: PackTime::from_utc_offset_datetime(now), manifest_bytes: vec![0x01], files, } } fn cernet_publication_point_snapshot_for_vcir_tests() -> (PublicationPointSnapshot, Vec, time::OffsetDateTime) { let dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0"); let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/"; let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft"; let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}"); let manifest_bytes = std::fs::read(dir.join(manifest_file)).expect("read manifest fixture"); let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode manifest fixture"); let candidate = manifest.manifest.this_update + time::Duration::seconds(60); let validation_time = if candidate < manifest.manifest.next_update { candidate } else { manifest.manifest.this_update }; let issuer_ca_der = std::fs::read( std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer", ), ) .expect("read issuer ca fixture"); let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let policy = Policy { sync_preference: crate::policy::SyncPreference::RsyncOnly, ..Policy::default() }; sync_publication_point( &store, &policy, None, rsync_base_uri, &NeverHttpFetcher, &LocalDirRsyncFetcher::new(&dir), None, None, ) .expect("sync cernet fixture"); let pp = crate::validation::manifest::process_manifest_publication_point( &store, &policy, &manifest_rsync_uri, rsync_base_uri, issuer_ca_der.as_slice(), Some( "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer", ), validation_time, ) .expect("process manifest publication point"); (pp.snapshot, issuer_ca_der, validation_time) } fn sample_vcir_for_projection( now: time::OffsetDateTime, child_cert_hash: &str, ) -> ValidatedCaInstanceResult { let manifest_uri = "rsync://example.test/repo/issuer/issuer.mft".to_string(); let current_crl_uri = "rsync://example.test/repo/issuer/issuer.crl".to_string(); let child_cert_uri = "rsync://example.test/repo/issuer/child.cer".to_string(); let child_manifest_uri = "rsync://example.test/repo/child/child.mft".to_string(); let roa_uri = "rsync://example.test/repo/issuer/a.roa".to_string(); let aspa_uri = "rsync://example.test/repo/issuer/a.asa".to_string(); let router_uri = "rsync://example.test/repo/issuer/router.cer".to_string(); let manifest_hash = sha256_hex(b"manifest-bytes"); let current_crl_hash = sha256_hex(b"current-crl-bytes"); let roa_hash = sha256_hex(b"roa-bytes"); let aspa_hash = sha256_hex(b"aspa-bytes"); let router_hash = sha256_hex(b"router-bytes"); let ee_hash = sha256_hex(b"ee-cert-bytes"); let gate_until = PackTime::from_utc_offset_datetime(now + time::Duration::hours(1)); let ccr_manifest_projection = VcirCcrManifestProjection { manifest_rsync_uri: manifest_uri.clone(), manifest_sha256: hex::decode(&manifest_hash).expect("decode manifest hash"), manifest_size: 2048, manifest_ee_aki: vec![0x11; 20], manifest_number_be: vec![1], manifest_this_update: PackTime::from_utc_offset_datetime(now), manifest_sia_locations_der: vec![ crate::ccr::manifest_location::encode_access_description_der(&AccessDescription { access_method_oid: OID_AD_SIGNED_OBJECT.to_string(), access_location: manifest_uri.clone(), }) .expect("encode signedObject"), ], subordinate_skis: vec![vec![0x33; 20]], }; ValidatedCaInstanceResult { manifest_rsync_uri: manifest_uri.clone(), parent_manifest_rsync_uri: None, tal_id: "test-tal".to_string(), ca_subject_name: "CN=Issuer".to_string(), ca_ski: "11".repeat(20), issuer_ski: "22".repeat(20), last_successful_validation_time: PackTime::from_utc_offset_datetime(now), current_manifest_rsync_uri: manifest_uri.clone(), current_crl_rsync_uri: current_crl_uri.clone(), validated_manifest_meta: ValidatedManifestMeta { validated_manifest_number: vec![1], validated_manifest_this_update: PackTime::from_utc_offset_datetime(now), validated_manifest_next_update: gate_until.clone(), }, ccr_manifest_projection, instance_gate: VcirInstanceGate { manifest_next_update: gate_until.clone(), current_crl_next_update: gate_until.clone(), self_ca_not_after: PackTime::from_utc_offset_datetime(now + time::Duration::hours(2)), instance_effective_until: gate_until.clone(), }, child_entries: vec![VcirChildEntry { child_manifest_rsync_uri: child_manifest_uri, child_cert_rsync_uri: child_cert_uri.clone(), child_cert_hash: child_cert_hash.to_string(), child_ski: "33".repeat(20), child_rsync_base_uri: "rsync://example.test/repo/child/".to_string(), child_publication_point_rsync_uri: "rsync://example.test/repo/child/".to_string(), child_rrdp_notification_uri: Some("https://example.test/child-notify.xml".to_string()), child_effective_ip_resources: None, child_effective_as_resources: None, accepted_at_validation_time: PackTime::from_utc_offset_datetime(now), }], local_outputs: vec![ VcirLocalOutput { output_type: VcirOutputType::Vrp, item_effective_until: PackTime::from_utc_offset_datetime( now + time::Duration::minutes(30), ), source_object_uri: roa_uri.clone(), source_object_type: VcirSourceObjectType::Roa, source_object_hash: sha256_hex_to_32(&roa_hash), source_ee_cert_hash: sha256_hex_to_32(&ee_hash), payload: VcirLocalOutputPayload::Vrp { asn: 64496, afi: RoaAfi::Ipv4, prefix_len: 24, addr: ipv4_addr([203, 0, 113, 0]), max_length: 24, }, rule_hash: sha256_32(b"roa-rule"), }, VcirLocalOutput { output_type: VcirOutputType::Aspa, item_effective_until: PackTime::from_utc_offset_datetime( now + time::Duration::minutes(30), ), source_object_uri: aspa_uri.clone(), source_object_type: VcirSourceObjectType::Aspa, source_object_hash: sha256_hex_to_32(&aspa_hash), source_ee_cert_hash: sha256_hex_to_32(&ee_hash), payload: VcirLocalOutputPayload::Aspa { customer_as_id: 64496, provider_as_ids: vec![64497, 64498], }, rule_hash: sha256_32(b"aspa-rule"), }, VcirLocalOutput { output_type: VcirOutputType::RouterKey, item_effective_until: PackTime::from_utc_offset_datetime( now + time::Duration::minutes(30), ), source_object_uri: router_uri.clone(), source_object_type: VcirSourceObjectType::RouterKey, source_object_hash: sha256_hex_to_32(&router_hash), source_ee_cert_hash: sha256_hex_to_32(&router_hash), payload: VcirLocalOutputPayload::RouterKey { as_id: 64496, ski: vec![0x11; 20], spki_der: vec![0x30, 0x00], }, rule_hash: sha256_32(b"router-key-rule"), }, ], related_artifacts: vec![ VcirRelatedArtifact { artifact_role: VcirArtifactRole::Manifest, artifact_kind: VcirArtifactKind::Mft, uri: Some(manifest_uri.clone()), sha256: manifest_hash, object_type: Some("mft".to_string()), validation_status: VcirArtifactValidationStatus::Accepted, reject_reason: None, }, VcirRelatedArtifact { artifact_role: VcirArtifactRole::CurrentCrl, artifact_kind: VcirArtifactKind::Crl, uri: Some(current_crl_uri), sha256: current_crl_hash, object_type: Some("crl".to_string()), validation_status: VcirArtifactValidationStatus::Accepted, reject_reason: None, }, VcirRelatedArtifact { artifact_role: VcirArtifactRole::ChildCaCert, artifact_kind: VcirArtifactKind::Cer, uri: Some(child_cert_uri), sha256: child_cert_hash.to_string(), object_type: Some("cer".to_string()), validation_status: VcirArtifactValidationStatus::Accepted, reject_reason: None, }, VcirRelatedArtifact { artifact_role: VcirArtifactRole::SignedObject, artifact_kind: VcirArtifactKind::Roa, uri: Some(roa_uri), sha256: roa_hash, object_type: Some("roa".to_string()), validation_status: VcirArtifactValidationStatus::Accepted, reject_reason: None, }, VcirRelatedArtifact { artifact_role: VcirArtifactRole::SignedObject, artifact_kind: VcirArtifactKind::Aspa, uri: Some(aspa_uri), sha256: aspa_hash, object_type: Some("aspa".to_string()), validation_status: VcirArtifactValidationStatus::Accepted, reject_reason: None, }, ], summary: VcirSummary { local_vrp_count: 1, local_aspa_count: 1, local_router_key_count: 1, child_count: 1, accepted_object_count: 4, rejected_object_count: 0, }, audit_summary: VcirAuditSummary { failed_fetch_eligible: true, last_failed_fetch_reason: None, warning_count: 0, audit_flags: Vec::new(), }, } } fn put_vcir_for_failed_fetch_reuse( store: &RocksStore, ca: &CaInstanceHandle, policy: &Policy, vcir: &ValidatedCaInstanceResult, ) { let validation_time = vcir .last_successful_validation_time .parse() .expect("parse VCIR validation time"); let identity = failed_fetch_reuse_identity_for_fresh_result( ca, policy, validation_time, vcir.instance_gate.instance_effective_until.clone(), ) .expect("build VCIR failed-fetch reuse identity"); store .put_vcir_with_failed_fetch_reuse_identity(vcir, &identity) .expect("put reusable VCIR"); } fn sample_ca_for_failed_fetch_reuse(vcir: &ValidatedCaInstanceResult) -> CaInstanceHandle { CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: vcir.manifest_rsync_uri.clone(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: Some("https://example.test/notify.xml".to_string()), } } #[test] fn never_http_fetcher_returns_error() { let f = NeverHttpFetcher; let err = f.fetch("https://example.test/").unwrap_err(); assert!(err.contains("disabled"), "{err}"); } #[test] fn kind_from_rsync_uri_classifies_known_extensions() { assert_eq!( kind_from_rsync_uri("rsync://example.test/x.crl"), AuditObjectKind::Crl ); assert_eq!( kind_from_rsync_uri("rsync://example.test/x.cer"), AuditObjectKind::Certificate ); assert_eq!( kind_from_rsync_uri("rsync://example.test/x.roa"), AuditObjectKind::Roa ); assert_eq!( kind_from_rsync_uri("rsync://example.test/x.asa"), AuditObjectKind::Aspa ); assert_eq!( kind_from_rsync_uri("rsync://example.test/x.bin"), AuditObjectKind::Other ); } #[test] fn build_vcir_local_outputs_prefers_cached_outputs() { let pack = dummy_pack_with_files(vec![]); let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(vec![1]), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: pack.publication_point_rsync_uri.clone(), manifest_rsync_uri: pack.manifest_rsync_uri.clone(), publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(), rrdp_notification_uri: None, }; let cached = vec![VcirLocalOutput { output_type: VcirOutputType::Vrp, item_effective_until: pack.next_update.clone(), source_object_uri: "rsync://example.test/repo/issuer/a.roa".to_string(), source_object_type: VcirSourceObjectType::Roa, source_object_hash: sha256_32(b"cached-roa"), source_ee_cert_hash: sha256_32(b"cached-ee"), payload: VcirLocalOutputPayload::Vrp { asn: 64500, afi: RoaAfi::Ipv4, prefix_len: 24, addr: ipv4_addr([203, 0, 113, 0]), max_length: 24, }, rule_hash: sha256_32(b"cached-rule"), }]; let mut objects = crate::validation::objects::ObjectsOutput { vrps: Vec::new(), aspas: Vec::new(), router_keys: Vec::new(), local_outputs_cache: cached.clone(), warnings: Vec::new(), stats: crate::validation::objects::ObjectsStats::default(), audit: Vec::new(), roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(), roa_cache_object_meta: Vec::new(), }; let outputs = take_or_build_vcir_local_outputs(&ca, &pack, &mut objects).expect("reuse cached outputs"); assert_eq!(outputs, cached); assert!(objects.local_outputs_cache.is_empty()); } #[test] fn persist_vcir_non_repository_evidence_stores_current_ca_cert_only() { let (pack, issuer_ca_der, validation_time) = cernet_publication_point_snapshot_for_vcir_tests(); let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca"); let objects = crate::validation::objects::process_publication_point_snapshot_for_issuer( &pack, &Policy::default(), issuer_ca_der.as_slice(), Some( "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer", ), issuer_ca.tbs.extensions.ip_resources.as_ref(), issuer_ca.tbs.extensions.as_resources.as_ref(), validation_time, None, ); assert!( !objects.local_outputs_cache.is_empty(), "expected local outputs from signed objects" ); let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(issuer_ca_der.clone()), ca_certificate_rsync_uri: Some( "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string(), ), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), rsync_base_uri: pack.publication_point_rsync_uri.clone(), manifest_rsync_uri: pack.manifest_rsync_uri.clone(), publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(), rrdp_notification_uri: None, }; persist_vcir_non_repository_evidence(&store, &ca).expect("persist embedded evidence"); let issuer_hash = sha256_hex(&issuer_ca_der); let issuer_entry = store .get_raw_by_hash_entry(&issuer_hash) .expect("load issuer raw entry") .expect("issuer raw entry present"); assert!( issuer_entry .origin_uris .iter() .any(|uri| uri.ends_with("BfycW4hQb3wNP4YsiJW-1n6fjro.cer")) ); let first_output = objects .local_outputs_cache .first() .expect("first local output"); assert!( store .get_raw_by_hash_entry(&first_output.source_ee_cert_hash_hex()) .expect("load source ee raw") .is_none() ); } #[test] fn build_router_key_local_outputs_encodes_router_key_payloads() { let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: None, }; let outputs = build_router_key_local_outputs( &ca, &[RouterKeyPayload { as_id: 64496, ski: vec![0x11; 20], spki_der: vec![0x30, 0x00], source_object_uri: "rsync://example.test/repo/issuer/router.cer".to_string(), source_object_hash: "11".repeat(32), source_ee_cert_hash: "11".repeat(32), item_effective_until: PackTime { rfc3339_utc: "2026-12-31T00:00:00Z".to_string(), }, }], ); assert_eq!(outputs.len(), 1); assert_eq!(outputs[0].output_type, VcirOutputType::RouterKey); assert_eq!( outputs[0].source_object_type, VcirSourceObjectType::RouterKey ); assert!(outputs[0].payload_json().contains("spki_der_base64")); } #[test] fn build_vcir_local_outputs_falls_back_to_decoding_accepted_objects_when_cache_is_empty() { let (pack, issuer_ca_der, validation_time) = cernet_publication_point_snapshot_for_vcir_tests(); let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca"); let objects = crate::validation::objects::process_publication_point_snapshot_for_issuer( &pack, &Policy::default(), issuer_ca_der.as_slice(), Some( "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer", ), issuer_ca.tbs.extensions.ip_resources.as_ref(), issuer_ca.tbs.extensions.as_resources.as_ref(), validation_time, None, ); let mut objects_without_cache = objects.clone(); objects_without_cache.local_outputs_cache.clear(); let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(issuer_ca_der), ca_certificate_rsync_uri: Some( "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string(), ), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), rsync_base_uri: pack.publication_point_rsync_uri.clone(), manifest_rsync_uri: pack.manifest_rsync_uri.clone(), publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(), rrdp_notification_uri: None, }; let local_outputs = build_vcir_local_outputs(&ca, &pack, &objects_without_cache) .expect("rebuild vcir local outputs"); assert!(!local_outputs.is_empty()); assert_eq!(local_outputs.len(), objects.vrps.len()); assert!( local_outputs .iter() .all(|output| output.output_type == VcirOutputType::Vrp) ); } #[test] fn finalize_fresh_publication_point_releases_local_outputs_cache_after_persist() { let (pack, issuer_ca_der, validation_time) = cernet_publication_point_snapshot_for_vcir_tests(); let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca"); let mut objects = crate::validation::objects::process_publication_point_snapshot_for_issuer( &pack, &Policy::default(), issuer_ca_der.as_slice(), Some( "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer", ), issuer_ca.tbs.extensions.ip_resources.as_ref(), issuer_ca.tbs.extensions.as_resources.as_ref(), validation_time, None, ); assert!( !objects.local_outputs_cache.is_empty(), "expected local outputs from signed objects" ); let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let policy = Policy::default(); let runner = Rpkiv1PublicationPointRunner { store: &store, policy: &policy, http_fetcher: &NeverHttpFetcher, rsync_fetcher: &FailingRsyncFetcher, validation_time, timing: None, download_log: None, replay_archive_index: None, replay_delta_index: None, rrdp_dedup: false, rrdp_repo_cache: Mutex::new(HashMap::new()), rsync_dedup: false, rsync_repo_cache: Mutex::new(HashMap::new()), current_repo_index: None, repo_sync_runtime: None, parallel_phase2_config: None, parallel_roa_worker_pool: None, ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(issuer_ca_der.clone()), ca_certificate_rsync_uri: Some( "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string(), ), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), rsync_base_uri: pack.publication_point_rsync_uri.clone(), manifest_rsync_uri: pack.manifest_rsync_uri.clone(), publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(), rrdp_notification_uri: None, }; let fresh_point = FreshValidatedPublicationPoint { manifest_rsync_uri: pack.manifest_rsync_uri.clone(), publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(), manifest_number_be: pack.manifest_number_be.clone(), this_update: pack.this_update.clone(), next_update: pack.next_update.clone(), verified_at: pack.verified_at.clone(), manifest_bytes: pack.manifest_bytes.clone(), files: pack.files.clone(), }; objects.local_outputs_cache.shrink_to_fit(); let original_cache_capacity = objects.local_outputs_cache.capacity(); let finalized = runner .finalize_fresh_publication_point_from_reducer( &ca, &fresh_point, Vec::new(), objects, Vec::new(), Vec::new(), None, None, 0, None, ) .expect("finalize fresh publication point"); assert!( finalized.result.objects.local_outputs_cache.is_empty(), "local outputs cache should be released after VCIR persistence" ); assert_eq!( finalized.result.objects.local_outputs_cache.capacity(), 0, "released cache should not keep its backing allocation" ); assert!(original_cache_capacity > 0); let persisted = store .get_vcir(&pack.manifest_rsync_uri) .expect("load persisted vcir") .expect("persisted vcir"); assert!( !persisted.local_outputs.is_empty(), "VCIR should still persist local outputs before cache release" ); } #[test] fn persist_vcir_for_fresh_result_stores_vcir_and_replay_meta_for_real_snapshot() { let (pack, issuer_ca_der, validation_time) = cernet_publication_point_snapshot_for_vcir_tests(); let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca"); let objects = crate::validation::objects::process_publication_point_snapshot_for_issuer( &pack, &Policy::default(), issuer_ca_der.as_slice(), Some( "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer", ), issuer_ca.tbs.extensions.ip_resources.as_ref(), issuer_ca.tbs.extensions.as_resources.as_ref(), validation_time, None, ); let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(issuer_ca_der.clone()), ca_certificate_rsync_uri: Some( "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string(), ), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), rsync_base_uri: pack.publication_point_rsync_uri.clone(), manifest_rsync_uri: pack.manifest_rsync_uri.clone(), publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(), rrdp_notification_uri: None, }; let mut objects = objects; persist_vcir_for_fresh_result_with_timing( &store, &Policy::default(), &ca, &pack, &mut objects, &[], &[], &[], validation_time, false, ) .map(|_timing| ()) .expect("persist vcir for fresh result"); let vcir = store .get_vcir(&pack.manifest_rsync_uri) .expect("get vcir") .expect("vcir exists"); assert_eq!(vcir.manifest_rsync_uri, pack.manifest_rsync_uri); assert_eq!(vcir.summary.local_vrp_count as usize, objects.vrps.len()); assert_eq!( vcir.ccr_manifest_projection.manifest_rsync_uri, pack.manifest_rsync_uri ); assert_eq!( vcir.ccr_manifest_projection.manifest_number_be, pack.manifest_number_be ); assert_eq!( vcir.ccr_manifest_projection.manifest_this_update, pack.this_update ); assert_eq!( vcir.ccr_manifest_projection.manifest_size, pack.manifest_bytes.len() as u64 ); assert!(vcir.local_outputs.first().is_some(), "local outputs stored"); let replay_meta = store .get_manifest_replay_meta(&pack.manifest_rsync_uri) .expect("get replay meta") .expect("replay meta exists"); assert_eq!(replay_meta.manifest_rsync_uri, pack.manifest_rsync_uri); assert_eq!( replay_meta.manifest_sha256, sha2::Sha256::digest(&pack.manifest_bytes).to_vec() ); } #[test] fn build_vcir_ccr_manifest_projection_from_fresh_real_snapshot_matches_manifest_contents() { let (pack, issuer_ca_der, validation_time) = cernet_publication_point_snapshot_for_vcir_tests(); let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca"); let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(issuer_ca_der), ca_certificate_rsync_uri: Some( "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string(), ), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), rsync_base_uri: pack.publication_point_rsync_uri.clone(), manifest_rsync_uri: pack.manifest_rsync_uri.clone(), publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(), rrdp_notification_uri: None, }; let child_discovery = discover_children_from_fresh_snapshot_with_audit(&ca, &pack, validation_time, None) .expect("discover children"); let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let child_entries = build_vcir_child_entries(&store, &child_discovery.children, validation_time) .expect("build child entries"); let projection = build_vcir_ccr_manifest_projection_from_fresh(&ca, &pack, &child_entries) .expect("build ccr manifest projection"); let manifest = ManifestObject::decode_der(&pack.manifest_bytes).expect("decode manifest"); let expected_locations = match manifest.signed_object.signed_data.certificates[0] .resource_cert .tbs .extensions .subject_info_access .as_ref() .expect("manifest sia") { SubjectInfoAccess::Ee(ee_sia) => vec![ crate::ccr::manifest_location::select_manifest_signed_object_location( &pack.manifest_rsync_uri, &ee_sia.access_descriptions, ) .expect("select manifest signedObject"), ], SubjectInfoAccess::Ca(_) => panic!("manifest ee SIA should not be CA variant"), }; assert_eq!(projection.manifest_rsync_uri, pack.manifest_rsync_uri); assert_eq!( projection.manifest_sha256, sha2::Sha256::digest(&pack.manifest_bytes).to_vec() ); assert_eq!(projection.manifest_size, pack.manifest_bytes.len() as u64); assert_eq!( projection.manifest_ee_aki, manifest.signed_object.signed_data.certificates[0] .resource_cert .tbs .extensions .authority_key_identifier .clone() .expect("manifest aki") ); assert_eq!( projection.manifest_number_be, manifest.manifest.manifest_number.bytes_be ); assert_eq!(projection.manifest_this_update, pack.this_update); assert_eq!(projection.manifest_sia_locations_der, expected_locations); let expected_subordinate_skis = child_entries .iter() .map(|child| hex::decode(&child.child_ski).expect("decode child ski")) .collect::>(); assert_eq!(projection.subordinate_skis, expected_subordinate_skis); } #[test] fn build_vcir_child_entries_uses_projection_without_repo_bytes() { let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let validation_time = time::OffsetDateTime::parse( "2026-06-26T00:00:00Z", &time::format_description::well_known::Rfc3339, ) .expect("parse time"); let child = DiscoveredChildCaInstance { handle: CaInstanceHandle { depth: 1, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: Some("rsync://example.test/repo/root.mft".to_string()), ca_certificate: CaCertificateRef::repo_bytes("aa".repeat(32)), ca_certificate_rsync_uri: Some("rsync://example.test/repo/child.cer".to_string()), effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: "rsync://example.test/repo/child/".to_string(), manifest_rsync_uri: "rsync://example.test/repo/child/child.mft".to_string(), publication_point_rsync_uri: "rsync://example.test/repo/child/".to_string(), rrdp_notification_uri: Some("https://example.test/notify.xml".to_string()), }, discovered_from: crate::audit::DiscoveredFrom { parent_manifest_rsync_uri: "rsync://example.test/repo/root.mft".to_string(), child_ca_certificate_rsync_uri: "rsync://example.test/repo/child.cer".to_string(), child_ca_certificate_sha256_hex: "aa".repeat(32), }, child_entry_projection: Some(DiscoveredChildEntryProjection { child_ski: "11".repeat(20), }), }; let entries = build_vcir_child_entries(&store, &[child], validation_time) .expect("projection should avoid repo-bytes load"); assert_eq!(entries.len(), 1); assert_eq!(entries[0].child_ski, "11".repeat(20)); } #[test] fn build_vcir_related_artifacts_classifies_snapshot_files_and_audit_statuses() { let manifest_bytes = std::fs::read( std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", ), ) .expect("read manifest fixture"); let crl_bytes = std::fs::read( std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl", ), ) .expect("read crl fixture"); let pack = PublicationPointSnapshot { format_version: PublicationPointSnapshot::FORMAT_VERSION_V1, manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_number_be: vec![1], this_update: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()), next_update: PackTime::from_utc_offset_datetime( time::OffsetDateTime::now_utc() + time::Duration::hours(1), ), verified_at: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()), manifest_bytes, files: vec![ PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/issuer.crl", crl_bytes, ), PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/child.cer", vec![1u8, 2], ), PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/a.roa", vec![3u8, 4], ), PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/a.asa", vec![5u8, 6], ), PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/a.gbr", vec![7u8, 8], ), PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/extra.bin", vec![9u8], ), ], }; let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(vec![0x11, 0x22]), ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()), effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: pack.publication_point_rsync_uri.clone(), manifest_rsync_uri: pack.manifest_rsync_uri.clone(), publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(), rrdp_notification_uri: None, }; let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let objects = crate::validation::objects::ObjectsOutput { vrps: Vec::new(), aspas: Vec::new(), router_keys: Vec::new(), local_outputs_cache: Vec::new(), warnings: Vec::new(), stats: crate::validation::objects::ObjectsStats::default(), audit: vec![ ObjectAuditEntry { rsync_uri: "rsync://example.test/repo/issuer/a.roa".to_string(), sha256_hex: sha256_hex_from_32(&pack.files[2].sha256), kind: AuditObjectKind::Roa, result: AuditObjectResult::Error, detail: Some("bad roa".to_string()), }, ObjectAuditEntry { rsync_uri: "rsync://example.test/repo/issuer/a.asa".to_string(), sha256_hex: sha256_hex_from_32(&pack.files[3].sha256), kind: AuditObjectKind::Aspa, result: AuditObjectResult::Skipped, detail: Some("skipped aspa".to_string()), }, ], roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(), roa_cache_object_meta: Vec::new(), }; let artifacts = build_vcir_related_artifacts( &store, &ca, &pack, "rsync://example.test/repo/issuer/issuer.crl", &objects, &[], ); assert!( artifacts .iter() .any(|artifact| artifact.artifact_role == VcirArtifactRole::Manifest) ); assert!( artifacts .iter() .any(|artifact| artifact.artifact_role == VcirArtifactRole::TrustAnchorCert) ); assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref() == Some("rsync://example.test/repo/issuer/issuer.crl") && artifact.artifact_role == VcirArtifactRole::CurrentCrl)); assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref() == Some("rsync://example.test/repo/issuer/child.cer") && artifact.artifact_role == VcirArtifactRole::ChildCaCert)); assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref() == Some("rsync://example.test/repo/issuer/a.roa") && artifact.validation_status == VcirArtifactValidationStatus::Rejected && artifact.reject_reason.as_deref() == Some("bad roa"))); assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref() == Some("rsync://example.test/repo/issuer/a.asa") && artifact.validation_status == VcirArtifactValidationStatus::WarningOnly && artifact.reject_reason.is_none())); assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref() == Some("rsync://example.test/repo/issuer/a.gbr") && artifact.artifact_kind == VcirArtifactKind::Gbr)); assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref() == Some("rsync://example.test/repo/issuer/extra.bin") && artifact.artifact_kind == VcirArtifactKind::Other)); assert!( !artifacts .iter() .any(|artifact| artifact.uri.is_none() && artifact.sha256 == sha256_hex(b"embedded-ee")), "embedded EE cert artifacts should no longer be persisted separately" ); } #[test] fn select_issuer_crl_from_snapshot_reports_missing_crldp_for_self_signed_cert() { let ta_der = std::fs::read( std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/ta/apnic-ta.cer"), ) .expect("read TA fixture"); let pack = dummy_pack_with_files(vec![]); let err = select_issuer_crl_from_snapshot(&ta_der, &pack).unwrap_err(); assert!(err.contains("CRLDistributionPoints missing"), "{err}"); } #[test] fn select_issuer_crl_from_snapshot_finds_matching_crl() { // Use real fixtures to ensure child cert has CRLDP rsync URI and CRL exists. let child_cert_der = std::fs::read(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( "tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer", )) .expect("read child cert fixture"); let crl_der = std::fs::read(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( "tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl", )) .expect("read crl fixture"); let pack = dummy_pack_with_files(vec![PackFile::from_bytes_compute_sha256( "rsync://ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl", crl_der.clone(), )]); let (uri, found) = select_issuer_crl_from_snapshot(child_cert_der.as_slice(), &pack).expect("find crl"); assert_eq!( uri, "rsync://ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.crl" ); assert_eq!(found, crl_der.as_slice()); } #[test] fn discover_children_from_fresh_pack_discovers_child_ca() { let g = generate_chain_and_crl(); let pack = dummy_pack_with_files(vec![ PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/issuer.crl", g.issuer_crl_der.clone(), ), PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/child.cer", g.child_ca_der.clone(), ), ]); let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer"); let issuer = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()), ca_certificate_rsync_uri: None, effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: None, }; let now = time::OffsetDateTime::now_utc(); let children = discover_children_from_fresh_snapshot_with_audit(&issuer, &pack, now, None) .expect("discover children") .children; assert_eq!(children.len(), 1); assert_eq!( children[0].discovered_from.parent_manifest_rsync_uri, issuer.manifest_rsync_uri ); assert_eq!( children[0].discovered_from.child_ca_certificate_rsync_uri, "rsync://example.test/repo/issuer/child.cer" ); assert_eq!( children[0].handle.rsync_base_uri, "rsync://example.test/repo/child/".to_string() ); assert_eq!( children[0].handle.manifest_rsync_uri, "rsync://example.test/repo/child/child.mft".to_string() ); assert_eq!( children[0].handle.publication_point_rsync_uri, "rsync://example.test/repo/child/".to_string() ); assert_eq!( children[0].handle.rrdp_notification_uri.as_deref(), Some("https://example.test/notification.xml") ); } #[test] fn discover_children_child_certificate_cache_reuses_successful_child_ca() { let g = generate_chain_and_crl(); let pack = dummy_pack_with_files(vec![ PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/issuer.crl", g.issuer_crl_der.clone(), ), PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/child.cer", g.child_ca_der.clone(), ), ]); let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer"); let issuer = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()), ca_certificate_rsync_uri: None, effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: None, }; let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let policy = Policy::default(); let cache_context = ChildCertificateValidationCacheContext { store: &store, issuer_ca_sha256: issuer.ca_certificate_sha256_32().unwrap(), ca_validation_context_digest: ca_validation_context_digest_for_ca(&issuer), policy_fingerprint: publication_point_cache_policy_fingerprint(&policy), }; let validation_time = time::OffsetDateTime::now_utc(); let first = discover_children_from_fresh_snapshot_with_audit_cached( &issuer, &pack, validation_time, None, Some(cache_context), ) .expect("first discovery writes cache"); assert_eq!(first.children.len(), 1); let child_file = pack .files .iter() .find(|file| file.rsync_uri.ends_with("child.cer")) .expect("child file"); let cache_key = child_certificate_cache_key_sha256_hex( &child_file.rsync_uri, &child_file.sha256, &cache_context.issuer_ca_sha256, &cache_context.ca_validation_context_digest, &cache_context.policy_fingerprint, ); assert!( store .get_child_certificate_cache_projection(&cache_key) .expect("get projection") .is_some() ); let timing = TimingHandle::new(crate::analysis::timing::TimingMeta { recorded_at_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(), validation_time_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(), tal_url: None, db_path: None, }); let second = discover_children_from_fresh_snapshot_with_audit_cached( &issuer, &pack, validation_time, Some(&timing), Some(cache_context), ) .expect("second discovery reuses cache"); assert_eq!(second.children.len(), 1); assert_eq!( second.children[0].handle.manifest_rsync_uri, first.children[0].handle.manifest_rsync_uri ); assert!(second.audits.iter().any(|audit| { audit .detail .as_deref() .unwrap_or("") .contains("child certificate validation cache") })); let counts = timing.counts_snapshot(); assert_eq!( counts.get("child_certificate_cache_hit_ca").copied(), Some(1) ); assert_eq!( counts .get("child_certificate_cache_batch_lookup_publication_points") .copied(), Some(1) ); assert_eq!( counts .get("child_certificate_cache_batch_lookup_entries") .copied(), Some(1) ); assert_eq!( counts .get("child_certificate_der_load_fresh_count") .copied(), Some(0) ); } #[test] fn discover_children_child_certificate_cache_rechecks_changed_valid_crl() { let g = generate_chain_and_crl(); let pack = dummy_pack_with_files(vec![ PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/issuer.crl", g.issuer_crl_der.clone(), ), PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/child.cer", g.child_ca_der.clone(), ), ]); let mut changed_pack = pack.clone(); changed_pack.files[0] = PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/issuer.crl", g.issuer_crl_der_next.clone(), ); let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer"); let issuer = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()), ca_certificate_rsync_uri: None, effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: None, }; let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let policy = Policy::default(); let cache_context = ChildCertificateValidationCacheContext { store: &store, issuer_ca_sha256: issuer.ca_certificate_sha256_32().unwrap(), ca_validation_context_digest: ca_validation_context_digest_for_ca(&issuer), policy_fingerprint: publication_point_cache_policy_fingerprint(&policy), }; let validation_time = time::OffsetDateTime::now_utc(); discover_children_from_fresh_snapshot_with_audit_cached( &issuer, &pack, validation_time, None, Some(cache_context), ) .expect("populate cache"); let child_file = pack .files .iter() .find(|file| file.rsync_uri.ends_with("child.cer")) .expect("child file"); let cache_key = child_certificate_cache_key_sha256_hex( &child_file.rsync_uri, &child_file.sha256, &cache_context.issuer_ca_sha256, &cache_context.ca_validation_context_digest, &cache_context.policy_fingerprint, ); let projection = store .get_child_certificate_cache_projection(&cache_key) .expect("get child certificate cache projection") .expect("child certificate cache projection present"); let projected_until = parse_snapshot_time_value(&projection.effective_until).expect("parse projected until"); let initial_crl = crate::data_model::crl::RpkixCrl::decode_der(&g.issuer_crl_der) .expect("decode initial crl"); assert!( projected_until > initial_crl.next_update.utc, "child certificate cache projection must not be hard-capped by the issuing CRL nextUpdate" ); let timing = TimingHandle::new(crate::analysis::timing::TimingMeta { recorded_at_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(), validation_time_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(), tal_url: None, db_path: None, }); let out = discover_children_from_fresh_snapshot_with_audit_cached( &issuer, &changed_pack, validation_time, Some(&timing), Some(cache_context), ) .expect("changed valid CRL should recheck revocation and reuse cache"); assert_eq!(out.children.len(), 1); assert!( out.audits .iter() .any(|audit| audit.detail.as_deref().unwrap_or("").contains("cache")) ); let counts = timing.counts_snapshot(); assert_eq!( counts .get("child_certificate_cache_crl_recheck_hit") .copied(), Some(1) ); assert_eq!( counts.get("child_certificate_cache_hit_ca").copied(), Some(1) ); } #[test] fn discover_children_child_certificate_cache_misses_when_current_crl_invalid() { let g = generate_chain_and_crl(); let pack = dummy_pack_with_files(vec![ PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/issuer.crl", g.issuer_crl_der.clone(), ), PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/child.cer", g.child_ca_der.clone(), ), ]); let mut changed_pack = pack.clone(); changed_pack.files[0] = PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/issuer.crl", b"not-a-valid-crl".to_vec(), ); let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer"); let issuer = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()), ca_certificate_rsync_uri: None, effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: None, }; let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let policy = Policy::default(); let cache_context = ChildCertificateValidationCacheContext { store: &store, issuer_ca_sha256: issuer.ca_certificate_sha256_32().unwrap(), ca_validation_context_digest: ca_validation_context_digest_for_ca(&issuer), policy_fingerprint: publication_point_cache_policy_fingerprint(&policy), }; let validation_time = time::OffsetDateTime::now_utc(); discover_children_from_fresh_snapshot_with_audit_cached( &issuer, &pack, validation_time, None, Some(cache_context), ) .expect("populate cache"); let timing = TimingHandle::new(crate::analysis::timing::TimingMeta { recorded_at_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(), validation_time_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(), tal_url: None, db_path: None, }); let out = discover_children_from_fresh_snapshot_with_audit_cached( &issuer, &changed_pack, validation_time, Some(&timing), Some(cache_context), ) .expect("invalid CRL should miss cache and continue with audit error"); assert!(out.children.is_empty()); assert!( out.audits .iter() .all(|audit| !audit.detail.as_deref().unwrap_or("").contains("cache")) ); let counts = timing.counts_snapshot(); assert_eq!( counts .get("child_certificate_cache_miss_crl_invalid") .copied(), Some(1) ); } #[test] fn discover_children_child_certificate_cache_misses_when_unchanged_crl_expired() { let g = generate_chain_and_crl(); let pack = dummy_pack_with_files(vec![ PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/issuer.crl", g.issuer_crl_der.clone(), ), PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/child.cer", g.child_ca_der.clone(), ), ]); let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer"); let issuer = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()), ca_certificate_rsync_uri: None, effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: None, }; let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let policy = Policy::default(); let cache_context = ChildCertificateValidationCacheContext { store: &store, issuer_ca_sha256: issuer.ca_certificate_sha256_32().unwrap(), ca_validation_context_digest: ca_validation_context_digest_for_ca(&issuer), policy_fingerprint: publication_point_cache_policy_fingerprint(&policy), }; let validation_time = time::OffsetDateTime::now_utc(); discover_children_from_fresh_snapshot_with_audit_cached( &issuer, &pack, validation_time, None, Some(cache_context), ) .expect("populate cache"); let timing = TimingHandle::new(crate::analysis::timing::TimingMeta { recorded_at_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(), validation_time_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(), tal_url: None, db_path: None, }); let out = discover_children_from_fresh_snapshot_with_audit_cached( &issuer, &pack, validation_time + time::Duration::days(2), Some(&timing), Some(cache_context), ) .expect("expired unchanged CRL should miss cache and continue with audit error"); assert!(out.children.is_empty()); assert!( out.audits .iter() .all(|audit| !audit.detail.as_deref().unwrap_or("").contains("cache")) ); let counts = timing.counts_snapshot(); assert_eq!( counts .get("child_certificate_cache_miss_crl_expired") .copied(), Some(1) ); } #[test] fn discover_children_with_audit_records_missing_crl_for_child_certificate() { let now = time::OffsetDateTime::now_utc(); let child_ca_der = std::fs::read(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( "tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer", )) .expect("read child ca fixture"); // Pack contains the child CA cert but does not contain the CRL referenced by the child // certificate CRLDistributionPoints extension. let pack = dummy_pack_with_files(vec![PackFile::from_bytes_compute_sha256( "rsync://ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer", child_ca_der, )]); let issuer = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(vec![1]), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: pack.manifest_rsync_uri.clone(), publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(), rrdp_notification_uri: None, }; let out = discover_children_from_fresh_snapshot_with_audit(&issuer, &pack, now, None) .expect("discovery should succeed with audit error"); assert_eq!(out.children.len(), 0); assert_eq!(out.audits.len(), 1); assert_eq!( out.audits[0].rsync_uri, "rsync://ca.rg.net/rpki/RGnet-OU/R-lVU1XGsAeqzV1Fv0HjOD6ZFkE.cer" ); assert_eq!(out.audits[0].result, AuditObjectResult::Error); assert!( out.audits[0] .detail .as_deref() .unwrap_or("") .contains("cannot select issuer CRL"), "expected deterministic CRL selection failure to be recorded" ); } #[test] fn runner_offline_rsync_fixture_produces_pack_and_warnings() { let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0"); assert!(fixture_dir.is_dir(), "fixture directory must exist"); let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string(); let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft"; let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}"); // Pick a validation_time inside the fixture manifest's validity window to keep this // test stable across wall-clock time. let fixture_manifest_bytes = std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture"); let fixture_manifest = crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes) .expect("decode manifest fixture"); let validation_time = { let this_update = fixture_manifest.manifest.this_update; let next_update = fixture_manifest.manifest.next_update; let candidate = this_update + time::Duration::seconds(60); if candidate < next_update { candidate } else { this_update } }; let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let policy = Policy { sync_preference: crate::policy::SyncPreference::RsyncOnly, ..Policy::default() }; let runner = Rpkiv1PublicationPointRunner { store: &store, policy: &policy, http_fetcher: &NeverHttpFetcher, rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir), validation_time, timing: None, download_log: None, replay_archive_index: None, replay_delta_index: None, rrdp_dedup: false, rrdp_repo_cache: Mutex::new(HashMap::new()), rsync_dedup: false, rsync_repo_cache: Mutex::new(HashMap::new()), current_repo_index: None, repo_sync_runtime: None, parallel_phase2_config: None, parallel_roa_worker_pool: None, ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; // For this fixture-driven smoke, we provide the correct issuer CA certificate (the CA for // this publication point) so ROA EE certificate paths can validate. let issuer_ca_der = std::fs::read( std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer", ), ) .expect("read issuer ca fixture"); let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca"); let handle = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(issuer_ca_der), ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), rsync_base_uri: rsync_base_uri.clone(), manifest_rsync_uri: manifest_rsync_uri.clone(), publication_point_rsync_uri: rsync_base_uri.clone(), rrdp_notification_uri: None, }; let out = runner .run_publication_point(&handle) .expect("run publication point"); assert_eq!(out.source, PublicationPointSource::Fresh); let pack = out.snapshot.expect("fresh run pack"); assert_eq!(pack.manifest_rsync_uri, manifest_rsync_uri); assert!(pack.files.len() > 1); assert!( out.objects.vrps.len() > 1, "expected to extract VRPs from ROAs" ); let vcir = store .get_vcir(&manifest_rsync_uri) .expect("get vcir") .expect("vcir exists after fresh run"); assert_eq!(vcir.manifest_rsync_uri, manifest_rsync_uri); assert_eq!(vcir.tal_id, "test-tal"); assert!( vcir.local_outputs .iter() .any(|output| output.output_type == crate::storage::VcirOutputType::Vrp), "expected VCIR local_outputs to contain VRP entries" ); let first_vrp = vcir .local_outputs .iter() .find(|output| output.output_type == crate::storage::VcirOutputType::Vrp) .expect("first VCIR VRP output"); assert!(!first_vrp.rule_hash_hex().is_empty()); assert!(!first_vrp.output_id().is_empty()); let replay_meta = store .get_manifest_replay_meta(&manifest_rsync_uri) .expect("get replay meta") .expect("replay meta exists"); assert_eq!(replay_meta.manifest_rsync_uri, manifest_rsync_uri); } #[test] fn runner_roa_validation_cache_reuses_vcir_outputs_on_second_fixture_run() { let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0"); assert!(fixture_dir.is_dir(), "fixture directory must exist"); let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string(); let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft"; let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}"); let fixture_manifest_bytes = std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture"); let fixture_manifest = crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes) .expect("decode manifest fixture"); let validation_time = fixture_manifest.manifest.this_update + time::Duration::seconds(60); let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let policy = Policy { sync_preference: crate::policy::SyncPreference::RsyncOnly, ..Policy::default() }; let issuer_ca_der = std::fs::read( std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer", ), ) .expect("read issuer ca fixture"); let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca"); let handle = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(issuer_ca_der), ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), rsync_base_uri: rsync_base_uri.clone(), manifest_rsync_uri: manifest_rsync_uri.clone(), publication_point_rsync_uri: rsync_base_uri, rrdp_notification_uri: None, }; let first_runner = Rpkiv1PublicationPointRunner { store: &store, policy: &policy, http_fetcher: &NeverHttpFetcher, rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir), validation_time, timing: None, download_log: None, replay_archive_index: None, replay_delta_index: None, rrdp_dedup: false, rrdp_repo_cache: Mutex::new(HashMap::new()), rsync_dedup: false, rsync_repo_cache: Mutex::new(HashMap::new()), current_repo_index: None, repo_sync_runtime: None, parallel_phase2_config: None, parallel_roa_worker_pool: None, ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; let first = first_runner .run_publication_point(&handle) .expect("first fresh run"); assert!(first.objects.vrps.len() > 1); assert_eq!(first.objects.roa_cache_stats.hit_roas, 0); let second_runner = Rpkiv1PublicationPointRunner { store: &store, policy: &policy, http_fetcher: &NeverHttpFetcher, rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir), validation_time, timing: None, download_log: None, replay_archive_index: None, replay_delta_index: None, rrdp_dedup: false, rrdp_repo_cache: Mutex::new(HashMap::new()), rsync_dedup: false, rsync_repo_cache: Mutex::new(HashMap::new()), current_repo_index: None, repo_sync_runtime: None, parallel_phase2_config: None, parallel_roa_worker_pool: None, ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: true, enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; let second = second_runner .run_publication_point(&handle) .expect("second cache-enabled run"); assert_eq!(second.objects.vrps, first.objects.vrps); assert_eq!(second.objects.roa_cache_stats.enabled_publication_points, 1); assert_eq!( second.objects.roa_cache_stats.vcir_hit_publication_points, 1 ); assert_eq!( second.objects.roa_cache_stats.vcir_miss_publication_points, 0 ); assert!(second.objects.roa_cache_stats.hit_roas > 1); assert_eq!(second.objects.roa_cache_stats.miss_roas, 0); assert_eq!(second.objects.roa_cache_stats.blocked_roas, 0); assert_eq!(second.objects.roa_cache_stats.fresh_roas, 0); } #[test] fn runner_publication_point_cache_observe_and_reuse_path() { let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0"); assert!(fixture_dir.is_dir(), "fixture directory must exist"); let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string(); let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft"; let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}"); let fixture_manifest_bytes = std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture"); let fixture_manifest = crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes) .expect("decode manifest fixture"); let validation_time = fixture_manifest.manifest.this_update + time::Duration::seconds(60); let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let policy = Policy { sync_preference: crate::policy::SyncPreference::RsyncOnly, ..Policy::default() }; let issuer_ca_der = std::fs::read( std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer", ), ) .expect("read issuer ca fixture"); let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca"); let handle = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(issuer_ca_der), ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), rsync_base_uri: rsync_base_uri.clone(), manifest_rsync_uri: manifest_rsync_uri.clone(), publication_point_rsync_uri: rsync_base_uri, rrdp_notification_uri: None, }; let first_runner = Rpkiv1PublicationPointRunner { store: &store, policy: &policy, http_fetcher: &NeverHttpFetcher, rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir), validation_time, timing: None, download_log: None, replay_archive_index: None, replay_delta_index: None, rrdp_dedup: false, rrdp_repo_cache: Mutex::new(HashMap::new()), rsync_dedup: false, rsync_repo_cache: Mutex::new(HashMap::new()), current_repo_index: None, repo_sync_runtime: None, parallel_phase2_config: None, parallel_roa_worker_pool: None, ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: true, enable_publication_point_validation_cache: false, }; let first = first_runner .run_publication_point(&handle) .expect("first fresh run"); assert_eq!(first.source, PublicationPointSource::Fresh); assert!( store .get_publication_point_cache_projection(&manifest_rsync_uri) .expect("load publication-point projection") .is_some() ); let timing = crate::analysis::timing::TimingHandle::new(crate::analysis::timing::TimingMeta { recorded_at_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(), validation_time_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(), tal_url: None, db_path: None, }); let observe_runner = Rpkiv1PublicationPointRunner { store: &store, policy: &policy, http_fetcher: &NeverHttpFetcher, rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir), validation_time, timing: Some(timing.clone()), download_log: None, replay_archive_index: None, replay_delta_index: None, rrdp_dedup: false, rrdp_repo_cache: Mutex::new(HashMap::new()), rsync_dedup: false, rsync_repo_cache: Mutex::new(HashMap::new()), current_repo_index: None, repo_sync_runtime: None, parallel_phase2_config: None, parallel_roa_worker_pool: None, ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: true, enable_publication_point_validation_cache: false, }; let observed = observe_runner .run_publication_point(&handle) .expect("observe-only run"); assert_eq!(observed.source, PublicationPointSource::Fresh); assert_eq!(observed.objects.vrps, first.objects.vrps); assert_eq!( timing .counts_snapshot() .get("publication_point_cache_theoretical_hits") .copied(), Some(1) ); let cache_runner = Rpkiv1PublicationPointRunner { store: &store, policy: &policy, http_fetcher: &NeverHttpFetcher, rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir), validation_time, timing: None, download_log: None, replay_archive_index: None, replay_delta_index: None, rrdp_dedup: false, rrdp_repo_cache: Mutex::new(HashMap::new()), rsync_dedup: false, rsync_repo_cache: Mutex::new(HashMap::new()), current_repo_index: None, repo_sync_runtime: None, parallel_phase2_config: None, parallel_roa_worker_pool: None, ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: true, }; let cached = cache_runner .run_publication_point(&handle) .expect("publication-point cache run"); assert_eq!(cached.source, PublicationPointSource::PublicationPointCache); assert_eq!(cached.objects.vrps, first.objects.vrps); assert_eq!(cached.objects.aspas, first.objects.aspas); assert_eq!( cached.discovered_children.len(), first.discovered_children.len() ); assert!(!cached.cir_cached_objects.is_empty()); } fn seed_publication_point_cache_projection( store: &RocksStore, policy: &Policy, ca: &CaInstanceHandle, validation_time: time::OffsetDateTime, ) -> ValidatedCaInstanceResult { let child_bytes = b"child-cert".to_vec(); let child_hash = sha256_hex(&child_bytes); let vcir = sample_vcir_for_projection(validation_time, &child_hash); store .put_blob_bytes_batch(&[ (child_hash, child_bytes), (sha256_hex(b"manifest-bytes"), b"manifest-bytes".to_vec()), ]) .expect("put cache bytes"); store .put_repository_view_entry(&RepositoryViewEntry { rsync_uri: ca.manifest_rsync_uri.clone(), current_hash: Some(sha256_hex(b"manifest-bytes")), repository_source: Some(ca.publication_point_rsync_uri.clone()), object_type: Some("mft".to_string()), state: RepositoryViewState::Present, }) .expect("put manifest current view"); let projection = PublicationPointCacheProjection::from_vcir_with_context( &vcir, ca.publication_point_rsync_uri.clone(), ca.ca_certificate_rsync_uri.clone(), ca.ca_certificate_sha256_32().unwrap(), sha256_32(b"manifest-bytes"), ta_context_digest_for_ca(ca), ca_validation_context_digest_for_ca(ca), publication_point_cache_policy_fingerprint(policy), ) .expect("build publication point projection"); store .put_vcir_with_publication_point_cache_projection(&vcir, Some(&projection)) .expect("put publication point projection"); vcir } #[test] fn publication_point_cache_future_notbefore_guard_detects_future_roa_notbefore_only() { let uri = "rsync://example.test/repo/issuer/future.roa"; let bytes = std::fs::read( std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa"), ) .expect("read ROA fixture"); let roa = RoaObject::decode_der(&bytes).expect("decode ROA fixture"); let ee = &roa.signed_object.signed_data.certificates[0].resource_cert; let file = PackFile::from_bytes_compute_sha256(uri, bytes); let pack = dummy_pack_with_files(vec![file]); let objects = crate::validation::objects::ObjectsOutput { vrps: Vec::new(), aspas: Vec::new(), router_keys: Vec::new(), local_outputs_cache: Vec::new(), warnings: Vec::new(), stats: Default::default(), audit: vec![ObjectAuditEntry { rsync_uri: uri.to_string(), sha256_hex: sha256_hex_from_32(&pack.files[0].sha256), kind: AuditObjectKind::Roa, result: AuditObjectResult::Error, detail: Some( "EE certificate path validation failed: certificate not valid at validation_time" .to_string(), ), }], roa_cache_stats: Default::default(), roa_cache_object_meta: Vec::new(), }; assert!(publication_point_cache_has_future_not_before_risk( &pack, &objects, &[], ee.tbs.validity_not_before - time::Duration::seconds(1), &Policy::default(), )); assert!(!publication_point_cache_has_future_not_before_risk( &pack, &objects, &[], ee.tbs.validity_not_after + time::Duration::seconds(1), &Policy::default(), )); } #[test] fn publication_point_cache_delete_action_removes_existing_projection() { let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let policy = Policy::default(); let validation_time = time::OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(1); let ca = publication_point_cache_fixture_ca(); let vcir = seed_publication_point_cache_projection(&store, &policy, &ca, validation_time); assert!( store .get_publication_point_cache_projection_cached(&ca.manifest_rsync_uri) .expect("load projection") .is_some() ); store .replace_vcir_manifest_replay_meta_and_projection_action( &vcir, None, PublicationPointCacheProjectionWriteAction::Delete { manifest_rsync_uri: &vcir.manifest_rsync_uri, }, ) .expect("delete projection"); assert!( store .get_publication_point_cache_projection_cached(&ca.manifest_rsync_uri) .expect("load projection after delete") .is_none() ); } fn publication_point_cache_fixture_ca() -> CaInstanceHandle { CaInstanceHandle { depth: 1, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(b"ca-cert".to_vec()), ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()), effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: Some("https://example.test/notification.xml".to_string()), } } #[test] fn runner_publication_point_cache_reuses_projection_outputs_children_and_ccr() { let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let policy = Policy::default(); let validation_time = time::OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(1); let ca = publication_point_cache_fixture_ca(); let vcir = seed_publication_point_cache_projection(&store, &policy, &ca, validation_time); let timing = crate::analysis::timing::TimingHandle::new(crate::analysis::timing::TimingMeta { recorded_at_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(), validation_time_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(), tal_url: None, db_path: None, }); let runner = Rpkiv1PublicationPointRunner { store: &store, policy: &policy, http_fetcher: &NeverHttpFetcher, rsync_fetcher: &FailingRsyncFetcher, validation_time, timing: Some(timing.clone()), download_log: None, replay_archive_index: None, replay_delta_index: None, rrdp_dedup: false, rrdp_repo_cache: Mutex::new(HashMap::new()), rsync_dedup: false, rsync_repo_cache: Mutex::new(HashMap::new()), current_repo_index: None, repo_sync_runtime: None, parallel_phase2_config: None, parallel_roa_worker_pool: None, ccr_accumulator: Some(Mutex::new(CcrAccumulator::new(Vec::new()))), persist_vcir: true, enable_roa_validation_cache: false, enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: true, }; let result = runner .observe_or_reuse_publication_point_cache(&ca, Some("rrdp"), Some("delta"), 7, None, &[]) .expect("cache result"); assert_eq!(result.source, PublicationPointSource::PublicationPointCache); assert_eq!(result.objects.vrps.len(), 1); assert_eq!(result.objects.aspas.len(), 1); assert_eq!(result.objects.router_keys.len(), 1); assert_eq!(result.discovered_children.len(), 1); assert_eq!( result.discovered_children[0].handle.manifest_rsync_uri, vcir.child_entries[0].child_manifest_rsync_uri ); assert!(result.cir_fresh_objects.is_empty()); assert!(!result.cir_cached_objects.is_empty()); assert_eq!( runner .ccr_accumulator_snapshot() .expect("ccr accumulator") .manifest_count(), 1 ); let counts = timing.counts_snapshot(); assert_eq!( counts.get("publication_point_cache_reuse_hits").copied(), Some(1) ); assert_eq!( counts .get("publication_point_cache_outputs_reused") .copied(), Some(3) ); assert_eq!( counts .get("publication_point_cache_children_reused") .copied(), Some(1) ); assert_eq!( counts .get("publication_point_cache_related_objects_reused") .copied(), Some(vcir.related_artifacts.len() as u64) ); assert_eq!( counts .get("publication_point_cache_audit_objects_reused") .copied(), Some(result.cir_cached_objects.len() as u64) ); let timing_dir = tempfile::tempdir().expect("timing dir"); let timing_path = timing_dir.path().join("timing.json"); timing.write_json(&timing_path, 20).expect("write timing"); let timing_json: serde_json::Value = serde_json::from_slice(&std::fs::read(&timing_path).expect("read timing")) .expect("parse timing"); let phase_keys = timing_json["phases"] .as_object() .expect("phases") .keys() .map(|key| key.as_str()) .collect::>(); assert!(phase_keys.contains("publication_point_cache_lookup_hit_total")); assert!(phase_keys.contains("publication_point_cache_reuse_build_total")); assert!(phase_keys.contains("publication_point_cache_build_objects_total")); } #[test] fn publication_point_cache_restore_children_parallel_keeps_order_and_audit() { let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let policy = Policy::default(); let validation_time = time::OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(1); let ca = publication_point_cache_fixture_ca(); seed_publication_point_cache_projection(&store, &policy, &ca, validation_time); let mut projection = store .get_publication_point_cache_projection(&ca.manifest_rsync_uri) .expect("load projection") .expect("projection"); let template = projection.children[0].clone(); let mut blobs = Vec::new(); let mut children = Vec::new(); for index in 0..300 { let bytes = format!("child-cert-{index}").into_bytes(); let child_hash = sha256_hex(&bytes); let mut child = template.clone(); child.child_cert_hash = child_hash.clone(); child.child_cert_rsync_uri = format!("rsync://example.test/repo/issuer/child-{index}.cer"); child.child_manifest_rsync_uri = format!("rsync://example.test/repo/child-{index}/child.mft"); child.child_publication_point_rsync_uri = format!("rsync://example.test/repo/child-{index}/"); child.child_rsync_base_uri = child.child_publication_point_rsync_uri.clone(); blobs.push((child_hash, bytes)); children.push(child); } projection.children = children; store.put_blob_bytes_batch(&blobs).expect("put child blobs"); let timing = crate::analysis::timing::TimingHandle::new(crate::analysis::timing::TimingMeta { recorded_at_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(), validation_time_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(), tal_url: None, db_path: None, }); let mut warnings = Vec::new(); let (restored_children, audits) = restore_children_from_publication_point_cache( &store, &ca, &projection, validation_time, &mut warnings, 4, Some(&timing), ); assert!(warnings.is_empty()); assert_eq!(restored_children.len(), 300); assert_eq!(audits.len(), 300); assert_eq!( restored_children[0].handle.ca_certificate_sha256_hex(), Some(sha256_hex(b"child-cert-0").as_str()) ); assert_eq!( restored_children[299] .handle .ca_certificate_der(&store) .unwrap() .as_ref(), b"child-cert-299" ); assert_eq!( restored_children[0].handle.manifest_rsync_uri, "rsync://example.test/repo/child-0/child.mft" ); assert!( audits .iter() .all(|audit| audit.result == AuditObjectResult::Ok) ); let counts = timing.counts_snapshot(); assert_eq!( counts .get("publication_point_cache_restore_children_parallel_publication_points") .copied(), Some(1) ); assert_eq!( counts .get("publication_point_cache_restore_children_parallel_children") .copied(), Some(300) ); assert_eq!( counts .get("publication_point_cache_restore_children_workers_total") .copied(), Some(4) ); } #[test] fn publication_point_cache_restore_children_does_not_require_child_der_bytes() { let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let policy = Policy::default(); let validation_time = time::OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(1); let ca = publication_point_cache_fixture_ca(); seed_publication_point_cache_projection(&store, &policy, &ca, validation_time); let mut projection = store .get_publication_point_cache_projection(&ca.manifest_rsync_uri) .expect("load projection") .expect("projection"); projection.children[0].child_cert_hash = "22".repeat(32); let mut warnings = Vec::new(); let (restored_children, audits) = restore_children_from_publication_point_cache( &store, &ca, &projection, validation_time, &mut warnings, 1, None, ); assert!(warnings.is_empty()); assert!(!restored_children.is_empty()); assert_eq!(audits.len(), restored_children.len()); assert_eq!( restored_children[0].handle.ca_certificate_sha256_hex(), Some(projection.children[0].child_cert_hash.as_str()) ); assert!( restored_children[0] .handle .ca_certificate_der(&store) .is_err(), "lazy child handle should not require repo bytes until a fresh path asks for DER" ); } #[test] fn runner_publication_point_cache_blocks_parent_policy_and_output_time_mismatch() { let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let policy = Policy::default(); let validation_time = time::OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(1); let ca = publication_point_cache_fixture_ca(); seed_publication_point_cache_projection(&store, &policy, &ca, validation_time); let timing = crate::analysis::timing::TimingHandle::new(crate::analysis::timing::TimingMeta { recorded_at_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(), validation_time_utc_rfc3339: "2026-01-01T00:00:00Z".to_string(), tal_url: None, db_path: None, }); let mut parent_changed_ca = ca.clone(); parent_changed_ca.parent_manifest_rsync_uri = Some("rsync://example.test/repo/other-parent.mft".to_string()); let parent_changed_runner = Rpkiv1PublicationPointRunner { store: &store, policy: &policy, http_fetcher: &NeverHttpFetcher, rsync_fetcher: &FailingRsyncFetcher, validation_time, timing: Some(timing.clone()), download_log: None, replay_archive_index: None, replay_delta_index: None, rrdp_dedup: false, rrdp_repo_cache: Mutex::new(HashMap::new()), rsync_dedup: false, rsync_repo_cache: Mutex::new(HashMap::new()), current_repo_index: None, repo_sync_runtime: None, parallel_phase2_config: None, parallel_roa_worker_pool: None, ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: true, }; assert!( parent_changed_runner .observe_or_reuse_publication_point_cache( &parent_changed_ca, Some("rrdp"), Some("delta"), 7, None, &[] ) .is_none() ); assert_eq!( timing .counts_snapshot() .get("publication_point_cache_miss_parent_context_mismatch") .copied(), Some(1) ); let strict_policy = Policy { strict: crate::policy::StrictPolicy::all(), ..Policy::default() }; let policy_changed_runner = Rpkiv1PublicationPointRunner { store: &store, policy: &strict_policy, http_fetcher: &NeverHttpFetcher, rsync_fetcher: &FailingRsyncFetcher, validation_time, timing: Some(timing.clone()), download_log: None, replay_archive_index: None, replay_delta_index: None, rrdp_dedup: false, rrdp_repo_cache: Mutex::new(HashMap::new()), rsync_dedup: false, rsync_repo_cache: Mutex::new(HashMap::new()), current_repo_index: None, repo_sync_runtime: None, parallel_phase2_config: None, parallel_roa_worker_pool: None, ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: true, }; assert!( policy_changed_runner .observe_or_reuse_publication_point_cache( &ca, Some("rrdp"), Some("delta"), 7, None, &[] ) .is_none() ); assert_eq!( timing .counts_snapshot() .get("publication_point_cache_miss_policy_mismatch") .copied(), Some(1) ); let output_expired_runner = Rpkiv1PublicationPointRunner { store: &store, policy: &policy, http_fetcher: &NeverHttpFetcher, rsync_fetcher: &FailingRsyncFetcher, validation_time: validation_time + time::Duration::minutes(40), timing: Some(timing.clone()), download_log: None, replay_archive_index: None, replay_delta_index: None, rrdp_dedup: false, rrdp_repo_cache: Mutex::new(HashMap::new()), rsync_dedup: false, rsync_repo_cache: Mutex::new(HashMap::new()), current_repo_index: None, repo_sync_runtime: None, parallel_phase2_config: None, parallel_roa_worker_pool: None, ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: true, }; assert!( output_expired_runner .observe_or_reuse_publication_point_cache( &ca, Some("rrdp"), Some("delta"), 7, None, &[] ) .is_none() ); assert_eq!( timing .counts_snapshot() .get("publication_point_cache_miss_output_time_gate") .copied(), Some(1) ); } #[test] fn runner_rsync_dedup_skips_second_sync_for_same_base() { let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0"); assert!(fixture_dir.is_dir(), "fixture directory must exist"); let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string(); let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft"; let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}"); let fixture_manifest_bytes = std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture"); let fixture_manifest = crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes) .expect("decode manifest fixture"); let validation_time = fixture_manifest.manifest.this_update + time::Duration::seconds(60); let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let policy = Policy { sync_preference: crate::policy::SyncPreference::RsyncOnly, ..Policy::default() }; let issuer_ca_der = std::fs::read( std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer", ), ) .expect("read issuer ca fixture"); let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca"); let handle = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(issuer_ca_der), ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), rsync_base_uri: rsync_base_uri.clone(), manifest_rsync_uri: manifest_rsync_uri.clone(), publication_point_rsync_uri: rsync_base_uri.clone(), rrdp_notification_uri: None, }; struct CountingRsyncFetcher { inner: LocalDirRsyncFetcher, calls: Arc, } impl RsyncFetcher for CountingRsyncFetcher { fn fetch_objects( &self, rsync_base_uri: &str, ) -> Result)>, RsyncFetchError> { self.calls.fetch_add(1, Ordering::SeqCst); self.inner.fetch_objects(rsync_base_uri) } } let calls = Arc::new(AtomicUsize::new(0)); let rsync = CountingRsyncFetcher { inner: LocalDirRsyncFetcher::new(&fixture_dir), calls: calls.clone(), }; let runner = Rpkiv1PublicationPointRunner { store: &store, policy: &policy, http_fetcher: &NeverHttpFetcher, rsync_fetcher: &rsync, validation_time, timing: None, download_log: None, replay_archive_index: None, replay_delta_index: None, rrdp_dedup: false, rrdp_repo_cache: Mutex::new(HashMap::new()), rsync_dedup: true, rsync_repo_cache: Mutex::new(HashMap::new()), current_repo_index: None, repo_sync_runtime: None, parallel_phase2_config: None, parallel_roa_worker_pool: None, ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; let first = runner.run_publication_point(&handle).expect("first run ok"); assert_eq!(first.source, PublicationPointSource::Fresh); let second = runner .run_publication_point(&handle) .expect("second run ok"); assert_eq!(second.source, PublicationPointSource::Fresh); assert_eq!( calls.load(Ordering::SeqCst), 1, "rsync should be called once" ); } #[test] fn runner_rsync_dedup_skips_second_sync_for_same_module_scope() { let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0"); assert!(fixture_dir.is_dir(), "fixture directory must exist"); let first_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string(); let second_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/sub/".to_string(); let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft"; let manifest_rsync_uri = format!("{first_base_uri}{manifest_file}"); let fixture_manifest_bytes = std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture"); let fixture_manifest = crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes) .expect("decode manifest fixture"); let validation_time = fixture_manifest.manifest.this_update + time::Duration::seconds(60); let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let policy = Policy { sync_preference: crate::policy::SyncPreference::RsyncOnly, ..Policy::default() }; let issuer_ca_der = std::fs::read( std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer", ), ) .expect("read issuer ca fixture"); let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca"); let handle = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(issuer_ca_der), ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), rsync_base_uri: first_base_uri.clone(), manifest_rsync_uri: manifest_rsync_uri.clone(), publication_point_rsync_uri: first_base_uri.clone(), rrdp_notification_uri: None, }; let second_handle = CaInstanceHandle { rsync_base_uri: second_base_uri.clone(), publication_point_rsync_uri: second_base_uri.clone(), ..handle.clone() }; struct ModuleScopeRsyncFetcher { inner: LocalDirRsyncFetcher, calls: Arc, } impl RsyncFetcher for ModuleScopeRsyncFetcher { fn fetch_objects( &self, rsync_base_uri: &str, ) -> Result)>, RsyncFetchError> { self.calls.fetch_add(1, Ordering::SeqCst); self.inner.fetch_objects(rsync_base_uri) } fn dedup_key(&self, _rsync_base_uri: &str) -> String { "rsync://rpki.cernet.net/repo/".to_string() } } let calls = Arc::new(AtomicUsize::new(0)); let rsync = ModuleScopeRsyncFetcher { inner: LocalDirRsyncFetcher::new(&fixture_dir), calls: calls.clone(), }; let runner = Rpkiv1PublicationPointRunner { store: &store, policy: &policy, http_fetcher: &NeverHttpFetcher, rsync_fetcher: &rsync, validation_time, timing: None, download_log: None, replay_archive_index: None, replay_delta_index: None, rrdp_dedup: false, rrdp_repo_cache: Mutex::new(HashMap::new()), rsync_dedup: true, rsync_repo_cache: Mutex::new(HashMap::new()), current_repo_index: None, repo_sync_runtime: None, parallel_phase2_config: None, parallel_roa_worker_pool: None, ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; let first = runner.run_publication_point(&handle).expect("first run ok"); assert_eq!(first.source, PublicationPointSource::Fresh); let second = runner .run_publication_point(&second_handle) .expect("second run ok"); assert!(matches!( second.source, PublicationPointSource::Fresh | PublicationPointSource::VcirCurrentInstance )); assert_eq!( calls.load(Ordering::SeqCst), 1, "module-scope dedup should skip second sync" ); } #[test] fn runner_rsync_dedup_works_in_rsync_only_mode_even_when_rrdp_notify_exists() { let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0"); assert!(fixture_dir.is_dir(), "fixture directory must exist"); let first_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string(); let second_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/sub/".to_string(); let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft"; let manifest_rsync_uri = format!("{first_base_uri}{manifest_file}"); let fixture_manifest_bytes = std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture"); let fixture_manifest = crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes) .expect("decode manifest fixture"); let validation_time = fixture_manifest.manifest.this_update + time::Duration::seconds(60); let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let policy = Policy { sync_preference: crate::policy::SyncPreference::RsyncOnly, ..Policy::default() }; let issuer_ca_der = std::fs::read( std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer", ), ) .expect("read issuer ca fixture"); let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca"); let handle = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(issuer_ca_der), ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), rsync_base_uri: first_base_uri.clone(), manifest_rsync_uri: manifest_rsync_uri.clone(), publication_point_rsync_uri: first_base_uri.clone(), rrdp_notification_uri: Some("https://rrdp.example.test/notification.xml".to_string()), }; let second_handle = CaInstanceHandle { rsync_base_uri: second_base_uri.clone(), publication_point_rsync_uri: second_base_uri.clone(), ..handle.clone() }; struct ModuleScopeRsyncFetcher { inner: LocalDirRsyncFetcher, calls: Arc, } impl RsyncFetcher for ModuleScopeRsyncFetcher { fn fetch_objects( &self, rsync_base_uri: &str, ) -> Result)>, RsyncFetchError> { self.calls.fetch_add(1, Ordering::SeqCst); self.inner.fetch_objects(rsync_base_uri) } fn dedup_key(&self, _rsync_base_uri: &str) -> String { "rsync://rpki.cernet.net/repo/".to_string() } } let calls = Arc::new(AtomicUsize::new(0)); let rsync = ModuleScopeRsyncFetcher { inner: LocalDirRsyncFetcher::new(&fixture_dir), calls: calls.clone(), }; let runner = Rpkiv1PublicationPointRunner { store: &store, policy: &policy, http_fetcher: &NeverHttpFetcher, rsync_fetcher: &rsync, validation_time, timing: None, download_log: None, replay_archive_index: None, replay_delta_index: None, rrdp_dedup: true, rrdp_repo_cache: Mutex::new(HashMap::new()), rsync_dedup: true, rsync_repo_cache: Mutex::new(HashMap::new()), current_repo_index: None, repo_sync_runtime: None, parallel_phase2_config: None, parallel_roa_worker_pool: None, ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; let first = runner.run_publication_point(&handle).expect("first run ok"); assert_eq!(first.source, PublicationPointSource::Fresh); let second = runner .run_publication_point(&second_handle) .expect("second run ok"); assert!(matches!( second.source, PublicationPointSource::Fresh | PublicationPointSource::VcirCurrentInstance )); assert_eq!( calls.load(Ordering::SeqCst), 1, "rsync-only mode must deduplicate by rsync scope even when RRDP notification is present" ); } #[test] fn runner_when_repo_sync_fails_uses_current_instance_vcir_and_keeps_children_empty_for_fixture() { let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0"); assert!(fixture_dir.is_dir(), "fixture directory must exist"); let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string(); let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft"; let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}"); let fixture_manifest_bytes = std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture"); let fixture_manifest = crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes) .expect("decode manifest fixture"); let validation_time = fixture_manifest.manifest.this_update + time::Duration::seconds(60); let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let policy = Policy { sync_preference: crate::policy::SyncPreference::RsyncOnly, ..Policy::default() }; let issuer_ca_der = std::fs::read( std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer", ), ) .expect("read issuer ca fixture"); let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca"); let handle = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(issuer_ca_der), ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), rsync_base_uri: rsync_base_uri.clone(), manifest_rsync_uri: manifest_rsync_uri.clone(), publication_point_rsync_uri: rsync_base_uri.clone(), rrdp_notification_uri: None, }; // First: successful fresh run to populate the latest VCIR baseline. let ok_runner = Rpkiv1PublicationPointRunner { store: &store, policy: &policy, http_fetcher: &NeverHttpFetcher, rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir), validation_time, timing: None, download_log: None, replay_archive_index: None, replay_delta_index: None, rrdp_dedup: false, rrdp_repo_cache: Mutex::new(HashMap::new()), rsync_dedup: false, rsync_repo_cache: Mutex::new(HashMap::new()), current_repo_index: None, repo_sync_runtime: None, parallel_phase2_config: None, parallel_roa_worker_pool: None, ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; let first = ok_runner .run_publication_point(&handle) .expect("first run ok"); assert_eq!(first.source, PublicationPointSource::Fresh); assert!( first.discovered_children.is_empty(), "fixture has no child .cer" ); // Second: repo sync fails, but we can still reuse current-instance VCIR. let bad_runner = Rpkiv1PublicationPointRunner { store: &store, policy: &policy, http_fetcher: &NeverHttpFetcher, rsync_fetcher: &FailingRsyncFetcher, validation_time, timing: None, download_log: None, replay_archive_index: None, replay_delta_index: None, rrdp_dedup: false, rrdp_repo_cache: Mutex::new(HashMap::new()), rsync_dedup: false, rsync_repo_cache: Mutex::new(HashMap::new()), current_repo_index: None, repo_sync_runtime: None, parallel_phase2_config: None, parallel_roa_worker_pool: None, ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; let second = bad_runner .run_publication_point(&handle) .expect("should reuse current-instance VCIR"); assert_eq!(second.source, PublicationPointSource::VcirCurrentInstance); assert!(second.discovered_children.is_empty()); assert!( second .warnings .iter() .any(|w| w.message.contains("repo sync failed")), "expected warning about repo sync failure" ); // Verification-only replays have the frozen repository bytes available, // so a fresh read would succeed and never naturally enter the fallback // branch. A contract-selected URI must therefore select the same VCIR // projection without attempting fresh manifest/object validation. let mut forced_policy = policy.clone(); forced_policy .verification_forced_vcir_reuse_manifest_uris .insert(manifest_rsync_uri.clone()); let forced_runner = Rpkiv1PublicationPointRunner { store: &store, policy: &forced_policy, http_fetcher: &NeverHttpFetcher, rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir), validation_time, timing: None, download_log: None, replay_archive_index: None, replay_delta_index: None, rrdp_dedup: false, rrdp_repo_cache: Mutex::new(HashMap::new()), rsync_dedup: false, rsync_repo_cache: Mutex::new(HashMap::new()), current_repo_index: None, repo_sync_runtime: None, parallel_phase2_config: None, parallel_roa_worker_pool: None, ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; let forced = forced_runner .run_publication_point(&handle) .expect("contract-selected fallback should reuse current-instance VCIR"); assert_eq!(forced.source, PublicationPointSource::VcirCurrentInstance); assert!(forced.discovered_children.is_empty()); } #[test] fn build_publication_point_audit_emits_no_audit_entry_for_duplicate_pack_uri() { let pack = dummy_pack_with_files(vec![ PackFile::from_bytes_compute_sha256("rsync://example.test/repo/dup.roa", vec![1u8]), PackFile::from_bytes_compute_sha256("rsync://example.test/repo/dup.roa", vec![2u8]), ]); let pp = crate::validation::manifest::PublicationPointResult { source: crate::validation::manifest::PublicationPointSource::VcirCurrentInstance, snapshot: pack.clone(), warnings: Vec::new(), }; let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(vec![1]), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: pack.publication_point_rsync_uri.clone(), manifest_rsync_uri: pack.manifest_rsync_uri.clone(), publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(), rrdp_notification_uri: None, }; let objects = crate::validation::objects::ObjectsOutput { vrps: Vec::new(), aspas: Vec::new(), router_keys: Vec::new(), local_outputs_cache: Vec::new(), warnings: Vec::new(), stats: crate::validation::objects::ObjectsStats::default(), audit: Vec::new(), roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(), roa_cache_object_meta: Vec::new(), }; let audit = build_publication_point_audit_from_snapshot( &ca, pp.source, None, None, None, None, &pp.snapshot, &[], &objects, &[], ); assert_eq!(audit.source, "vcir_current_instance"); assert_eq!(audit.repo_sync_phase, None); assert_eq!(audit.repo_terminal_state, "fallback_current_instance"); assert!( audit .objects .iter() .any(|e| e.detail.as_deref() == Some("skipped: no audit entry")), "expected a duplicate key to produce a 'no audit entry' placeholder" ); } #[test] fn build_publication_point_audit_marks_invalid_crl_as_error_and_overlays_roa_audit() { let now = time::OffsetDateTime::now_utc(); let pack = dummy_pack_with_files(vec![ PackFile::from_bytes_compute_sha256("rsync://example.test/repo/issuer/bad.crl", vec![0u8]), PackFile::from_bytes_compute_sha256("rsync://example.test/repo/issuer/x.roa", vec![1u8]), ]); let pp = crate::validation::manifest::PublicationPointResult { source: crate::validation::manifest::PublicationPointSource::Fresh, snapshot: pack.clone(), warnings: Vec::new(), }; let issuer = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(vec![1]), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: pack.manifest_rsync_uri.clone(), publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(), rrdp_notification_uri: None, }; let objects = crate::validation::objects::ObjectsOutput { vrps: Vec::new(), aspas: Vec::new(), router_keys: Vec::new(), local_outputs_cache: Vec::new(), warnings: Vec::new(), stats: crate::validation::objects::ObjectsStats::default(), audit: vec![ObjectAuditEntry { rsync_uri: "rsync://example.test/repo/issuer/x.roa".to_string(), sha256_hex: sha256_hex_from_32(&pack.files[1].sha256), kind: AuditObjectKind::Roa, result: AuditObjectResult::Ok, detail: None, }], roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(), roa_cache_object_meta: Vec::new(), }; let audit = build_publication_point_audit_from_snapshot( &issuer, pp.source, Some("rsync"), Some("rsync_only_ok"), Some(123), Some("none"), &pp.snapshot, &[], &objects, &[], ); assert_eq!(audit.objects[0].kind, AuditObjectKind::Manifest); assert_eq!(audit.repo_sync_source.as_deref(), Some("rsync")); assert_eq!(audit.repo_sync_phase.as_deref(), Some("rsync_only_ok")); assert_eq!(audit.repo_sync_duration_ms, Some(123)); assert_eq!(audit.repo_sync_error.as_deref(), Some("none")); assert_eq!(audit.repo_terminal_state, "fresh"); let crl = audit .objects .iter() .find(|e| e.rsync_uri.ends_with("bad.crl")) .expect("crl entry"); assert!(matches!(crl.result, AuditObjectResult::Error)); let roa = audit .objects .iter() .find(|e| e.rsync_uri.ends_with("x.roa")) .expect("roa entry"); assert!(matches!(roa.result, AuditObjectResult::Ok)); // Smoke that time fields are populated from pack. assert!(audit.verified_at_rfc3339_utc.contains('T')); assert!(audit.this_update_rfc3339_utc.contains('T')); assert!(audit.next_update_rfc3339_utc.contains('T')); let _ = now; } #[test] fn discover_children_with_router_certificate_records_ok_audit_and_no_child() { let g = generate_router_cert_with_variant("ec-p256", true); let pack = dummy_pack_with_files(vec![ PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/issuer.crl", g.issuer_crl_der.clone(), ), PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/router.cer", g.router_der.clone(), ), ]); let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer"); let issuer = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()), ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: None, }; let out = discover_children_from_fresh_snapshot_with_audit( &issuer, &pack, time::OffsetDateTime::now_utc(), None, ) .expect("discover router cert"); assert!(out.children.is_empty()); assert_eq!(out.audits.len(), 1); assert!(matches!(out.audits[0].result, AuditObjectResult::Ok)); assert!( out.audits[0] .detail .as_deref() .unwrap_or("") .contains("validated BGPsec router certificate") ); } #[test] fn discover_children_with_non_router_ee_certificate_records_skipped_audit() { let g = generate_router_cert_with_variant("ec-p256", false); let pack = dummy_pack_with_files(vec![ PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/issuer.crl", g.issuer_crl_der.clone(), ), PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/router-no-eku.cer", g.router_der.clone(), ), ]); let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer"); let issuer = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()), ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: None, }; let out = discover_children_from_fresh_snapshot_with_audit( &issuer, &pack, time::OffsetDateTime::now_utc(), None, ) .expect("discover non-router cert"); assert!(out.children.is_empty()); assert_eq!(out.audits.len(), 1); assert!(matches!(out.audits[0].result, AuditObjectResult::Skipped)); assert!( out.audits[0] .detail .as_deref() .unwrap_or("") .contains("not a CA resource certificate or BGPsec router certificate") ); } #[test] fn discover_children_with_invalid_router_certificate_records_error_audit() { let g = generate_router_cert_with_variant("ec-p384", true); let pack = dummy_pack_with_files(vec![ PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/issuer.crl", g.issuer_crl_der.clone(), ), PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/router-invalid.cer", g.router_der.clone(), ), ]); let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer"); let issuer = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()), ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: None, }; let out = discover_children_from_fresh_snapshot_with_audit( &issuer, &pack, time::OffsetDateTime::now_utc(), None, ) .expect("discover invalid router cert"); assert!(out.children.is_empty()); assert_eq!(out.audits.len(), 1); assert!(matches!(out.audits[0].result, AuditObjectResult::Error)); assert!( out.audits[0] .detail .as_deref() .unwrap_or("") .contains("router certificate validation failed") ); } #[test] fn discover_children_with_audit_records_decode_error_for_corrupt_cer() { let g = generate_chain_and_crl(); let pack = dummy_pack_with_files(vec![ PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/issuer.crl", g.issuer_crl_der.clone(), ), PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/corrupt.cer", vec![0u8], ), ]); let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer"); let issuer = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()), ca_certificate_rsync_uri: None, effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: None, }; let now = time::OffsetDateTime::now_utc(); let out = discover_children_from_fresh_snapshot_with_audit(&issuer, &pack, now, None) .expect("discover children"); assert!(out.children.is_empty()); assert_eq!(out.audits.len(), 1); assert!(matches!(out.audits[0].result, AuditObjectResult::Error)); } #[test] fn select_issuer_crl_uri_for_child_covers_missing_and_not_found_paths() { let g = generate_chain_and_crl(); let child = ResourceCertificate::decode_der(&g.child_ca_der).expect("decode child cert"); let empty: std::collections::HashMap = std::collections::HashMap::new(); let err = select_issuer_crl_uri_for_child(&child, &empty).unwrap_err(); assert!(err.contains("no CRL available"), "{err}"); let ta_der = std::fs::read( std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/ta/apnic-ta.cer"), ) .expect("read TA fixture"); let ta = ResourceCertificate::decode_der(&ta_der).expect("decode TA fixture"); let mut cache = std::collections::HashMap::new(); cache.insert( "rsync://example.test/repo/issuer/issuer.crl".to_string(), CachedIssuerCrl::Pending { bytes: g.issuer_crl_der.clone(), sha256_hex: None, }, ); let err = select_issuer_crl_uri_for_child(&ta, &cache).unwrap_err(); assert!(err.contains("CRLDistributionPoints missing"), "{err}"); let mut wrong = std::collections::HashMap::new(); wrong.insert( "rsync://example.test/repo/issuer/other.crl".to_string(), CachedIssuerCrl::Pending { bytes: g.issuer_crl_der, sha256_hex: None, }, ); let err = select_issuer_crl_uri_for_child(&child, &wrong).unwrap_err(); assert!( err.contains("not found in publication point snapshot"), "{err}" ); } #[test] fn ensure_issuer_crl_verified_promotes_pending_cache_entry() { let g = generate_chain_and_crl(); let mut cache = std::collections::HashMap::new(); let crl_uri = "rsync://example.test/repo/issuer/issuer.crl".to_string(); cache.insert( crl_uri.clone(), CachedIssuerCrl::Pending { bytes: g.issuer_crl_der.clone(), sha256_hex: None, }, ); let first = ensure_issuer_crl_verified(&crl_uri, &mut cache, &g.issuer_ca_der) .expect("verify pending CRL"); assert!(first.revoked_serials.is_empty()); assert!(matches!(cache.get(&crl_uri), Some(CachedIssuerCrl::Ok(_)))); let second = ensure_issuer_crl_verified(&crl_uri, &mut cache, &g.issuer_ca_der) .expect("reuse verified CRL"); assert!(second.revoked_serials.is_empty()); } #[test] fn discover_children_with_invalid_issuer_der_records_error_audit() { let g = generate_chain_and_crl(); let pack = dummy_pack_with_files(vec![ PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/issuer.crl", g.issuer_crl_der.clone(), ), PackFile::from_bytes_compute_sha256( "rsync://example.test/repo/issuer/child.cer", g.child_ca_der.clone(), ), ]); let issuer = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(vec![0u8]), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: None, }; let out = discover_children_from_fresh_snapshot_with_audit( &issuer, &pack, time::OffsetDateTime::now_utc(), None, ) .expect("discover children with invalid issuer der"); assert!(out.children.is_empty()); assert_eq!(out.audits.len(), 1); assert!(matches!(out.audits[0].result, AuditObjectResult::Error)); assert!( out.audits[0] .detail .as_deref() .unwrap_or("") .contains("issuer CA decode failed") ); } #[test] fn project_current_instance_vcir_reuses_local_outputs_and_restores_children() { let now = time::OffsetDateTime::now_utc(); let g = generate_chain_and_crl(); let child_cert_hash = sha256_hex(&g.child_ca_der); let vcir = sample_vcir_for_projection(now, &child_cert_hash); let store_dir = tempfile::tempdir().expect("store dir"); let main_db = store_dir.path().join("work-db"); let repo_bytes_db = store_dir.path().join("repo-bytes.db"); let store = RocksStore::open_with_external_repo_bytes(&main_db, &repo_bytes_db) .expect("open rocksdb with external repo bytes"); store .put_blob_bytes_batch(&[(child_cert_hash.clone(), g.child_ca_der.clone())]) .expect("put child cert repo bytes"); assert!( store .get_raw_by_hash_entry(&child_cert_hash) .expect("lookup child raw_by_hash") .is_none(), "child cert restoration should not require raw_by_hash entries" ); let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: vcir.manifest_rsync_uri.clone(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: Some("https://example.test/notify.xml".to_string()), }; let policy = Policy::default(); put_vcir_for_failed_fetch_reuse(&store, &ca, &policy, &vcir); let projection = project_current_instance_vcir_on_failed_fetch( &store, &ca, &ManifestFreshError::RepoSyncFailed { detail: "synthetic".to_string(), }, &policy, now, ) .expect("project vcir"); assert_eq!( projection.source, PublicationPointSource::VcirCurrentInstance ); assert_eq!(projection.objects.vrps.len(), 1); assert_eq!(projection.objects.aspas.len(), 1); assert_eq!(projection.objects.router_keys.len(), 1); assert_eq!(projection.discovered_children.len(), 1); assert_eq!( projection.discovered_children[0].handle.manifest_rsync_uri, "rsync://example.test/repo/child/child.mft" ); assert_eq!( projection.ccr_manifest_projection.as_ref(), Some(&vcir.ccr_manifest_projection) ); assert!( projection.snapshot.is_none(), "current-instance reuse should not reconstruct a byte-backed snapshot" ); assert!( !projection .warnings .iter() .any(|warning| warning.message.contains("manifest failed fetch")), "successful current-instance reuse should not duplicate the fresh fetch error" ); assert!( !projection .warnings .iter() .any(|warning| warning.message.contains("using latest validated result")), "successful current-instance reuse should be tracked by source, not warning" ); assert!( !projection .warnings .iter() .any(|warning| warning.message.contains("manifest raw bytes missing")), "successful current-instance reuse should not load repo bytes for audit reconstruction" ); assert!( !projection .warnings .iter() .any(|warning| warning.message.contains("child certificate bytes missing")), "child discovery restoration should read child certs from repo bytes" ); } #[test] fn project_current_instance_vcir_does_not_reuse_without_identity() { let now = time::OffsetDateTime::now_utc(); let child_cert_hash = sha256_hex(b"child-cert"); let vcir = sample_vcir_for_projection(now, &child_cert_hash); let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); store.put_vcir(&vcir).expect("put legacy-style VCIR"); let ca = sample_ca_for_failed_fetch_reuse(&vcir); let projection = project_current_instance_vcir_on_failed_fetch( &store, &ca, &ManifestFreshError::RepoSyncFailed { detail: "synthetic".to_string(), }, &Policy::default(), now, ) .expect("project VCIR"); assert_eq!( projection.source, PublicationPointSource::FailedFetchNoCache ); assert!(projection.objects.vrps.is_empty()); assert!(projection.discovered_children.is_empty()); assert!( projection .warnings .iter() .any(|warning| warning.message.contains("reuse identity is missing")) ); } #[test] fn project_current_instance_vcir_does_not_reuse_when_ta_context_changes() { let now = time::OffsetDateTime::now_utc(); let child_cert_hash = sha256_hex(b"child-cert"); let vcir = sample_vcir_for_projection(now, &child_cert_hash); let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let policy = Policy::default(); let original_ca = sample_ca_for_failed_fetch_reuse(&vcir); put_vcir_for_failed_fetch_reuse(&store, &original_ca, &policy, &vcir); let mut changed_ca = original_ca; changed_ca.tal_id = "different-tal".to_string(); let projection = project_current_instance_vcir_on_failed_fetch( &store, &changed_ca, &ManifestFreshError::RepoSyncFailed { detail: "synthetic".to_string(), }, &policy, now, ) .expect("project VCIR"); assert_eq!( projection.source, PublicationPointSource::FailedFetchNoCache ); assert!(projection.objects.vrps.is_empty()); assert!(projection.discovered_children.is_empty()); assert!( projection .warnings .iter() .any(|warning| warning.message.contains("identity does not match")) ); } #[test] fn project_current_instance_vcir_returns_no_output_when_instance_gate_expired() { let now = time::OffsetDateTime::now_utc(); let child_cert_hash = sha256_hex(b"child-cert"); let mut vcir = sample_vcir_for_projection(now, &child_cert_hash); let this_update = PackTime::from_utc_offset_datetime(now - time::Duration::minutes(2)); let expired = PackTime::from_utc_offset_datetime(now - time::Duration::minutes(1)); vcir.validated_manifest_meta.validated_manifest_this_update = this_update; vcir.validated_manifest_meta.validated_manifest_next_update = expired.clone(); vcir.instance_gate.manifest_next_update = expired.clone(); vcir.instance_gate.current_crl_next_update = expired.clone(); vcir.instance_gate.instance_effective_until = expired; let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); store.put_vcir(&vcir).expect("put vcir"); let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: vcir.manifest_rsync_uri.clone(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: None, }; let projection = project_current_instance_vcir_on_failed_fetch( &store, &ca, &ManifestFreshError::RepoSyncFailed { detail: "synthetic".to_string(), }, &Policy::default(), now, ) .expect("project vcir"); assert_eq!( projection.source, PublicationPointSource::FailedFetchNoCache ); assert!(projection.ccr_manifest_projection.is_none()); assert!(projection.objects.vrps.is_empty()); assert!(projection.objects.aspas.is_empty()); assert!(projection.discovered_children.is_empty()); } #[test] fn project_current_instance_vcir_keeps_real_fresh_validation_warning() { let now = time::OffsetDateTime::now_utc(); let child_cert_hash = sha256_hex(b"child-cert"); let vcir = sample_vcir_for_projection(now, &child_cert_hash); let store_dir = tempfile::tempdir().expect("store dir"); let main_db = store_dir.path().join("work-db"); let repo_bytes_db = store_dir.path().join("repo-bytes.db"); let store = RocksStore::open_with_external_repo_bytes(&main_db, &repo_bytes_db) .expect("open rocksdb with external repo bytes"); store .put_blob_bytes_batch(&[(child_cert_hash, b"child-cert".to_vec())]) .expect("put child cert repo bytes"); let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: vcir.manifest_rsync_uri.clone(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: None, }; let policy = Policy::default(); put_vcir_for_failed_fetch_reuse(&store, &ca, &policy, &vcir); let projection = project_current_instance_vcir_on_failed_fetch( &store, &ca, &ManifestFreshError::HashMismatch { rsync_uri: "rsync://example.test/repo/issuer/a.roa".to_string(), }, &policy, now, ) .expect("project vcir"); assert_eq!( projection.source, PublicationPointSource::VcirCurrentInstance ); assert!( projection .warnings .iter() .any(|warning| { warning.message.contains("manifest file hash mismatch") }) ); assert!( !projection .warnings .iter() .any(|warning| warning.message.contains("using latest validated result")), "successful current-instance reuse should not emit bookkeeping warnings" ); } #[test] fn project_current_instance_vcir_returns_no_output_when_latest_result_missing() { let now = time::OffsetDateTime::now_utc(); let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: None, }; let projection = project_current_instance_vcir_on_failed_fetch( &store, &ca, &ManifestFreshError::RepoSyncFailed { detail: "synthetic".to_string(), }, &Policy::default(), now, ) .expect("project without cached vcir"); assert_eq!( projection.source, PublicationPointSource::FailedFetchNoCache ); assert!(projection.vcir.is_none()); assert!(projection.ccr_manifest_projection.is_none()); assert!(projection.snapshot.is_none()); assert!(projection.objects.audit.is_empty()); assert!(projection.discovered_children.is_empty()); assert!(projection.warnings.iter().any(|warning| { warning .message .contains("no latest validated result for current CA instance") })); } #[test] fn project_current_instance_vcir_returns_no_output_when_latest_result_is_ineligible() { let now = time::OffsetDateTime::now_utc(); let child_cert_hash = sha256_hex(b"child-cert"); let mut vcir = sample_vcir_for_projection(now, &child_cert_hash); vcir.audit_summary.failed_fetch_eligible = false; let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); store.put_vcir(&vcir).expect("put vcir"); let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: vcir.manifest_rsync_uri.clone(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: None, }; let projection = project_current_instance_vcir_on_failed_fetch( &store, &ca, &ManifestFreshError::RepoSyncFailed { detail: "synthetic".to_string(), }, &Policy::default(), now, ) .expect("project ineligible vcir"); assert_eq!( projection.source, PublicationPointSource::FailedFetchNoCache ); assert!(projection.vcir.is_some()); assert!(projection.ccr_manifest_projection.is_none()); assert!(projection.snapshot.is_none()); assert!(projection.discovered_children.is_empty()); assert!(projection.warnings.iter().any(|warning| { warning .message .contains("latest VCIR is not marked failed-fetch eligible") })); } #[test] fn project_current_instance_vcir_rejects_mismatched_ccr_projection_uri() { let now = time::OffsetDateTime::now_utc(); let child_cert_hash = sha256_hex(b"child-cert"); let mut vcir = sample_vcir_for_projection(now, &child_cert_hash); vcir.ccr_manifest_projection.manifest_rsync_uri = "rsync://example.test/repo/issuer/other.mft".to_string(); let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: vcir.manifest_rsync_uri.clone(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: None, }; let policy = Policy::default(); put_vcir_for_failed_fetch_reuse(&store, &ca, &policy, &vcir); let err = project_current_instance_vcir_on_failed_fetch( &store, &ca, &ManifestFreshError::RepoSyncFailed { detail: "synthetic".to_string(), }, &policy, now, ) .unwrap_err(); assert!( err.contains("vcir CCR manifest projection URI mismatch"), "{err}" ); } #[test] fn fresh_and_reuse_paths_produce_equivalent_ccr_manifest_projection() { let (pack, issuer_ca_der, validation_time) = cernet_publication_point_snapshot_for_vcir_tests(); let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(issuer_ca_der), ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()), effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: pack.publication_point_rsync_uri.clone(), manifest_rsync_uri: pack.manifest_rsync_uri.clone(), publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(), rrdp_notification_uri: None, }; let child_discovery = discover_children_from_fresh_snapshot_with_audit(&ca, &pack, validation_time, None) .expect("discover children"); let mut objects = empty_objects_output(); let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let (fresh_vcir, _timing) = build_vcir_from_fresh_result_with_timing( &store, &ca, &pack, &mut objects, &[], &child_discovery.audits, &child_discovery.children, validation_time, ) .expect("build fresh vcir"); let reuse_projection = reuse_ccr_manifest_projection_from_vcir(&ca, &fresh_vcir) .expect("reuse projection from vcir"); assert_eq!(fresh_vcir.ccr_manifest_projection, reuse_projection); } #[test] fn append_ccr_manifest_projection_from_reuse_requires_projection_for_current_instance() { let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let policy = Policy::default(); let runner = sample_runner_with_ccr_accumulator(&store, &policy); let err = runner .append_ccr_manifest_projection_from_reuse(&VcirReuseProjection { source: PublicationPointSource::VcirCurrentInstance, vcir: None, ccr_manifest_projection: None, snapshot: None, objects: empty_objects_output(), child_audits: Vec::new(), discovered_children: Vec::new(), warnings: Vec::new(), }) .unwrap_err(); assert!(err.contains("missing CCR manifest projection"), "{err}"); assert_eq!( runner .ccr_accumulator_snapshot() .expect("ccr accumulator snapshot") .manifest_count(), 0 ); } #[test] fn append_ccr_manifest_projection_from_reuse_skips_failed_fetch_no_cache() { let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let policy = Policy::default(); let runner = sample_runner_with_ccr_accumulator(&store, &policy); runner .append_ccr_manifest_projection_from_reuse(&VcirReuseProjection { source: PublicationPointSource::FailedFetchNoCache, vcir: None, ccr_manifest_projection: None, snapshot: None, objects: empty_objects_output(), child_audits: Vec::new(), discovered_children: Vec::new(), warnings: Vec::new(), }) .expect("failed-fetch no-cache should not append"); assert_eq!( runner .ccr_accumulator_snapshot() .expect("ccr accumulator snapshot") .manifest_count(), 0 ); } #[test] fn parse_snapshot_time_value_reports_invalid_timestamp() { let err = parse_snapshot_time_value(&PackTime { rfc3339_utc: "not-a-time".to_string(), }) .unwrap_err(); assert!(err.contains("invalid RFC3339 time 'not-a-time'"), "{err}"); } #[test] fn runner_roa_validation_cache_uses_projection_not_full_vcir_fallback() { let now = time::OffsetDateTime::now_utc(); let child_cert_hash = sha256_hex(b"child-cert"); let mut vcir = sample_vcir_for_projection(now, &child_cert_hash); vcir.local_outputs .retain(|output| output.source_object_type != VcirSourceObjectType::Roa); vcir.summary.local_vrp_count = 0; vcir.summary.local_aspa_count = 1; vcir.summary.local_router_key_count = 1; let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); store.put_vcir(&vcir).expect("put vcir without projection"); assert!( store .get_vcir(&vcir.manifest_rsync_uri) .expect("get vcir") .is_some() ); assert!( store .get_roa_cache_projection(&vcir.manifest_rsync_uri) .expect("get projection") .is_none() ); let timing = crate::analysis::timing::TimingHandle::new(crate::analysis::timing::TimingMeta { recorded_at_utc_rfc3339: "2026-06-07T00:00:00Z".to_string(), validation_time_utc_rfc3339: "2026-06-07T00:00:00Z".to_string(), tal_url: None, db_path: None, }); let policy = Policy::default(); let runner = Rpkiv1PublicationPointRunner { store: &store, policy: &policy, http_fetcher: &NeverHttpFetcher, rsync_fetcher: &FailingRsyncFetcher, validation_time: now, timing: Some(timing.clone()), download_log: None, replay_archive_index: None, replay_delta_index: None, rrdp_dedup: false, rrdp_repo_cache: Mutex::new(HashMap::new()), rsync_dedup: false, rsync_repo_cache: Mutex::new(HashMap::new()), current_repo_index: None, repo_sync_runtime: None, parallel_phase2_config: None, parallel_roa_worker_pool: None, ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: true, enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; assert!( runner .roa_validation_cache_view_for_fresh_point(&vcir.manifest_rsync_uri) .is_none() ); let dir = tempfile::tempdir().expect("timing dir"); let path = dir.path().join("timing.json"); timing.write_json(&path, 10).expect("write timing"); let report: serde_json::Value = serde_json::from_slice(&std::fs::read(path).expect("read timing")).expect("parse timing"); assert_eq!( report["counts"]["roa_validation_cache_projection_missing_publication_points"], 1 ); assert!( report["phases"]["roa_validation_cache_projection_load_total"]["count"] .as_u64() .unwrap_or_default() >= 1 ); } #[test] fn build_objects_output_from_vcir_tracks_expired_and_invalid_cached_outputs() { let now = time::OffsetDateTime::now_utc(); let child_cert_hash = sha256_hex(b"child-cert"); let mut vcir = sample_vcir_for_projection(now, &child_cert_hash); let bad_time_uri = "rsync://example.test/repo/issuer/bad-time.roa".to_string(); let expired_uri = "rsync://example.test/repo/issuer/expired.asa".to_string(); let bad_json_uri = "rsync://example.test/repo/issuer/bad-json.roa".to_string(); let bad_prefix_uri = "rsync://example.test/repo/issuer/bad-prefix.roa".to_string(); let bad_aspa_uri = "rsync://example.test/repo/issuer/bad-aspa.asa".to_string(); for (uri, kind) in [ (bad_time_uri.clone(), VcirArtifactKind::Roa), (expired_uri.clone(), VcirArtifactKind::Aspa), (bad_json_uri.clone(), VcirArtifactKind::Roa), (bad_prefix_uri.clone(), VcirArtifactKind::Roa), (bad_aspa_uri.clone(), VcirArtifactKind::Aspa), ] { vcir.related_artifacts.push(VcirRelatedArtifact { artifact_role: VcirArtifactRole::SignedObject, artifact_kind: kind, uri: Some(uri.clone()), sha256: sha256_hex(uri.as_bytes()), object_type: Some( match kind { VcirArtifactKind::Aspa => "aspa", _ => "roa", } .to_string(), ), validation_status: VcirArtifactValidationStatus::Accepted, reject_reason: None, }); } vcir.local_outputs.push(VcirLocalOutput { output_type: VcirOutputType::Vrp, item_effective_until: PackTime { rfc3339_utc: "bad-time-value".to_string(), }, source_object_uri: bad_time_uri.clone(), source_object_type: VcirSourceObjectType::Roa, source_object_hash: sha256_32(b"bad-time-src"), source_ee_cert_hash: sha256_32(b"bad-time-ee"), payload: VcirLocalOutputPayload::Vrp { asn: 64496, afi: RoaAfi::Ipv4, prefix_len: 24, addr: ipv4_addr([203, 0, 113, 0]), max_length: 24, }, rule_hash: sha256_32(b"bad-time-rule"), }); vcir.local_outputs.push(VcirLocalOutput { output_type: VcirOutputType::Aspa, item_effective_until: PackTime::from_utc_offset_datetime(now - time::Duration::minutes(1)), source_object_uri: expired_uri.clone(), source_object_type: VcirSourceObjectType::Aspa, source_object_hash: sha256_32(b"expired-src"), source_ee_cert_hash: sha256_32(b"expired-ee"), payload: VcirLocalOutputPayload::Aspa { customer_as_id: 64500, provider_as_ids: vec![64501], }, rule_hash: sha256_32(b"expired-rule"), }); vcir.local_outputs.push(VcirLocalOutput { output_type: VcirOutputType::Vrp, item_effective_until: PackTime::from_utc_offset_datetime(now + time::Duration::minutes(5)), source_object_uri: bad_json_uri.clone(), source_object_type: VcirSourceObjectType::Roa, source_object_hash: sha256_32(b"bad-json-src"), source_ee_cert_hash: sha256_32(b"bad-json-ee"), payload: VcirLocalOutputPayload::Aspa { customer_as_id: 64510, provider_as_ids: vec![64511], }, rule_hash: sha256_32(b"bad-json-rule"), }); vcir.local_outputs.push(VcirLocalOutput { output_type: VcirOutputType::Vrp, item_effective_until: PackTime::from_utc_offset_datetime(now + time::Duration::minutes(5)), source_object_uri: bad_prefix_uri.clone(), source_object_type: VcirSourceObjectType::Roa, source_object_hash: sha256_32(b"bad-prefix-src"), source_ee_cert_hash: sha256_32(b"bad-prefix-ee"), payload: VcirLocalOutputPayload::Aspa { customer_as_id: 64512, provider_as_ids: vec![64513], }, rule_hash: sha256_32(b"bad-prefix-rule"), }); vcir.local_outputs.push(VcirLocalOutput { output_type: VcirOutputType::Aspa, item_effective_until: PackTime::from_utc_offset_datetime(now + time::Duration::minutes(5)), source_object_uri: bad_aspa_uri.clone(), source_object_type: VcirSourceObjectType::Aspa, source_object_hash: sha256_32(b"bad-aspa-src"), source_ee_cert_hash: sha256_32(b"bad-aspa-ee"), payload: VcirLocalOutputPayload::Vrp { asn: 64520, afi: RoaAfi::Ipv4, prefix_len: 24, addr: ipv4_addr([198, 51, 100, 0]), max_length: 24, }, rule_hash: sha256_32(b"bad-aspa-rule"), }); let mut warnings = Vec::new(); let output = build_objects_output_from_vcir(&vcir, now, &mut warnings); assert_eq!(output.vrps.len(), 1); assert_eq!(output.aspas.len(), 1); assert_eq!(output.stats.roa_total, 4); assert_eq!(output.stats.roa_ok, 1); assert_eq!(output.stats.aspa_total, 3); assert_eq!(output.stats.aspa_ok, 1); assert!(warnings.iter().any(|warning| { warning .message .contains("cached local output has invalid item_effective_until") })); assert!(warnings.iter().any(|warning| { warning .message .contains("cached ROA local output parse failed") })); assert!(warnings.iter().any(|warning| { warning .message .contains("cached ASPA local output parse failed") })); assert!(output.audit.iter().any(|entry| { entry.rsync_uri == expired_uri && matches!(entry.result, AuditObjectResult::Skipped) && entry.detail.as_deref() == Some("skipped: cached local output expired") })); assert!(output.audit.iter().any(|entry| { entry.rsync_uri == bad_time_uri && matches!(entry.result, AuditObjectResult::Error) })); assert!(output.audit.iter().any(|entry| { entry.rsync_uri == bad_prefix_uri && matches!(entry.result, AuditObjectResult::Error) && entry .detail .as_deref() .unwrap_or("") .contains("cached ROA local output parse failed") })); } #[test] fn build_publication_point_audit_from_vcir_uses_vcir_metadata_and_overlays_child_and_object_audits() { let now = time::OffsetDateTime::now_utc(); let child_cert_hash = sha256_hex(b"child-cert"); let mut vcir = sample_vcir_for_projection(now, &child_cert_hash); vcir.related_artifacts .retain(|artifact| artifact.artifact_role != VcirArtifactRole::Manifest); let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: vcir.manifest_rsync_uri.clone(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: Some("https://example.test/notify.xml".to_string()), }; let runner_warnings = vec![Warning::new("runner warning")]; let objects = crate::validation::objects::ObjectsOutput { vrps: Vec::new(), aspas: Vec::new(), router_keys: Vec::new(), local_outputs_cache: Vec::new(), warnings: vec![Warning::new("objects warning")], stats: crate::validation::objects::ObjectsStats::default(), audit: vec![ObjectAuditEntry { rsync_uri: "rsync://example.test/repo/issuer/a.roa".to_string(), sha256_hex: sha256_hex(b"override-roa"), kind: AuditObjectKind::Roa, result: AuditObjectResult::Error, detail: Some("overridden from object audit".to_string()), }], roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(), roa_cache_object_meta: Vec::new(), }; let child_audits = vec![ObjectAuditEntry { rsync_uri: vcir.child_entries[0].child_cert_rsync_uri.clone(), sha256_hex: vcir.child_entries[0].child_cert_hash.clone(), kind: AuditObjectKind::Certificate, result: AuditObjectResult::Ok, detail: Some("restored child CA instance from VCIR".to_string()), }]; let audit = build_publication_point_audit_from_vcir( &ca, PublicationPointSource::VcirCurrentInstance, Some("rsync"), Some("rrdp_failed_rsync_failed"), Some(456), Some("rsync failed"), Some(&vcir), None, &runner_warnings, &objects, &child_audits, &[], ); assert_eq!(audit.source, "vcir_current_instance"); assert_eq!(audit.repo_sync_source.as_deref(), Some("rsync")); assert_eq!( audit.repo_sync_phase.as_deref(), Some("rrdp_failed_rsync_failed") ); assert_eq!(audit.repo_sync_duration_ms, Some(456)); assert_eq!(audit.repo_sync_error.as_deref(), Some("rsync failed")); assert_eq!(audit.repo_terminal_state, "fallback_current_instance"); assert_eq!(audit.objects[0].rsync_uri, vcir.current_manifest_rsync_uri); assert_eq!(audit.objects[0].kind, AuditObjectKind::Manifest); assert_eq!( audit.this_update_rfc3339_utc, vcir.validated_manifest_meta .validated_manifest_this_update .rfc3339_utc ); assert_eq!( audit.next_update_rfc3339_utc, vcir.validated_manifest_meta .validated_manifest_next_update .rfc3339_utc ); assert_eq!( audit.verified_at_rfc3339_utc, vcir.last_successful_validation_time.rfc3339_utc ); assert_eq!(audit.warnings.len(), 2); assert!(audit.objects.iter().any(|entry| { entry.rsync_uri == "rsync://example.test/repo/issuer/a.roa" && matches!(entry.result, AuditObjectResult::Error) && entry.detail.as_deref() == Some("overridden from object audit") })); assert!(audit.objects.iter().any(|entry| { entry.rsync_uri == vcir.child_entries[0].child_cert_rsync_uri && matches!(entry.result, AuditObjectResult::Ok) })); } #[test] fn build_publication_point_audit_from_vcir_restores_reject_reason_with_legacy_fallback() { let now = time::OffsetDateTime::now_utc(); let child_cert_hash = sha256_hex(b"child-cert"); let mut vcir = sample_vcir_for_projection(now, &child_cert_hash); vcir.related_artifacts.push(VcirRelatedArtifact { artifact_role: VcirArtifactRole::SignedObject, artifact_kind: VcirArtifactKind::Roa, uri: Some("rsync://example.test/repo/issuer/rejected-with-reason.roa".to_string()), sha256: sha256_hex(b"rejected-with-reason"), object_type: Some("roa".to_string()), validation_status: VcirArtifactValidationStatus::Rejected, reject_reason: Some("EE certificate path validation failed: test".to_string()), }); vcir.related_artifacts.push(VcirRelatedArtifact { artifact_role: VcirArtifactRole::SignedObject, artifact_kind: VcirArtifactKind::Roa, uri: Some("rsync://example.test/repo/issuer/rejected-legacy.roa".to_string()), sha256: sha256_hex(b"rejected-legacy"), object_type: Some("roa".to_string()), validation_status: VcirArtifactValidationStatus::Rejected, reject_reason: None, }); let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: vcir.manifest_rsync_uri.clone(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: None, }; let objects = crate::validation::objects::ObjectsOutput { vrps: Vec::new(), aspas: Vec::new(), router_keys: Vec::new(), local_outputs_cache: Vec::new(), warnings: Vec::new(), stats: crate::validation::objects::ObjectsStats::default(), audit: Vec::new(), roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(), roa_cache_object_meta: Vec::new(), }; let audit = build_publication_point_audit_from_vcir( &ca, PublicationPointSource::VcirCurrentInstance, None, None, None, None, Some(&vcir), None, &[], &objects, &[], &[], ); assert!(audit.objects.iter().any(|entry| { entry.rsync_uri == "rsync://example.test/repo/issuer/rejected-with-reason.roa" && matches!(entry.result, AuditObjectResult::Error) && entry.detail.as_deref() == Some("EE certificate path validation failed: test") })); assert!(audit.objects.iter().any(|entry| { entry.rsync_uri == "rsync://example.test/repo/issuer/rejected-legacy.roa" && matches!(entry.result, AuditObjectResult::Error) && entry.detail.as_deref() == Some(CACHED_REJECT_REASON_NOT_RECORDED) })); assert!( audit.objects.iter().all(|entry| { !matches!(entry.result, AuditObjectResult::Ok) || entry.detail.is_none() }) ); } #[test] fn build_publication_point_audit_from_pp_cache_projection_restores_reject_reason() { let now = time::OffsetDateTime::now_utc(); let child_cert_hash = sha256_hex(b"child-cert"); let mut vcir = sample_vcir_for_projection(now, &child_cert_hash); vcir.related_artifacts.push(VcirRelatedArtifact { artifact_role: VcirArtifactRole::SignedObject, artifact_kind: VcirArtifactKind::Roa, uri: Some("rsync://example.test/repo/issuer/rejected-with-reason.roa".to_string()), sha256: sha256_hex(b"rejected-with-reason"), object_type: Some("roa".to_string()), validation_status: VcirArtifactValidationStatus::Rejected, reject_reason: Some("EE certificate path validation failed: test".to_string()), }); vcir.related_artifacts.push(VcirRelatedArtifact { artifact_role: VcirArtifactRole::SignedObject, artifact_kind: VcirArtifactKind::Roa, uri: Some("rsync://example.test/repo/issuer/rejected-legacy.roa".to_string()), sha256: sha256_hex(b"rejected-legacy"), object_type: Some("roa".to_string()), validation_status: VcirArtifactValidationStatus::Rejected, reject_reason: None, }); let projection = PublicationPointCacheProjection::from_vcir_with_context( &vcir, "rsync://example.test/repo/issuer/".to_string(), None, [0x11; 32], [0x22; 32], [0x33; 32], [0x44; 32], [0x55; 32], ) .expect("build publication point projection"); let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: vcir.manifest_rsync_uri.clone(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: None, }; let objects = crate::validation::objects::ObjectsOutput { vrps: Vec::new(), aspas: Vec::new(), router_keys: Vec::new(), local_outputs_cache: Vec::new(), warnings: Vec::new(), stats: crate::validation::objects::ObjectsStats::default(), audit: Vec::new(), roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(), roa_cache_object_meta: Vec::new(), }; let audit = build_publication_point_audit_from_publication_point_cache_projection( &ca, PublicationPointSource::PublicationPointCache, None, None, None, None, &projection, now, &[], &objects, &[], ); assert!(audit.objects.iter().any(|entry| { entry.rsync_uri == "rsync://example.test/repo/issuer/rejected-with-reason.roa" && matches!(entry.result, AuditObjectResult::Error) && entry.detail.as_deref() == Some("EE certificate path validation failed: test") })); assert!(audit.objects.iter().any(|entry| { entry.rsync_uri == "rsync://example.test/repo/issuer/rejected-legacy.roa" && matches!(entry.result, AuditObjectResult::Error) && entry.detail.as_deref() == Some(CACHED_REJECT_REASON_NOT_RECORDED) })); } #[test] fn build_publication_point_audit_from_vcir_failed_no_cache_keeps_current_reject_only() { let now = time::OffsetDateTime::now_utc(); let child_cert_hash = sha256_hex(b"child-cert"); let vcir = sample_vcir_for_projection(now, &child_cert_hash); let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: vcir.manifest_rsync_uri.clone(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: Some("https://example.test/notify.xml".to_string()), }; let objects = crate::validation::objects::ObjectsOutput { vrps: Vec::new(), aspas: Vec::new(), router_keys: Vec::new(), local_outputs_cache: Vec::new(), warnings: Vec::new(), stats: crate::validation::objects::ObjectsStats::default(), audit: vec![ObjectAuditEntry { rsync_uri: vcir.current_manifest_rsync_uri.clone(), sha256_hex: sha256_hex(b"current-manifest"), kind: AuditObjectKind::Manifest, result: AuditObjectResult::Error, detail: Some("manifest is not valid at validation_time".to_string()), }], roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(), roa_cache_object_meta: Vec::new(), }; let audit = build_publication_point_audit_from_vcir( &ca, PublicationPointSource::FailedFetchNoCache, Some("rsync"), Some("rsync_only_ok"), Some(123), None, Some(&vcir), None, &[Warning::new("latest VCIR instance_gate expired")], &objects, &[], &[], ); assert_eq!(audit.source, "failed_fetch_no_cache"); assert_eq!(audit.repo_terminal_state, "failed_no_cache"); assert_eq!( audit.this_update_rfc3339_utc, vcir.validated_manifest_meta .validated_manifest_this_update .rfc3339_utc ); assert_eq!(audit.objects.len(), 1); assert_eq!(audit.objects[0].rsync_uri, vcir.current_manifest_rsync_uri); assert!(matches!(audit.objects[0].result, AuditObjectResult::Error)); assert!( !audit .objects .iter() .any(|entry| entry.rsync_uri == "rsync://example.test/repo/issuer/a.roa"), "failed-no-cache must not expand old VCIR related artifacts into current-run audit", ); assert!( !audit .objects .iter() .any(|entry| entry.rsync_uri == "rsync://example.test/repo/issuer/issuer.crl"), "failed-no-cache must not expose old CRL as current-run CIR input", ); } #[test] fn rejected_manifest_audit_entry_for_failed_fetch_uses_current_repo_hash() { let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let policy = Policy::default(); let runner = sample_runner_with_ccr_accumulator(&store, &policy); let manifest_uri = "rsync://example.test/repo/issuer/issuer.mft"; let manifest_hash = sha256_hex(b"manifest-bytes"); store .put_blob_bytes_batch(&[(manifest_hash.clone(), b"manifest-bytes".to_vec())]) .expect("put manifest bytes"); store .put_repository_view_entry(&crate::storage::RepositoryViewEntry { rsync_uri: manifest_uri.to_string(), current_hash: Some(manifest_hash.clone()), repository_source: Some("rsync://example.test/repo/issuer/".to_string()), object_type: Some("mft".to_string()), state: crate::storage::RepositoryViewState::Present, }) .expect("put repository view"); let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: manifest_uri.to_string(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: None, }; let entry = runner .rejected_manifest_audit_entry_for_failed_fetch( &ca, &ManifestFreshError::StaleOrEarly { this_update_rfc3339_utc: "2026-05-27T08:37:07Z".to_string(), next_update_rfc3339_utc: "2026-05-28T10:01:07Z".to_string(), validation_time_rfc3339_utc: "2026-05-28T10:11:00Z".to_string(), }, ) .expect("rejected manifest audit entry"); assert_eq!(entry.rsync_uri, manifest_uri); assert_eq!(entry.sha256_hex, manifest_hash); assert_eq!(entry.kind, AuditObjectKind::Manifest); assert_eq!(entry.result, AuditObjectResult::Error); assert!( entry .detail .as_deref() .unwrap_or("") .contains("manifest is not valid at validation_time") ); } #[test] fn build_publication_point_audit_from_vcir_without_cached_inputs_returns_empty_listing() { let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: None, }; let audit = build_publication_point_audit_from_vcir( &ca, PublicationPointSource::FailedFetchNoCache, Some("rsync"), Some("rsync_only_failed"), Some(789), Some("load from network failed, fallback to cache"), None, None, &[Warning::new("runner warning")], &crate::validation::objects::ObjectsOutput { vrps: Vec::new(), aspas: Vec::new(), router_keys: Vec::new(), local_outputs_cache: Vec::new(), warnings: vec![Warning::new("object warning")], stats: crate::validation::objects::ObjectsStats::default(), audit: Vec::new(), roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(), roa_cache_object_meta: Vec::new(), }, &[], &[], ); assert_eq!(audit.source, "failed_fetch_no_cache"); assert_eq!(audit.repo_sync_source.as_deref(), Some("rsync")); assert_eq!(audit.repo_sync_phase.as_deref(), Some("rsync_only_failed")); assert_eq!(audit.repo_sync_duration_ms, Some(789)); assert_eq!( audit.repo_sync_error.as_deref(), Some("load from network failed, fallback to cache") ); assert_eq!(audit.repo_terminal_state, "failed_no_cache"); assert!(audit.this_update_rfc3339_utc.is_empty()); assert!(audit.next_update_rfc3339_utc.is_empty()); assert!(audit.verified_at_rfc3339_utc.is_empty()); assert_eq!(audit.warnings.len(), 2); assert!(audit.objects.is_empty()); } #[test] fn effective_repo_sync_duration_uses_runtime_duration_for_failures() { assert_eq!(effective_repo_sync_duration_ms(0, Some(12), false), 12); assert_eq!(effective_repo_sync_duration_ms(5, Some(12), false), 12); assert_eq!(effective_repo_sync_duration_ms(20, Some(12), false), 20); assert_eq!(effective_repo_sync_duration_ms(5, None, false), 5); assert_eq!(effective_repo_sync_duration_ms(5, Some(12), true), 5); } #[test] fn reconstruct_snapshot_from_vcir_reports_missing_manifest_and_related_raw_bytes() { let now = time::OffsetDateTime::now_utc(); let child_cert_hash = sha256_hex(b"child-cert"); let mut vcir = sample_vcir_for_projection(now, &child_cert_hash); let dup_uri = "rsync://example.test/repo/issuer/dup.roa".to_string(); vcir.related_artifacts.push(VcirRelatedArtifact { artifact_role: VcirArtifactRole::SignedObject, artifact_kind: VcirArtifactKind::Roa, uri: Some(dup_uri.clone()), sha256: sha256_hex(b"dup-roa-1"), object_type: Some("roa".to_string()), validation_status: VcirArtifactValidationStatus::Accepted, reject_reason: None, }); vcir.related_artifacts.push(VcirRelatedArtifact { artifact_role: VcirArtifactRole::SignedObject, artifact_kind: VcirArtifactKind::Roa, uri: Some(dup_uri.clone()), sha256: sha256_hex(b"dup-roa-2"), object_type: Some("roa".to_string()), validation_status: VcirArtifactValidationStatus::Accepted, reject_reason: None, }); vcir.related_artifacts.push(VcirRelatedArtifact { artifact_role: VcirArtifactRole::IssuerCert, artifact_kind: VcirArtifactKind::Cer, uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()), sha256: sha256_hex(b"issuer-cert"), object_type: Some("cer".to_string()), validation_status: VcirArtifactValidationStatus::Accepted, reject_reason: None, }); let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: vcir.manifest_rsync_uri.clone(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: None, }; let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let mut warnings = Vec::new(); assert!(reconstruct_snapshot_from_vcir(&store, &ca, &vcir, &mut warnings).is_none()); assert!(warnings.iter().any(|warning| { warning .message .contains("manifest raw bytes missing for VCIR audit reconstruction") })); let manifest_bytes = b"manifest-bytes".to_vec(); let current_crl_bytes = b"current-crl-bytes".to_vec(); let child_bytes = b"child-cert".to_vec(); let roa_bytes = b"roa-bytes".to_vec(); for (bytes, uri, object_type) in [ ( manifest_bytes.clone(), Some(vcir.manifest_rsync_uri.clone()), Some("mft".to_string()), ), ( current_crl_bytes, Some(vcir.current_crl_rsync_uri.clone()), Some("crl".to_string()), ), ( child_bytes, Some(vcir.child_entries[0].child_cert_rsync_uri.clone()), Some("cer".to_string()), ), ( roa_bytes, Some("rsync://example.test/repo/issuer/a.roa".to_string()), Some("roa".to_string()), ), ] { let mut entry = RawByHashEntry::from_bytes(sha256_hex(&bytes), bytes); if let Some(uri) = uri { entry.origin_uris.push(uri); } entry.object_type = object_type; entry.encoding = Some("der".to_string()); store.put_raw_by_hash_entry(&entry).expect("put raw entry"); } warnings.clear(); let pack = reconstruct_snapshot_from_vcir(&store, &ca, &vcir, &mut warnings) .expect("reconstruct pack with partial related artifacts"); assert_eq!(pack.manifest_bytes, manifest_bytes); assert_eq!(pack.files.len(), 3, "crl + child cert + roa only"); assert!( pack.files .iter() .any(|file| file.rsync_uri.ends_with("issuer.crl")) ); assert!( pack.files .iter() .any(|file| file.rsync_uri.ends_with("child.cer")) ); assert!( pack.files .iter() .any(|file| file.rsync_uri.ends_with("a.roa")) ); assert!( !pack .files .iter() .any(|file| file.rsync_uri.ends_with("issuer.cer")) ); assert!(warnings.iter().any(|warning| { warning .message .contains("related artifact raw bytes missing for VCIR audit reconstruction") })); } #[test] fn reconstruct_snapshot_from_vcir_reads_repo_bytes_without_raw_entries() { let now = time::OffsetDateTime::now_utc(); let child_cert_hash = sha256_hex(b"child-cert"); let vcir = sample_vcir_for_projection(now, &child_cert_hash); let ca = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), manifest_rsync_uri: vcir.manifest_rsync_uri.clone(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), rrdp_notification_uri: None, }; let store_dir = tempfile::tempdir().expect("store dir"); let main_db = store_dir.path().join("work-db"); let repo_bytes_db = store_dir.path().join("repo-bytes.db"); let store = RocksStore::open_with_external_repo_bytes(&main_db, &repo_bytes_db) .expect("open rocksdb with external repo bytes"); let repo_blobs = [ b"manifest-bytes".to_vec(), b"current-crl-bytes".to_vec(), b"child-cert".to_vec(), b"roa-bytes".to_vec(), b"aspa-bytes".to_vec(), ] .into_iter() .map(|bytes| (sha256_hex(&bytes), bytes)) .collect::>(); store .put_blob_bytes_batch(&repo_blobs) .expect("put external repo bytes"); let manifest_hash = vcir .related_artifacts .iter() .find(|artifact| artifact.artifact_role == VcirArtifactRole::Manifest) .expect("manifest artifact") .sha256 .clone(); assert!( store .get_raw_by_hash_entry(&manifest_hash) .expect("raw manifest lookup") .is_none(), "repo object bytes must not require raw_by_hash entries" ); let mut warnings = Vec::new(); let pack = reconstruct_snapshot_from_vcir(&store, &ca, &vcir, &mut warnings) .expect("reconstruct pack from external repo bytes"); assert_eq!(pack.manifest_bytes, b"manifest-bytes".to_vec()); assert_eq!(pack.files.len(), 4, "crl + child cert + roa + aspa"); assert!( warnings.iter().all(|warning| { !warning .message .contains("raw bytes missing for VCIR audit reconstruction") }), "external repo bytes should satisfy VCIR audit reconstruction without raw warnings" ); } #[test] fn runner_dedup_paths_execute_with_timing_enabled() { let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0"); let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string(); let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft"; let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}"); let fixture_manifest_bytes = std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture"); let fixture_manifest = crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes) .expect("decode manifest fixture"); let validation_time = fixture_manifest.manifest.this_update + time::Duration::seconds(60); let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let issuer_ca_der = std::fs::read( std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( "tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer", ), ) .expect("read issuer ca fixture"); let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca"); let handle = CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, ca_certificate: CaCertificateRef::inline_der(issuer_ca_der), ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), rsync_base_uri: rsync_base_uri.clone(), manifest_rsync_uri: manifest_rsync_uri.clone(), publication_point_rsync_uri: rsync_base_uri.clone(), rrdp_notification_uri: Some("https://example.test/notification.xml".to_string()), }; let timing = crate::analysis::timing::TimingHandle::new(crate::analysis::timing::TimingMeta { recorded_at_utc_rfc3339: "2026-03-11T00:00:00Z".to_string(), validation_time_utc_rfc3339: "2026-03-11T00:00:00Z".to_string(), tal_url: None, db_path: None, }); let policy_rrdp = Policy::default(); let runner_rrdp = Rpkiv1PublicationPointRunner { store: &store, policy: &policy_rrdp, http_fetcher: &NeverHttpFetcher, rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir), validation_time, timing: Some(timing.clone()), download_log: None, replay_archive_index: None, replay_delta_index: None, rrdp_dedup: true, rrdp_repo_cache: Mutex::new(HashMap::new()), rsync_dedup: false, rsync_repo_cache: Mutex::new(HashMap::new()), current_repo_index: None, repo_sync_runtime: None, parallel_phase2_config: None, parallel_roa_worker_pool: None, ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; let first = runner_rrdp .run_publication_point(&handle) .expect("rrdp fallback to rsync"); assert_eq!(first.source, PublicationPointSource::Fresh); let second = runner_rrdp .run_publication_point(&handle) .expect("rrdp dedup skip"); assert_eq!(second.source, PublicationPointSource::Fresh); let policy_rsync = Policy { sync_preference: crate::policy::SyncPreference::RsyncOnly, ..Policy::default() }; let runner_rsync = Rpkiv1PublicationPointRunner { store: &store, policy: &policy_rsync, http_fetcher: &NeverHttpFetcher, rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir), validation_time, timing: Some(timing), download_log: None, replay_archive_index: None, replay_delta_index: None, rrdp_dedup: false, rrdp_repo_cache: Mutex::new(HashMap::new()), rsync_dedup: true, rsync_repo_cache: Mutex::new(HashMap::new()), current_repo_index: None, repo_sync_runtime: None, parallel_phase2_config: None, parallel_roa_worker_pool: None, ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; let third = runner_rsync .run_publication_point(&handle) .expect("rsync first run"); assert_eq!(third.source, PublicationPointSource::Fresh); let fourth = runner_rsync .run_publication_point(&handle) .expect("rsync dedup run"); assert_eq!(fourth.source, PublicationPointSource::Fresh); assert_eq!( crate::fetch::rsync::normalize_rsync_base_uri("rsync://example.test/repo"), "rsync://example.test/repo/" ); } #[derive(Debug, serde::Deserialize)] #[serde(rename_all = "camelCase")] struct RipeRootFinalizeFixtureEvent { event_type: String, validation_time: String, pp_manifest_uri: Option, object_uri: Option, sha256: Option, object_type: Option, result: Option, reason: Option, } #[derive(Debug)] struct RipeRootFinalizeObject { uri: String, sha256_hex: String, object_type: String, result: String, reason: Option, } struct RipeRootFinalizeFixture { manifest_uri: String, publication_point_uri: String, validation_time: time::OffsetDateTime, manifest_sha256_hex: String, objects: Vec, } fn load_ripe_root_finalize_fixture( fixture_root: &std::path::Path, run_id: &str, ) -> RipeRootFinalizeFixture { let events_path = fixture_root .join(format!("run_{run_id}")) .join("ripe-root-events.jsonl"); let file = std::fs::File::open(&events_path) .unwrap_or_else(|e| panic!("open fixture events {} failed: {e}", events_path.display())); let reader = std::io::BufReader::new(file); let mut manifest_uri = None; let mut validation_time = None; let mut manifest_sha256_hex = None; let mut objects = Vec::new(); for line in std::io::BufRead::lines(reader) { let line = line.expect("read fixture event line"); let event: RipeRootFinalizeFixtureEvent = serde_json::from_str(&line).expect("decode fixture event"); if event.event_type == "publication_point" { manifest_uri = event.pp_manifest_uri; validation_time = Some( time::OffsetDateTime::parse( &event.validation_time, &time::format_description::well_known::Rfc3339, ) .expect("parse validation_time"), ); continue; } if event.event_type != "object" { continue; } let uri = event.object_uri.expect("fixture object uri"); let sha256_hex = event.sha256.expect("fixture object sha256"); let object_type = event.object_type.expect("fixture object type"); let result = event.result.expect("fixture object result"); if object_type == "manifest" { manifest_sha256_hex = Some(sha256_hex.clone()); } objects.push(RipeRootFinalizeObject { uri, sha256_hex, object_type, result, reason: event.reason, }); } let manifest_uri = manifest_uri.expect("fixture publication_point event"); let publication_point_uri = manifest_uri .rsplit_once('/') .map(|(parent, _)| format!("{parent}/")) .expect("manifest uri parent"); RipeRootFinalizeFixture { manifest_uri, publication_point_uri, validation_time: validation_time.expect("fixture validation_time"), manifest_sha256_hex: manifest_sha256_hex.expect("fixture manifest object"), objects, } } fn pack_file_from_fixture_object( object: &RipeRootFinalizeObject, repo_bytes: &Arc, ) -> PackFile { PackFile::from_lazy_repo_bytes( object.uri.clone(), object.sha256_hex.clone(), sha256_hex_to_32(&object.sha256_hex), repo_bytes.clone(), ) } fn child_audit_from_fixture_object(object: &RipeRootFinalizeObject) -> ObjectAuditEntry { ObjectAuditEntry { rsync_uri: object.uri.clone(), sha256_hex: object.sha256_hex.clone(), kind: AuditObjectKind::Certificate, result: match object.result.as_str() { "ok" => AuditObjectResult::Ok, "skipped" => AuditObjectResult::Skipped, _ => AuditObjectResult::Error, }, detail: object.reason.clone(), } } fn discovered_child_from_fixture_object( issuer: &CaInstanceHandle, object: &RipeRootFinalizeObject, child_entry_projection: Option, ) -> DiscoveredChildCaInstance { let stem = object .uri .rsplit_once('/') .map(|(_, file)| file.trim_end_matches(".cer")) .unwrap_or("child"); let child_publication_point = format!("{}synthetic-child-{stem}/", issuer.rsync_base_uri); let child_manifest = format!("{child_publication_point}child.mft"); DiscoveredChildCaInstance { handle: CaInstanceHandle { depth: issuer.depth + 1, tal_id: issuer.tal_id.clone(), parent_manifest_rsync_uri: Some(issuer.manifest_rsync_uri.clone()), ca_certificate: CaCertificateRef::repo_bytes(object.sha256_hex.clone()), ca_certificate_rsync_uri: Some(object.uri.clone()), effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: child_publication_point.clone(), manifest_rsync_uri: child_manifest, publication_point_rsync_uri: child_publication_point, rrdp_notification_uri: issuer.rrdp_notification_uri.clone(), }, discovered_from: crate::audit::DiscoveredFrom { parent_manifest_rsync_uri: issuer.manifest_rsync_uri.clone(), child_ca_certificate_rsync_uri: object.uri.clone(), child_ca_certificate_sha256_hex: object.sha256_hex.clone(), }, child_entry_projection, } } fn child_entry_projection_from_fixture_object( store: &RocksStore, object: &RipeRootFinalizeObject, ) -> DiscoveredChildEntryProjection { let child_der = store .get_blob_bytes(&object.sha256_hex) .expect("load child certificate bytes for projection") .expect("child certificate bytes exist for projection"); let child_cert = ResourceCertificate::decode_der(&child_der).expect("decode child certificate projection"); let child_ski = child_cert .tbs .extensions .subject_key_identifier .as_ref() .expect("child certificate projection SKI"); DiscoveredChildEntryProjection { child_ski: hex::encode(child_ski), } } struct RipeRootChildEntryProfile { count: usize, load_der_nanos: u128, decode_cert_nanos: u128, build_entry_nanos: u128, } fn profile_ripe_root_child_entry_build( store: &RocksStore, discovered_children: &[DiscoveredChildCaInstance], validation_time: time::OffsetDateTime, ) -> Result { let mut out = Vec::with_capacity(discovered_children.len()); let mut load_der_nanos = 0; let mut decode_cert_nanos = 0; let mut build_entry_nanos = 0; for child in discovered_children { let load_started = std::time::Instant::now(); let child_der = child.handle.ca_certificate_der(store)?; load_der_nanos += load_started.elapsed().as_nanos(); let decode_started = std::time::Instant::now(); let child_cert = ResourceCertificate::decode_der(child_der.as_ref()) .map_err(|e| format!("decode child certificate for VCIR failed: {e}"))?; decode_cert_nanos += decode_started.elapsed().as_nanos(); let build_started = std::time::Instant::now(); let child_ski = child_cert .tbs .extensions .subject_key_identifier .as_ref() .ok_or_else(|| "child certificate missing SubjectKeyIdentifier".to_string())?; out.push(VcirChildEntry { child_manifest_rsync_uri: child.handle.manifest_rsync_uri.clone(), child_cert_rsync_uri: child.discovered_from.child_ca_certificate_rsync_uri.clone(), child_cert_hash: child .discovered_from .child_ca_certificate_sha256_hex .clone(), child_ski: hex::encode(child_ski), child_rsync_base_uri: child.handle.rsync_base_uri.clone(), child_publication_point_rsync_uri: child.handle.publication_point_rsync_uri.clone(), child_rrdp_notification_uri: child.handle.rrdp_notification_uri.clone(), child_effective_ip_resources: child.handle.effective_ip_resources.clone(), child_effective_as_resources: child.handle.effective_as_resources.clone(), accepted_at_validation_time: PackTime::from_utc_offset_datetime(validation_time), }); build_entry_nanos += build_started.elapsed().as_nanos(); } Ok(RipeRootChildEntryProfile { count: out.len(), load_der_nanos, decode_cert_nanos, build_entry_nanos, }) } #[test] #[ignore = "manual performance repro: requires target/ripe-root-finalize-repro repo-bytes fixture"] fn ripe_root_finalize_repro_from_remote_fixture() { let fixture_root = std::env::var("RPKI_RIPE_ROOT_FINALIZE_FIXTURE") .map(std::path::PathBuf::from) .unwrap_or_else(|_| { std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("target/ripe-root-finalize-repro") }); let run_id = std::env::var("RPKI_RIPE_ROOT_FINALIZE_RUN").unwrap_or_else(|_| "0262".into()); assert!( fixture_root.exists(), "fixture root missing: {}; expected copied remote fixture", fixture_root.display() ); let fixture = load_ripe_root_finalize_fixture(&fixture_root, &run_id); let repo_bytes_db = fixture_root.join("db/repo-bytes.db"); assert!( repo_bytes_db.exists(), "repo-bytes fixture missing: {}", repo_bytes_db.display() ); let store_dir = tempfile::tempdir().expect("store dir"); let work_db = store_dir.path().join("work-db"); let store = RocksStore::open_with_external_repo_bytes(&work_db, &repo_bytes_db) .expect("open work-db with external repo-bytes"); let repo_bytes = Arc::new( store .external_repo_bytes_ref() .expect("external repo bytes") .clone(), ); let manifest_bytes = store .get_blob_bytes(&fixture.manifest_sha256_hex) .expect("load fixture manifest bytes") .expect("fixture manifest bytes exist"); let manifest = ManifestObject::decode_der(&manifest_bytes).expect("decode fixture manifest"); let mut files = Vec::with_capacity(fixture.objects.len().saturating_sub(1)); let mut child_audits = Vec::new(); let current_ca_hash = "007ad0c291b01ede4bd60e1204074ce3f7192186a022c9577cef5d8e91d5171a"; let current_ca_uri = "rsync://rpki.ripe.net/repository/aca/KpSo3VVK5wEHIJnHC2QHVV3d5mk.cer"; let parent_manifest_uri = "rsync://rpki.ripe.net/repository/aca/7DNNDzoYvgAht7joQih2Qayxcxo.mft"; let ca = CaInstanceHandle { depth: 1, tal_id: "ripe-ncc".to_string(), parent_manifest_rsync_uri: Some(parent_manifest_uri.to_string()), ca_certificate: CaCertificateRef::repo_bytes(current_ca_hash.to_string()), ca_certificate_rsync_uri: Some(current_ca_uri.to_string()), effective_ip_resources: None, effective_as_resources: None, rsync_base_uri: fixture.publication_point_uri.clone(), manifest_rsync_uri: fixture.manifest_uri.clone(), publication_point_rsync_uri: fixture.publication_point_uri.clone(), rrdp_notification_uri: Some("https://rrdp.ripe.net/notification.xml".to_string()), }; let mut discovered_children = Vec::new(); let use_child_projection = std::env::var("RPKI_RIPE_ROOT_FINALIZE_USE_CHILD_PROJECTION") .map(|value| value != "0" && value.to_ascii_lowercase() != "false") .unwrap_or(false); for object in &fixture.objects { if object.uri == fixture.manifest_uri { continue; } files.push(pack_file_from_fixture_object(object, &repo_bytes)); if object.object_type == "certificate" { child_audits.push(child_audit_from_fixture_object(object)); if object.result == "ok" { let child_entry_projection = use_child_projection .then(|| child_entry_projection_from_fixture_object(&store, object)); discovered_children.push(discovered_child_from_fixture_object( &ca, object, child_entry_projection, )); } } } if std::env::var("RPKI_RIPE_ROOT_FINALIZE_PROFILE_CHILD").is_ok() { let profile_children = fixture .objects .iter() .filter(|object| object.object_type == "certificate" && object.result == "ok") .map(|object| discovered_child_from_fixture_object(&ca, object, None)) .collect::>(); let profile_started = std::time::Instant::now(); let profile = profile_ripe_root_child_entry_build(&store, &profile_children, fixture.validation_time) .expect("profile child entry build"); eprintln!( "ripe root child-entry profile: run={} count={} total_ms={} load_der_ms={:.3} decode_cert_ms={:.3} build_entry_ms={:.3}", run_id, profile.count, profile_started.elapsed().as_millis(), profile.load_der_nanos as f64 / 1_000_000.0, profile.decode_cert_nanos as f64 / 1_000_000.0, profile.build_entry_nanos as f64 / 1_000_000.0, ); if std::env::var("RPKI_RIPE_ROOT_FINALIZE_ONLY_CHILD_PROFILE").is_ok() { assert!(profile.count > 22_000); return; } } let fresh_point = FreshValidatedPublicationPoint { manifest_rsync_uri: fixture.manifest_uri.clone(), publication_point_rsync_uri: fixture.publication_point_uri.clone(), manifest_number_be: manifest.manifest.manifest_number.bytes_be.clone(), this_update: PackTime::from_utc_offset_datetime(manifest.manifest.this_update), next_update: PackTime::from_utc_offset_datetime(manifest.manifest.next_update), verified_at: PackTime::from_utc_offset_datetime(fixture.validation_time), manifest_bytes, files, }; let policy = Policy::default(); let enable_ccr_accumulator = std::env::var("RPKI_RIPE_ROOT_FINALIZE_CCR") .map(|value| value != "0" && value.to_ascii_lowercase() != "false") .unwrap_or(true); let runner = Rpkiv1PublicationPointRunner { store: &store, policy: &policy, http_fetcher: &NeverHttpFetcher, rsync_fetcher: &FailingRsyncFetcher, validation_time: fixture.validation_time, timing: None, download_log: None, replay_archive_index: None, replay_delta_index: None, rrdp_dedup: false, rrdp_repo_cache: Mutex::new(HashMap::new()), rsync_dedup: false, rsync_repo_cache: Mutex::new(HashMap::new()), current_repo_index: None, repo_sync_runtime: None, parallel_phase2_config: None, parallel_roa_worker_pool: None, ccr_accumulator: enable_ccr_accumulator .then(|| Mutex::new(CcrAccumulator::new(Vec::new()))), persist_vcir: true, enable_roa_validation_cache: false, enable_child_certificate_validation_cache: true, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: true, }; eprintln!( "ripe root finalize repro setup: run={} ccr_accumulator={} child_projection={} objects={} files={} child_audits={} discovered_children={}", run_id, enable_ccr_accumulator, use_child_projection, fixture.objects.len(), fresh_point.files.len(), child_audits.len(), discovered_children.len() ); let started = std::time::Instant::now(); let output = runner .finalize_fresh_publication_point_from_reducer( &ca, &fresh_point, Vec::new(), empty_objects_output(), child_audits, discovered_children, Some("rrdp"), Some("rrdp_ok"), 0, None, ) .expect("finalize fixture publication point"); let finalize_ms = started.elapsed().as_millis(); eprintln!( "ripe root finalize repro timing: run={} finalize_ms={} snapshot_pack_ms={} persist_vcir_ms={} build_vcir_ms={} child_entries_ms={} related_artifacts_ms={} replace_vcir_ms={} replace_vcir_encode_ms={} replace_vcir_write_batch_ms={} ccr_projection_build_ms={} audit_build_ms={}", run_id, finalize_ms, output.snapshot_pack_ms, output.persist_vcir_ms, output.persist_vcir_timing.build_vcir_ms, output.persist_vcir_timing.build_vcir.child_entries_ms, output.persist_vcir_timing.build_vcir.related_artifacts_ms, output.persist_vcir_timing.replace_vcir_ms, output.persist_vcir_timing.replace_vcir.vcir_encode_ms, output.persist_vcir_timing.replace_vcir.write_batch_ms, output.ccr_projection_build_ms, output.audit_build_ms, ); eprintln!( "ripe root finalize repro result: audit_objects={} discovered_children={} ccr_manifest_count={}", output.result.audit.objects.len(), output.result.discovered_children.len(), runner .ccr_accumulator_snapshot() .map(|snapshot| snapshot.manifest_count()) .unwrap_or(0), ); assert!(fresh_point.files.len() > 22_000); assert!(output.result.discovered_children.len() > 22_000); if enable_ccr_accumulator { assert_eq!( runner .ccr_accumulator_snapshot() .expect("ccr snapshot") .manifest_count(), 1 ); } }