mod vcir_der; use crate::analysis::timing::TimingHandle; use crate::audit::{ AuditObjectKind, AuditObjectResult, AuditWarning, ObjectAuditEntry, PublicationPointAudit, sha256_hex, sha256_hex_from_32, }; use crate::audit_downloads::DownloadLogHandle; use crate::ccr::CcrAccumulator; use crate::current_repo_index::CurrentRepoIndexHandle; use crate::data_model::aspa::AspaObject; use crate::data_model::crl::RpkixCrl; use crate::data_model::manifest::ManifestObject; use crate::data_model::rc::{ResourceCertificate, SubjectInfoAccess}; use crate::data_model::roa::{RoaAfi, RoaObject}; use crate::data_model::router_cert::{ BgpsecRouterCertificate, BgpsecRouterCertificateDecodeError, BgpsecRouterCertificatePathError, BgpsecRouterCertificateProfileError, }; use crate::fetch::rsync::RsyncFetcher; use crate::parallel::config::ParallelPhase2Config; use crate::parallel::repo_runtime::{RepoSyncRuntime, RepoSyncRuntimeOutcome}; use crate::policy::Policy; use crate::replay::archive::ReplayArchiveIndex; use crate::replay::delta_archive::ReplayDeltaArchiveIndex; use crate::report::{RfcRef, Warning}; use crate::storage::{ PackFile, PackTime, PublicationPointCacheChild, PublicationPointCacheOutput, PublicationPointCacheProjection, PublicationPointCacheProjectionWriteAction, RawByHashEntry, RoaCacheProjectionContext, RocksStore, ValidatedCaInstanceResult, VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary, VcirCcrManifestProjection, VcirChildEntry, VcirInstanceGate, VcirLocalOutput, VcirLocalOutputPayload, VcirOutputType, VcirRelatedArtifact, VcirReplaceTimingBreakdown, VcirSourceObjectType, VcirSummary, }; use crate::sync::repo::{ sync_publication_point, sync_publication_point_replay, sync_publication_point_replay_delta, }; use crate::sync::rrdp::Fetcher; use crate::validation::ca_instance::ca_instance_uris_from_ca_certificate; use crate::validation::ca_path::{ CaPathError, IssuerEffectiveResourcesIndex, ValidatedSubordinateCaLite, validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources, }; use crate::validation::manifest::{ FreshPublicationPointTimingBreakdown, FreshValidatedPublicationPoint, ManifestFreshError, PublicationPointData, PublicationPointSource, process_manifest_publication_point_fresh_after_repo_sync_with_timing, }; use crate::validation::objects::{ AspaAttestation, ParallelRoaWorkerPool, RoaValidationCacheInput, RoaValidationCacheView, RouterKeyPayload, Vrp, process_publication_point_for_issuer_parallel_roa_with_cache_options, process_publication_point_for_issuer_parallel_roa_with_pool_cache_options, }; use crate::validation::publication_point::PublicationPointSnapshot; use crate::validation::tree::{ CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult, PublicationPointRunner, }; use sha2::Digest; use std::collections::{HashMap, HashSet}; use std::sync::{Arc, Mutex}; use base64::Engine as _; use x509_parser::prelude::FromDer; use x509_parser::x509::SubjectPublicKeyInfo; use vcir_der::encode_access_description_der_for_vcir_ccr_projection; const PUBLICATION_POINT_CACHE_CHILD_RESTORE_PARALLEL_MIN_CHILDREN: usize = 256; const PUBLICATION_POINT_CACHE_CHILD_RESTORE_MAX_WORKERS: usize = 16; fn sha256_hex_to_32(hex_value: &str) -> [u8; 32] { let mut out = [0u8; 32]; hex::decode_to_slice(hex_value, &mut out).expect("internal sha256 hex should decode"); out } #[derive(Clone, Debug, Default)] pub(crate) struct BuildVcirTimingBreakdown { pub(crate) select_crl_ms: u64, pub(crate) current_ca_decode_ms: u64, pub(crate) local_outputs_ms: u64, pub(crate) child_entries_ms: u64, pub(crate) related_artifacts_ms: u64, pub(crate) struct_build_ms: u64, } #[derive(Clone, Debug, Default)] pub(crate) struct PersistVcirTimingBreakdown { pub(crate) embedded_collect_ms: u64, pub(crate) embedded_store_ms: u64, pub(crate) build_vcir_ms: u64, pub(crate) replace_vcir_ms: u64, pub(crate) publication_point_cache_future_notbefore_guarded: bool, pub(crate) build_vcir: BuildVcirTimingBreakdown, pub(crate) replace_vcir: VcirReplaceTimingBreakdown, } #[derive(Clone, Debug)] pub(crate) struct FreshPublicationPointStage { pub(crate) fresh_point: FreshValidatedPublicationPoint, pub(crate) snapshot_prepare_timing: FreshPublicationPointTimingBreakdown, pub(crate) snapshot_prepare_ms: u64, pub(crate) discovered_children: Vec, pub(crate) child_audits: Vec, pub(crate) discovered_router_keys: Vec, pub(crate) child_discovery_ms: u64, pub(crate) warnings: Vec, } #[derive(Debug)] pub(crate) struct FreshPublicationPointStageError { pub(crate) error: ManifestFreshError, pub(crate) snapshot_prepare_ms: u64, } #[derive(Clone, Debug)] pub(crate) struct FreshPublicationPointFinalizeOutput { pub(crate) result: PublicationPointRunResult, pub(crate) snapshot_pack_ms: u64, pub(crate) persist_vcir_ms: u64, pub(crate) persist_vcir_timing: PersistVcirTimingBreakdown, pub(crate) ccr_projection_build_ms: u64, pub(crate) ccr_append_ms: u64, pub(crate) audit_build_ms: u64, } pub struct Rpkiv1PublicationPointRunner<'a> { pub store: &'a RocksStore, pub policy: &'a Policy, pub http_fetcher: &'a dyn Fetcher, pub rsync_fetcher: &'a dyn RsyncFetcher, pub validation_time: time::OffsetDateTime, pub timing: Option, pub download_log: Option, pub replay_archive_index: Option>, pub replay_delta_index: Option>, /// In-run RRDP dedup: when RRDP is enabled, only sync each `rrdp_notification_uri` once per run. /// /// - If RRDP succeeded for a repo, later publication points referencing that same RRDP repo /// skip network fetches and reuse the already-populated current repository view. /// - If RRDP failed for a repo, later publication points skip RRDP attempts and go straight /// to rsync for their own `rsync_base_uri` (still per-publication-point). pub rrdp_dedup: bool, pub rrdp_repo_cache: Mutex>, // notification_uri -> rrdp_ok /// In-run rsync dedup: when rsync is used, only sync each `rsync_base_uri` once per run. /// /// This reduces duplicate rsync network fetches when multiple publication points share the /// same `rsync_base_uri` (observed in APNIC full sync timing reports). pub rsync_dedup: bool, pub rsync_repo_cache: Mutex>, // rsync_base_uri -> rsync_ok pub current_repo_index: Option, pub repo_sync_runtime: Option>, pub parallel_phase2_config: Option, pub parallel_roa_worker_pool: Option, pub ccr_accumulator: Option>, /// When false, skip VCIR persistence and per-output VCIR projection building. /// /// This is intended for replay/compare-only runs where the caller does not need /// the resulting DB to be reused by a later delta run. pub persist_vcir: bool, pub enable_roa_validation_cache: bool, pub publication_point_cache_observe_only: bool, pub enable_publication_point_validation_cache: bool, } impl<'a> Rpkiv1PublicationPointRunner<'a> { pub(crate) fn roa_validation_cache_view_for_fresh_point( &self, manifest_rsync_uri: &str, ) -> Option { if !self.enable_roa_validation_cache { return None; } let load_started = std::time::Instant::now(); let loaded_projection = self.store.get_roa_cache_projection(manifest_rsync_uri); if let Some(timing) = self.timing.as_ref() { timing.record_phase_nanos( "roa_validation_cache_projection_load_total", load_started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64, ); } match loaded_projection { Ok(Some(projection)) => { if let Some(timing) = self.timing.as_ref() { timing .record_count("roa_validation_cache_projection_hit_publication_points", 1); } let view_started = std::time::Instant::now(); let view = RoaValidationCacheView::from_projection(&projection, self.validation_time); if let Some(timing) = self.timing.as_ref() { timing.record_phase_nanos( "roa_validation_cache_projection_build_total", view_started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64, ); } Some(view) } Ok(None) => { if let Some(timing) = self.timing.as_ref() { timing.record_count( "roa_validation_cache_projection_missing_publication_points", 1, ); } None } Err(err) => { if let Some(timing) = self.timing.as_ref() { timing.record_count("roa_validation_cache_projection_load_errors", 1); } crate::progress_log::emit( "roa_validation_cache_projection_load_error", serde_json::json!({ "manifest_rsync_uri": manifest_rsync_uri, "error": err.to_string(), }), ); None } } } pub(crate) fn observe_or_reuse_publication_point_cache( &self, ca: &CaInstanceHandle, repo_sync_source: Option<&str>, repo_sync_phase: Option<&str>, repo_sync_duration_ms: u64, repo_sync_err: Option<&str>, warnings: &[Warning], ) -> Option { if !self.publication_point_cache_observe_only && !self.enable_publication_point_validation_cache { return None; } let lookup_started = std::time::Instant::now(); if let Some(timing) = self.timing.as_ref() { timing.record_count("publication_point_cache_lookup_total", 1); } let projection = match self .store .get_publication_point_cache_projection_cached(&ca.manifest_rsync_uri) { Ok(Some(projection)) => projection, Ok(None) => { self.finish_publication_point_cache_miss(ca, "missing_projection", lookup_started); return None; } Err(e) => { self.finish_publication_point_cache_miss( ca, "projection_load_error", lookup_started, ); crate::progress_log::emit( "publication_point_cache_lookup_error", serde_json::json!({ "manifest_rsync_uri": ca.manifest_rsync_uri, "error": e.to_string(), }), ); return None; } }; let current_identity = match self.current_publication_point_cache_identity(ca) { Ok(identity) => identity, Err(reason) => { self.finish_publication_point_cache_miss(ca, reason.as_str(), lookup_started); return None; } }; if projection.ca_cert_uri != ca.ca_certificate_rsync_uri { self.finish_publication_point_cache_miss(ca, "ca_uri_mismatch", lookup_started); return None; } if projection.ca_cert_sha256 != current_identity.ca_cert_sha256 { self.finish_publication_point_cache_miss(ca, "ca_hash_mismatch", lookup_started); return None; } if projection.manifest_sha256 != current_identity.manifest_sha256 { self.finish_publication_point_cache_miss(ca, "manifest_hash_mismatch", lookup_started); return None; } if projection.tal_id != ca.tal_id { self.finish_publication_point_cache_miss(ca, "tal_mismatch", lookup_started); return None; } if projection.ta_context_digest != current_identity.ta_context_digest { self.finish_publication_point_cache_miss(ca, "ta_context_mismatch", lookup_started); return None; } if projection.parent_context_digest != current_identity.parent_context_digest { self.finish_publication_point_cache_miss(ca, "parent_context_mismatch", lookup_started); return None; } if projection.validation_policy_fingerprint != current_identity.policy_fingerprint { self.finish_publication_point_cache_miss(ca, "policy_mismatch", lookup_started); return None; } let instance_not_before = match parse_snapshot_time_value(&projection.instance_effective_not_before) { Ok(value) => value, Err(_) => { self.finish_publication_point_cache_miss( ca, "instance_not_before_invalid", lookup_started, ); return None; } }; let instance_until = match parse_snapshot_time_value(&projection.instance_effective_until) { Ok(value) => value, Err(_) => { self.finish_publication_point_cache_miss( ca, "instance_until_invalid", lookup_started, ); return None; } }; if self.validation_time < instance_not_before || self.validation_time >= instance_until { self.finish_publication_point_cache_miss(ca, "instance_time_gate_miss", lookup_started); return None; } if let Err(reason) = publication_point_cache_projection_items_valid(&projection, self.validation_time) { self.finish_publication_point_cache_miss(ca, reason, lookup_started); return None; } if let Some(timing) = self.timing.as_ref() { let lookup_nanos = lookup_started .elapsed() .as_nanos() .min(u128::from(u64::MAX)) as u64; timing.record_count("publication_point_cache_theoretical_hits", 1); timing.record_phase_nanos("publication_point_cache_lookup_total", lookup_nanos); timing.record_phase_nanos("publication_point_cache_lookup_hit_total", lookup_nanos); timing.record_phase_nanos( "publication_point_cache_lookup_duration_total", lookup_nanos, ); } if self.publication_point_cache_observe_only { return None; } match self.build_publication_point_cache_result( ca, projection, repo_sync_source, repo_sync_phase, repo_sync_duration_ms, repo_sync_err, warnings, ) { Ok(result) => { if let Some(timing) = self.timing.as_ref() { timing.record_count("publication_point_cache_reuse_hits", 1); } Some(result) } Err(e) => { self.record_publication_point_cache_miss("reuse_build_error"); crate::progress_log::emit( "publication_point_cache_reuse_error", serde_json::json!({ "manifest_rsync_uri": ca.manifest_rsync_uri, "error": e, }), ); None } } } fn finish_publication_point_cache_miss( &self, ca: &CaInstanceHandle, reason: &str, lookup_started: std::time::Instant, ) { self.record_publication_point_cache_miss(reason); let lookup_nanos = lookup_started .elapsed() .as_nanos() .min(u128::from(u64::MAX)) as u64; if let Some(timing) = self.timing.as_ref() { timing.record_phase_nanos("publication_point_cache_lookup_miss_total", lookup_nanos); timing.record_phase_nanos( "publication_point_cache_lookup_duration_total", lookup_nanos, ); } let elapsed_ms = lookup_nanos / 1_000_000; if elapsed_ms >= crate::progress_log::pp_cache_slow_threshold_ms() { crate::progress_log::emit( "publication_point_cache_miss_slow", serde_json::json!({ "manifest_rsync_uri": ca.manifest_rsync_uri.as_str(), "publication_point_rsync_uri": ca.publication_point_rsync_uri.as_str(), "ca_certificate_rsync_uri": ca.ca_certificate_rsync_uri.as_deref(), "reason": reason, "elapsed_ms": elapsed_ms, "slow_threshold_ms": crate::progress_log::pp_cache_slow_threshold_ms(), }), ); } } fn record_publication_point_cache_miss(&self, reason: &str) { if let Some(timing) = self.timing.as_ref() { timing.record_count("publication_point_cache_miss_total", 1); match reason { "missing_projection" => { timing.record_count("publication_point_cache_miss_missing_projection", 1) } "projection_load_error" => { timing.record_count("publication_point_cache_miss_projection_load_error", 1) } "current_manifest_missing" => { timing.record_count("publication_point_cache_miss_current_manifest_missing", 1) } "ca_uri_mismatch" => { timing.record_count("publication_point_cache_miss_ca_uri_mismatch", 1) } "ca_hash_mismatch" => { timing.record_count("publication_point_cache_miss_ca_hash_mismatch", 1) } "manifest_hash_mismatch" => { timing.record_count("publication_point_cache_miss_manifest_hash_mismatch", 1) } "tal_mismatch" => { timing.record_count("publication_point_cache_miss_tal_mismatch", 1) } "ta_context_mismatch" => { timing.record_count("publication_point_cache_miss_ta_context_mismatch", 1) } "parent_context_mismatch" => { timing.record_count("publication_point_cache_miss_parent_context_mismatch", 1) } "policy_mismatch" => { timing.record_count("publication_point_cache_miss_policy_mismatch", 1) } "instance_not_before_invalid" => timing.record_count( "publication_point_cache_miss_instance_not_before_invalid", 1, ), "instance_until_invalid" => { timing.record_count("publication_point_cache_miss_instance_until_invalid", 1) } "instance_time_gate_miss" => { timing.record_count("publication_point_cache_miss_instance_time_gate", 1) } "output_time_gate_miss" => { timing.record_count("publication_point_cache_miss_output_time_gate", 1) } "child_time_gate_miss" => { timing.record_count("publication_point_cache_miss_child_time_gate", 1) } "reuse_build_error" => { timing.record_count("publication_point_cache_miss_reuse_build_error", 1) } _ => timing.record_count("publication_point_cache_miss_other", 1), } } } fn current_publication_point_cache_identity( &self, ca: &CaInstanceHandle, ) -> Result { let ca_cert_sha256 = match ca.ca_certificate_rsync_uri.as_deref() { Some(uri) => self .current_hash_for_uri(uri) .unwrap_or_else(|| sha256_digest_32(&ca.ca_certificate_der)), None => sha256_digest_32(&ca.ca_certificate_der), }; let manifest_sha256 = self .current_hash_for_uri(&ca.manifest_rsync_uri) .ok_or_else(|| "current_manifest_missing".to_string())?; Ok(PublicationPointCacheIdentity { ca_cert_sha256, manifest_sha256, ta_context_digest: ta_context_digest_for_ca(ca), parent_context_digest: parent_context_digest_for_ca(ca), policy_fingerprint: publication_point_cache_policy_fingerprint(self.policy), }) } fn current_hash_for_uri(&self, uri: &str) -> Option<[u8; 32]> { if let Some(index) = self.current_repo_index.as_ref() { if let Ok(index) = index.lock() { if let Some(entry) = index.get_by_uri(uri) { return Some(entry.current_hash); } } } self.store .load_current_object_with_hash_by_uri(uri) .ok() .flatten() .map(|entry| entry.current_hash) } fn build_publication_point_cache_result( &self, ca: &CaInstanceHandle, projection: PublicationPointCacheProjection, repo_sync_source: Option<&str>, repo_sync_phase: Option<&str>, repo_sync_duration_ms: u64, repo_sync_err: Option<&str>, warnings: &[Warning], ) -> Result { let build_started = std::time::Instant::now(); let mut warnings = warnings.to_vec(); let output_reuse_count = projection.outputs.len() as u64; let child_reuse_count = projection.children.len() as u64; let related_object_reuse_count = projection.related_objects.len() as u64; let build_objects_started = std::time::Instant::now(); let mut objects = build_objects_output_from_publication_point_cache_projection( &projection, self.validation_time, &mut warnings, ); let build_objects_ms = self.record_publication_point_cache_phase_ms( "publication_point_cache_build_objects_total", build_objects_started, ); let restore_children_started = std::time::Instant::now(); let child_restore_workers = self.publication_point_cache_child_restore_worker_count(); let (discovered_children, child_audits) = restore_children_from_publication_point_cache( self.store, ca, &projection, self.validation_time, &mut warnings, child_restore_workers, self.timing.as_ref(), ); let restore_children_ms = self.record_publication_point_cache_phase_ms( "publication_point_cache_restore_children_total", restore_children_started, ); let ccr_projection = projection.ccr_manifest_projection.clone(); let ccr_append_started = std::time::Instant::now(); self.append_ccr_manifest_projection(&ccr_projection)?; let ccr_append_ms = self.record_publication_point_cache_phase_ms( "publication_point_cache_ccr_append_total", ccr_append_started, ); let audit_build_started = std::time::Instant::now(); let audit = build_publication_point_audit_from_publication_point_cache_projection( ca, PublicationPointSource::PublicationPointCache, repo_sync_source, repo_sync_phase, Some(repo_sync_duration_ms), repo_sync_err, &projection, self.validation_time, &warnings, &objects, &child_audits, ); let audit_build_ms = self.record_publication_point_cache_phase_ms( "publication_point_cache_audit_build_total", audit_build_started, ); let audit_object_count = audit.objects.len() as u64; let cir_cached_objects = audit.objects.clone(); objects.local_outputs_cache.clear(); let total_ms = self.record_publication_point_cache_phase_ms( "publication_point_cache_reuse_build_total", build_started, ); if let Some(timing) = self.timing.as_ref() { timing.record_count("publication_point_cache_outputs_reused", output_reuse_count); timing.record_count("publication_point_cache_children_reused", child_reuse_count); timing.record_count( "publication_point_cache_related_objects_reused", related_object_reuse_count, ); timing.record_count( "publication_point_cache_audit_objects_reused", audit_object_count, ); } if total_ms >= crate::progress_log::pp_cache_slow_threshold_ms() { crate::progress_log::emit( "publication_point_cache_reuse_slow", serde_json::json!({ "manifest_rsync_uri": ca.manifest_rsync_uri.as_str(), "publication_point_rsync_uri": ca.publication_point_rsync_uri.as_str(), "repo_sync_source": repo_sync_source, "repo_sync_phase": repo_sync_phase, "repo_sync_duration_ms": repo_sync_duration_ms, "repo_sync_err": repo_sync_err, "outputs_reused": output_reuse_count, "children_reused": child_reuse_count, "related_objects_reused": related_object_reuse_count, "audit_objects_reused": audit_object_count, "build_objects_ms": build_objects_ms, "restore_children_ms": restore_children_ms, "restore_children_workers": child_restore_workers, "ccr_append_ms": ccr_append_ms, "audit_build_ms": audit_build_ms, "total_ms": total_ms, "slow_threshold_ms": crate::progress_log::pp_cache_slow_threshold_ms(), }), ); } Ok(PublicationPointRunResult { source: PublicationPointSource::PublicationPointCache, snapshot: None, warnings, objects, audit, cir_fresh_objects: Vec::new(), cir_cached_objects, discovered_children, }) } fn record_publication_point_cache_phase_ms( &self, phase: &'static str, started: std::time::Instant, ) -> u64 { let nanos = started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64; if let Some(timing) = self.timing.as_ref() { timing.record_phase_nanos(phase, nanos); } nanos / 1_000_000 } pub(crate) fn record_publication_point_total_ms(&self, manifest_rsync_uri: &str, ms: u64) { if let Some(timing) = self.timing.as_ref() { timing.record_publication_point_nanos(manifest_rsync_uri, ms.saturating_mul(1_000_000)); } } pub(crate) fn record_publication_point_step_ms( &self, manifest_rsync_uri: &str, step: &'static str, ms: u64, ) { if let Some(timing) = self.timing.as_ref() { timing.record_publication_point_step_nanos( manifest_rsync_uri, step, ms.saturating_mul(1_000_000), ); } } fn publication_point_cache_child_restore_worker_count(&self) -> usize { self.parallel_phase2_config .as_ref() .map(|config| config.object_workers) .unwrap_or(1) .clamp(1, PUBLICATION_POINT_CACHE_CHILD_RESTORE_MAX_WORKERS) } pub(crate) fn ccr_accumulator_snapshot(&self) -> Option { self.ccr_accumulator .as_ref() .and_then(|accumulator| accumulator.lock().ok().map(|guard| guard.clone())) } pub(crate) fn append_ccr_manifest_projection( &self, projection: &VcirCcrManifestProjection, ) -> Result<(), String> { if let Some(accumulator) = self.ccr_accumulator.as_ref() { accumulator .lock() .map_err(|_| "lock CCR accumulator failed".to_string())? .append_manifest_projection(projection)?; } Ok(()) } fn append_ccr_manifest_projection_from_reuse( &self, projection: &VcirReuseProjection, ) -> Result<(), String> { match projection.source { PublicationPointSource::Fresh => Err( "invalid reuse projection source: fresh does not belong to failed-fetch reuse" .to_string(), ), PublicationPointSource::PublicationPointCache => self.append_ccr_manifest_projection( projection.ccr_manifest_projection.as_ref().ok_or_else(|| { "publication-point cache reuse is missing CCR manifest projection".to_string() })?, ), PublicationPointSource::VcirCurrentInstance => self.append_ccr_manifest_projection( projection.ccr_manifest_projection.as_ref().ok_or_else(|| { "vcir current-instance reuse is missing CCR manifest projection".to_string() })?, ), PublicationPointSource::FailedFetchNoCache => Ok(()), } } fn current_manifest_hash_hex_for_audit(&self, ca: &CaInstanceHandle) -> Option { if let Some(index_handle) = self.current_repo_index.as_ref() && let Ok(index) = index_handle.lock() && let Some(entry) = index.get_by_uri(&ca.manifest_rsync_uri) { return Some(entry.current_hash_hex.clone()); } self.store .load_current_object_with_hash_by_uri(&ca.manifest_rsync_uri) .ok() .flatten() .map(|current| current.current_hash_hex) } fn rejected_manifest_audit_entry_for_failed_fetch( &self, ca: &CaInstanceHandle, fresh_err: &ManifestFreshError, ) -> Option { let sha256_hex = self.current_manifest_hash_hex_for_audit(ca)?; Some(ObjectAuditEntry { rsync_uri: ca.manifest_rsync_uri.clone(), sha256_hex, kind: AuditObjectKind::Manifest, result: AuditObjectResult::Error, detail: Some(fresh_err.to_string()), }) } fn fresh_failure_audit_entries_for_cir( &self, ca: &CaInstanceHandle, fresh_err: &ManifestFreshError, ) -> Vec { if !fresh_err.should_warn_when_current_instance_reused() { return Vec::new(); } self.rejected_manifest_audit_entry_for_failed_fetch(ca, fresh_err) .into_iter() .collect() } pub(crate) fn stage_fresh_publication_point_after_repo_ready( &self, ca: &CaInstanceHandle, repo_sync_ok: bool, repo_sync_err: Option<&str>, ) -> Result { let snapshot_prepare_started = std::time::Instant::now(); let fresh_publication_point = { let _manifest_total = self .timing .as_ref() .map(|t| t.span_phase("manifest_processing_total")); process_manifest_publication_point_fresh_after_repo_sync_with_timing( self.store, &ca.manifest_rsync_uri, &ca.publication_point_rsync_uri, self.current_repo_index.as_ref(), &ca.ca_certificate_der, ca.ca_certificate_rsync_uri.as_deref(), self.validation_time, repo_sync_ok, repo_sync_err, ) }; let snapshot_prepare_ms = snapshot_prepare_started.elapsed().as_millis() as u64; let (fresh_point, snapshot_prepare_timing) = fresh_publication_point.map_err(|error| FreshPublicationPointStageError { error, snapshot_prepare_ms, })?; if let Some(timing) = self.timing.as_ref() { timing.record_phase_nanos( "fresh_snapshot_prepare_total", snapshot_prepare_ms.saturating_mul(1_000_000), ); timing.record_phase_nanos( "fresh_snapshot_manifest_load_total", snapshot_prepare_timing .manifest_load_ms .saturating_mul(1_000_000), ); timing.record_phase_nanos( "fresh_snapshot_manifest_decode_total", snapshot_prepare_timing .manifest_decode_ms .saturating_mul(1_000_000), ); timing.record_phase_nanos( "fresh_snapshot_replay_guard_total", snapshot_prepare_timing .replay_guard_ms .saturating_mul(1_000_000), ); timing.record_phase_nanos( "fresh_snapshot_manifest_entries_total", snapshot_prepare_timing .manifest_entries_ms .saturating_mul(1_000_000), ); timing.record_phase_nanos( "fresh_snapshot_pack_files_total", snapshot_prepare_timing .pack_files_ms .saturating_mul(1_000_000), ); timing.record_phase_nanos( "fresh_snapshot_ee_path_validate_total", snapshot_prepare_timing .ee_path_validate_ms .saturating_mul(1_000_000), ); timing.record_count("fresh_publication_points", 1); timing.record_count( "fresh_manifest_files_total", snapshot_prepare_timing.manifest_file_count as u64, ); } self.record_publication_point_step_ms( &ca.manifest_rsync_uri, "fresh_snapshot_prepare", snapshot_prepare_ms, ); self.record_publication_point_step_ms( &ca.manifest_rsync_uri, "fresh_snapshot_manifest_load", snapshot_prepare_timing.manifest_load_ms, ); self.record_publication_point_step_ms( &ca.manifest_rsync_uri, "fresh_snapshot_manifest_decode", snapshot_prepare_timing.manifest_decode_ms, ); self.record_publication_point_step_ms( &ca.manifest_rsync_uri, "fresh_snapshot_replay_guard", snapshot_prepare_timing.replay_guard_ms, ); self.record_publication_point_step_ms( &ca.manifest_rsync_uri, "fresh_snapshot_manifest_entries", snapshot_prepare_timing.manifest_entries_ms, ); self.record_publication_point_step_ms( &ca.manifest_rsync_uri, "fresh_snapshot_pack_files", snapshot_prepare_timing.pack_files_ms, ); self.record_publication_point_step_ms( &ca.manifest_rsync_uri, "fresh_snapshot_ee_path_validate", snapshot_prepare_timing.ee_path_validate_ms, ); let child_discovery_started = std::time::Instant::now(); let out = { let _child_disc_total = self .timing .as_ref() .map(|t| t.span_phase("child_discovery_total")); discover_children_from_fresh_snapshot_with_audit( ca, &fresh_point, self.validation_time, self.timing.as_ref(), ) }; let (discovered_children, child_audits, discovered_router_keys, warnings) = match out { Ok(out) => (out.children, out.audits, out.router_keys, Vec::new()), Err(e) => ( Vec::new(), Vec::new(), Vec::new(), vec![ Warning::new(format!("child CA discovery failed: {e}")) .with_rfc_refs(&[RfcRef("RFC 6487 §7.2")]) .with_context(&ca.manifest_rsync_uri), ], ), }; let child_discovery_ms = child_discovery_started.elapsed().as_millis() as u64; if let Some(timing) = self.timing.as_ref() { timing.record_phase_nanos( "fresh_child_discovery_total", child_discovery_ms.saturating_mul(1_000_000), ); timing.record_count( "fresh_children_discovered", discovered_children.len() as u64, ); timing.record_count("fresh_child_audits", child_audits.len() as u64); timing.record_count( "fresh_router_keys_discovered", discovered_router_keys.len() as u64, ); } self.record_publication_point_step_ms( &ca.manifest_rsync_uri, "fresh_child_discovery", child_discovery_ms, ); Ok(FreshPublicationPointStage { fresh_point, snapshot_prepare_timing, snapshot_prepare_ms, discovered_children, child_audits, discovered_router_keys, child_discovery_ms, warnings, }) } pub(crate) fn finalize_fresh_publication_point_from_reducer( &self, ca: &CaInstanceHandle, fresh_point: &FreshValidatedPublicationPoint, warnings: Vec, mut objects: crate::validation::objects::ObjectsOutput, child_audits: Vec, discovered_children: Vec, repo_sync_source: Option<&str>, repo_sync_phase: Option<&str>, repo_sync_duration_ms: u64, repo_sync_err: Option<&str>, ) -> Result { let snapshot_pack_started = std::time::Instant::now(); let pack = fresh_point.to_publication_point_snapshot(); let snapshot_pack_ms = snapshot_pack_started.elapsed().as_millis() as u64; if let Some(timing) = self.timing.as_ref() { timing.record_phase_nanos( "fresh_snapshot_pack_total", snapshot_pack_ms.saturating_mul(1_000_000), ); } self.record_publication_point_step_ms( &ca.manifest_rsync_uri, "fresh_snapshot_pack", snapshot_pack_ms, ); let persist_vcir_started = std::time::Instant::now(); let persist_vcir_timing = if self.persist_vcir { persist_vcir_for_fresh_result_with_timing( self.store, self.policy, ca, &pack, &mut objects, &warnings, &child_audits, &discovered_children, self.validation_time, self.publication_point_cache_observe_only || self.enable_publication_point_validation_cache, ) .map_err(|e| format!("persist VCIR failed: {e}"))? } else { PersistVcirTimingBreakdown::default() }; let persist_vcir_ms = persist_vcir_started.elapsed().as_millis() as u64; if let Some(timing) = self.timing.as_ref() { timing.record_phase_nanos( "fresh_persist_vcir_total", persist_vcir_ms.saturating_mul(1_000_000), ); timing.record_phase_nanos( "fresh_persist_embedded_store_total", persist_vcir_timing .embedded_store_ms .saturating_mul(1_000_000), ); timing.record_phase_nanos( "fresh_persist_build_vcir_total", persist_vcir_timing.build_vcir_ms.saturating_mul(1_000_000), ); timing.record_phase_nanos( "fresh_persist_replace_vcir_total", persist_vcir_timing .replace_vcir_ms .saturating_mul(1_000_000), ); timing.record_phase_nanos( "fresh_persist_local_outputs_total", persist_vcir_timing .build_vcir .local_outputs_ms .saturating_mul(1_000_000), ); timing.record_phase_nanos( "fresh_persist_child_entries_total", persist_vcir_timing .build_vcir .child_entries_ms .saturating_mul(1_000_000), ); timing.record_phase_nanos( "fresh_persist_related_artifacts_total", persist_vcir_timing .build_vcir .related_artifacts_ms .saturating_mul(1_000_000), ); if persist_vcir_timing.publication_point_cache_future_notbefore_guarded { timing.record_count("publication_point_cache_future_notbefore_guarded", 1); } } self.record_publication_point_step_ms( &ca.manifest_rsync_uri, "fresh_persist_vcir", persist_vcir_ms, ); self.record_publication_point_step_ms( &ca.manifest_rsync_uri, "fresh_persist_build_vcir", persist_vcir_timing.build_vcir_ms, ); self.record_publication_point_step_ms( &ca.manifest_rsync_uri, "fresh_persist_replace_vcir", persist_vcir_timing.replace_vcir_ms, ); // local_outputs_cache only exists to build/persist VCIR. Release it before the // publication point result is retained for the rest of the run. let _released_local_outputs = std::mem::take(&mut objects.local_outputs_cache); let _released_roa_cache_object_meta = std::mem::take(&mut objects.roa_cache_object_meta); let mut ccr_projection_build_ms = 0; let mut ccr_append_ms = 0; if self.ccr_accumulator.is_some() { let ccr_projection_build_started = std::time::Instant::now(); let child_entries = build_vcir_child_entries(&discovered_children, self.validation_time)?; let ccr_manifest_projection = build_vcir_ccr_manifest_projection_from_fresh(ca, &pack, &child_entries)?; ccr_projection_build_ms = ccr_projection_build_started.elapsed().as_millis() as u64; let ccr_append_started = std::time::Instant::now(); self.append_ccr_manifest_projection(&ccr_manifest_projection)?; ccr_append_ms = ccr_append_started.elapsed().as_millis() as u64; } if let Some(timing) = self.timing.as_ref() { timing.record_phase_nanos( "fresh_ccr_projection_build_total", ccr_projection_build_ms.saturating_mul(1_000_000), ); timing.record_phase_nanos( "fresh_ccr_append_total", ccr_append_ms.saturating_mul(1_000_000), ); } self.record_publication_point_step_ms( &ca.manifest_rsync_uri, "fresh_ccr_projection_build", ccr_projection_build_ms, ); self.record_publication_point_step_ms( &ca.manifest_rsync_uri, "fresh_ccr_append", ccr_append_ms, ); let audit_build_started = std::time::Instant::now(); let audit = build_publication_point_audit_from_snapshot( ca, PublicationPointSource::Fresh, repo_sync_source, repo_sync_phase, Some(repo_sync_duration_ms), repo_sync_err, &pack, &warnings, &objects, &child_audits, ); let audit_build_ms = audit_build_started.elapsed().as_millis() as u64; if let Some(timing) = self.timing.as_ref() { timing.record_phase_nanos( "fresh_audit_build_total", audit_build_ms.saturating_mul(1_000_000), ); } self.record_publication_point_step_ms( &ca.manifest_rsync_uri, "fresh_audit_build", audit_build_ms, ); Ok(FreshPublicationPointFinalizeOutput { result: PublicationPointRunResult { source: PublicationPointSource::Fresh, snapshot: Some(pack), warnings, objects, audit, cir_fresh_objects: Vec::new(), cir_cached_objects: Vec::new(), discovered_children, }, snapshot_pack_ms, persist_vcir_ms, persist_vcir_timing, ccr_projection_build_ms, ccr_append_ms, audit_build_ms, }) } } impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> { fn prefetch_discovered_children( &self, children: &[DiscoveredChildCaInstance], ) -> Result<(), String> { if let Some(runtime) = self.repo_sync_runtime.as_ref() { runtime.prefetch_discovered_children(children)?; } Ok(()) } fn run_publication_point( &self, ca: &CaInstanceHandle, ) -> Result { let publication_point_started = std::time::Instant::now(); let _pp_total = self .timing .as_ref() .map(|t| t.span_publication_point(&ca.manifest_rsync_uri)); if let Some(t) = self.timing.as_ref() { t.record_count("publication_points_seen", 1); if ca.rrdp_notification_uri.is_some() { t.record_count("publication_points_rrdp_notify_present_total", 1); } else { t.record_count("publication_points_rrdp_notify_missing_total", 1); } } let mut warnings: Vec = Vec::new(); crate::progress_log::emit( "publication_point_start", serde_json::json!({ "manifest_rsync_uri": ca.manifest_rsync_uri, "publication_point_rsync_uri": ca.publication_point_rsync_uri, "rsync_base_uri": ca.rsync_base_uri, "rrdp_notification_uri": ca.rrdp_notification_uri, }), ); let attempted_rrdp = self.policy.sync_preference == crate::policy::SyncPreference::RrdpThenRsync; let original_notification_uri = ca.rrdp_notification_uri.as_deref(); let mut effective_notification_uri = if attempted_rrdp { original_notification_uri } else { None }; let mut skip_sync_due_to_dedup = false; if attempted_rrdp && self.rrdp_dedup { if let Some(notification_uri) = original_notification_uri { if let Some(rrdp_ok) = self .rrdp_repo_cache .lock() .expect("rrdp_repo_cache lock") .get(notification_uri) .copied() { if let Some(t) = self.timing.as_ref() { t.record_count("rrdp_repo_dedup_hits", 1); } if rrdp_ok { if let Some(t) = self.timing.as_ref() { t.record_count("rrdp_repo_dedup_rrdp_ok_skip", 1); } skip_sync_due_to_dedup = true; } else { if let Some(t) = self.timing.as_ref() { t.record_count("rrdp_repo_dedup_rrdp_failed_skip", 1); } effective_notification_uri = None; } } else if let Some(t) = self.timing.as_ref() { t.record_count("rrdp_repo_dedup_misses", 1); } } } if !skip_sync_due_to_dedup && effective_notification_uri.is_none() && self.rsync_dedup { let base = self.rsync_fetcher.dedup_key(&ca.rsync_base_uri); let hit_ok = self .rsync_repo_cache .lock() .expect("rsync_repo_cache lock") .get(&base) .copied() .unwrap_or(false); if hit_ok { if let Some(t) = self.timing.as_ref() { t.record_count("rsync_repo_dedup_hits", 1); t.record_count("rsync_repo_dedup_skipped_sync", 1); } skip_sync_due_to_dedup = true; } else if let Some(t) = self.timing.as_ref() { t.record_count("rsync_repo_dedup_misses", 1); } } let repo_sync_started = std::time::Instant::now(); let mut runtime_repo_sync_duration_ms = None; let (repo_sync_ok, repo_sync_err, repo_sync_source, repo_sync_phase): ( bool, Option, Option, Option, ) = if let Some(runtime) = self.repo_sync_runtime.as_ref() { let RepoSyncRuntimeOutcome { repo_sync_ok, repo_sync_err, repo_sync_source, repo_sync_phase, repo_sync_duration_ms, warnings: repo_warnings, } = runtime.sync_publication_point_repo(ca)?; runtime_repo_sync_duration_ms = Some(repo_sync_duration_ms); warnings.extend(repo_warnings); ( repo_sync_ok, repo_sync_err, repo_sync_source, repo_sync_phase, ) } else if skip_sync_due_to_dedup { let source = if effective_notification_uri.is_some() { Some("rrdp_dedup_skip".to_string()) } else { Some("rsync_dedup_skip".to_string()) }; let phase = source.clone(); (true, None, source, phase) } else { let repo_key = effective_notification_uri.unwrap_or_else(|| ca.rsync_base_uri.as_str()); let _repo_total = self .timing .as_ref() .map(|t| t.span_phase("repo_sync_total")); let _repo_span = self.timing.as_ref().map(|t| t.span_rrdp_repo(repo_key)); match if let Some(delta_index) = self.replay_delta_index.as_ref() { sync_publication_point_replay_delta( self.store, delta_index, effective_notification_uri, &ca.rsync_base_uri, self.http_fetcher, self.rsync_fetcher, self.timing.as_ref(), self.download_log.as_ref(), ) } else if let Some(replay_index) = self.replay_archive_index.as_ref() { sync_publication_point_replay( self.store, replay_index, effective_notification_uri, &ca.rsync_base_uri, self.http_fetcher, self.rsync_fetcher, self.timing.as_ref(), self.download_log.as_ref(), ) } else { sync_publication_point( self.store, self.policy, effective_notification_uri, &ca.rsync_base_uri, self.http_fetcher, self.rsync_fetcher, self.timing.as_ref(), self.download_log.as_ref(), ) } { Ok(res) => { if self.rsync_dedup && res.source == crate::sync::repo::RepoSyncSource::Rsync { let base = self.rsync_fetcher.dedup_key(&ca.rsync_base_uri); self.rsync_repo_cache .lock() .expect("rsync_repo_cache lock") .insert(base, true); if let Some(t) = self.timing.as_ref() { t.record_count("rsync_repo_dedup_mark_ok", 1); } } if attempted_rrdp && self.rrdp_dedup { if let Some(notification_uri) = original_notification_uri { if effective_notification_uri.is_some() { let rrdp_ok = res.source == crate::sync::repo::RepoSyncSource::Rrdp; self.rrdp_repo_cache .lock() .expect("rrdp_repo_cache lock") .insert(notification_uri.to_string(), rrdp_ok); if let Some(t) = self.timing.as_ref() { if rrdp_ok { t.record_count("rrdp_repo_dedup_mark_ok", 1); } else { t.record_count("rrdp_repo_dedup_mark_failed", 1); } } } } } warnings.extend(res.warnings); ( true, None, Some(repo_sync_source_label(res.source).to_string()), Some(repo_sync_phase_label(res.phase).to_string()), ) } Err(e) => { if attempted_rrdp && self.rrdp_dedup { if let Some(notification_uri) = original_notification_uri { if effective_notification_uri.is_some() { self.rrdp_repo_cache .lock() .expect("rrdp_repo_cache lock") .insert(notification_uri.to_string(), false); } } } warnings.push( Warning::new(format!("repo sync failed (fresh processing stopped): {e}")) .with_rfc_refs(&[RfcRef("RFC 8182 §3.4.5"), RfcRef("RFC 9286 §6.6")]) .with_context(&ca.rsync_base_uri), ); ( false, Some(e.to_string()), None, Some( repo_sync_failure_phase_label( attempted_rrdp, original_notification_uri, effective_notification_uri, ) .to_string(), ), ) } } }; let repo_sync_duration_ms = effective_repo_sync_duration_ms( repo_sync_started.elapsed().as_millis() as u64, runtime_repo_sync_duration_ms, repo_sync_ok, ); crate::progress_log::emit( "publication_point_repo_sync_done", serde_json::json!({ "manifest_rsync_uri": ca.manifest_rsync_uri, "publication_point_rsync_uri": ca.publication_point_rsync_uri, "repo_sync_ok": repo_sync_ok, "repo_sync_source": repo_sync_source, "repo_sync_phase": repo_sync_phase, "repo_sync_error": repo_sync_err, "repo_sync_duration_ms": repo_sync_duration_ms, }), ); if let Some(result) = self.observe_or_reuse_publication_point_cache( ca, repo_sync_source.as_deref(), repo_sync_phase.as_deref(), repo_sync_duration_ms, repo_sync_err.as_deref(), &warnings, ) { let total_duration_ms = publication_point_started.elapsed().as_millis() as u64; crate::progress_log::emit( "publication_point_finish", serde_json::json!({ "manifest_rsync_uri": ca.manifest_rsync_uri, "publication_point_rsync_uri": ca.publication_point_rsync_uri, "source": source_label(result.source), "repo_sync_source": repo_sync_source, "repo_sync_phase": repo_sync_phase, "repo_sync_duration_ms": repo_sync_duration_ms, "total_duration_ms": total_duration_ms, "post_repo_duration_ms": total_duration_ms.saturating_sub(repo_sync_duration_ms), "warning_count": result.warnings.len(), "vrp_count": result.objects.vrps.len(), "vap_count": result.objects.aspas.len(), "router_key_count": result.objects.router_keys.len(), "child_count": result.discovered_children.len(), }), ); return Ok(result); } let fresh_stage = self.stage_fresh_publication_point_after_repo_ready( ca, repo_sync_ok, repo_sync_err.as_deref(), ); match fresh_stage { Ok(stage) => { let FreshPublicationPointStage { fresh_point, snapshot_prepare_timing, snapshot_prepare_ms, discovered_children, child_audits, discovered_router_keys, child_discovery_ms, warnings: stage_warnings, } = stage; warnings.extend(stage_warnings); let has_roa = fresh_point .files() .iter() .any(|file| file.rsync_uri.ends_with(".roa")); if self.enable_roa_validation_cache { if let Some(timing) = self.timing.as_ref() { if has_roa { timing.record_count( "roa_validation_cache_roa_candidate_publication_points", 1, ); } else { timing.record_count( "roa_validation_cache_skipped_no_roa_publication_points", 1, ); } } } let roa_cache_view = if has_roa { self.roa_validation_cache_view_for_fresh_point(fresh_point.manifest_rsync_uri()) } else { None }; let roa_cache = if self.enable_roa_validation_cache && has_roa { RoaValidationCacheInput::enabled_with_context( roa_cache_view.as_ref(), parent_context_digest_for_ca(ca), publication_point_cache_policy_fingerprint(self.policy), ) } else { RoaValidationCacheInput::disabled() }; let objects_processing_started = std::time::Instant::now(); let mut objects = { let _objects_total = self .timing .as_ref() .map(|t| t.span_phase("objects_processing_total")); if let Some(phase2_pool) = self.parallel_roa_worker_pool.as_ref() { process_publication_point_for_issuer_parallel_roa_with_pool_cache_options( &fresh_point, self.policy, &ca.ca_certificate_der, ca.ca_certificate_rsync_uri.as_deref(), ca.effective_ip_resources.as_ref(), ca.effective_as_resources.as_ref(), self.validation_time, self.timing.as_ref(), phase2_pool, false, roa_cache, ) } else if let Some(phase2_config) = self.parallel_phase2_config.as_ref() { process_publication_point_for_issuer_parallel_roa_with_cache_options( &fresh_point, self.policy, &ca.ca_certificate_der, ca.ca_certificate_rsync_uri.as_deref(), ca.effective_ip_resources.as_ref(), ca.effective_as_resources.as_ref(), self.validation_time, self.timing.as_ref(), phase2_config, false, roa_cache, ) } else { crate::validation::objects::process_publication_point_for_issuer_with_cache_options( &fresh_point, self.policy, &ca.ca_certificate_der, ca.ca_certificate_rsync_uri.as_deref(), ca.effective_ip_resources.as_ref(), ca.effective_as_resources.as_ref(), self.validation_time, self.timing.as_ref(), false, roa_cache, ) } }; let objects_processing_ms = objects_processing_started.elapsed().as_millis() as u64; self.record_publication_point_step_ms( &ca.manifest_rsync_uri, "fresh_objects_processing", objects_processing_ms, ); objects.router_keys.extend(discovered_router_keys); objects .local_outputs_cache .extend(build_router_key_local_outputs(ca, &objects.router_keys)); let finalized = self.finalize_fresh_publication_point_from_reducer( ca, &fresh_point, warnings, objects, child_audits, discovered_children, repo_sync_source.as_deref(), repo_sync_phase.as_deref(), repo_sync_duration_ms, repo_sync_err.as_deref(), )?; let FreshPublicationPointFinalizeOutput { result, snapshot_pack_ms, persist_vcir_ms, persist_vcir_timing, ccr_projection_build_ms, ccr_append_ms, audit_build_ms, } = finalized; let total_duration_ms = publication_point_started.elapsed().as_millis() as u64; crate::progress_log::emit( "publication_point_finish", serde_json::json!({ "manifest_rsync_uri": ca.manifest_rsync_uri, "publication_point_rsync_uri": ca.publication_point_rsync_uri, "source": "fresh", "repo_sync_source": repo_sync_source, "repo_sync_phase": repo_sync_phase, "repo_sync_duration_ms": repo_sync_duration_ms, "total_duration_ms": total_duration_ms, "post_repo_duration_ms": total_duration_ms.saturating_sub(repo_sync_duration_ms), "snapshot_prepare_ms": snapshot_prepare_ms, "snapshot_manifest_load_ms": snapshot_prepare_timing.manifest_load_ms, "snapshot_manifest_decode_ms": snapshot_prepare_timing.manifest_decode_ms, "snapshot_replay_guard_ms": snapshot_prepare_timing.replay_guard_ms, "snapshot_manifest_entries_ms": snapshot_prepare_timing.manifest_entries_ms, "snapshot_pack_files_ms": snapshot_prepare_timing.pack_files_ms, "snapshot_ee_path_validate_ms": snapshot_prepare_timing.ee_path_validate_ms, "objects_processing_ms": objects_processing_ms, "child_discovery_ms": child_discovery_ms, "snapshot_pack_ms": snapshot_pack_ms, "persist_vcir_ms": persist_vcir_ms, "persist_embedded_collect_ms": persist_vcir_timing.embedded_collect_ms, "persist_embedded_store_ms": persist_vcir_timing.embedded_store_ms, "persist_build_vcir_ms": persist_vcir_timing.build_vcir_ms, "persist_replace_vcir_ms": persist_vcir_timing.replace_vcir_ms, "persist_select_crl_ms": persist_vcir_timing.build_vcir.select_crl_ms, "persist_current_ca_decode_ms": persist_vcir_timing.build_vcir.current_ca_decode_ms, "persist_local_outputs_ms": persist_vcir_timing.build_vcir.local_outputs_ms, "persist_child_entries_ms": persist_vcir_timing.build_vcir.child_entries_ms, "persist_related_artifacts_ms": persist_vcir_timing.build_vcir.related_artifacts_ms, "persist_vcir_struct_ms": persist_vcir_timing.build_vcir.struct_build_ms, "persist_replace_breakdown": &persist_vcir_timing.replace_vcir, "publication_point_cache_future_notbefore_guarded": persist_vcir_timing.publication_point_cache_future_notbefore_guarded, "ccr_projection_build_ms": ccr_projection_build_ms, "ccr_append_ms": ccr_append_ms, "audit_build_ms": audit_build_ms, "warning_count": result.warnings.len(), "vrp_count": result.objects.vrps.len(), "vap_count": result.objects.aspas.len(), "router_key_count": result.objects.router_keys.len(), "child_count": result.discovered_children.len(), }), ); if (total_duration_ms as f64) / 1000.0 >= crate::progress_log::slow_threshold_secs() { crate::progress_log::emit( "publication_point_slow", serde_json::json!({ "manifest_rsync_uri": ca.manifest_rsync_uri, "publication_point_rsync_uri": ca.publication_point_rsync_uri, "source": "fresh", "repo_sync_source": repo_sync_source, "repo_sync_phase": repo_sync_phase, "repo_sync_duration_ms": repo_sync_duration_ms, "total_duration_ms": total_duration_ms, "post_repo_duration_ms": total_duration_ms.saturating_sub(repo_sync_duration_ms), "snapshot_prepare_ms": snapshot_prepare_ms, "snapshot_manifest_load_ms": snapshot_prepare_timing.manifest_load_ms, "snapshot_manifest_decode_ms": snapshot_prepare_timing.manifest_decode_ms, "snapshot_replay_guard_ms": snapshot_prepare_timing.replay_guard_ms, "snapshot_manifest_entries_ms": snapshot_prepare_timing.manifest_entries_ms, "snapshot_pack_files_ms": snapshot_prepare_timing.pack_files_ms, "snapshot_ee_path_validate_ms": snapshot_prepare_timing.ee_path_validate_ms, "objects_processing_ms": objects_processing_ms, "child_discovery_ms": child_discovery_ms, "snapshot_pack_ms": snapshot_pack_ms, "persist_vcir_ms": persist_vcir_ms, "persist_embedded_collect_ms": persist_vcir_timing.embedded_collect_ms, "persist_embedded_store_ms": persist_vcir_timing.embedded_store_ms, "persist_build_vcir_ms": persist_vcir_timing.build_vcir_ms, "persist_replace_vcir_ms": persist_vcir_timing.replace_vcir_ms, "persist_select_crl_ms": persist_vcir_timing.build_vcir.select_crl_ms, "persist_current_ca_decode_ms": persist_vcir_timing.build_vcir.current_ca_decode_ms, "persist_local_outputs_ms": persist_vcir_timing.build_vcir.local_outputs_ms, "persist_child_entries_ms": persist_vcir_timing.build_vcir.child_entries_ms, "persist_related_artifacts_ms": persist_vcir_timing.build_vcir.related_artifacts_ms, "persist_vcir_struct_ms": persist_vcir_timing.build_vcir.struct_build_ms, "persist_replace_breakdown": &persist_vcir_timing.replace_vcir, "publication_point_cache_future_notbefore_guarded": persist_vcir_timing.publication_point_cache_future_notbefore_guarded, "ccr_projection_build_ms": ccr_projection_build_ms, "ccr_append_ms": ccr_append_ms, "audit_build_ms": audit_build_ms, }), ); } Ok(result) } Err(stage_err) => { let snapshot_prepare_ms = stage_err.snapshot_prepare_ms; let fresh_err = stage_err.error; match self.policy.ca_failed_fetch_policy { crate::policy::CaFailedFetchPolicy::StopAllOutput => { let total_duration_ms = publication_point_started.elapsed().as_millis() as u64; crate::progress_log::emit( "publication_point_finish", serde_json::json!({ "manifest_rsync_uri": ca.manifest_rsync_uri, "publication_point_rsync_uri": ca.publication_point_rsync_uri, "source": "error", "repo_sync_source": repo_sync_source, "repo_sync_phase": repo_sync_phase, "repo_sync_duration_ms": repo_sync_duration_ms, "total_duration_ms": total_duration_ms, "post_repo_duration_ms": total_duration_ms.saturating_sub(repo_sync_duration_ms), "snapshot_prepare_ms": snapshot_prepare_ms, "projection_ms": 0, "audit_build_ms": 0, "error": fresh_err.to_string(), }), ); crate::progress_log::emit( "repo_terminal_failure", serde_json::json!({ "manifest_rsync_uri": ca.manifest_rsync_uri, "publication_point_rsync_uri": ca.publication_point_rsync_uri, "repo_sync_source": repo_sync_source, "repo_sync_phase": repo_sync_phase, "repo_sync_error": repo_sync_err, "repo_sync_duration_ms": repo_sync_duration_ms, "terminal_state": "stop_all_output", "error": fresh_err.to_string(), }), ); Err(format!("{fresh_err}")) } crate::policy::CaFailedFetchPolicy::ReuseCurrentInstanceVcir => { let projection_started = std::time::Instant::now(); let projection = project_current_instance_vcir_on_failed_fetch( self.store, ca, &fresh_err, self.validation_time, ) .map_err(|e| format!("failed fetch VCIR projection failed: {e}"))?; let fresh_failure_audits = self.fresh_failure_audit_entries_for_cir(ca, &fresh_err); self.append_ccr_manifest_projection_from_reuse(&projection)?; let projection_ms = projection_started.elapsed().as_millis() as u64; warnings.extend(projection.warnings.clone()); let audit_build_started = std::time::Instant::now(); let audit = build_publication_point_audit_from_vcir( ca, projection.source, repo_sync_source.as_deref(), repo_sync_phase.as_deref(), Some(repo_sync_duration_ms), repo_sync_err.as_deref(), projection.vcir.as_ref(), projection.snapshot.as_ref(), &warnings, &projection.objects, &projection.child_audits, &fresh_failure_audits, ); let audit_build_ms = audit_build_started.elapsed().as_millis() as u64; let cir_cached_objects = if projection.source == PublicationPointSource::VcirCurrentInstance { audit .objects .iter() .filter(|entry| { !fresh_failure_audits.iter().any(|fresh| fresh == *entry) }) .cloned() .collect() } else { Vec::new() }; let result = PublicationPointRunResult { source: projection.source, snapshot: projection.snapshot, warnings, objects: projection.objects, audit, cir_fresh_objects: if projection.source == PublicationPointSource::VcirCurrentInstance { fresh_failure_audits } else { Vec::new() }, cir_cached_objects, discovered_children: projection.discovered_children, }; let total_duration_ms = publication_point_started.elapsed().as_millis() as u64; crate::progress_log::emit( "publication_point_finish", serde_json::json!({ "manifest_rsync_uri": ca.manifest_rsync_uri, "publication_point_rsync_uri": ca.publication_point_rsync_uri, "source": source_label(result.source), "repo_sync_source": repo_sync_source, "repo_sync_phase": repo_sync_phase, "repo_sync_duration_ms": repo_sync_duration_ms, "total_duration_ms": total_duration_ms, "post_repo_duration_ms": total_duration_ms.saturating_sub(repo_sync_duration_ms), "snapshot_prepare_ms": snapshot_prepare_ms, "projection_ms": projection_ms, "audit_build_ms": audit_build_ms, "warning_count": result.warnings.len(), "vrp_count": result.objects.vrps.len(), "vap_count": result.objects.aspas.len(), "router_key_count": result.objects.router_keys.len(), "child_count": result.discovered_children.len(), }), ); match result.source { PublicationPointSource::VcirCurrentInstance if !repo_sync_ok => { crate::progress_log::emit( "rsync_failed_fallback_current_instance", serde_json::json!({ "manifest_rsync_uri": ca.manifest_rsync_uri, "publication_point_rsync_uri": ca.publication_point_rsync_uri, "repo_sync_source": repo_sync_source, "repo_sync_phase": repo_sync_phase, "repo_sync_error": repo_sync_err, "repo_sync_duration_ms": repo_sync_duration_ms, "terminal_state": "fallback_current_instance", }), ); } PublicationPointSource::FailedFetchNoCache => { if !repo_sync_ok { crate::progress_log::emit( "rsync_failed_no_cache", serde_json::json!({ "manifest_rsync_uri": ca.manifest_rsync_uri, "publication_point_rsync_uri": ca.publication_point_rsync_uri, "repo_sync_source": repo_sync_source, "repo_sync_phase": repo_sync_phase, "repo_sync_error": repo_sync_err, "repo_sync_duration_ms": repo_sync_duration_ms, "terminal_state": "failed_no_cache", }), ); } crate::progress_log::emit( "repo_terminal_failure", serde_json::json!({ "manifest_rsync_uri": ca.manifest_rsync_uri, "publication_point_rsync_uri": ca.publication_point_rsync_uri, "repo_sync_source": repo_sync_source, "repo_sync_phase": repo_sync_phase, "repo_sync_error": repo_sync_err, "repo_sync_duration_ms": repo_sync_duration_ms, "terminal_state": "failed_no_cache", }), ); } PublicationPointSource::Fresh => {} PublicationPointSource::PublicationPointCache => {} PublicationPointSource::VcirCurrentInstance => {} } if (total_duration_ms as f64) / 1000.0 >= crate::progress_log::slow_threshold_secs() { crate::progress_log::emit( "publication_point_slow", serde_json::json!({ "manifest_rsync_uri": ca.manifest_rsync_uri, "publication_point_rsync_uri": ca.publication_point_rsync_uri, "source": source_label(result.source), "repo_sync_source": repo_sync_source, "repo_sync_phase": repo_sync_phase, "repo_sync_duration_ms": repo_sync_duration_ms, "total_duration_ms": total_duration_ms, "post_repo_duration_ms": total_duration_ms.saturating_sub(repo_sync_duration_ms), "snapshot_prepare_ms": snapshot_prepare_ms, "projection_ms": projection_ms, "audit_build_ms": audit_build_ms, }), ); } Ok(result) } } } } } } struct ChildDiscoveryOutput { children: Vec, audits: Vec, router_keys: Vec, } #[derive(Clone, Debug)] struct VerifiedIssuerCrl { crl: crate::data_model::crl::RpkixCrl, revoked_serials: std::collections::HashSet>, } #[derive(Clone, Debug)] enum CachedIssuerCrl { Pending(Vec), Ok(VerifiedIssuerCrl), } struct PublicationPointCacheIdentity { ca_cert_sha256: [u8; 32], manifest_sha256: [u8; 32], ta_context_digest: [u8; 32], parent_context_digest: [u8; 32], policy_fingerprint: [u8; 32], } fn sha256_digest_32(bytes: impl AsRef<[u8]>) -> [u8; 32] { let digest = sha2::Sha256::digest(bytes.as_ref()); let mut out = [0u8; 32]; out.copy_from_slice(&digest); out } fn hash_serialized_parts(parts: &[(&str, Vec)]) -> [u8; 32] { let mut hasher = sha2::Sha256::new(); for (label, value) in parts { hasher.update((label.len() as u64).to_be_bytes()); hasher.update(label.as_bytes()); hasher.update((value.len() as u64).to_be_bytes()); hasher.update(value); } let digest = hasher.finalize(); let mut out = [0u8; 32]; out.copy_from_slice(&digest); out } fn cbor_or_debug_bytes(value: &T) -> Vec { serde_cbor::to_vec(value).unwrap_or_else(|_| format!("{value:?}").into_bytes()) } fn ta_context_digest_for_ca(ca: &CaInstanceHandle) -> [u8; 32] { hash_serialized_parts(&[ ("version", b"publication-point-cache-ta-v1".to_vec()), ("tal_id", ca.tal_id.as_bytes().to_vec()), ]) } pub(crate) fn parent_context_digest_for_ca(ca: &CaInstanceHandle) -> [u8; 32] { hash_serialized_parts(&[ ( "version", b"publication-point-cache-parent-context-v1".to_vec(), ), ("tal_id", ca.tal_id.as_bytes().to_vec()), ( "parent_manifest", ca.parent_manifest_rsync_uri .as_deref() .unwrap_or("") .as_bytes() .to_vec(), ), ( "effective_ip", cbor_or_debug_bytes(&ca.effective_ip_resources), ), ( "effective_as", cbor_or_debug_bytes(&ca.effective_as_resources), ), ]) } pub(crate) fn publication_point_cache_policy_fingerprint(policy: &Policy) -> [u8; 32] { hash_serialized_parts(&[ ("version", b"publication-point-cache-policy-v1".to_vec()), ("policy", cbor_or_debug_bytes(policy)), ]) } fn publication_point_cache_projection_items_valid( projection: &PublicationPointCacheProjection, validation_time: time::OffsetDateTime, ) -> Result<(), &'static str> { for output in &projection.outputs { if !pack_time_window_contains( &output.item_effective_not_before, &output.item_effective_until, validation_time, ) { return Err("output_time_gate_miss"); } } for child in &projection.children { if !pack_time_window_contains( &child.child_effective_not_before, &child.child_effective_until, validation_time, ) { return Err("child_time_gate_miss"); } } Ok(()) } fn pack_time_window_contains( not_before: &PackTime, until: &PackTime, validation_time: time::OffsetDateTime, ) -> bool { let Ok(not_before) = parse_snapshot_time_value(not_before) else { return false; }; let Ok(until) = parse_snapshot_time_value(until) else { return false; }; validation_time >= not_before && validation_time < until } #[derive(Clone, Debug)] struct VcirReuseProjection { source: PublicationPointSource, vcir: Option, ccr_manifest_projection: Option, snapshot: Option, objects: crate::validation::objects::ObjectsOutput, child_audits: Vec, discovered_children: Vec, warnings: Vec, } fn discover_children_from_fresh_snapshot_with_audit( issuer: &CaInstanceHandle, publication_point: &P, validation_time: time::OffsetDateTime, timing: Option<&TimingHandle>, ) -> Result { let locked_files = publication_point.files(); let issuer_ca_der = issuer.ca_certificate_der.as_slice(); // Issuer CA is only required when we actually attempt to validate a subordinate CA. For some // audit-only error paths (e.g., missing CRL in the snapshot), we still want discovery to succeed. let issuer_ca_decode_error: Option; let issuer_ca = match crate::data_model::rc::ResourceCertificate::decode_der(issuer_ca_der) { Ok(v) => { issuer_ca_decode_error = None; Some(v) } Err(e) => { issuer_ca_decode_error = Some(format!( "issuer CA decode failed: {e} (RFC 5280 §4.1; RFC 6487 §4)" )); None } }; let issuer_spki_error: Option; let issuer_spki: Option> = if let Some(ca) = issuer_ca.as_ref() { match SubjectPublicKeyInfo::from_der(&ca.tbs.subject_public_key_info) { Ok((rem, spki)) if rem.is_empty() => { issuer_spki_error = None; Some(spki) } Ok((rem, _)) => { issuer_spki_error = Some(format!( "trailing bytes after issuer SubjectPublicKeyInfo DER: {} bytes (DER; RFC 5280 §4.1.2.7)", rem.len() )); None } Err(e) => { issuer_spki_error = Some(format!( "issuer SubjectPublicKeyInfo parse error: {e} (RFC 5280 §4.1.2.7)" )); None } } } else { issuer_spki_error = issuer_ca_decode_error.clone(); None }; let mut crl_cache: std::collections::HashMap = locked_files .iter() .filter(|f| f.rsync_uri.ends_with(".crl")) .map(|f| -> Result<(String, CachedIssuerCrl), String> { let bytes = f .bytes_cloned() .map_err(|e| format!("snapshot CRL bytes load failed: {e}"))?; Ok((f.rsync_uri.clone(), CachedIssuerCrl::Pending(bytes))) }) .collect::>()?; let mut out: Vec = Vec::new(); let mut audits: Vec = Vec::new(); let mut router_keys: Vec = Vec::new(); let issuer_resources_index = IssuerEffectiveResourcesIndex::from_effective_resources( issuer.effective_ip_resources.as_ref(), issuer.effective_as_resources.as_ref(), ) .map_err(|e| format!("build issuer effective resources index failed: {e}"))?; let mut cer_seen: u64 = 0; let mut ca_skipped_not_ca: u64 = 0; let mut ca_ok: u64 = 0; let mut ca_error: u64 = 0; let mut router_ok: u64 = 0; let mut router_error: u64 = 0; let mut router_skipped_non_router: u64 = 0; let mut crl_select_error: u64 = 0; let mut uri_discovery_error: u64 = 0; let mut select_crl_nanos: u64 = 0; let mut child_decode_nanos: u64 = 0; let mut validate_sub_ca_nanos: u64 = 0; let mut validate_router_nanos: u64 = 0; let mut uri_discovery_nanos: u64 = 0; let mut enqueue_nanos: u64 = 0; let mut eff_ip_items_bucket_le_10: u64 = 0; let mut eff_ip_items_bucket_le_100: u64 = 0; let mut eff_ip_items_bucket_gt_100: u64 = 0; let mut eff_as_items_bucket_le_10: u64 = 0; let mut eff_as_items_bucket_le_100: u64 = 0; let mut eff_as_items_bucket_gt_100: u64 = 0; fn bucketize(v: usize) -> u8 { if v <= 10 { 0 } else if v <= 100 { 1 } else { 2 } } fn ip_item_count(ip: Option<&crate::data_model::rc::IpResourceSet>) -> usize { let Some(ip) = ip else { return 0 }; ip.families .iter() .map(|f| match &f.choice { crate::data_model::rc::IpAddressChoice::Inherit => 0usize, crate::data_model::rc::IpAddressChoice::AddressesOrRanges(items) => items.len(), }) .sum() } fn as_item_count(asr: Option<&crate::data_model::rc::AsResourceSet>) -> usize { let Some(asr) = asr else { return 0 }; let mut n = 0usize; if let Some(c) = asr.asnum.as_ref() { if let crate::data_model::rc::AsIdentifierChoice::AsIdsOrRanges(items) = c { n = n.saturating_add(items.len()); } } if let Some(c) = asr.rdi.as_ref() { if let crate::data_model::rc::AsIdentifierChoice::AsIdsOrRanges(items) = c { n = n.saturating_add(items.len()); } } n } for f in locked_files { if !f.rsync_uri.ends_with(".cer") { continue; } cer_seen = cer_seen.saturating_add(1); let child_der = f .bytes() .map_err(|e| format!("child certificate bytes load failed: {e}"))?; let tdecode = std::time::Instant::now(); let child_cert = match crate::data_model::rc::ResourceCertificate::decode_der(child_der) { Ok(v) => v, Err(e) => { ca_error = ca_error.saturating_add(1); audits.push(ObjectAuditEntry { rsync_uri: f.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&f.sha256), kind: AuditObjectKind::Certificate, result: AuditObjectResult::Error, detail: Some(format!("child certificate decode failed: {e}")), }); continue; } }; child_decode_nanos = child_decode_nanos .saturating_add(tdecode.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64); let t0 = std::time::Instant::now(); let issuer_crl_uri = match select_issuer_crl_uri_for_child(&child_cert, &crl_cache) { Ok(v) => v.to_string(), Err(e) => { crl_select_error = crl_select_error.saturating_add(1); audits.push(ObjectAuditEntry { rsync_uri: f.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&f.sha256), kind: AuditObjectKind::Certificate, result: AuditObjectResult::Error, detail: Some(format!( "cannot select issuer CRL for child certificate: {e}" )), }); continue; } }; select_crl_nanos = select_crl_nanos .saturating_add(t0.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64); let t1 = std::time::Instant::now(); let Some(issuer_ca_ref) = issuer_ca.as_ref() else { ca_error = ca_error.saturating_add(1); audits.push(ObjectAuditEntry { rsync_uri: f.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&f.sha256), kind: AuditObjectKind::Certificate, result: AuditObjectResult::Error, detail: Some( issuer_ca_decode_error .clone() .unwrap_or_else(|| "issuer CA decode failed".to_string()), ), }); continue; }; let Some(issuer_spki_ref) = issuer_spki.as_ref() else { ca_error = ca_error.saturating_add(1); audits.push(ObjectAuditEntry { rsync_uri: f.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&f.sha256), kind: AuditObjectKind::Certificate, result: AuditObjectResult::Error, detail: Some( issuer_spki_error .clone() .unwrap_or_else(|| "issuer SubjectPublicKeyInfo unavailable".to_string()), ), }); continue; }; let validated = match validate_subordinate_ca_cert_with_cached_issuer( child_der, child_cert, issuer_ca_der, issuer_ca_ref, issuer_spki_ref, issuer_crl_uri.as_str(), &mut crl_cache, issuer.ca_certificate_rsync_uri.as_deref(), issuer.effective_ip_resources.as_ref(), issuer.effective_as_resources.as_ref(), &issuer_resources_index, validation_time, ) { Ok(v) => v, Err(CaPathError::ChildNotCa) => { let tr = std::time::Instant::now(); let router_result = match ensure_issuer_crl_verified( issuer_crl_uri.as_str(), &mut crl_cache, issuer_ca_der, ) { Ok(verified_crl) => { BgpsecRouterCertificate::validate_path_with_prevalidated_issuer( child_der, issuer_ca_ref, issuer_spki_ref, &verified_crl.crl, &verified_crl.revoked_serials, issuer.ca_certificate_rsync_uri.as_deref(), Some(issuer_crl_uri.as_str()), validation_time, ) } Err(err) => { validate_router_nanos = validate_router_nanos.saturating_add( tr.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64, ); router_error = router_error.saturating_add(1); audits.push(ObjectAuditEntry { rsync_uri: f.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&f.sha256), kind: AuditObjectKind::RouterCertificate, result: AuditObjectResult::Error, detail: Some(format!( "router certificate issuer CRL validation failed: {err}" )), }); continue; } }; validate_router_nanos = validate_router_nanos .saturating_add(tr.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64); match router_result { Ok(router) => { router_ok = router_ok.saturating_add(1); let source_object_hash = sha256_hex_from_32(&f.sha256); let item_effective_until = PackTime::from_utc_offset_datetime( router.resource_cert.tbs.validity_not_after, ); for as_id in &router.asns { router_keys.push(RouterKeyPayload { as_id: *as_id, ski: router.subject_key_identifier.clone(), spki_der: router.spki_der.clone(), source_object_uri: f.rsync_uri.clone(), source_object_hash: source_object_hash.clone(), source_ee_cert_hash: source_object_hash.clone(), item_effective_until: item_effective_until.clone(), }); } audits.push(ObjectAuditEntry { rsync_uri: f.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&f.sha256), kind: AuditObjectKind::RouterCertificate, result: AuditObjectResult::Ok, detail: Some( "validated BGPsec router certificate (RFC 8209); no child CA instance enqueued" .to_string(), ), }); } Err(err) if is_non_router_certificate(&err) => { ca_skipped_not_ca = ca_skipped_not_ca.saturating_add(1); router_skipped_non_router = router_skipped_non_router.saturating_add(1); audits.push(ObjectAuditEntry { rsync_uri: f.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&f.sha256), kind: AuditObjectKind::Certificate, result: AuditObjectResult::Skipped, detail: Some( "skipped: not a CA resource certificate or BGPsec router certificate" .to_string(), ), }); } Err(err) => { router_error = router_error.saturating_add(1); audits.push(ObjectAuditEntry { rsync_uri: f.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&f.sha256), kind: AuditObjectKind::RouterCertificate, result: AuditObjectResult::Error, detail: Some(format!("router certificate validation failed: {err}")), }); } } continue; } Err(e) => { ca_error = ca_error.saturating_add(1); audits.push(ObjectAuditEntry { rsync_uri: f.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&f.sha256), kind: AuditObjectKind::Certificate, result: AuditObjectResult::Error, detail: Some(format!("child CA validation failed: {e}")), }); continue; } }; validate_sub_ca_nanos = validate_sub_ca_nanos .saturating_add(t1.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64); let eff_ip_items = ip_item_count(validated.effective_ip_resources.as_ref()); match bucketize(eff_ip_items) { 0 => eff_ip_items_bucket_le_10 = eff_ip_items_bucket_le_10.saturating_add(1), 1 => eff_ip_items_bucket_le_100 = eff_ip_items_bucket_le_100.saturating_add(1), _ => eff_ip_items_bucket_gt_100 = eff_ip_items_bucket_gt_100.saturating_add(1), } let eff_as_items = as_item_count(validated.effective_as_resources.as_ref()); match bucketize(eff_as_items) { 0 => eff_as_items_bucket_le_10 = eff_as_items_bucket_le_10.saturating_add(1), 1 => eff_as_items_bucket_le_100 = eff_as_items_bucket_le_100.saturating_add(1), _ => eff_as_items_bucket_gt_100 = eff_as_items_bucket_gt_100.saturating_add(1), } let t2 = std::time::Instant::now(); let uris = match ca_instance_uris_from_ca_certificate(&validated.child_ca) { Ok(v) => v, Err(e) => { uri_discovery_error = uri_discovery_error.saturating_add(1); audits.push(ObjectAuditEntry { rsync_uri: f.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&f.sha256), kind: AuditObjectKind::Certificate, result: AuditObjectResult::Error, detail: Some(format!("CA instance URI discovery failed: {e}")), }); continue; } }; uri_discovery_nanos = uri_discovery_nanos .saturating_add(t2.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64); let t3 = std::time::Instant::now(); out.push(DiscoveredChildCaInstance { handle: CaInstanceHandle { depth: 0, tal_id: issuer.tal_id.clone(), parent_manifest_rsync_uri: Some(issuer.manifest_rsync_uri.clone()), ca_certificate_der: child_der.to_vec(), ca_certificate_rsync_uri: Some(f.rsync_uri.clone()), effective_ip_resources: validated.effective_ip_resources.clone(), effective_as_resources: validated.effective_as_resources.clone(), rsync_base_uri: uris.rsync_base_uri, manifest_rsync_uri: uris.manifest_rsync_uri, publication_point_rsync_uri: uris.publication_point_rsync_uri, rrdp_notification_uri: uris.rrdp_notification_uri, }, discovered_from: crate::audit::DiscoveredFrom { parent_manifest_rsync_uri: issuer.manifest_rsync_uri.clone(), child_ca_certificate_rsync_uri: f.rsync_uri.clone(), child_ca_certificate_sha256_hex: sha256_hex_from_32(&f.sha256), }, }); enqueue_nanos = enqueue_nanos.saturating_add(t3.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64); ca_ok = ca_ok.saturating_add(1); audits.push(ObjectAuditEntry { rsync_uri: f.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&f.sha256), kind: AuditObjectKind::Certificate, result: AuditObjectResult::Ok, detail: Some("validated subordinate CA certificate; enqueued CA instance".to_string()), }); } if let Some(t) = timing { t.record_count("child_cer_seen", cer_seen); t.record_count("child_ca_ok", ca_ok); t.record_count("child_ca_error", ca_error); t.record_count("child_ca_skipped_not_ca", ca_skipped_not_ca); t.record_count("child_router_ok", router_ok); t.record_count("child_router_error", router_error); t.record_count("child_router_skipped_non_router", router_skipped_non_router); t.record_count("child_crl_select_error", crl_select_error); t.record_count("child_uri_discovery_error", uri_discovery_error); t.record_count("child_effective_ip_items_le_10", eff_ip_items_bucket_le_10); t.record_count( "child_effective_ip_items_le_100", eff_ip_items_bucket_le_100, ); t.record_count( "child_effective_ip_items_gt_100", eff_ip_items_bucket_gt_100, ); t.record_count("child_effective_as_items_le_10", eff_as_items_bucket_le_10); t.record_count( "child_effective_as_items_le_100", eff_as_items_bucket_le_100, ); t.record_count( "child_effective_as_items_gt_100", eff_as_items_bucket_gt_100, ); t.record_phase_nanos("child_select_issuer_crl_total", select_crl_nanos); t.record_phase_nanos("child_decode_certificate_total", child_decode_nanos); t.record_phase_nanos("child_validate_subordinate_total", validate_sub_ca_nanos); t.record_phase_nanos( "child_validate_router_certificate_total", validate_router_nanos, ); t.record_phase_nanos("child_ca_instance_uri_discovery_total", uri_discovery_nanos); t.record_phase_nanos("child_enqueue_total", enqueue_nanos); } Ok(ChildDiscoveryOutput { children: out, audits, router_keys, }) } fn is_non_router_certificate(err: &BgpsecRouterCertificatePathError) -> bool { matches!( err, BgpsecRouterCertificatePathError::Decode(BgpsecRouterCertificateDecodeError::Validate( BgpsecRouterCertificateProfileError::NotEe | BgpsecRouterCertificateProfileError::MissingExtendedKeyUsage | BgpsecRouterCertificateProfileError::MissingBgpsecRouterEku )) ) } fn select_issuer_crl_uri_for_child<'a>( child: &'a crate::data_model::rc::ResourceCertificate, crl_cache: &std::collections::HashMap, ) -> Result<&'a str, String> { if crl_cache.is_empty() { return Err( "no CRL available in publication point snapshot (cannot validate certificates) (RFC 9286 §7; RFC 6487 §4.8.6)" .to_string(), ); } let Some(crldp_uris) = child.tbs.extensions.crl_distribution_points_uris.as_ref() else { return Err( "child certificate CRLDistributionPoints missing (RFC 6487 §4.8.6)".to_string(), ); }; for u in crldp_uris { let s = u.as_str(); if crl_cache.contains_key(s) { return Ok(s); } } Err(format!( "CRL referenced by child certificate CRLDistributionPoints not found in publication point snapshot: {} (RFC 6487 §4.8.6; RFC 9286 §4.2.1)", crldp_uris .iter() .map(|u| u.as_str()) .collect::>() .join(", ") )) } fn ensure_issuer_crl_verified<'a>( crl_rsync_uri: &str, crl_cache: &'a mut std::collections::HashMap, issuer_ca_der: &[u8], ) -> Result<&'a VerifiedIssuerCrl, CaPathError> { let entry = crl_cache .get_mut(crl_rsync_uri) .expect("CRL must exist in cache"); match entry { CachedIssuerCrl::Ok(v) => Ok(v), CachedIssuerCrl::Pending(bytes) => { let der = std::mem::take(bytes); let crl = crate::data_model::crl::RpkixCrl::decode_der(&der)?; crl.verify_signature_with_issuer_certificate_der(issuer_ca_der)?; let mut revoked_serials: std::collections::HashSet> = std::collections::HashSet::with_capacity(crl.revoked_certs.len()); for rc in &crl.revoked_certs { revoked_serials.insert(rc.serial_number.bytes_be.clone()); } *entry = CachedIssuerCrl::Ok(VerifiedIssuerCrl { crl, revoked_serials, }); match entry { CachedIssuerCrl::Ok(v) => Ok(v), _ => unreachable!(), } } } } fn validate_subordinate_ca_cert_with_cached_issuer( child_ca_der: &[u8], child_ca: crate::data_model::rc::ResourceCertificate, issuer_ca_der: &[u8], issuer_ca: &crate::data_model::rc::ResourceCertificate, issuer_spki: &SubjectPublicKeyInfo<'_>, issuer_crl_rsync_uri: &str, crl_cache: &mut std::collections::HashMap, issuer_ca_rsync_uri: Option<&str>, issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>, issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>, issuer_resources_index: &IssuerEffectiveResourcesIndex, validation_time: time::OffsetDateTime, ) -> Result { let verified_crl = ensure_issuer_crl_verified(issuer_crl_rsync_uri, crl_cache, issuer_ca_der)?; validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources( child_ca_der, child_ca, issuer_ca, issuer_spki, &verified_crl.crl, &verified_crl.revoked_serials, issuer_ca_rsync_uri, issuer_crl_rsync_uri, issuer_effective_ip, issuer_effective_as, issuer_resources_index, validation_time, ) } #[cfg(test)] fn select_issuer_crl_from_snapshot<'a>( child_cert_der: &[u8], pack: &'a PublicationPointSnapshot, ) -> Result<(&'a str, &'a [u8]), String> { let child = crate::data_model::rc::ResourceCertificate::decode_der(child_cert_der) .map_err(|e| format!("child certificate decode failed: {e}"))?; let Some(crldp_uris) = child.tbs.extensions.crl_distribution_points_uris.as_ref() else { return Err( "child certificate CRLDistributionPoints missing (RFC 6487 §4.8.6)".to_string(), ); }; for u in crldp_uris { let s = u.as_str(); if let Some(f) = pack.files.iter().find(|f| f.rsync_uri == s) { let bytes = f .bytes() .map_err(|e| format!("snapshot CRL bytes load failed: {e}"))?; return Ok((f.rsync_uri.as_str(), bytes)); } } Err(format!( "CRL referenced by child certificate CRLDistributionPoints not found in publication point snapshot: {} (RFC 6487 §4.8.6; RFC 9286 §4.2.1)", crldp_uris .iter() .map(|u| u.as_str()) .collect::>() .join(", ") )) } fn kind_from_rsync_uri(uri: &str) -> AuditObjectKind { if uri.ends_with(".crl") { AuditObjectKind::Crl } else if uri.ends_with(".cer") { AuditObjectKind::Certificate } else if uri.ends_with(".roa") { AuditObjectKind::Roa } else if uri.ends_with(".asa") { AuditObjectKind::Aspa } else { AuditObjectKind::Other } } fn source_label(source: PublicationPointSource) -> String { match source { PublicationPointSource::Fresh => "fresh".to_string(), PublicationPointSource::PublicationPointCache => "publication_point_cache".to_string(), PublicationPointSource::VcirCurrentInstance => "vcir_current_instance".to_string(), PublicationPointSource::FailedFetchNoCache => "failed_fetch_no_cache".to_string(), } } fn repo_sync_phase_label(phase: crate::sync::repo::RepoSyncPhase) -> &'static str { match phase { crate::sync::repo::RepoSyncPhase::RrdpOk => "rrdp_ok", crate::sync::repo::RepoSyncPhase::RrdpFailedRsyncOk => "rrdp_failed_rsync_ok", crate::sync::repo::RepoSyncPhase::RsyncOnlyOk => "rsync_only_ok", crate::sync::repo::RepoSyncPhase::ReplayRrdpOk => "replay_rrdp_ok", crate::sync::repo::RepoSyncPhase::ReplayRsyncOk => "replay_rsync_ok", crate::sync::repo::RepoSyncPhase::ReplayNoopRrdp => "replay_noop_rrdp", crate::sync::repo::RepoSyncPhase::ReplayNoopRsync => "replay_noop_rsync", } } fn repo_sync_failure_phase_label( attempted_rrdp: bool, original_notification_uri: Option<&str>, effective_notification_uri: Option<&str>, ) -> &'static str { if attempted_rrdp && original_notification_uri.is_some() && effective_notification_uri.is_some() { "rrdp_failed_rsync_failed" } else if attempted_rrdp && original_notification_uri.is_some() && effective_notification_uri.is_none() { "rsync_only_failed_after_rrdp_dedup" } else { "rsync_only_failed" } } fn terminal_state_label(source: PublicationPointSource) -> &'static str { match source { PublicationPointSource::Fresh => "fresh", PublicationPointSource::PublicationPointCache => "publication_point_cache", PublicationPointSource::VcirCurrentInstance => "fallback_current_instance", PublicationPointSource::FailedFetchNoCache => "failed_no_cache", } } fn repo_sync_source_label(source: crate::sync::repo::RepoSyncSource) -> &'static str { match source { crate::sync::repo::RepoSyncSource::Rrdp => "rrdp", crate::sync::repo::RepoSyncSource::Rsync => "rsync", } } fn effective_repo_sync_duration_ms( elapsed_ms: u64, runtime_reported_duration_ms: Option, repo_sync_ok: bool, ) -> u64 { if repo_sync_ok { return elapsed_ms; } runtime_reported_duration_ms .map(|runtime_ms| elapsed_ms.max(runtime_ms)) .unwrap_or(elapsed_ms) } fn kind_from_vcir_artifact_kind(kind: VcirArtifactKind) -> AuditObjectKind { match kind { VcirArtifactKind::Mft => AuditObjectKind::Manifest, VcirArtifactKind::Crl => AuditObjectKind::Crl, VcirArtifactKind::Cer => AuditObjectKind::Certificate, VcirArtifactKind::Roa => AuditObjectKind::Roa, VcirArtifactKind::Aspa => AuditObjectKind::Aspa, VcirArtifactKind::Gbr | VcirArtifactKind::Tal | VcirArtifactKind::Other => { AuditObjectKind::Other } } } fn audit_result_from_vcir_status(status: VcirArtifactValidationStatus) -> AuditObjectResult { match status { VcirArtifactValidationStatus::Accepted => AuditObjectResult::Ok, VcirArtifactValidationStatus::Rejected => AuditObjectResult::Error, VcirArtifactValidationStatus::WarningOnly => AuditObjectResult::Skipped, } } fn build_publication_point_audit_from_snapshot( ca: &CaInstanceHandle, source: PublicationPointSource, repo_sync_source: Option<&str>, repo_sync_phase: Option<&str>, repo_sync_duration_ms: Option, repo_sync_error: Option<&str>, pack: &PublicationPointSnapshot, runner_warnings: &[Warning], objects: &crate::validation::objects::ObjectsOutput, child_audits: &[ObjectAuditEntry], ) -> PublicationPointAudit { use crate::data_model::crl::RpkixCrl; use std::collections::HashMap; let locked_files = &pack.files; let mut audit_by_uri: HashMap = HashMap::new(); for f in locked_files { audit_by_uri.insert( f.rsync_uri.clone(), ObjectAuditEntry { rsync_uri: f.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&f.sha256), kind: kind_from_rsync_uri(&f.rsync_uri), result: AuditObjectResult::Skipped, detail: Some("skipped: not processed in stage2".to_string()), }, ); } for f in locked_files { if !f.rsync_uri.ends_with(".crl") { continue; } let ok = f .bytes() .ok() .and_then(|bytes| RpkixCrl::decode_der(bytes).ok()) .is_some(); audit_by_uri.insert( f.rsync_uri.clone(), ObjectAuditEntry { rsync_uri: f.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&f.sha256), kind: AuditObjectKind::Crl, result: if ok { AuditObjectResult::Ok } else { AuditObjectResult::Error }, detail: if ok { None } else { Some("CRL decode failed".to_string()) }, }, ); } for e in child_audits { audit_by_uri.insert(e.rsync_uri.clone(), e.clone()); } for e in &objects.audit { audit_by_uri.insert(e.rsync_uri.clone(), e.clone()); } let mut objects_out: Vec = Vec::with_capacity(pack.files.len() + 1); objects_out.push(ObjectAuditEntry { rsync_uri: pack.manifest_rsync_uri.clone(), sha256_hex: sha256_hex(&pack.manifest_bytes), kind: AuditObjectKind::Manifest, result: AuditObjectResult::Ok, detail: None, }); for f in locked_files { if let Some(e) = audit_by_uri.remove(&f.rsync_uri) { objects_out.push(e); } else { objects_out.push(ObjectAuditEntry { rsync_uri: f.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&f.sha256), kind: kind_from_rsync_uri(&f.rsync_uri), result: AuditObjectResult::Skipped, detail: Some("skipped: no audit entry".to_string()), }); } } let mut warnings = Vec::new(); warnings.extend(runner_warnings.iter().map(AuditWarning::from)); warnings.extend(objects.warnings.iter().map(AuditWarning::from)); PublicationPointAudit { node_id: None, parent_node_id: None, discovered_from: None, rsync_base_uri: ca.rsync_base_uri.clone(), manifest_rsync_uri: ca.manifest_rsync_uri.clone(), publication_point_rsync_uri: ca.publication_point_rsync_uri.clone(), rrdp_notification_uri: ca.rrdp_notification_uri.clone(), source: source_label(source), repo_sync_source: repo_sync_source.map(ToString::to_string), repo_sync_phase: repo_sync_phase.map(ToString::to_string), repo_sync_duration_ms, repo_sync_error: repo_sync_error.map(ToString::to_string), repo_terminal_state: terminal_state_label(source).to_string(), this_update_rfc3339_utc: pack.this_update.rfc3339_utc.clone(), next_update_rfc3339_utc: pack.next_update.rfc3339_utc.clone(), verified_at_rfc3339_utc: pack.verified_at.rfc3339_utc.clone(), warnings, objects: objects_out, } } fn build_publication_point_audit_from_vcir( ca: &CaInstanceHandle, source: PublicationPointSource, repo_sync_source: Option<&str>, repo_sync_phase: Option<&str>, repo_sync_duration_ms: Option, repo_sync_error: Option<&str>, vcir: Option<&ValidatedCaInstanceResult>, pack: Option<&PublicationPointSnapshot>, runner_warnings: &[Warning], objects: &crate::validation::objects::ObjectsOutput, child_audits: &[ObjectAuditEntry], fresh_failure_audits: &[ObjectAuditEntry], ) -> PublicationPointAudit { if let Some(pack) = pack { return build_publication_point_audit_from_snapshot( ca, source, repo_sync_source, repo_sync_phase, repo_sync_duration_ms, repo_sync_error, pack, runner_warnings, objects, child_audits, ); } let mut warnings = Vec::new(); warnings.extend(runner_warnings.iter().map(AuditWarning::from)); warnings.extend(objects.warnings.iter().map(AuditWarning::from)); let Some(vcir) = vcir else { return PublicationPointAudit { node_id: None, parent_node_id: None, discovered_from: None, rsync_base_uri: ca.rsync_base_uri.clone(), manifest_rsync_uri: ca.manifest_rsync_uri.clone(), publication_point_rsync_uri: ca.publication_point_rsync_uri.clone(), rrdp_notification_uri: ca.rrdp_notification_uri.clone(), source: source_label(source), repo_sync_source: repo_sync_source.map(ToString::to_string), repo_sync_phase: repo_sync_phase.map(ToString::to_string), repo_sync_duration_ms, repo_sync_error: repo_sync_error.map(ToString::to_string), repo_terminal_state: terminal_state_label(source).to_string(), this_update_rfc3339_utc: String::new(), next_update_rfc3339_utc: String::new(), verified_at_rfc3339_utc: String::new(), warnings, objects: fresh_failure_audits.to_vec(), }; }; if source == PublicationPointSource::FailedFetchNoCache { let mut objects_out = Vec::with_capacity( objects.audit.len() + child_audits.len() + fresh_failure_audits.len(), ); objects_out.extend(child_audits.iter().cloned()); objects_out.extend(objects.audit.iter().cloned()); objects_out.extend(fresh_failure_audits.iter().cloned()); return PublicationPointAudit { node_id: None, parent_node_id: None, discovered_from: None, rsync_base_uri: ca.rsync_base_uri.clone(), manifest_rsync_uri: ca.manifest_rsync_uri.clone(), publication_point_rsync_uri: ca.publication_point_rsync_uri.clone(), rrdp_notification_uri: ca.rrdp_notification_uri.clone(), source: source_label(source), repo_sync_source: repo_sync_source.map(ToString::to_string), repo_sync_phase: repo_sync_phase.map(ToString::to_string), repo_sync_duration_ms, repo_sync_error: repo_sync_error.map(ToString::to_string), repo_terminal_state: terminal_state_label(source).to_string(), this_update_rfc3339_utc: vcir .validated_manifest_meta .validated_manifest_this_update .rfc3339_utc .clone(), next_update_rfc3339_utc: vcir .validated_manifest_meta .validated_manifest_next_update .rfc3339_utc .clone(), verified_at_rfc3339_utc: vcir.last_successful_validation_time.rfc3339_utc.clone(), warnings, objects: objects_out, }; } let mut audit_by_uri: HashMap = HashMap::new(); for artifact in &vcir.related_artifacts { let Some(uri) = artifact.uri.as_ref() else { continue; }; audit_by_uri.insert( uri.clone(), ObjectAuditEntry { rsync_uri: uri.clone(), sha256_hex: artifact.sha256.clone(), kind: kind_from_vcir_artifact_kind(artifact.artifact_kind), result: audit_result_from_vcir_status(artifact.validation_status), detail: None, }, ); } for e in child_audits { audit_by_uri.insert(e.rsync_uri.clone(), e.clone()); } for e in &objects.audit { audit_by_uri.insert(e.rsync_uri.clone(), e.clone()); } let mut ordered_uris: Vec = vcir .related_artifacts .iter() .filter_map(|artifact| artifact.uri.clone()) .collect(); ordered_uris.sort(); ordered_uris.dedup(); let mut objects_out: Vec = Vec::new(); if let Some(entry) = audit_by_uri.remove(&vcir.current_manifest_rsync_uri) { objects_out.push(entry); } else { objects_out.push(ObjectAuditEntry { rsync_uri: vcir.current_manifest_rsync_uri.clone(), sha256_hex: vcir .related_artifacts .iter() .find(|artifact| { artifact.artifact_role == VcirArtifactRole::Manifest && artifact.uri.as_deref() == Some(vcir.current_manifest_rsync_uri.as_str()) }) .map(|artifact| artifact.sha256.clone()) .unwrap_or_default(), kind: AuditObjectKind::Manifest, result: AuditObjectResult::Ok, detail: None, }); } for uri in ordered_uris { if uri == vcir.current_manifest_rsync_uri { continue; } if let Some(entry) = audit_by_uri.remove(&uri) { objects_out.push(entry); } } let mut audit_objects = objects_out.clone(); audit_objects.extend(fresh_failure_audits.iter().cloned()); PublicationPointAudit { node_id: None, parent_node_id: None, discovered_from: None, rsync_base_uri: ca.rsync_base_uri.clone(), manifest_rsync_uri: ca.manifest_rsync_uri.clone(), publication_point_rsync_uri: ca.publication_point_rsync_uri.clone(), rrdp_notification_uri: ca.rrdp_notification_uri.clone(), source: source_label(source), repo_sync_source: repo_sync_source.map(ToString::to_string), repo_sync_phase: repo_sync_phase.map(ToString::to_string), repo_sync_duration_ms, repo_sync_error: repo_sync_error.map(ToString::to_string), repo_terminal_state: terminal_state_label(source).to_string(), this_update_rfc3339_utc: vcir .validated_manifest_meta .validated_manifest_this_update .rfc3339_utc .clone(), next_update_rfc3339_utc: vcir .validated_manifest_meta .validated_manifest_next_update .rfc3339_utc .clone(), verified_at_rfc3339_utc: vcir.last_successful_validation_time.rfc3339_utc.clone(), warnings, objects: audit_objects, } } fn build_publication_point_audit_from_publication_point_cache_projection( ca: &CaInstanceHandle, source: PublicationPointSource, repo_sync_source: Option<&str>, repo_sync_phase: Option<&str>, repo_sync_duration_ms: Option, repo_sync_error: Option<&str>, projection: &PublicationPointCacheProjection, validation_time: time::OffsetDateTime, runner_warnings: &[Warning], objects: &crate::validation::objects::ObjectsOutput, child_audits: &[ObjectAuditEntry], ) -> PublicationPointAudit { let mut warnings = Vec::new(); warnings.extend(runner_warnings.iter().map(AuditWarning::from)); warnings.extend(objects.warnings.iter().map(AuditWarning::from)); let mut audit_by_uri: HashMap = HashMap::new(); for artifact in &projection.related_objects { let Some(uri) = artifact.uri.as_ref() else { continue; }; audit_by_uri.insert( uri.clone(), ObjectAuditEntry { rsync_uri: uri.clone(), sha256_hex: artifact.sha256.clone(), kind: kind_from_vcir_artifact_kind(artifact.artifact_kind), result: audit_result_from_vcir_status(artifact.validation_status), detail: None, }, ); } for entry in child_audits { audit_by_uri.insert(entry.rsync_uri.clone(), entry.clone()); } for entry in &objects.audit { audit_by_uri.insert(entry.rsync_uri.clone(), entry.clone()); } let mut ordered_uris: Vec = projection .related_objects .iter() .filter_map(|artifact| artifact.uri.clone()) .collect(); ordered_uris.sort(); ordered_uris.dedup(); let mut objects_out: Vec = Vec::with_capacity(ordered_uris.len().max(1)); if let Some(entry) = audit_by_uri.remove(&projection.manifest_rsync_uri) { objects_out.push(entry); } else { objects_out.push(ObjectAuditEntry { rsync_uri: projection.manifest_rsync_uri.clone(), sha256_hex: hex::encode(projection.manifest_sha256), kind: AuditObjectKind::Manifest, result: AuditObjectResult::Ok, detail: None, }); } for uri in ordered_uris { if uri == projection.manifest_rsync_uri { continue; } if let Some(entry) = audit_by_uri.remove(&uri) { objects_out.push(entry); } } PublicationPointAudit { node_id: None, parent_node_id: None, discovered_from: None, rsync_base_uri: ca.rsync_base_uri.clone(), manifest_rsync_uri: ca.manifest_rsync_uri.clone(), publication_point_rsync_uri: ca.publication_point_rsync_uri.clone(), rrdp_notification_uri: ca.rrdp_notification_uri.clone(), source: source_label(source), repo_sync_source: repo_sync_source.map(ToString::to_string), repo_sync_phase: repo_sync_phase.map(ToString::to_string), repo_sync_duration_ms, repo_sync_error: repo_sync_error.map(ToString::to_string), repo_terminal_state: terminal_state_label(source).to_string(), this_update_rfc3339_utc: projection.manifest_this_update.rfc3339_utc.clone(), next_update_rfc3339_utc: projection.manifest_next_update.rfc3339_utc.clone(), verified_at_rfc3339_utc: PackTime::from_utc_offset_datetime(validation_time).rfc3339_utc, warnings, objects: objects_out, } } fn parse_snapshot_time_value(pack_time: &PackTime) -> Result { time::OffsetDateTime::parse( &pack_time.rfc3339_utc, &time::format_description::well_known::Rfc3339, ) .map_err(|e| format!("invalid RFC3339 time '{}': {e}", pack_time.rfc3339_utc)) } fn empty_objects_output() -> crate::validation::objects::ObjectsOutput { 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(), } } fn reuse_ccr_manifest_projection_from_vcir( ca: &CaInstanceHandle, vcir: &ValidatedCaInstanceResult, ) -> Result { if vcir.ccr_manifest_projection.manifest_rsync_uri != ca.manifest_rsync_uri { return Err(format!( "vcir CCR manifest projection URI mismatch: expected {}, got {}", ca.manifest_rsync_uri, vcir.ccr_manifest_projection.manifest_rsync_uri )); } Ok(vcir.ccr_manifest_projection.clone()) } fn project_current_instance_vcir_on_failed_fetch( store: &RocksStore, ca: &CaInstanceHandle, fresh_err: &ManifestFreshError, validation_time: time::OffsetDateTime, ) -> Result { let mut warnings = Vec::new(); let Some(vcir) = store .get_vcir(&ca.manifest_rsync_uri) .map_err(|e| format!("load VCIR failed: {e}"))? else { warnings.push( Warning::new(format!("manifest failed fetch: {fresh_err}")) .with_rfc_refs(&[RfcRef("RFC 9286 §6.6")]) .with_context(&ca.manifest_rsync_uri), ); warnings.push( Warning::new( "no latest validated result for current CA instance; no cached output reused", ) .with_rfc_refs(&[RfcRef("RFC 9286 §6.6")]) .with_context(&ca.manifest_rsync_uri), ); return Ok(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, }); }; if !vcir.audit_summary.failed_fetch_eligible { warnings.push( Warning::new(format!("manifest failed fetch: {fresh_err}")) .with_rfc_refs(&[RfcRef("RFC 9286 §6.6")]) .with_context(&ca.manifest_rsync_uri), ); warnings.push( Warning::new( "latest VCIR is not marked failed-fetch eligible; no cached output reused", ) .with_rfc_refs(&[RfcRef("RFC 9286 §6.6")]) .with_context(&ca.manifest_rsync_uri), ); return Ok(VcirReuseProjection { source: PublicationPointSource::FailedFetchNoCache, vcir: Some(vcir), ccr_manifest_projection: None, snapshot: None, objects: empty_objects_output(), child_audits: Vec::new(), discovered_children: Vec::new(), warnings, }); } let instance_effective_until = parse_snapshot_time_value(&vcir.instance_gate.instance_effective_until)?; if validation_time > instance_effective_until { warnings.push( Warning::new(format!("manifest failed fetch: {fresh_err}")) .with_rfc_refs(&[RfcRef("RFC 9286 §6.6")]) .with_context(&ca.manifest_rsync_uri), ); warnings.push( Warning::new( "latest VCIR instance_gate expired; current instance contributes no cached output", ) .with_rfc_refs(&[RfcRef("RFC 9286 §6.6")]) .with_context(&ca.manifest_rsync_uri), ); return Ok(VcirReuseProjection { source: PublicationPointSource::FailedFetchNoCache, vcir: Some(vcir), ccr_manifest_projection: None, snapshot: None, objects: empty_objects_output(), child_audits: Vec::new(), discovered_children: Vec::new(), warnings, }); } let ccr_manifest_projection = reuse_ccr_manifest_projection_from_vcir(ca, &vcir)?; if fresh_err.should_warn_when_current_instance_reused() { warnings.push( Warning::new(format!("manifest failed fetch: {fresh_err}")) .with_rfc_refs(&[RfcRef("RFC 9286 §6.6")]) .with_context(&ca.manifest_rsync_uri), ); } // Current-instance reuse is fully described by VCIR projections; rebuilding a // byte-backed snapshot here only duplicates repo-byte I/O and creates warning noise. let snapshot = None; let objects = build_objects_output_from_vcir(&vcir, validation_time, &mut warnings); let (discovered_children, child_audits) = restore_children_from_vcir(store, ca, &vcir, &mut warnings); Ok(VcirReuseProjection { source: PublicationPointSource::VcirCurrentInstance, vcir: Some(vcir), ccr_manifest_projection: Some(ccr_manifest_projection), snapshot, objects, child_audits, discovered_children, warnings, }) } #[cfg(test)] fn reconstruct_snapshot_from_vcir( store: &RocksStore, ca: &CaInstanceHandle, vcir: &ValidatedCaInstanceResult, warnings: &mut Vec, ) -> Option { let manifest_artifact = vcir.related_artifacts.iter().find(|artifact| { artifact.artifact_role == VcirArtifactRole::Manifest && artifact.uri.as_deref() == Some(ca.manifest_rsync_uri.as_str()) })?; let manifest_bytes = match store.get_blob_bytes(&manifest_artifact.sha256) { Ok(Some(bytes)) => bytes, Ok(None) => { warnings.push( Warning::new("manifest raw bytes missing for VCIR audit reconstruction") .with_context(&ca.manifest_rsync_uri), ); return None; } Err(e) => { warnings.push( Warning::new(format!( "manifest raw bytes load failed for VCIR audit reconstruction: {e}" )) .with_context(&ca.manifest_rsync_uri), ); return None; } }; let mut seen = HashSet::new(); let mut files = Vec::new(); for artifact in &vcir.related_artifacts { let Some(uri) = artifact.uri.as_ref() else { continue; }; if artifact.artifact_role == VcirArtifactRole::Manifest || artifact.artifact_role == VcirArtifactRole::IssuerCert || artifact.artifact_role == VcirArtifactRole::TrustAnchorCert || artifact.artifact_role == VcirArtifactRole::Tal { continue; } if !seen.insert(uri.clone()) { continue; } match store.get_blob_bytes(&artifact.sha256) { Ok(Some(bytes)) => files.push(PackFile::from_bytes_compute_sha256(uri, bytes)), Ok(None) => warnings.push( Warning::new("related artifact raw bytes missing for VCIR audit reconstruction") .with_context(uri), ), Err(e) => warnings.push( Warning::new(format!( "related artifact raw bytes load failed for VCIR audit reconstruction: {e}" )) .with_context(uri), ), } } Some(PublicationPointSnapshot { format_version: PublicationPointSnapshot::FORMAT_VERSION_V1, publication_point_rsync_uri: ca.publication_point_rsync_uri.clone(), manifest_rsync_uri: ca.manifest_rsync_uri.clone(), manifest_number_be: vcir .validated_manifest_meta .validated_manifest_number .clone(), this_update: vcir .validated_manifest_meta .validated_manifest_this_update .clone(), next_update: vcir .validated_manifest_meta .validated_manifest_next_update .clone(), verified_at: vcir.last_successful_validation_time.clone(), manifest_bytes, files, }) } fn audit_kind_for_vcir_output_type(output_type: VcirOutputType) -> AuditObjectKind { match output_type { VcirOutputType::Vrp => AuditObjectKind::Roa, VcirOutputType::Aspa => AuditObjectKind::Aspa, VcirOutputType::RouterKey => AuditObjectKind::RouterCertificate, } } fn build_objects_output_from_vcir( vcir: &ValidatedCaInstanceResult, validation_time: time::OffsetDateTime, warnings: &mut Vec, ) -> crate::validation::objects::ObjectsOutput { let mut output = empty_objects_output(); let mut audit_by_uri: HashMap = HashMap::new(); let mut roa_total: HashSet = HashSet::new(); let mut aspa_total: HashSet = HashSet::new(); let mut roa_ok: HashSet = HashSet::new(); let mut aspa_ok: HashSet = HashSet::new(); for artifact in &vcir.related_artifacts { if artifact.artifact_role != VcirArtifactRole::SignedObject { continue; } if let Some(uri) = artifact.uri.as_ref() { match artifact.artifact_kind { VcirArtifactKind::Roa => { roa_total.insert(uri.clone()); } VcirArtifactKind::Aspa => { aspa_total.insert(uri.clone()); } _ => {} } } } for local in &vcir.local_outputs { let effective_until = match parse_snapshot_time_value(&local.item_effective_until) { Ok(v) => v, Err(e) => { warnings.push( Warning::new(format!( "cached local output has invalid item_effective_until: {e}" )) .with_context(&local.source_object_uri), ); audit_by_uri.insert( local.source_object_uri.clone(), ObjectAuditEntry { rsync_uri: local.source_object_uri.clone(), sha256_hex: local.source_object_hash_hex(), kind: audit_kind_for_vcir_output_type(local.output_type), result: AuditObjectResult::Error, detail: Some( "cached local output has invalid item_effective_until".to_string(), ), }, ); continue; } }; if validation_time > effective_until { audit_by_uri .entry(local.source_object_uri.clone()) .or_insert_with(|| ObjectAuditEntry { rsync_uri: local.source_object_uri.clone(), sha256_hex: local.source_object_hash_hex(), kind: audit_kind_for_vcir_output_type(local.output_type), result: AuditObjectResult::Skipped, detail: Some("skipped: cached local output expired".to_string()), }); continue; } match local.output_type { VcirOutputType::Vrp => match parse_vcir_vrp_output(local) { Ok(vrp) => { roa_ok.insert(local.source_object_uri.clone()); output.vrps.push(vrp); audit_by_uri.insert( local.source_object_uri.clone(), ObjectAuditEntry { rsync_uri: local.source_object_uri.clone(), sha256_hex: local.source_object_hash_hex(), kind: AuditObjectKind::Roa, result: AuditObjectResult::Ok, detail: None, }, ); } Err(e) => { warnings.push( Warning::new(format!("cached ROA local output parse failed: {e}")) .with_context(&local.source_object_uri), ); audit_by_uri.insert( local.source_object_uri.clone(), ObjectAuditEntry { rsync_uri: local.source_object_uri.clone(), sha256_hex: local.source_object_hash_hex(), kind: AuditObjectKind::Roa, result: AuditObjectResult::Error, detail: Some(format!("cached ROA local output parse failed: {e}")), }, ); } }, VcirOutputType::Aspa => match parse_vcir_aspa_output(local) { Ok(aspa) => { aspa_ok.insert(local.source_object_uri.clone()); output.aspas.push(aspa); audit_by_uri.insert( local.source_object_uri.clone(), ObjectAuditEntry { rsync_uri: local.source_object_uri.clone(), sha256_hex: local.source_object_hash_hex(), kind: AuditObjectKind::Aspa, result: AuditObjectResult::Ok, detail: None, }, ); } Err(e) => { warnings.push( Warning::new(format!("cached ASPA local output parse failed: {e}")) .with_context(&local.source_object_uri), ); audit_by_uri.insert( local.source_object_uri.clone(), ObjectAuditEntry { rsync_uri: local.source_object_uri.clone(), sha256_hex: local.source_object_hash_hex(), kind: AuditObjectKind::Aspa, result: AuditObjectResult::Error, detail: Some(format!("cached ASPA local output parse failed: {e}")), }, ); } }, VcirOutputType::RouterKey => match parse_vcir_router_key_output(local) { Ok(router_key) => { output.router_keys.push(router_key); audit_by_uri.insert( local.source_object_uri.clone(), ObjectAuditEntry { rsync_uri: local.source_object_uri.clone(), sha256_hex: local.source_object_hash_hex(), kind: AuditObjectKind::RouterCertificate, result: AuditObjectResult::Ok, detail: Some("cached Router Key local output restored".to_string()), }, ); } Err(e) => { warnings.push( Warning::new(format!("cached Router Key local output parse failed: {e}")) .with_context(&local.source_object_uri), ); audit_by_uri.insert( local.source_object_uri.clone(), ObjectAuditEntry { rsync_uri: local.source_object_uri.clone(), sha256_hex: local.source_object_hash_hex(), kind: AuditObjectKind::RouterCertificate, result: AuditObjectResult::Error, detail: Some(format!( "cached Router Key local output parse failed: {e}" )), }, ); } }, } } output.stats.roa_total = roa_total.len(); output.stats.roa_ok = roa_ok.len(); output.stats.aspa_total = aspa_total.len(); output.stats.aspa_ok = aspa_ok.len(); let mut audit: Vec<_> = audit_by_uri.into_values().collect(); audit.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri)); output.audit = audit; output } fn build_objects_output_from_publication_point_cache_projection( projection: &PublicationPointCacheProjection, validation_time: time::OffsetDateTime, warnings: &mut Vec, ) -> crate::validation::objects::ObjectsOutput { let mut output = empty_objects_output(); let mut audit_by_uri: HashMap = HashMap::new(); let mut roa_total: HashSet = HashSet::new(); let mut aspa_total: HashSet = HashSet::new(); let mut roa_ok: HashSet = HashSet::new(); let mut aspa_ok: HashSet = HashSet::new(); for artifact in &projection.related_objects { if artifact.artifact_role != VcirArtifactRole::SignedObject { continue; } if let Some(uri) = artifact.uri.as_ref() { match artifact.artifact_kind { VcirArtifactKind::Roa => { roa_total.insert(uri.clone()); } VcirArtifactKind::Aspa => { aspa_total.insert(uri.clone()); } _ => {} } } } for projected in &projection.outputs { let effective_until = match parse_snapshot_time_value(&projected.item_effective_until) { Ok(value) => value, Err(err) => { warnings.push( Warning::new(format!( "publication-point cached local output has invalid item_effective_until: {err}" )) .with_context(&projected.source_object_uri), ); audit_by_uri.insert( projected.source_object_uri.clone(), ObjectAuditEntry { rsync_uri: projected.source_object_uri.clone(), sha256_hex: hex::encode(projected.source_object_hash), kind: audit_kind_for_vcir_output_type(projected.output_type), result: AuditObjectResult::Error, detail: Some( "publication-point cached local output has invalid item_effective_until" .to_string(), ), }, ); continue; } }; if validation_time > effective_until { audit_by_uri .entry(projected.source_object_uri.clone()) .or_insert_with(|| ObjectAuditEntry { rsync_uri: projected.source_object_uri.clone(), sha256_hex: hex::encode(projected.source_object_hash), kind: audit_kind_for_vcir_output_type(projected.output_type), result: AuditObjectResult::Skipped, detail: Some( "skipped: publication-point cached local output expired".to_string(), ), }); continue; } match projected.output_type { VcirOutputType::Vrp => match parse_publication_point_cache_vrp_output(projected) { Ok(vrp) => { roa_ok.insert(projected.source_object_uri.clone()); output.vrps.push(vrp); audit_by_uri.insert( projected.source_object_uri.clone(), ObjectAuditEntry { rsync_uri: projected.source_object_uri.clone(), sha256_hex: hex::encode(projected.source_object_hash), kind: AuditObjectKind::Roa, result: AuditObjectResult::Ok, detail: None, }, ); } Err(err) => { warnings.push( Warning::new(format!( "publication-point cached ROA local output parse failed: {err}" )) .with_context(&projected.source_object_uri), ); audit_by_uri.insert( projected.source_object_uri.clone(), ObjectAuditEntry { rsync_uri: projected.source_object_uri.clone(), sha256_hex: hex::encode(projected.source_object_hash), kind: AuditObjectKind::Roa, result: AuditObjectResult::Error, detail: Some(format!( "publication-point cached ROA local output parse failed: {err}" )), }, ); } }, VcirOutputType::Aspa => match parse_publication_point_cache_aspa_output(projected) { Ok(aspa) => { aspa_ok.insert(projected.source_object_uri.clone()); output.aspas.push(aspa); audit_by_uri.insert( projected.source_object_uri.clone(), ObjectAuditEntry { rsync_uri: projected.source_object_uri.clone(), sha256_hex: hex::encode(projected.source_object_hash), kind: AuditObjectKind::Aspa, result: AuditObjectResult::Ok, detail: None, }, ); } Err(err) => { warnings.push( Warning::new(format!( "publication-point cached ASPA local output parse failed: {err}" )) .with_context(&projected.source_object_uri), ); audit_by_uri.insert( projected.source_object_uri.clone(), ObjectAuditEntry { rsync_uri: projected.source_object_uri.clone(), sha256_hex: hex::encode(projected.source_object_hash), kind: AuditObjectKind::Aspa, result: AuditObjectResult::Error, detail: Some(format!( "publication-point cached ASPA local output parse failed: {err}" )), }, ); } }, VcirOutputType::RouterKey => { match parse_publication_point_cache_router_key_output(projected) { Ok(router_key) => { output.router_keys.push(router_key); audit_by_uri.insert( projected.source_object_uri.clone(), ObjectAuditEntry { rsync_uri: projected.source_object_uri.clone(), sha256_hex: hex::encode(projected.source_object_hash), kind: AuditObjectKind::RouterCertificate, result: AuditObjectResult::Ok, detail: Some( "publication-point cached Router Key local output restored" .to_string(), ), }, ); } Err(err) => { warnings.push( Warning::new(format!( "publication-point cached Router Key local output parse failed: {err}" )) .with_context(&projected.source_object_uri), ); audit_by_uri.insert( projected.source_object_uri.clone(), ObjectAuditEntry { rsync_uri: projected.source_object_uri.clone(), sha256_hex: hex::encode(projected.source_object_hash), kind: AuditObjectKind::RouterCertificate, result: AuditObjectResult::Error, detail: Some(format!( "publication-point cached Router Key local output parse failed: {err}" )), }, ); } } } } } output.stats.roa_total = roa_total.len(); output.stats.roa_ok = roa_ok.len(); output.stats.aspa_total = aspa_total.len(); output.stats.aspa_ok = aspa_ok.len(); let mut audit: Vec<_> = audit_by_uri.into_values().collect(); audit.sort_by(|left, right| left.rsync_uri.cmp(&right.rsync_uri)); output.audit = audit; output } fn parse_vcir_vrp_output(local: &VcirLocalOutput) -> Result { match &local.payload { VcirLocalOutputPayload::Vrp { asn, afi, prefix_len, addr, max_length, } => Ok(Vrp { asn: *asn, prefix: crate::data_model::roa::IpPrefix { afi: *afi, prefix_len: *prefix_len, addr: *addr, }, max_length: *max_length, }), _ => Err("VCIR local output payload is not VRP".to_string()), } } fn parse_vcir_aspa_output(local: &VcirLocalOutput) -> Result { match &local.payload { VcirLocalOutputPayload::Aspa { customer_as_id, provider_as_ids, } => Ok(AspaAttestation { customer_as_id: *customer_as_id, provider_as_ids: provider_as_ids.clone(), }), _ => Err("VCIR local output payload is not ASPA".to_string()), } } fn parse_vcir_router_key_output(local: &VcirLocalOutput) -> Result { match &local.payload { VcirLocalOutputPayload::RouterKey { as_id, ski, spki_der, } => Ok(RouterKeyPayload { as_id: *as_id, ski: ski.clone(), spki_der: spki_der.clone(), source_object_uri: local.source_object_uri.clone(), source_object_hash: local.source_object_hash_hex(), source_ee_cert_hash: local.source_ee_cert_hash_hex(), item_effective_until: local.item_effective_until.clone(), }), _ => Err("VCIR local output payload is not Router Key".to_string()), } } fn parse_publication_point_cache_vrp_output( projected: &PublicationPointCacheOutput, ) -> Result { match &projected.payload { VcirLocalOutputPayload::Vrp { asn, afi, prefix_len, addr, max_length, } => Ok(Vrp { asn: *asn, prefix: crate::data_model::roa::IpPrefix { afi: *afi, prefix_len: *prefix_len, addr: *addr, }, max_length: *max_length, }), _ => Err("publication-point cache output payload is not VRP".to_string()), } } fn parse_publication_point_cache_aspa_output( projected: &PublicationPointCacheOutput, ) -> Result { match &projected.payload { VcirLocalOutputPayload::Aspa { customer_as_id, provider_as_ids, } => Ok(AspaAttestation { customer_as_id: *customer_as_id, provider_as_ids: provider_as_ids.clone(), }), _ => Err("publication-point cache output payload is not ASPA".to_string()), } } fn parse_publication_point_cache_router_key_output( projected: &PublicationPointCacheOutput, ) -> Result { match &projected.payload { VcirLocalOutputPayload::RouterKey { as_id, ski, spki_der, } => Ok(RouterKeyPayload { as_id: *as_id, ski: ski.clone(), spki_der: spki_der.clone(), source_object_uri: projected.source_object_uri.clone(), source_object_hash: hex::encode(projected.source_object_hash), source_ee_cert_hash: hex::encode(projected.source_ee_cert_hash), item_effective_until: projected.item_effective_until.clone(), }), _ => Err("publication-point cache output payload is not Router Key".to_string()), } } fn restore_children_from_vcir( store: &RocksStore, ca: &CaInstanceHandle, vcir: &ValidatedCaInstanceResult, warnings: &mut Vec, ) -> (Vec, Vec) { let mut children = Vec::new(); let mut audits = Vec::new(); for child in &vcir.child_entries { match store.get_blob_bytes(&child.child_cert_hash) { Ok(Some(bytes)) => { children.push(DiscoveredChildCaInstance { handle: CaInstanceHandle { depth: 0, tal_id: ca.tal_id.clone(), parent_manifest_rsync_uri: Some(ca.manifest_rsync_uri.clone()), ca_certificate_der: bytes, ca_certificate_rsync_uri: Some(child.child_cert_rsync_uri.clone()), effective_ip_resources: child.child_effective_ip_resources.clone(), effective_as_resources: child.child_effective_as_resources.clone(), rsync_base_uri: child.child_rsync_base_uri.clone(), manifest_rsync_uri: child.child_manifest_rsync_uri.clone(), publication_point_rsync_uri: child .child_publication_point_rsync_uri .clone(), rrdp_notification_uri: child.child_rrdp_notification_uri.clone(), }, discovered_from: crate::audit::DiscoveredFrom { parent_manifest_rsync_uri: ca.manifest_rsync_uri.clone(), child_ca_certificate_rsync_uri: child.child_cert_rsync_uri.clone(), child_ca_certificate_sha256_hex: child.child_cert_hash.clone(), }, }); audits.push(ObjectAuditEntry { rsync_uri: child.child_cert_rsync_uri.clone(), sha256_hex: child.child_cert_hash.clone(), kind: AuditObjectKind::Certificate, result: AuditObjectResult::Ok, detail: Some("restored child CA instance from VCIR".to_string()), }); } Ok(None) => { warnings.push( Warning::new("child certificate bytes missing for VCIR child restoration") .with_context(&child.child_cert_rsync_uri), ); audits.push(ObjectAuditEntry { rsync_uri: child.child_cert_rsync_uri.clone(), sha256_hex: child.child_cert_hash.clone(), kind: AuditObjectKind::Certificate, result: AuditObjectResult::Error, detail: Some( "child certificate bytes missing for VCIR child restoration".to_string(), ), }); } Err(e) => { warnings.push( Warning::new(format!( "child certificate bytes load failed for VCIR child restoration: {e}" )) .with_context(&child.child_cert_rsync_uri), ); audits.push(ObjectAuditEntry { rsync_uri: child.child_cert_rsync_uri.clone(), sha256_hex: child.child_cert_hash.clone(), kind: AuditObjectKind::Certificate, result: AuditObjectResult::Error, detail: Some(format!( "child certificate bytes load failed for VCIR child restoration: {e}" )), }); } } } (children, audits) } fn restore_children_from_publication_point_cache( store: &RocksStore, ca: &CaInstanceHandle, projection: &PublicationPointCacheProjection, validation_time: time::OffsetDateTime, warnings: &mut Vec, worker_count: usize, timing: Option<&TimingHandle>, ) -> (Vec, Vec) { let worker_count = worker_count .clamp(1, PUBLICATION_POINT_CACHE_CHILD_RESTORE_MAX_WORKERS) .min(projection.children.len().max(1)); let outcomes = if worker_count > 1 && projection.children.len() >= PUBLICATION_POINT_CACHE_CHILD_RESTORE_PARALLEL_MIN_CHILDREN { if let Some(timing) = timing { timing.record_count( "publication_point_cache_restore_children_parallel_publication_points", 1, ); timing.record_count( "publication_point_cache_restore_children_parallel_children", projection.children.len() as u64, ); timing.record_count( "publication_point_cache_restore_children_workers_total", worker_count as u64, ); } restore_publication_point_cache_children_parallel( store, ca, &projection.children, validation_time, worker_count, ) } else { if let Some(timing) = timing { timing.record_count( "publication_point_cache_restore_children_batch_publication_points", 1, ); timing.record_count( "publication_point_cache_restore_children_batch_children", projection.children.len() as u64, ); } restore_publication_point_cache_children_chunk( store, ca, &projection.children, validation_time, ) }; collect_publication_point_cache_child_restore_outcomes(outcomes, warnings) } fn restore_publication_point_cache_children_parallel( store: &RocksStore, ca: &CaInstanceHandle, children: &[PublicationPointCacheChild], validation_time: time::OffsetDateTime, worker_count: usize, ) -> Vec { let chunk_size = children.len().div_ceil(worker_count).max(1); let mut chunk_results = Vec::new(); std::thread::scope(|scope| { let mut handles = Vec::new(); for chunk in children.chunks(chunk_size) { handles.push(scope.spawn(move || { restore_publication_point_cache_children_chunk(store, ca, chunk, validation_time) })); } for handle in handles { chunk_results.extend( handle .join() .expect("publication-point cache child restore worker panicked"), ); } }); chunk_results } fn restore_publication_point_cache_children_chunk( store: &RocksStore, ca: &CaInstanceHandle, children: &[PublicationPointCacheChild], validation_time: time::OffsetDateTime, ) -> Vec { let mut outcomes = vec![None; children.len()]; let mut valid_positions = Vec::new(); let mut hashes = Vec::new(); for (position, child) in children.iter().enumerate() { let effective_not_before = match parse_snapshot_time_value(&child.child_effective_not_before) { Ok(value) => value, Err(e) => { outcomes[position] = Some(PublicationPointCacheChildRestoreOutcome { child: None, audit: None, warning: Some( Warning::new(format!( "publication-point cache child has invalid effective notBefore: {e}" )) .with_context(&child.child_cert_rsync_uri), ), }); continue; } }; let effective_until = match parse_snapshot_time_value(&child.child_effective_until) { Ok(value) => value, Err(e) => { outcomes[position] = Some(PublicationPointCacheChildRestoreOutcome { child: None, audit: None, warning: Some( Warning::new(format!( "publication-point cache child has invalid effective until: {e}" )) .with_context(&child.child_cert_rsync_uri), ), }); continue; } }; if validation_time < effective_not_before || validation_time > effective_until { outcomes[position] = Some(PublicationPointCacheChildRestoreOutcome { child: None, warning: None, audit: Some(publication_point_cache_child_audit( child, AuditObjectResult::Skipped, Some("skipped: publication-point cache child expired".to_string()), )), }); continue; } valid_positions.push(position); hashes.push(child.child_cert_hash.clone()); } match store.get_blob_bytes_batch(&hashes) { Ok(bytes_by_position) => { for (position, maybe_bytes) in valid_positions.into_iter().zip(bytes_by_position) { let child = &children[position]; outcomes[position] = Some(match maybe_bytes { Some(bytes) => PublicationPointCacheChildRestoreOutcome { child: Some(publication_point_cache_discovered_child(ca, child, bytes)), warning: None, audit: Some(publication_point_cache_child_audit( child, AuditObjectResult::Ok, Some( "restored child CA instance from publication-point cache" .to_string(), ), )), }, None => PublicationPointCacheChildRestoreOutcome { child: None, warning: Some( Warning::new( "child certificate bytes missing for publication-point cache restoration", ) .with_context(&child.child_cert_rsync_uri), ), audit: Some(publication_point_cache_child_audit( child, AuditObjectResult::Error, Some( "child certificate bytes missing for publication-point cache restoration" .to_string(), ), )), }, }); } } Err(e) => { for position in valid_positions { let child = &children[position]; outcomes[position] = Some(PublicationPointCacheChildRestoreOutcome { child: None, warning: Some( Warning::new(format!( "child certificate bytes load failed for publication-point cache restoration: {e}" )) .with_context(&child.child_cert_rsync_uri), ), audit: Some(publication_point_cache_child_audit( child, AuditObjectResult::Error, Some(format!( "child certificate bytes load failed for publication-point cache restoration: {e}" )), )), }); } } } outcomes .into_iter() .flatten() .collect::>() } fn collect_publication_point_cache_child_restore_outcomes( outcomes: Vec, warnings: &mut Vec, ) -> (Vec, Vec) { let mut children = Vec::new(); let mut audits = Vec::new(); for outcome in outcomes { if let Some(warning) = outcome.warning { warnings.push(warning); } if let Some(audit) = outcome.audit { audits.push(audit); } if let Some(child) = outcome.child { children.push(child); } } (children, audits) } #[derive(Clone)] struct PublicationPointCacheChildRestoreOutcome { child: Option, audit: Option, warning: Option, } fn publication_point_cache_discovered_child( ca: &CaInstanceHandle, child: &PublicationPointCacheChild, bytes: Vec, ) -> DiscoveredChildCaInstance { DiscoveredChildCaInstance { handle: CaInstanceHandle { depth: 0, tal_id: ca.tal_id.clone(), parent_manifest_rsync_uri: Some(ca.manifest_rsync_uri.clone()), ca_certificate_der: bytes, ca_certificate_rsync_uri: Some(child.child_cert_rsync_uri.clone()), effective_ip_resources: child.child_effective_ip_resources.clone(), effective_as_resources: child.child_effective_as_resources.clone(), rsync_base_uri: child.child_rsync_base_uri.clone(), manifest_rsync_uri: child.child_manifest_rsync_uri.clone(), publication_point_rsync_uri: child.child_publication_point_rsync_uri.clone(), rrdp_notification_uri: child.child_rrdp_notification_uri.clone(), }, discovered_from: crate::audit::DiscoveredFrom { parent_manifest_rsync_uri: ca.manifest_rsync_uri.clone(), child_ca_certificate_rsync_uri: child.child_cert_rsync_uri.clone(), child_ca_certificate_sha256_hex: child.child_cert_hash.clone(), }, } } fn publication_point_cache_child_audit( child: &PublicationPointCacheChild, result: AuditObjectResult, detail: Option, ) -> ObjectAuditEntry { ObjectAuditEntry { rsync_uri: child.child_cert_rsync_uri.clone(), sha256_hex: child.child_cert_hash.clone(), kind: AuditObjectKind::Certificate, result, detail, } } fn persist_vcir_for_fresh_result_with_timing( store: &RocksStore, policy: &Policy, ca: &CaInstanceHandle, pack: &PublicationPointSnapshot, objects: &mut crate::validation::objects::ObjectsOutput, warnings: &[Warning], child_audits: &[ObjectAuditEntry], discovered_children: &[DiscoveredChildCaInstance], validation_time: time::OffsetDateTime, write_publication_point_cache_projection: bool, ) -> Result { let mut timing = PersistVcirTimingBreakdown::default(); if objects.stats.publication_point_dropped { return Ok(timing); } let embedded_store_started = std::time::Instant::now(); persist_vcir_non_repository_evidence(store, ca) .map_err(|e| format!("store VCIR audit evidence failed: {e}"))?; timing.embedded_store_ms = embedded_store_started.elapsed().as_millis() as u64; let build_vcir_started = std::time::Instant::now(); let (vcir, build_vcir_timing) = build_vcir_from_fresh_result_with_timing( ca, pack, objects, warnings, child_audits, discovered_children, validation_time, )?; timing.build_vcir_ms = build_vcir_started.elapsed().as_millis() as u64; timing.build_vcir = build_vcir_timing; let replace_vcir_started = std::time::Instant::now(); let future_not_before_cache_guard = write_publication_point_cache_projection && publication_point_cache_has_future_not_before_risk( pack, objects, child_audits, validation_time, policy, ); timing.publication_point_cache_future_notbefore_guarded = future_not_before_cache_guard; let publication_point_cache_projection = if write_publication_point_cache_projection && !future_not_before_cache_guard { Some(build_publication_point_cache_projection_from_fresh( policy, ca, pack, &vcir, )?) } else { None }; let publication_point_cache_projection_action = if !write_publication_point_cache_projection { PublicationPointCacheProjectionWriteAction::Keep } else if future_not_before_cache_guard { PublicationPointCacheProjectionWriteAction::Delete { manifest_rsync_uri: &vcir.manifest_rsync_uri, } } else { PublicationPointCacheProjectionWriteAction::Write( publication_point_cache_projection .as_ref() .expect("publication point projection must exist when guard is not active"), ) }; let replace_timing = store .replace_vcir_manifest_replay_meta_and_projection_action( &vcir, Some(&RoaCacheProjectionContext { parent_context_digest: parent_context_digest_for_ca(ca), policy_fingerprint: publication_point_cache_policy_fingerprint(policy), object_meta: objects.roa_cache_object_meta.clone(), }), publication_point_cache_projection_action, ) .map_err(|e| format!("store VCIR and manifest replay meta failed: {e}"))?; timing.replace_vcir_ms = replace_vcir_started.elapsed().as_millis() as u64; timing.replace_vcir = replace_timing; Ok(timing) } fn build_publication_point_cache_projection_from_fresh( policy: &Policy, ca: &CaInstanceHandle, pack: &PublicationPointSnapshot, vcir: &ValidatedCaInstanceResult, ) -> Result { let ca_cert_sha256 = sha256_digest_32(&ca.ca_certificate_der); let manifest_sha256 = sha256_digest_32(&pack.manifest_bytes); PublicationPointCacheProjection::from_vcir_with_context( vcir, pack.publication_point_rsync_uri.clone(), ca.ca_certificate_rsync_uri.clone(), ca_cert_sha256, manifest_sha256, ta_context_digest_for_ca(ca), parent_context_digest_for_ca(ca), publication_point_cache_policy_fingerprint(policy), ) .map_err(|e| e.to_string()) } fn publication_point_cache_has_future_not_before_risk( pack: &PublicationPointSnapshot, objects: &crate::validation::objects::ObjectsOutput, child_audits: &[ObjectAuditEntry], validation_time: time::OffsetDateTime, policy: &Policy, ) -> bool { let mut files_by_uri: HashMap<&str, &PackFile> = HashMap::new(); for file in &pack.files { files_by_uri.insert(file.rsync_uri.as_str(), file); } objects .audit .iter() .chain(child_audits.iter()) .any(|entry| { audit_entry_has_certificate_time_error(entry) && files_by_uri .get(entry.rsync_uri.as_str()) .map(|file| { audit_entry_has_future_not_before(entry, file, validation_time, policy) }) .unwrap_or(true) }) } fn audit_entry_has_certificate_time_error(entry: &ObjectAuditEntry) -> bool { entry.result == AuditObjectResult::Error && entry .detail .as_deref() .is_some_and(|detail| detail.contains("certificate not valid at validation_time")) } fn audit_entry_has_future_not_before( entry: &ObjectAuditEntry, file: &PackFile, validation_time: time::OffsetDateTime, policy: &Policy, ) -> bool { match entry.kind { AuditObjectKind::Roa => signed_object_ee_not_before(file, policy, SignedObjectKind::Roa) .map(|not_before| validation_time < not_before) .unwrap_or(true), AuditObjectKind::Aspa => signed_object_ee_not_before(file, policy, SignedObjectKind::Aspa) .map(|not_before| validation_time < not_before) .unwrap_or(true), AuditObjectKind::Certificate | AuditObjectKind::RouterCertificate => file .bytes() .ok() .and_then(|bytes| ResourceCertificate::decode_der(bytes).ok()) .map(|cert| validation_time < cert.tbs.validity_not_before) .unwrap_or(true), _ => true, } } #[derive(Clone, Copy)] enum SignedObjectKind { Roa, Aspa, } fn signed_object_ee_not_before( file: &PackFile, policy: &Policy, kind: SignedObjectKind, ) -> Option { let bytes = file.bytes().ok()?; match kind { SignedObjectKind::Roa => { let object = RoaObject::decode_der_with_strict_options( bytes, policy.strict.cms_der, policy.strict.name, ) .ok()?; Some( object.signed_object.signed_data.certificates[0] .resource_cert .tbs .validity_not_before, ) } SignedObjectKind::Aspa => { let object = AspaObject::decode_der_with_strict_options( bytes, policy.strict.cms_der, policy.strict.name, ) .ok()?; Some( object.signed_object.signed_data.certificates[0] .resource_cert .tbs .validity_not_before, ) } } } fn build_vcir_from_fresh_result_with_timing( ca: &CaInstanceHandle, pack: &PublicationPointSnapshot, objects: &mut crate::validation::objects::ObjectsOutput, warnings: &[Warning], child_audits: &[ObjectAuditEntry], discovered_children: &[DiscoveredChildCaInstance], validation_time: time::OffsetDateTime, ) -> Result<(ValidatedCaInstanceResult, BuildVcirTimingBreakdown), String> { let mut timing = BuildVcirTimingBreakdown::default(); let select_crl_started = std::time::Instant::now(); let current_crl = select_manifest_current_crl_from_snapshot(pack)?; timing.select_crl_ms = select_crl_started.elapsed().as_millis() as u64; let current_ca_decode_started = std::time::Instant::now(); let ca_cert = ResourceCertificate::decode_der(&ca.ca_certificate_der) .map_err(|e| format!("decode current CA certificate failed: {e}"))?; timing.current_ca_decode_ms = current_ca_decode_started.elapsed().as_millis() as u64; let local_outputs_started = std::time::Instant::now(); let local_outputs = take_or_build_vcir_local_outputs(ca, pack, objects)?; timing.local_outputs_ms = local_outputs_started.elapsed().as_millis() as u64; let child_entries_started = std::time::Instant::now(); let child_entries = build_vcir_child_entries(discovered_children, validation_time)?; timing.child_entries_ms = child_entries_started.elapsed().as_millis() as u64; let related_artifacts_started = std::time::Instant::now(); let related_artifacts = build_vcir_related_artifacts( ca, pack, current_crl.file.rsync_uri.as_str(), objects, child_audits, ); timing.related_artifacts_ms = related_artifacts_started.elapsed().as_millis() as u64; let ccr_manifest_projection = build_vcir_ccr_manifest_projection_from_fresh(ca, pack, &child_entries)?; let local_vrp_count = local_outputs .iter() .filter(|output| output.output_type == VcirOutputType::Vrp) .count() as u32; let local_aspa_count = local_outputs .iter() .filter(|output| output.output_type == VcirOutputType::Aspa) .count() as u32; let local_router_key_count = local_outputs .iter() .filter(|output| output.output_type == VcirOutputType::RouterKey) .count() as u32; let accepted_object_count = related_artifacts .iter() .filter(|artifact| artifact.validation_status == VcirArtifactValidationStatus::Accepted) .count() as u32; let rejected_object_count = related_artifacts .iter() .filter(|artifact| artifact.validation_status == VcirArtifactValidationStatus::Rejected) .count() as u32; let ca_ski = hex::encode( ca_cert .tbs .extensions .subject_key_identifier .as_ref() .ok_or_else(|| "current CA certificate missing SubjectKeyIdentifier".to_string())?, ); let issuer_ski = hex::encode( ca_cert .tbs .extensions .authority_key_identifier .as_ref() .or(ca_cert.tbs.extensions.subject_key_identifier.as_ref()) .ok_or_else(|| "current CA certificate missing AuthorityKeyIdentifier".to_string())?, ); let struct_build_started = std::time::Instant::now(); let vcir = ValidatedCaInstanceResult { manifest_rsync_uri: pack.manifest_rsync_uri.clone(), parent_manifest_rsync_uri: ca.parent_manifest_rsync_uri.clone(), tal_id: ca.tal_id.clone(), ca_subject_name: ca_cert.tbs.subject_name.to_string(), ca_ski, issuer_ski, last_successful_validation_time: PackTime::from_utc_offset_datetime(validation_time), current_manifest_rsync_uri: pack.manifest_rsync_uri.clone(), current_crl_rsync_uri: current_crl.file.rsync_uri.clone(), validated_manifest_meta: crate::storage::ValidatedManifestMeta { validated_manifest_number: pack.manifest_number_be.clone(), validated_manifest_this_update: pack.this_update.clone(), validated_manifest_next_update: pack.next_update.clone(), }, ccr_manifest_projection, instance_gate: VcirInstanceGate { manifest_next_update: pack.next_update.clone(), current_crl_next_update: PackTime::from_utc_offset_datetime( current_crl.crl.next_update.utc, ), self_ca_not_after: PackTime::from_utc_offset_datetime(ca_cert.tbs.validity_not_after), instance_effective_until: PackTime::from_utc_offset_datetime( pack.next_update .parse() .map_err(|e| format!("parse snapshot next_update failed: {e}"))? .min(current_crl.crl.next_update.utc) .min(ca_cert.tbs.validity_not_after), ), }, child_entries, local_outputs, related_artifacts, summary: VcirSummary { local_vrp_count, local_aspa_count, local_router_key_count, child_count: discovered_children.len() as u32, accepted_object_count, rejected_object_count, }, audit_summary: VcirAuditSummary { failed_fetch_eligible: true, last_failed_fetch_reason: None, warning_count: (warnings.len() + objects.warnings.len()) as u32, audit_flags: Vec::new(), }, }; vcir.validate_internal().map_err(|e| e.to_string())?; timing.struct_build_ms = struct_build_started.elapsed().as_millis() as u64; Ok((vcir, timing)) } fn take_or_build_vcir_local_outputs( ca: &CaInstanceHandle, pack: &PublicationPointSnapshot, objects: &mut crate::validation::objects::ObjectsOutput, ) -> Result, String> { let mut cached_outputs = std::mem::take(&mut objects.local_outputs_cache); if cached_outputs.is_empty() { return build_vcir_local_outputs(ca, pack, objects); } let covered_roa_uris: HashSet = cached_outputs .iter() .filter(|output| output.source_object_type == VcirSourceObjectType::Roa) .map(|output| output.source_object_uri.clone()) .collect(); let covered_aspa_uris: HashSet = cached_outputs .iter() .filter(|output| output.source_object_type == VcirSourceObjectType::Aspa) .map(|output| output.source_object_uri.clone()) .collect(); cached_outputs.extend(build_vcir_local_outputs_excluding( ca, pack, objects, &covered_roa_uris, &covered_aspa_uris, )?); Ok(cached_outputs) } fn build_vcir_ccr_manifest_projection_from_fresh( ca: &CaInstanceHandle, pack: &PublicationPointSnapshot, child_entries: &[VcirChildEntry], ) -> Result { let manifest = ManifestObject::decode_der(&pack.manifest_bytes) .map_err(|e| format!("decode manifest for VCIR CCR projection failed: {e}"))?; let ee = &manifest.signed_object.signed_data.certificates[0].resource_cert; let manifest_ee_aki = ee .tbs .extensions .authority_key_identifier .clone() .ok_or_else(|| "manifest EE certificate missing AuthorityKeyIdentifier".to_string())?; let manifest_sia_locations_der = match ee .tbs .extensions .subject_info_access .as_ref() .ok_or_else(|| "manifest EE certificate missing Subject Information Access".to_string())? { SubjectInfoAccess::Ee(ee_sia) => ee_sia .access_descriptions .iter() .map(encode_access_description_der_for_vcir_ccr_projection) .collect::, _>>()?, SubjectInfoAccess::Ca(_) => { return Err( "manifest EE certificate Subject Information Access has CA variant".to_string(), ); } }; let mut subordinate_skis = child_entries .iter() .map(|child| { hex::decode(&child.child_ski) .map_err(|e| format!("decode child_ski for VCIR CCR projection failed: {e}")) }) .collect::, _>>()?; subordinate_skis.sort(); subordinate_skis.dedup(); Ok(VcirCcrManifestProjection { manifest_rsync_uri: ca.manifest_rsync_uri.clone(), manifest_sha256: sha2::Sha256::digest(&pack.manifest_bytes).to_vec(), manifest_size: pack.manifest_bytes.len() as u64, manifest_ee_aki, manifest_number_be: pack.manifest_number_be.clone(), manifest_this_update: pack.this_update.clone(), manifest_sia_locations_der, subordinate_skis, }) } struct CurrentCrlRef<'a> { file: &'a PackFile, crl: RpkixCrl, } fn select_manifest_current_crl_from_snapshot( pack: &PublicationPointSnapshot, ) -> Result, String> { let manifest = ManifestObject::decode_der(&pack.manifest_bytes) .map_err(|e| format!("decode snapshot manifest for VCIR failed: {e}"))?; let ee = &manifest.signed_object.signed_data.certificates[0].resource_cert; let crldp_uris = ee .tbs .extensions .crl_distribution_points_uris .as_ref() .ok_or_else(|| "manifest EE certificate missing CRLDistributionPoints".to_string())?; for uri in crldp_uris { if let Some(file) = pack .files .iter() .find(|candidate| candidate.rsync_uri == *uri) { let crl = RpkixCrl::decode_der( file.bytes() .map_err(|e| format!("load current CRL bytes for VCIR failed: {e}"))?, ) .map_err(|e| format!("decode current CRL for VCIR failed: {e}"))?; return Ok(CurrentCrlRef { file, crl }); } } Err(format!( "manifest EE certificate CRLDistributionPoints not found in pack: {}", crldp_uris.join(", ") )) } fn build_vcir_local_outputs( _ca: &CaInstanceHandle, pack: &PublicationPointSnapshot, objects: &crate::validation::objects::ObjectsOutput, ) -> Result, String> { build_vcir_local_outputs_excluding(_ca, pack, objects, &HashSet::new(), &HashSet::new()) } fn build_vcir_local_outputs_excluding( _ca: &CaInstanceHandle, pack: &PublicationPointSnapshot, objects: &crate::validation::objects::ObjectsOutput, covered_roa_uris: &HashSet, covered_aspa_uris: &HashSet, ) -> Result, String> { let accepted_roa_uris: HashSet<&str> = objects .audit .iter() .filter(|entry| entry.kind == AuditObjectKind::Roa && entry.result == AuditObjectResult::Ok) .map(|entry| entry.rsync_uri.as_str()) .collect(); let accepted_aspa_uris: HashSet<&str> = objects .audit .iter() .filter(|entry| { entry.kind == AuditObjectKind::Aspa && entry.result == AuditObjectResult::Ok }) .map(|entry| entry.rsync_uri.as_str()) .collect(); let mut out = Vec::new(); for file in &pack.files { let source_object_hash = sha256_hex_from_32(&file.sha256); if accepted_roa_uris.contains(file.rsync_uri.as_str()) && !covered_roa_uris.contains(file.rsync_uri.as_str()) { let roa = RoaObject::decode_der( file.bytes() .map_err(|e| format!("load accepted ROA bytes for VCIR failed: {e}"))?, ) .map_err(|e| format!("decode accepted ROA for VCIR failed: {e}"))?; let ee = &roa.signed_object.signed_data.certificates[0]; let source_ee_cert_hash = sha256_hex(ee.raw_der.as_slice()); let item_effective_until = PackTime::from_utc_offset_datetime(ee.resource_cert.tbs.validity_not_after); for vrp in roa_to_vrps_for_vcir(&roa) { let prefix = vrp_prefix_to_string(&vrp); let rule_hash = sha256_hex( format!( "roa-rule:{}:{}:{}:{}", source_object_hash, vrp.asn, prefix, vrp.max_length ) .as_bytes(), ); out.push(VcirLocalOutput { output_type: VcirOutputType::Vrp, item_effective_until: item_effective_until.clone(), source_object_uri: file.rsync_uri.clone(), source_object_type: VcirSourceObjectType::Roa, source_object_hash: file.sha256, source_ee_cert_hash: sha256_hex_to_32(&source_ee_cert_hash), payload: VcirLocalOutputPayload::Vrp { asn: vrp.asn, afi: vrp.prefix.afi, prefix_len: vrp.prefix.prefix_len, addr: vrp.prefix.addr, max_length: vrp.max_length, }, rule_hash: sha256_hex_to_32(&rule_hash), }); } } else if accepted_aspa_uris.contains(file.rsync_uri.as_str()) && !covered_aspa_uris.contains(file.rsync_uri.as_str()) { let aspa = AspaObject::decode_der( file.bytes() .map_err(|e| format!("load accepted ASPA bytes for VCIR failed: {e}"))?, ) .map_err(|e| format!("decode accepted ASPA for VCIR failed: {e}"))?; let ee = &aspa.signed_object.signed_data.certificates[0]; let source_ee_cert_hash = sha256_hex(ee.raw_der.as_slice()); let item_effective_until = PackTime::from_utc_offset_datetime(ee.resource_cert.tbs.validity_not_after); let providers = aspa .aspa .provider_as_ids .iter() .map(u32::to_string) .collect::>() .join(","); let rule_hash = sha256_hex( format!( "aspa-rule:{}:{}:{}", source_object_hash, aspa.aspa.customer_as_id, providers ) .as_bytes(), ); out.push(VcirLocalOutput { output_type: VcirOutputType::Aspa, item_effective_until, source_object_uri: file.rsync_uri.clone(), source_object_type: VcirSourceObjectType::Aspa, source_object_hash: file.sha256, source_ee_cert_hash: sha256_hex_to_32(&source_ee_cert_hash), payload: VcirLocalOutputPayload::Aspa { customer_as_id: aspa.aspa.customer_as_id, provider_as_ids: aspa.aspa.provider_as_ids.clone(), }, rule_hash: sha256_hex_to_32(&rule_hash), }); } } Ok(out) } pub(crate) fn build_router_key_local_outputs( _ca: &CaInstanceHandle, router_keys: &[RouterKeyPayload], ) -> Vec { router_keys .iter() .map(|router_key| { let ski_hex = hex::encode(&router_key.ski); let spki_der_base64 = base64::engine::general_purpose::STANDARD.encode(&router_key.spki_der); let rule_hash = sha256_hex( format!( "router-key-rule:{}:{}:{}:{}", router_key.source_object_hash, router_key.as_id, ski_hex, spki_der_base64 ) .as_bytes(), ); VcirLocalOutput { output_type: VcirOutputType::RouterKey, item_effective_until: router_key.item_effective_until.clone(), source_object_uri: router_key.source_object_uri.clone(), source_object_type: VcirSourceObjectType::RouterKey, source_object_hash: sha256_hex_to_32(&router_key.source_object_hash), source_ee_cert_hash: sha256_hex_to_32(&router_key.source_ee_cert_hash), payload: VcirLocalOutputPayload::RouterKey { as_id: router_key.as_id, ski: router_key.ski.clone(), spki_der: router_key.spki_der.clone(), }, rule_hash: sha256_hex_to_32(&rule_hash), } }) .collect() } fn build_vcir_child_entries( discovered_children: &[DiscoveredChildCaInstance], validation_time: time::OffsetDateTime, ) -> Result, String> { let mut out = Vec::with_capacity(discovered_children.len()); for child in discovered_children { let child_cert = ResourceCertificate::decode_der(&child.handle.ca_certificate_der) .map_err(|e| format!("decode child certificate for VCIR failed: {e}"))?; 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), }); } Ok(out) } fn persist_vcir_non_repository_evidence( store: &RocksStore, ca: &CaInstanceHandle, ) -> Result<(), String> { let current_ca_hash = sha256_hex(&ca.ca_certificate_der); let mut current_ca_entry = RawByHashEntry::from_bytes(current_ca_hash, ca.ca_certificate_der.clone()); if let Some(uri) = ca.ca_certificate_rsync_uri.as_ref() { current_ca_entry.origin_uris.push(uri.clone()); } current_ca_entry.object_type = Some("cer".to_string()); current_ca_entry.encoding = Some("der".to_string()); upsert_raw_by_hash_entry(store, current_ca_entry)?; Ok(()) } fn upsert_raw_by_hash_entry(store: &RocksStore, entry: RawByHashEntry) -> Result<(), String> { match store.get_raw_by_hash_entry(&entry.sha256_hex) { Ok(Some(existing)) => { if existing.bytes != entry.bytes { return Err(format!( "raw_by_hash collision for sha256 {} while storing VCIR audit evidence", entry.sha256_hex )); } let mut merged = existing; let mut changed = false; for uri in entry.origin_uris { if !merged .origin_uris .iter() .any(|existing_uri| existing_uri == &uri) { merged.origin_uris.push(uri); changed = true; } } if merged.object_type.is_none() && entry.object_type.is_some() { merged.object_type = entry.object_type; changed = true; } if merged.encoding.is_none() && entry.encoding.is_some() { merged.encoding = entry.encoding; changed = true; } if changed { store .put_raw_by_hash_entry(&merged) .map_err(|e| format!("update raw_by_hash entry failed: {e}"))?; } Ok(()) } Ok(None) => store .put_raw_by_hash_entry(&entry) .map_err(|e| format!("store raw_by_hash entry failed: {e}")), Err(e) => Err(format!("load raw_by_hash entry failed: {e}")), } } fn build_vcir_related_artifacts( ca: &CaInstanceHandle, pack: &PublicationPointSnapshot, current_crl_rsync_uri: &str, objects: &crate::validation::objects::ObjectsOutput, child_audits: &[ObjectAuditEntry], ) -> Vec { let mut audit_by_uri: HashMap<&str, AuditObjectResult> = HashMap::new(); for entry in child_audits.iter().chain(objects.audit.iter()) { audit_by_uri.insert(entry.rsync_uri.as_str(), entry.result.clone()); } let mut artifacts = Vec::with_capacity(pack.files.len() + 2); artifacts.push(VcirRelatedArtifact { artifact_role: VcirArtifactRole::Manifest, artifact_kind: VcirArtifactKind::Mft, uri: Some(pack.manifest_rsync_uri.clone()), sha256: sha256_hex(&pack.manifest_bytes), object_type: Some("mft".to_string()), validation_status: VcirArtifactValidationStatus::Accepted, }); artifacts.push(VcirRelatedArtifact { artifact_role: if ca.parent_manifest_rsync_uri.is_none() { VcirArtifactRole::TrustAnchorCert } else { VcirArtifactRole::IssuerCert }, artifact_kind: VcirArtifactKind::Cer, uri: ca.ca_certificate_rsync_uri.clone(), sha256: sha256_hex(&ca.ca_certificate_der), object_type: Some("cer".to_string()), validation_status: VcirArtifactValidationStatus::Accepted, }); for file in &pack.files { let result = audit_by_uri .get(file.rsync_uri.as_str()) .cloned() .unwrap_or(AuditObjectResult::Ok); let (artifact_role, artifact_kind) = artifact_role_and_kind(file, current_crl_rsync_uri); artifacts.push(VcirRelatedArtifact { artifact_role, artifact_kind, uri: Some(file.rsync_uri.clone()), sha256: sha256_hex_from_32(&file.sha256), object_type: object_type_from_uri(file.rsync_uri.as_str()), validation_status: audit_result_to_vcir_status(&result), }); } artifacts } fn artifact_role_and_kind( file: &PackFile, current_crl_rsync_uri: &str, ) -> (VcirArtifactRole, VcirArtifactKind) { if file.rsync_uri == current_crl_rsync_uri { (VcirArtifactRole::CurrentCrl, VcirArtifactKind::Crl) } else if file.rsync_uri.ends_with(".cer") { (VcirArtifactRole::ChildCaCert, VcirArtifactKind::Cer) } else if file.rsync_uri.ends_with(".roa") { (VcirArtifactRole::SignedObject, VcirArtifactKind::Roa) } else if file.rsync_uri.ends_with(".asa") { (VcirArtifactRole::SignedObject, VcirArtifactKind::Aspa) } else if file.rsync_uri.ends_with(".gbr") { (VcirArtifactRole::SignedObject, VcirArtifactKind::Gbr) } else if file.rsync_uri.ends_with(".crl") { (VcirArtifactRole::Other, VcirArtifactKind::Crl) } else if file.rsync_uri.ends_with(".mft") { (VcirArtifactRole::Manifest, VcirArtifactKind::Mft) } else { (VcirArtifactRole::Other, VcirArtifactKind::Other) } } fn object_type_from_uri(uri: &str) -> Option { uri.rsplit_once('.') .map(|(_, ext)| ext.to_ascii_lowercase()) } fn audit_result_to_vcir_status(result: &AuditObjectResult) -> VcirArtifactValidationStatus { match result { AuditObjectResult::Ok => VcirArtifactValidationStatus::Accepted, AuditObjectResult::Error => VcirArtifactValidationStatus::Rejected, AuditObjectResult::Skipped => VcirArtifactValidationStatus::WarningOnly, } } fn roa_to_vrps_for_vcir(roa: &RoaObject) -> Vec { let asn = roa.roa.as_id; let mut out = Vec::new(); for fam in &roa.roa.ip_addr_blocks { for entry in &fam.addresses { let max_length = entry.max_length.unwrap_or(entry.prefix.prefix_len); out.push(Vrp { asn, prefix: entry.prefix.clone(), max_length, }); } } out } fn vrp_prefix_to_string(vrp: &Vrp) -> String { match vrp.prefix.afi { RoaAfi::Ipv4 => { let addr = std::net::Ipv4Addr::new( vrp.prefix.addr[0], vrp.prefix.addr[1], vrp.prefix.addr[2], vrp.prefix.addr[3], ); format!("{addr}/{}", vrp.prefix.prefix_len) } RoaAfi::Ipv6 => { let addr = std::net::Ipv6Addr::from(vrp.prefix.addr); format!("{addr}/{}", vrp.prefix.prefix_len) } } } #[cfg(test)] #[path = "tree_runner/tests.rs"] mod tests;