4981 lines
202 KiB
Rust
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;
|