use crate::model::rc::{AsResourceSet, IpResourceSet}; use crate::output::audit::{DiscoveredFrom, PublicationPointAudit}; use crate::output::report::Warning; use crate::validation::manifest::PublicationPointSource; use crate::validation::objects::{AspaAttestation, ObjectsOutput, RouterKeyPayload, Vrp}; use crate::validation::publication_point::PublicationPointSnapshot; use std::borrow::Cow; use std::sync::{Arc, OnceLock}; pub const DEFAULT_MAX_CA_DEPTH: usize = 32; #[derive(Clone, Debug, PartialEq, Eq)] pub struct TreeRunConfig { /// Max CA instance depth to process (0 = root only). pub max_depth: Option, /// Max number of CA instances to process. pub max_instances: Option, /// Drop per-object audit payload when the caller only needs validation outputs. pub compact_audit: bool, /// Build online CCR manifest projections during phase2 validation. pub build_ccr_accumulator: bool, } impl Default for TreeRunConfig { fn default() -> Self { Self { max_depth: Some(DEFAULT_MAX_CA_DEPTH), max_instances: None, compact_audit: false, build_ccr_accumulator: true, } } } pub(crate) fn ca_depth_is_allowed(config: &TreeRunConfig, depth: usize) -> bool { config.max_depth.is_none_or(|max_depth| depth <= max_depth) } pub(crate) fn next_allowed_ca_depth(config: &TreeRunConfig, parent_depth: usize) -> Option { let child_depth = parent_depth.checked_add(1)?; ca_depth_is_allowed(config, child_depth).then_some(child_depth) } #[derive(Clone, Debug)] pub enum CaCertificateRef { InlineDer(Vec), RepoBytes { sha256_hex: String, cached_der: Arc>>, }, } impl PartialEq for CaCertificateRef { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::InlineDer(left), Self::InlineDer(right)) => left == right, ( Self::RepoBytes { sha256_hex: left, .. }, Self::RepoBytes { sha256_hex: right, .. }, ) => left == right, _ => false, } } } impl Eq for CaCertificateRef {} impl CaCertificateRef { pub fn inline_der(bytes: Vec) -> Self { Self::InlineDer(bytes) } pub fn repo_bytes(sha256_hex: String) -> Self { Self::RepoBytes { sha256_hex, cached_der: Arc::new(OnceLock::new()), } } pub fn sha256_hex(&self) -> Option<&str> { match self { Self::InlineDer(_) => None, Self::RepoBytes { sha256_hex, .. } => Some(sha256_hex.as_str()), } } pub fn der<'a>( &'a self, store: &crate::repository::storage::RocksStore, ) -> Result, String> { match self { Self::InlineDer(bytes) => Ok(Cow::Borrowed(bytes.as_slice())), Self::RepoBytes { sha256_hex, cached_der, } => { if cached_der.get().is_none() { let bytes = store .get_blob_bytes(sha256_hex) .map_err(|e| format!("load CA certificate bytes failed: {e}"))? .ok_or_else(|| { format!("missing CA certificate repo bytes for sha256={sha256_hex}") })?; let _ = cached_der.set(Arc::from(bytes)); } let bytes = cached_der.get().ok_or_else(|| { format!("missing cached CA certificate bytes for sha256={sha256_hex}") })?; Ok(Cow::Borrowed(bytes.as_ref())) } } } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct CaInstanceHandle { pub depth: usize, pub tal_id: String, pub parent_manifest_rsync_uri: Option, /// CA certificate bytes or a lazy repo-bytes reference for this CA instance. pub ca_certificate: CaCertificateRef, /// rsync URI of this CA certificate object (where it is published). /// /// This is used for strict AIA binding checks (RFC 6487 ยง4.8.7) when validating /// subordinate CA and EE certificates. Trust anchor certificates downloaded from /// TAL URIs may not have an rsync publication URI, so this can be `None`. pub ca_certificate_rsync_uri: Option, /// Effective (fully resolved) resources for this CA instance, used for resource-path /// validation of subordinate CA certificates. pub effective_ip_resources: Option, pub effective_as_resources: Option, pub rsync_base_uri: String, pub manifest_rsync_uri: String, pub publication_point_rsync_uri: String, pub rrdp_notification_uri: Option, } impl CaInstanceHandle { pub fn with_depth(mut self, depth: usize) -> Self { self.depth = depth; self } pub fn ca_certificate_der<'a>( &'a self, store: &crate::repository::storage::RocksStore, ) -> Result, String> { self.ca_certificate.der(store) } pub fn ca_certificate_sha256_hex(&self) -> Option<&str> { self.ca_certificate.sha256_hex() } pub fn ca_certificate_sha256_32(&self) -> Option<[u8; 32]> { match &self.ca_certificate { CaCertificateRef::InlineDer(bytes) => { use sha2::Digest as _; let digest = sha2::Sha256::digest(bytes); Some(digest.into()) } CaCertificateRef::RepoBytes { sha256_hex, .. } => { let mut out = [0u8; 32]; hex::decode_to_slice(sha256_hex, &mut out).ok()?; Some(out) } } } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct PublicationPointRunResult { pub source: PublicationPointSource, pub snapshot: Option, pub warnings: Vec, pub objects: ObjectsOutput, pub audit: PublicationPointAudit, /// Candidate child CA instances to enqueue after this publication point completes. /// /// - For `Fresh`, these are discovered from the current validated publication point. /// - For failed synchronization, the result contains no child instances; /// callers may retry the repository on a later run. pub discovered_children: Vec, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct DiscoveredChildEntryProjection { pub child_ski: String, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct DiscoveredChildCaInstance { pub handle: CaInstanceHandle, pub discovered_from: DiscoveredFrom, pub child_entry_projection: Option, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct TreeRunOutput { pub instances_processed: usize, pub instances_failed: usize, pub warnings: Vec, pub vrps: Vec, pub aspas: Vec, pub router_keys: Vec, } #[derive(Debug, thiserror::Error)] pub enum TreeRunError { #[error("publication point runner failed: {0}")] Runner(String), } pub trait PublicationPointRunner { fn run_publication_point( &self, ca: &CaInstanceHandle, ) -> Result; } #[derive(Clone, Debug, PartialEq, Eq)] pub struct TreeRunAuditOutput { pub tree: TreeRunOutput, pub publication_points: Vec, } pub fn run_tree_serial( root: CaInstanceHandle, runner: &dyn PublicationPointRunner, config: &TreeRunConfig, ) -> Result { Ok(run_tree_serial_audit(root, runner, config)?.tree) } pub fn run_tree_serial_audit( root: CaInstanceHandle, runner: &dyn PublicationPointRunner, config: &TreeRunConfig, ) -> Result { run_tree_serial_audit_multi_root(vec![root], runner, config) } pub fn run_tree_serial_audit_multi_root( roots: Vec, runner: &dyn PublicationPointRunner, config: &TreeRunConfig, ) -> Result { #[derive(Clone, Debug)] struct QueuedCaInstance { id: u64, handle: CaInstanceHandle, parent_id: Option, discovered_from: Option, } let mut next_id: u64 = 0; let mut queue: std::collections::VecDeque = std::collections::VecDeque::new(); for root in roots { queue.push_back(QueuedCaInstance { id: next_id, handle: root, parent_id: None, discovered_from: None, }); next_id += 1; } let mut visited_manifest_uris: std::collections::HashSet = std::collections::HashSet::new(); let mut instances_processed = 0usize; let mut instances_failed = 0usize; let mut warnings: Vec = Vec::new(); let mut vrps: Vec = Vec::new(); let mut aspas: Vec = Vec::new(); let mut router_keys: Vec = Vec::new(); let mut publication_points: Vec = Vec::new(); while let Some(node) = queue.pop_front() { let ca = &node.handle; if !visited_manifest_uris.insert(ca.manifest_rsync_uri.clone()) { continue; } if let Some(max_instances) = config.max_instances && instances_processed >= max_instances { break; } if !ca_depth_is_allowed(config, ca.depth) { continue; } let res = match runner.run_publication_point(ca) { Ok(v) => v, Err(e) => { instances_failed += 1; warnings.push( Warning::new(format!("publication point failed: {e}")) .with_context(&ca.manifest_rsync_uri), ); continue; } }; instances_processed += 1; crate::logging::observer::update(instances_processed as u64, || { serde_json::json!({ "publication_points_completed": instances_processed, "publication_points_failed": instances_failed, }) }); warnings.extend(res.warnings); warnings.extend(res.objects.warnings); vrps.extend(res.objects.vrps); aspas.extend(res.objects.aspas); router_keys.extend(res.objects.router_keys); let mut audit = res.audit; audit.node_id = Some(node.id); audit.parent_node_id = node.parent_id; audit.discovered_from = node.discovered_from.clone(); if config.compact_audit { audit.objects.clear(); audit.warnings.clear(); } publication_points.push(audit); let Some(child_depth) = next_allowed_ca_depth(config, ca.depth) else { continue; }; let mut children = res.discovered_children; children.sort_by(|a, b| { a.handle .manifest_rsync_uri .cmp(&b.handle.manifest_rsync_uri) .then_with(|| { a.discovered_from .child_ca_certificate_rsync_uri .cmp(&b.discovered_from.child_ca_certificate_rsync_uri) }) }); for child in children { queue.push_back(QueuedCaInstance { id: next_id, handle: child.handle.with_depth(child_depth), parent_id: Some(node.id), discovered_from: Some(child.discovered_from), }); next_id += 1; } } Ok(TreeRunAuditOutput { tree: TreeRunOutput { instances_processed, instances_failed, warnings, vrps, aspas, router_keys, }, publication_points, }) }