rpki/src/validation/tree_runner.rs

4981 lines
202 KiB
Rust

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<DiscoveredChildCaInstance>,
pub(crate) child_audits: Vec<ObjectAuditEntry>,
pub(crate) discovered_router_keys: Vec<RouterKeyPayload>,
pub(crate) child_discovery_ms: u64,
pub(crate) warnings: Vec<Warning>,
}
#[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<TimingHandle>,
pub download_log: Option<DownloadLogHandle>,
pub replay_archive_index: Option<Arc<ReplayArchiveIndex>>,
pub replay_delta_index: Option<Arc<ReplayDeltaArchiveIndex>>,
/// 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<HashMap<String, bool>>, // 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<HashMap<String, bool>>, // rsync_base_uri -> rsync_ok
pub current_repo_index: Option<CurrentRepoIndexHandle>,
pub repo_sync_runtime: Option<Arc<dyn RepoSyncRuntime>>,
pub parallel_phase2_config: Option<ParallelPhase2Config>,
pub parallel_roa_worker_pool: Option<ParallelRoaWorkerPool>,
pub ccr_accumulator: Option<Mutex<CcrAccumulator>>,
/// 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<RoaValidationCacheView> {
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<PublicationPointRunResult> {
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<PublicationPointCacheIdentity, String> {
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<PublicationPointRunResult, String> {
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<CcrAccumulator> {
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<String> {
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<ObjectAuditEntry> {
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<ObjectAuditEntry> {
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<FreshPublicationPointStage, FreshPublicationPointStageError> {
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<Warning>,
mut objects: crate::validation::objects::ObjectsOutput,
child_audits: Vec<ObjectAuditEntry>,
discovered_children: Vec<DiscoveredChildCaInstance>,
repo_sync_source: Option<&str>,
repo_sync_phase: Option<&str>,
repo_sync_duration_ms: u64,
repo_sync_err: Option<&str>,
) -> Result<FreshPublicationPointFinalizeOutput, String> {
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<PublicationPointRunResult, String> {
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<Warning> = 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<String>,
Option<String>,
Option<String>,
) = 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<DiscoveredChildCaInstance>,
audits: Vec<ObjectAuditEntry>,
router_keys: Vec<RouterKeyPayload>,
}
#[derive(Clone, Debug)]
struct VerifiedIssuerCrl {
crl: crate::data_model::crl::RpkixCrl,
revoked_serials: std::collections::HashSet<Vec<u8>>,
}
#[derive(Clone, Debug)]
enum CachedIssuerCrl {
Pending(Vec<u8>),
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>)]) -> [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<T: serde::Serialize + std::fmt::Debug>(value: &T) -> Vec<u8> {
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<ValidatedCaInstanceResult>,
ccr_manifest_projection: Option<VcirCcrManifestProjection>,
snapshot: Option<PublicationPointSnapshot>,
objects: crate::validation::objects::ObjectsOutput,
child_audits: Vec<ObjectAuditEntry>,
discovered_children: Vec<DiscoveredChildCaInstance>,
warnings: Vec<Warning>,
}
fn discover_children_from_fresh_snapshot_with_audit<P: PublicationPointData>(
issuer: &CaInstanceHandle,
publication_point: &P,
validation_time: time::OffsetDateTime,
timing: Option<&TimingHandle>,
) -> Result<ChildDiscoveryOutput, String> {
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<String>;
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<String>;
let issuer_spki: Option<SubjectPublicKeyInfo<'_>> = 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<String, CachedIssuerCrl> = 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::<Result<_, String>>()?;
let mut out: Vec<DiscoveredChildCaInstance> = Vec::new();
let mut audits: Vec<ObjectAuditEntry> = Vec::new();
let mut router_keys: Vec<RouterKeyPayload> = 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<String, CachedIssuerCrl>,
) -> 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::<Vec<_>>()
.join(", ")
))
}
fn ensure_issuer_crl_verified<'a>(
crl_rsync_uri: &str,
crl_cache: &'a mut std::collections::HashMap<String, CachedIssuerCrl>,
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<Vec<u8>> =
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<String, CachedIssuerCrl>,
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<ValidatedSubordinateCaLite, CaPathError> {
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::<Vec<_>>()
.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<u64>,
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<u64>,
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<String, ObjectAuditEntry> = 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<ObjectAuditEntry> = 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<u64>,
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<String, ObjectAuditEntry> = 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<String> = vcir
.related_artifacts
.iter()
.filter_map(|artifact| artifact.uri.clone())
.collect();
ordered_uris.sort();
ordered_uris.dedup();
let mut objects_out: Vec<ObjectAuditEntry> = 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<u64>,
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<String, ObjectAuditEntry> = 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<String> = projection
.related_objects
.iter()
.filter_map(|artifact| artifact.uri.clone())
.collect();
ordered_uris.sort();
ordered_uris.dedup();
let mut objects_out: Vec<ObjectAuditEntry> = 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, String> {
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<VcirCcrManifestProjection, String> {
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<VcirReuseProjection, String> {
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<Warning>,
) -> Option<PublicationPointSnapshot> {
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<Warning>,
) -> crate::validation::objects::ObjectsOutput {
let mut output = empty_objects_output();
let mut audit_by_uri: HashMap<String, ObjectAuditEntry> = HashMap::new();
let mut roa_total: HashSet<String> = HashSet::new();
let mut aspa_total: HashSet<String> = HashSet::new();
let mut roa_ok: HashSet<String> = HashSet::new();
let mut aspa_ok: HashSet<String> = 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<Warning>,
) -> crate::validation::objects::ObjectsOutput {
let mut output = empty_objects_output();
let mut audit_by_uri: HashMap<String, ObjectAuditEntry> = HashMap::new();
let mut roa_total: HashSet<String> = HashSet::new();
let mut aspa_total: HashSet<String> = HashSet::new();
let mut roa_ok: HashSet<String> = HashSet::new();
let mut aspa_ok: HashSet<String> = 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<Vrp, String> {
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<AspaAttestation, String> {
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<RouterKeyPayload, String> {
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<Vrp, String> {
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<AspaAttestation, String> {
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<RouterKeyPayload, String> {
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<Warning>,
) -> (Vec<DiscoveredChildCaInstance>, Vec<ObjectAuditEntry>) {
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<Warning>,
worker_count: usize,
timing: Option<&TimingHandle>,
) -> (Vec<DiscoveredChildCaInstance>, Vec<ObjectAuditEntry>) {
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<PublicationPointCacheChildRestoreOutcome> {
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<PublicationPointCacheChildRestoreOutcome> {
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::<Vec<PublicationPointCacheChildRestoreOutcome>>()
}
fn collect_publication_point_cache_child_restore_outcomes(
outcomes: Vec<PublicationPointCacheChildRestoreOutcome>,
warnings: &mut Vec<Warning>,
) -> (Vec<DiscoveredChildCaInstance>, Vec<ObjectAuditEntry>) {
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<DiscoveredChildCaInstance>,
audit: Option<ObjectAuditEntry>,
warning: Option<Warning>,
}
fn publication_point_cache_discovered_child(
ca: &CaInstanceHandle,
child: &PublicationPointCacheChild,
bytes: Vec<u8>,
) -> 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<String>,
) -> 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<PersistVcirTimingBreakdown, String> {
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<PublicationPointCacheProjection, String> {
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<time::OffsetDateTime> {
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<Vec<VcirLocalOutput>, 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<String> = cached_outputs
.iter()
.filter(|output| output.source_object_type == VcirSourceObjectType::Roa)
.map(|output| output.source_object_uri.clone())
.collect();
let covered_aspa_uris: HashSet<String> = 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<VcirCcrManifestProjection, String> {
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::<Result<Vec<_>, _>>()?,
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::<Result<Vec<_>, _>>()?;
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<CurrentCrlRef<'_>, 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<Vec<VcirLocalOutput>, 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<String>,
covered_aspa_uris: &HashSet<String>,
) -> Result<Vec<VcirLocalOutput>, 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::<Vec<_>>()
.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<VcirLocalOutput> {
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<Vec<VcirChildEntry>, 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<VcirRelatedArtifact> {
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<String> {
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<Vrp> {
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;