From 9e339e63e72612316f8211e7b7a97823b3e7ecc2 Mon Sep 17 00:00:00 2001 From: yuyr Date: Fri, 26 Jun 2026 07:19:11 +0800 Subject: [PATCH] =?UTF-8?q?20260626=20=E4=BC=98=E5=8C=96PP=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E7=B4=A2=E5=BC=95=E4=B8=8E=E8=AF=81=E4=B9=A6=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E9=95=BF=E5=B0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboards/ours-rp-soak-overview.json | 65 + scripts/soak/portable-soak.env.example | 4 + scripts/soak/run_soak.sh | 4 + src/cli.rs | 23 + src/cli/tests.rs | 21 + src/parallel/repo_runtime.rs | 4 +- src/storage.rs | 718 ++++++++- src/storage/config.rs | 4 + src/storage/keys.rs | 4 + src/storage/pp_cache_index.rs | 211 ++- src/storage/tests.rs | 343 +++++ src/validation/manifest.rs | 3 + src/validation/run.rs | 5 +- src/validation/run_tree_from_tal.rs | 29 +- src/validation/tree.rs | 111 +- src/validation/tree_parallel.rs | 9 +- src/validation/tree_runner.rs | 1302 ++++++++++++++--- src/validation/tree_runner/tests.rs | 577 +++++++- tests/test_apnic_stats_live_stage2.rs | 2 + tests/test_apnic_tree_live_m15.rs | 2 + tests/test_deterministic_semantics_m4.rs | 14 +- tests/test_run_tree_from_tal_offline_m17.rs | 18 +- tests/test_tree_failure_handling.rs | 6 +- tests/test_tree_traversal_m14.rs | 8 +- 24 files changed, 3158 insertions(+), 329 deletions(-) diff --git a/monitor/grafana/dashboards/ours-rp-soak-overview.json b/monitor/grafana/dashboards/ours-rp-soak-overview.json index 0762bc0..3316330 100644 --- a/monitor/grafana/dashboards/ours-rp-soak-overview.json +++ b/monitor/grafana/dashboards/ours-rp-soak-overview.json @@ -785,6 +785,71 @@ ], "title": "State DB File Count Over Time", "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "fieldConfig": { + "defaults": { + "unit": "none", + "decimals": 0, + "min": 0 + }, + "overrides": [] + }, + "gridPos": { + "x": 0, + "y": 48, + "w": 24, + "h": 8 + }, + "id": 18, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "expr": "sum by (job, instance, exported_instance) (ours_rp_repo_terminal_state_count{terminal_state=\"fresh\"})", + "legendFormat": "fresh pp", + "refId": "A" + }, + { + "expr": "sum by (job, instance, exported_instance) (ours_rp_cir_objects_by_source_type{exported_source=\"fresh\",object_type=\"roa\"})", + "legendFormat": "fresh roa", + "refId": "B" + }, + { + "expr": "sum by (job, instance, exported_instance) (ours_rp_cir_objects_by_source_type{exported_source=\"fresh\",object_type=\"manifest\"})", + "legendFormat": "fresh mft", + "refId": "C" + }, + { + "expr": "sum by (job, instance, exported_instance) (ours_rp_cir_objects_by_source_type{exported_source=\"fresh\",object_type=\"certificate\"})", + "legendFormat": "fresh crt", + "refId": "D" + }, + { + "expr": "sum by (job, instance, exported_instance) (ours_rp_cir_objects_by_source_type{exported_source=\"fresh\",object_type=\"crl\"})", + "legendFormat": "fresh crl", + "refId": "E" + } + ], + "title": "Fresh PP / Object Counts by Run", + "type": "timeseries" } ], "refresh": "5s", diff --git a/scripts/soak/portable-soak.env.example b/scripts/soak/portable-soak.env.example index d8f7be9..44eb188 100644 --- a/scripts/soak/portable-soak.env.example +++ b/scripts/soak/portable-soak.env.example @@ -71,6 +71,10 @@ RPKI_PROGRESS_PP_CONTROL_SLOW_MS=100 # 是否在运行前尝试禁用 rpki-client timer 并杀掉竞争 RP 进程。 DISABLE_COMPETING_RPS=1 +# 是否启用实验性 child CRT 验证缓存。独立于 ROA cache / PP cache;默认关闭。 +# 开启后会额外传入 --enable-child-certificate-validation-cache。 +ENABLE_CHILD_CERTIFICATE_VALIDATION_CACHE=0 + # 传给 rpki 子进程的额外参数。多个参数用空格分隔。 # 示例:RPKI_EXTRA_ARGS="--enable-roa-validation-cache" # 实验性 transport 预热:RPKI_EXTRA_ARGS="--enable-transport-request-prefetch --enable-roa-validation-cache" diff --git a/scripts/soak/run_soak.sh b/scripts/soak/run_soak.sh index f81e557..bb70a70 100755 --- a/scripts/soak/run_soak.sh +++ b/scripts/soak/run_soak.sh @@ -30,6 +30,7 @@ RPKI_PROGRESS_PP_CONTROL_SLOW_MS="${RPKI_PROGRESS_PP_CONTROL_SLOW_MS:-100}" RPKI_PROGRESS_PP_CACHE_SLOW_MS="${RPKI_PROGRESS_PP_CACHE_SLOW_MS:-50}" RPKI_PROGRESS_CONTROL_LOOP_SLOW_MS="${RPKI_PROGRESS_CONTROL_LOOP_SLOW_MS:-1000}" DISABLE_COMPETING_RPS="${DISABLE_COMPETING_RPS:-1}" +ENABLE_CHILD_CERTIFICATE_VALIDATION_CACHE="${ENABLE_CHILD_CERTIFICATE_VALIDATION_CACHE:-0}" RPKI_EXTRA_ARGS="${RPKI_EXTRA_ARGS:-}" RPKI_ANALYZE="${RPKI_ANALYZE:-0}" @@ -571,6 +572,9 @@ build_child_args() { --vaps-csv-out "{run_out}/vaps.csv" --compare-view-trust-anchor "$(compare_view_trust_anchor)" ) + if is_true "$ENABLE_CHILD_CERTIFICATE_VALIDATION_CACHE"; then + CHILD_ARGS+=(--enable-child-certificate-validation-cache) + fi if [[ -n "$RPKI_EXTRA_ARGS" ]]; then # shellcheck disable=SC2206 local extra_args=( $RPKI_EXTRA_ARGS ) diff --git a/src/cli.rs b/src/cli.rs index 5858f94..e1685f0 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -52,6 +52,7 @@ use std::sync::Arc; struct RunStageTiming { validation_ms: u64, enable_roa_validation_cache: bool, + enable_child_certificate_validation_cache: bool, publication_point_cache_observe_only: bool, enable_publication_point_validation_cache: bool, enable_transport_request_prefetch: bool, @@ -78,6 +79,7 @@ struct RunStageTiming { analysis_phases: HashMap, analysis_top_publication_points: Vec, analysis_top_publication_point_steps: Vec, + analysis_top_publication_point_cache_steps: Vec, vcir_storage_summary_ms: Option, vcir_storage: Option, publication_point_cache_index_load: Option, @@ -133,6 +135,7 @@ pub struct CliArgs { pub skip_report_build: bool, pub skip_vcir_persist: bool, pub enable_roa_validation_cache: bool, + pub enable_child_certificate_validation_cache: bool, pub publication_point_cache_observe_only: bool, pub enable_publication_point_validation_cache: bool, pub enable_transport_request_prefetch: bool, @@ -192,6 +195,8 @@ Options: --skip-vcir-persist Skip VCIR persistence/projection building for compare-only runs --enable-roa-validation-cache Reuse accepted ROA validation outputs from previous VCIR records (default: off) + --enable-child-certificate-validation-cache + Experimental: reuse validated child certificate discovery results --publication-point-cache-observe-only Evaluate publication-point cache eligibility without changing results --enable-publication-point-validation-cache @@ -278,6 +283,7 @@ pub fn parse_args(argv: &[String]) -> Result { let mut skip_report_build: bool = false; let mut skip_vcir_persist: bool = false; let mut enable_roa_validation_cache: bool = false; + let mut enable_child_certificate_validation_cache: bool = false; let mut publication_point_cache_observe_only: bool = false; let mut enable_publication_point_validation_cache: bool = false; let mut enable_transport_request_prefetch: bool = false; @@ -486,6 +492,9 @@ pub fn parse_args(argv: &[String]) -> Result { "--enable-roa-validation-cache" => { enable_roa_validation_cache = true; } + "--enable-child-certificate-validation-cache" => { + enable_child_certificate_validation_cache = true; + } "--publication-point-cache-observe-only" => { publication_point_cache_observe_only = true; } @@ -925,6 +934,7 @@ pub fn parse_args(argv: &[String]) -> Result { skip_report_build, skip_vcir_persist, enable_roa_validation_cache, + enable_child_certificate_validation_cache, publication_point_cache_observe_only, enable_publication_point_validation_cache, enable_transport_request_prefetch, @@ -1992,6 +2002,7 @@ pub fn run(argv: &[String]) -> Result<(), String> { persist_vcir: !args.skip_vcir_persist, build_ccr_accumulator: args.ccr_out_path.is_some(), enable_roa_validation_cache: args.enable_roa_validation_cache, + enable_child_certificate_validation_cache: args.enable_child_certificate_validation_cache, publication_point_cache_observe_only: args.publication_point_cache_observe_only, enable_publication_point_validation_cache: args.enable_publication_point_validation_cache, enable_transport_request_prefetch: args.enable_transport_request_prefetch, @@ -2487,6 +2498,7 @@ pub fn run(argv: &[String]) -> Result<(), String> { let stage_timing = RunStageTiming { validation_ms, enable_roa_validation_cache: args.enable_roa_validation_cache, + enable_child_certificate_validation_cache: args.enable_child_certificate_validation_cache, publication_point_cache_observe_only: args.publication_point_cache_observe_only, enable_publication_point_validation_cache: args.enable_publication_point_validation_cache, enable_transport_request_prefetch: args.enable_transport_request_prefetch, @@ -2525,6 +2537,17 @@ pub fn run(argv: &[String]) -> Result<(), String> { .as_ref() .map(|report| report.top_publication_point_steps.clone()) .unwrap_or_default(), + analysis_top_publication_point_cache_steps: timing_report_snapshot + .as_ref() + .map(|report| { + report + .top_publication_point_steps + .iter() + .filter(|entry| entry.key.contains("::publication_point_cache_")) + .cloned() + .collect() + }) + .unwrap_or_default(), vcir_storage_summary_ms, vcir_storage, publication_point_cache_index_load, diff --git a/src/cli/tests.rs b/src/cli/tests.rs index 0219863..6ec038b 100644 --- a/src/cli/tests.rs +++ b/src/cli/tests.rs @@ -147,6 +147,22 @@ fn parse_accepts_enable_roa_validation_cache() { assert!(args.enable_roa_validation_cache); } +#[test] +fn parse_accepts_enable_child_certificate_validation_cache() { + let argv = vec![ + "rpki".to_string(), + "--db".to_string(), + "db".to_string(), + "--tal-path".to_string(), + "x.tal".to_string(), + "--ta-path".to_string(), + "x.cer".to_string(), + "--enable-child-certificate-validation-cache".to_string(), + ]; + let args = parse_args(&argv).expect("parse args"); + assert!(args.enable_child_certificate_validation_cache); +} + #[test] fn parse_accepts_publication_point_cache_flags() { let argv = vec![ @@ -194,6 +210,7 @@ fn parse_disables_roa_validation_cache_by_default() { ]; let args = parse_args(&argv).expect("parse args"); assert!(!args.enable_roa_validation_cache); + assert!(!args.enable_child_certificate_validation_cache); assert!(!args.publication_point_cache_observe_only); assert!(!args.enable_publication_point_validation_cache); assert!(!args.enable_transport_request_prefetch); @@ -1602,6 +1619,7 @@ fn run_report_task_and_stage_timing_work() { let stage_timing = RunStageTiming { validation_ms: 1, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, enable_transport_request_prefetch: false, @@ -1628,6 +1646,7 @@ fn run_report_task_and_stage_timing_work() { analysis_phases: std::collections::HashMap::new(), analysis_top_publication_points: Vec::new(), analysis_top_publication_point_steps: Vec::new(), + analysis_top_publication_point_cache_steps: Vec::new(), vcir_storage_summary_ms: Some(16), vcir_storage: Some(VcirStorageSummary { entry_count: 2, @@ -1706,6 +1725,7 @@ fn stage_timing_serializes_memory_telemetry() { let stage_timing = RunStageTiming { validation_ms: 1, enable_roa_validation_cache: true, + enable_child_certificate_validation_cache: true, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, enable_transport_request_prefetch: true, @@ -1738,6 +1758,7 @@ fn stage_timing_serializes_memory_telemetry() { analysis_phases: std::collections::HashMap::new(), analysis_top_publication_points: Vec::new(), analysis_top_publication_point_steps: Vec::new(), + analysis_top_publication_point_cache_steps: Vec::new(), vcir_storage_summary_ms: None, vcir_storage: None, publication_point_cache_index_load: None, diff --git a/src/parallel/repo_runtime.rs b/src/parallel/repo_runtime.rs index cbfb8df..fef41b0 100644 --- a/src/parallel/repo_runtime.rs +++ b/src/parallel/repo_runtime.rs @@ -710,14 +710,14 @@ mod tests { }; use crate::policy::SyncPreference; use crate::report::Warning; - use crate::validation::tree::{CaInstanceHandle, DiscoveredChildCaInstance}; + use crate::validation::tree::{CaCertificateRef, CaInstanceHandle, DiscoveredChildCaInstance}; fn sample_ca(manifest: &str) -> CaInstanceHandle { CaInstanceHandle { depth: 0, tal_id: "arin".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: vec![1, 2, 3], + ca_certificate: CaCertificateRef::inline_der(vec![1, 2, 3]), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, diff --git a/src/storage.rs b/src/storage.rs index 1c93834..e7a5b42 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -16,17 +16,18 @@ use crate::data_model::rc::{AsResourceSet, IpResourceSet}; use config::*; pub use config::{ - ALL_COLUMN_FAMILY_NAMES, CF_MANIFEST_REPLAY_META, CF_PUBLICATION_POINT_CACHE_PROJECTION, - CF_RAW_BY_HASH, CF_REPOSITORY_VIEW, CF_ROA_CACHE_PROJECTION, CF_RRDP_SOURCE, - CF_RRDP_SOURCE_MEMBER, CF_RRDP_URI_OWNER, CF_TRANSPORT_PREFETCH, CF_VCIR, - column_family_descriptors, + ALL_COLUMN_FAMILY_NAMES, CF_CHILD_CERTIFICATE_CACHE_PROJECTION, CF_MANIFEST_REPLAY_META, + CF_PUBLICATION_POINT_CACHE_PROJECTION, CF_RAW_BY_HASH, CF_REPOSITORY_VIEW, + CF_ROA_CACHE_PROJECTION, CF_RRDP_SOURCE, CF_RRDP_SOURCE_MEMBER, CF_RRDP_URI_OWNER, + CF_TRANSPORT_PREFETCH, CF_VCIR, column_family_descriptors, }; use keys::*; use pack::compute_sha256_32; pub use pack::{PackBytes, PackFile, PackTime}; pub use pp_cache_index::{PpCacheIndexLoadStats, PpCacheIndexRefreshStats}; use pp_cache_index::{ - PpCacheMmapIndexSet, default_pp_cache_index_dir, load_pp_cache_mmap_index_set, + PpCacheIndexLookup, PpCacheMmapIndexSet, compact_pp_cache_index, default_pp_cache_index_dir, + load_pp_cache_mmap_index, load_pp_cache_mmap_index_set, pp_cache_index_directory_stats, write_pp_cache_index_atomic, write_pp_cache_index_segment, }; #[derive(Debug, thiserror::Error)] @@ -52,11 +53,20 @@ pub enum StorageError { pub type StorageResult = Result; +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)] +pub struct ChildCertificateCacheMmapLookup { + pub projections: Vec>, + pub hits: usize, + pub misses: usize, + pub file_bytes: u64, +} + pub struct RocksStore { db: DB, external_raw_store: Option, external_repo_bytes: Option, publication_point_cache_index_dir: PathBuf, + child_certificate_cache_index_dir: PathBuf, publication_point_cache_projection_index: Mutex, } @@ -95,6 +105,21 @@ fn process_vm_rss_kb() -> Option { }) } +fn default_child_certificate_cache_index_dir(db_path: &Path) -> PathBuf { + let file_name = db_path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("work-db"); + db_path.with_file_name(format!("{file_name}.child-cert-cache-index")) +} + +fn child_certificate_cache_segment_file_name(manifest_rsync_uri: &str) -> String { + format!( + "{}.idx", + hex::encode(compute_sha256_32(manifest_rsync_uri.as_bytes())) + ) +} + const ROCKSDB_MEMORY_PROPERTY_NAMES: &[(&str, &str)] = &[ ("cur_size_all_mem_tables", "rocksdb.cur-size-all-mem-tables"), ("size_all_mem_tables", "rocksdb.size-all-mem-tables"), @@ -116,6 +141,8 @@ const PP_CACHE_RAW_INDEX_ENV: &str = "RPKI_PP_CACHE_RAW_INDEX"; const PP_CACHE_RAW_INDEX_EMPTY_BUILD_LIMIT_ENV: &str = "RPKI_PP_CACHE_RAW_INDEX_EMPTY_BUILD_LIMIT_BYTES"; const DEFAULT_PP_CACHE_RAW_INDEX_EMPTY_BUILD_LIMIT_BYTES: usize = 32 * 1024 * 1024; +const PP_CACHE_INDEX_COMPACTION_SEGMENT_THRESHOLD: usize = 16; +const PP_CACHE_INDEX_COMPACTION_BYTES_THRESHOLD: u64 = 1_610_612_736; fn pp_cache_raw_index_enabled() -> bool { match std::env::var(PP_CACHE_RAW_INDEX_ENV) { @@ -1179,6 +1206,216 @@ pub struct RoaCacheProjection { pub entries: Vec, } +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ChildCertificateCacheRouterKeyProjection { + #[serde(rename = "a")] + pub as_id: u32, + #[serde(rename = "s")] + #[serde(with = "serde_byte_vec")] + pub ski: Vec, + #[serde(rename = "p")] + #[serde(with = "serde_byte_vec")] + pub spki_der: Vec, + #[serde(rename = "e")] + pub item_effective_until: PackTime, +} + +impl ChildCertificateCacheRouterKeyProjection { + pub fn validate_internal(&self) -> StorageResult<()> { + if self.ski.is_empty() { + return Err(StorageError::InvalidData { + entity: "child_certificate_cache_projection.router_keys[].ski", + detail: "must not be empty".to_string(), + }); + } + if self.spki_der.is_empty() { + return Err(StorageError::InvalidData { + entity: "child_certificate_cache_projection.router_keys[].spki_der", + detail: "must not be empty".to_string(), + }); + } + parse_time( + "child_certificate_cache_projection.router_keys[].item_effective_until", + &self.item_effective_until, + )?; + Ok(()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ChildCertificateCachePayload { + ChildCa { + #[serde(rename = "cm")] + child_manifest_rsync_uri: String, + #[serde(rename = "ski")] + child_ski: String, + #[serde(rename = "rb")] + child_rsync_base_uri: String, + #[serde(rename = "pp")] + child_publication_point_rsync_uri: String, + #[serde(rename = "rn")] + child_rrdp_notification_uri: Option, + #[serde(rename = "ip")] + child_effective_ip_resources: Option, + #[serde(rename = "as")] + child_effective_as_resources: Option, + }, + Router { + #[serde(rename = "r")] + router_keys: Vec, + }, +} + +impl ChildCertificateCachePayload { + pub fn validate_internal(&self) -> StorageResult<()> { + match self { + Self::ChildCa { + child_manifest_rsync_uri, + child_ski, + child_rsync_base_uri, + child_publication_point_rsync_uri, + child_rrdp_notification_uri, + .. + } => { + validate_non_empty( + "child_certificate_cache_projection.child_manifest_rsync_uri", + child_manifest_rsync_uri, + )?; + validate_non_empty("child_certificate_cache_projection.child_ski", child_ski)?; + validate_non_empty( + "child_certificate_cache_projection.child_rsync_base_uri", + child_rsync_base_uri, + )?; + validate_non_empty( + "child_certificate_cache_projection.child_publication_point_rsync_uri", + child_publication_point_rsync_uri, + )?; + if let Some(uri) = child_rrdp_notification_uri { + validate_non_empty( + "child_certificate_cache_projection.child_rrdp_notification_uri", + uri, + )?; + } + Ok(()) + } + Self::Router { router_keys } => { + if router_keys.is_empty() { + return Err(StorageError::InvalidData { + entity: "child_certificate_cache_projection.router_keys", + detail: "must not be empty".to_string(), + }); + } + for router_key in router_keys { + router_key.validate_internal()?; + } + Ok(()) + } + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ChildCertificateCacheProjection { + #[serde(rename = "sv")] + pub schema_version: u32, + #[serde(rename = "av")] + pub algorithm_version: u32, + #[serde(rename = "k")] + pub cache_key_sha256_hex: String, + #[serde(rename = "cu")] + pub child_cert_uri: String, + #[serde(rename = "ch")] + pub child_cert_sha256_hex: String, + #[serde(rename = "cs")] + #[serde(with = "serde_byte_vec")] + pub child_cert_serial: Vec, + #[serde(rename = "ih")] + pub issuer_ca_sha256_hex: String, + #[serde(rename = "cru")] + pub issuer_crl_uri: String, + #[serde(rename = "crh")] + pub issuer_crl_sha256_hex: String, + #[serde(rename = "pc")] + #[serde(with = "serde_bytes_32")] + pub parent_context_digest: [u8; 32], + #[serde(rename = "pf")] + #[serde(with = "serde_bytes_32")] + pub validation_policy_fingerprint: [u8; 32], + #[serde(rename = "nb")] + pub effective_not_before: PackTime, + #[serde(rename = "nu")] + pub effective_until: PackTime, + #[serde(rename = "p")] + pub payload: ChildCertificateCachePayload, +} + +pub const CHILD_CERTIFICATE_CACHE_SCHEMA_VERSION: u32 = 2; +pub const CHILD_CERTIFICATE_CACHE_ALGORITHM_VERSION: u32 = 3; + +impl ChildCertificateCacheProjection { + pub fn validate_internal(&self) -> StorageResult<()> { + if self.schema_version != CHILD_CERTIFICATE_CACHE_SCHEMA_VERSION { + return Err(StorageError::InvalidData { + entity: "child_certificate_cache_projection.schema_version", + detail: format!("unsupported schema_version {}", self.schema_version), + }); + } + if self.algorithm_version != CHILD_CERTIFICATE_CACHE_ALGORITHM_VERSION { + return Err(StorageError::InvalidData { + entity: "child_certificate_cache_projection.algorithm_version", + detail: format!("unsupported algorithm_version {}", self.algorithm_version), + }); + } + validate_sha256_hex( + "child_certificate_cache_projection.cache_key_sha256_hex", + &self.cache_key_sha256_hex, + )?; + validate_non_empty( + "child_certificate_cache_projection.child_cert_uri", + &self.child_cert_uri, + )?; + validate_sha256_hex( + "child_certificate_cache_projection.child_cert_sha256_hex", + &self.child_cert_sha256_hex, + )?; + if self.child_cert_serial.is_empty() { + return Err(StorageError::InvalidData { + entity: "child_certificate_cache_projection.child_cert_serial", + detail: "must not be empty".to_string(), + }); + } + validate_sha256_hex( + "child_certificate_cache_projection.issuer_ca_sha256_hex", + &self.issuer_ca_sha256_hex, + )?; + validate_non_empty( + "child_certificate_cache_projection.issuer_crl_uri", + &self.issuer_crl_uri, + )?; + validate_sha256_hex( + "child_certificate_cache_projection.issuer_crl_sha256_hex", + &self.issuer_crl_sha256_hex, + )?; + let effective_not_before = parse_time( + "child_certificate_cache_projection.effective_not_before", + &self.effective_not_before, + )?; + let effective_until = parse_time( + "child_certificate_cache_projection.effective_until", + &self.effective_until, + )?; + if effective_not_before >= effective_until { + return Err(StorageError::InvalidData { + entity: "child_certificate_cache_projection.effective_window", + detail: "effective_not_before must be before effective_until".to_string(), + }); + } + self.payload.validate_internal()?; + Ok(()) + } +} + pub const PUBLICATION_POINT_CACHE_SCHEMA_VERSION: u32 = 1; pub const PUBLICATION_POINT_CACHE_ALGORITHM_VERSION: u32 = 1; @@ -2584,6 +2821,7 @@ impl RocksStore { external_raw_store: None, external_repo_bytes: None, publication_point_cache_index_dir: default_pp_cache_index_dir(path), + child_certificate_cache_index_dir: default_child_certificate_cache_index_dir(path), publication_point_cache_projection_index: Mutex::new( PublicationPointCacheProjectionIndexState::Uninitialized, ), @@ -2658,6 +2896,59 @@ impl RocksStore { } } + fn try_load_publication_point_cache_mmap_index_for_update( + &self, + reason: &'static str, + ) -> StorageResult<()> { + if !pp_cache_raw_index_enabled() { + return Ok(()); + } + let mut guard = self + .publication_point_cache_projection_index + .lock() + .map_err(|e| { + StorageError::RocksDb(format!("publication point cache index lock poisoned: {e}")) + })?; + if !matches!( + *guard, + PublicationPointCacheProjectionIndexState::Uninitialized + ) { + return Ok(()); + } + match load_pp_cache_mmap_index_set(&self.publication_point_cache_index_dir) { + Ok((mmap, stats)) => { + crate::progress_log::emit( + "publication_point_cache_mmap_index_load", + serde_json::json!({ + "state": "loaded", + "reason": reason, + "entries": stats.entries, + "bytes": stats.bytes, + "file_bytes": stats.file_bytes, + "load_ms": stats.load_ms, + }), + ); + *guard = PublicationPointCacheProjectionIndexState::LoadedMmap { + mmap, + dirty: HashMap::new(), + dirty_bytes: 0, + load_stats: stats, + }; + } + Err(e) => { + crate::progress_log::emit( + "publication_point_cache_mmap_index_load", + serde_json::json!({ + "state": "deferred_fallback_scan", + "reason": reason, + "error": e.to_string(), + }), + ); + } + } + Ok(()) + } + fn cf(&self, name: &'static str) -> StorageResult<&ColumnFamily> { self.db .cf_handle(name) @@ -3210,6 +3501,226 @@ impl RocksStore { Ok(Some(projection)) } + pub fn put_child_certificate_cache_projection( + &self, + projection: &ChildCertificateCacheProjection, + ) -> StorageResult<()> { + projection.validate_internal()?; + let cf = self.cf(CF_CHILD_CERTIFICATE_CACHE_PROJECTION)?; + let key = child_certificate_cache_projection_key(&projection.cache_key_sha256_hex); + let value = encode_cbor(projection, "child_certificate_cache_projection")?; + self.db + .put_cf(cf, key.as_bytes(), value) + .map_err(|e| StorageError::RocksDb(e.to_string()))?; + Ok(()) + } + + pub fn get_child_certificate_cache_projection( + &self, + cache_key_sha256_hex: &str, + ) -> StorageResult> { + validate_sha256_hex( + "child_certificate_cache_projection.cache_key_sha256_hex", + cache_key_sha256_hex, + )?; + let cf = self.cf(CF_CHILD_CERTIFICATE_CACHE_PROJECTION)?; + let key = child_certificate_cache_projection_key(cache_key_sha256_hex); + let Some(bytes) = self + .db + .get_cf(cf, key.as_bytes()) + .map_err(|e| StorageError::RocksDb(e.to_string()))? + else { + return Ok(None); + }; + let projection = decode_cbor::( + &bytes, + "child_certificate_cache_projection", + )?; + projection.validate_internal()?; + Ok(Some(projection)) + } + + pub fn get_child_certificate_cache_projections_batch( + &self, + cache_key_sha256_hexes: &[String], + ) -> StorageResult>> { + if cache_key_sha256_hexes.is_empty() { + return Ok(Vec::new()); + } + + for cache_key_sha256_hex in cache_key_sha256_hexes { + validate_sha256_hex( + "child_certificate_cache_projection.cache_key_sha256_hex", + cache_key_sha256_hex, + )?; + } + + let cf = self.cf(CF_CHILD_CERTIFICATE_CACHE_PROJECTION)?; + let keys: Vec = cache_key_sha256_hexes + .iter() + .map(|key| child_certificate_cache_projection_key(key)) + .collect(); + self.db + .multi_get_cf(keys.iter().map(|key| (cf, key.as_bytes()))) + .into_iter() + .map(|res| { + let Some(bytes) = res.map_err(|e| StorageError::RocksDb(e.to_string()))? else { + return Ok(None); + }; + let projection = decode_cbor::( + &bytes, + "child_certificate_cache_projection", + )?; + projection.validate_internal()?; + Ok(Some(projection)) + }) + .collect() + } + + fn child_certificate_cache_segment_path(&self, manifest_rsync_uri: &str) -> PathBuf { + self.child_certificate_cache_index_dir + .join(child_certificate_cache_segment_file_name( + manifest_rsync_uri, + )) + } + + pub fn get_child_certificate_cache_projections_mmap_segment( + &self, + manifest_rsync_uri: &str, + cache_key_sha256_hexes: &[String], + ) -> StorageResult> { + if cache_key_sha256_hexes.is_empty() { + return Ok(Some(ChildCertificateCacheMmapLookup::default())); + } + validate_non_empty( + "child_certificate_cache_mmap_segment.manifest_rsync_uri", + manifest_rsync_uri, + )?; + for cache_key_sha256_hex in cache_key_sha256_hexes { + validate_sha256_hex( + "child_certificate_cache_projection.cache_key_sha256_hex", + cache_key_sha256_hex, + )?; + } + + let path = self.child_certificate_cache_segment_path(manifest_rsync_uri); + if !path.exists() { + return Ok(None); + } + + let (index, _) = load_pp_cache_mmap_index(&path)?; + let file_bytes = index.file_bytes(); + let mut hits = 0usize; + let mut misses = 0usize; + let mut projections = Vec::with_capacity(cache_key_sha256_hexes.len()); + for cache_key_sha256_hex in cache_key_sha256_hexes { + match index.lookup(cache_key_sha256_hex) { + Some(PpCacheIndexLookup::Hit(bytes)) => { + let projection = decode_cbor::( + bytes, + "child_certificate_cache_projection_mmap_segment", + )?; + projection.validate_internal()?; + hits = hits.saturating_add(1); + projections.push(Some(projection)); + } + Some(PpCacheIndexLookup::Deleted) | None => { + misses = misses.saturating_add(1); + projections.push(None); + } + } + } + + Ok(Some(ChildCertificateCacheMmapLookup { + projections, + hits, + misses, + file_bytes, + })) + } + + pub fn write_child_certificate_cache_mmap_segment( + &self, + manifest_rsync_uri: &str, + projections: &[ChildCertificateCacheProjection], + ) -> StorageResult { + validate_non_empty( + "child_certificate_cache_mmap_segment.manifest_rsync_uri", + manifest_rsync_uri, + )?; + let mut entries = Vec::with_capacity(projections.len()); + for projection in projections { + projection.validate_internal()?; + entries.push(( + projection.cache_key_sha256_hex.clone(), + encode_cbor( + projection, + "child_certificate_cache_projection_mmap_segment", + )?, + )); + } + let path = self.child_certificate_cache_segment_path(manifest_rsync_uri); + write_pp_cache_index_atomic(&path, entries) + } + + pub fn write_child_certificate_cache_mmap_segment_overlay( + &self, + manifest_rsync_uri: &str, + cache_key_sha256_hexes: &[String], + projections: &[ChildCertificateCacheProjection], + ) -> StorageResult { + validate_non_empty( + "child_certificate_cache_mmap_segment.manifest_rsync_uri", + manifest_rsync_uri, + )?; + let mut dirty = HashMap::>::with_capacity(projections.len()); + for projection in projections { + projection.validate_internal()?; + dirty.insert( + projection.cache_key_sha256_hex.clone(), + encode_cbor( + projection, + "child_certificate_cache_projection_mmap_segment", + )?, + ); + } + + let path = self.child_certificate_cache_segment_path(manifest_rsync_uri); + let existing = if path.exists() { + Some(load_pp_cache_mmap_index(&path)?.0) + } else { + None + }; + let mut entries = Vec::with_capacity(cache_key_sha256_hexes.len()); + let mut emitted = HashSet::::new(); + for cache_key_sha256_hex in cache_key_sha256_hexes { + validate_sha256_hex( + "child_certificate_cache_projection.cache_key_sha256_hex", + cache_key_sha256_hex, + )?; + if !emitted.insert(cache_key_sha256_hex.clone()) { + continue; + } + if let Some(value) = dirty.remove(cache_key_sha256_hex) { + entries.push((cache_key_sha256_hex.clone(), value)); + continue; + } + if let Some(existing) = existing.as_ref() { + if let Some(PpCacheIndexLookup::Hit(bytes)) = existing.lookup(cache_key_sha256_hex) + { + entries.push((cache_key_sha256_hex.clone(), bytes.to_vec())); + } + } + } + for (key, value) in dirty { + if emitted.insert(key.clone()) { + entries.push((key, value)); + } + } + + write_pp_cache_index_atomic(&path, entries) + } + pub fn get_publication_point_cache_projection( &self, manifest_rsync_uri: &str, @@ -3322,14 +3833,22 @@ impl RocksStore { } PublicationPointCacheProjectionIndexState::LoadedMmap { mmap, dirty, .. } => { if let Some(bytes) = dirty.get(manifest_rsync_uri).cloned() { + if bytes.is_empty() { + return Ok(None); + } owned_bytes = Some(bytes); - } else if let Some(bytes) = mmap.get(manifest_rsync_uri) { - let projection = decode_cbor::( - bytes, - "publication_point_cache_projection", - )?; - projection.validate_internal()?; - return Ok(Some(projection)); + } else if let Some(lookup) = mmap.lookup(manifest_rsync_uri) { + match lookup { + PpCacheIndexLookup::Hit(bytes) => { + let projection = decode_cbor::( + bytes, + "publication_point_cache_projection", + )?; + projection.validate_internal()?; + return Ok(Some(projection)); + } + PpCacheIndexLookup::Deleted => return Ok(None), + } } } PublicationPointCacheProjectionIndexState::Disabled @@ -3426,6 +3945,7 @@ impl RocksStore { &self, projection: &PublicationPointCacheProjection, ) -> StorageResult<()> { + self.try_load_publication_point_cache_mmap_index_for_update("write")?; let mut guard = self .publication_point_cache_projection_index .lock() @@ -3485,76 +4005,42 @@ impl RocksStore { &self, manifest_rsync_uri: &str, ) -> StorageResult<()> { - let mut invalidate_mmap_files = false; - { - let mut guard = self - .publication_point_cache_projection_index - .lock() - .map_err(|e| { - StorageError::RocksDb(format!( - "publication point cache index lock poisoned: {e}" - )) - })?; - match &mut *guard { - PublicationPointCacheProjectionIndexState::Loaded { - index, - bytes: total_bytes, - } => { - if let Some(previous) = index.remove(manifest_rsync_uri) { - *total_bytes = total_bytes.saturating_sub(previous.len()); - } + self.try_load_publication_point_cache_mmap_index_for_update("delete")?; + let mut guard = self + .publication_point_cache_projection_index + .lock() + .map_err(|e| { + StorageError::RocksDb(format!("publication point cache index lock poisoned: {e}")) + })?; + match &mut *guard { + PublicationPointCacheProjectionIndexState::Loaded { + index, + bytes: total_bytes, + } => { + if let Some(previous) = index.remove(manifest_rsync_uri) { + *total_bytes = total_bytes.saturating_sub(previous.len()); } - PublicationPointCacheProjectionIndexState::BuildingFromEmpty { - index, - bytes: total_bytes, - .. - } => { - if let Some(previous) = index.remove(manifest_rsync_uri) { - *total_bytes = total_bytes.saturating_sub(previous.len()); - } + } + PublicationPointCacheProjectionIndexState::BuildingFromEmpty { + index, + bytes: total_bytes, + .. + } => { + if let Some(previous) = index.remove(manifest_rsync_uri) { + *total_bytes = total_bytes.saturating_sub(previous.len()); } - PublicationPointCacheProjectionIndexState::LoadedMmap { .. } => { - *guard = PublicationPointCacheProjectionIndexState::Disabled; - invalidate_mmap_files = true; + } + PublicationPointCacheProjectionIndexState::LoadedMmap { + dirty, dirty_bytes, .. + } => { + if let Some(previous) = + dirty.insert(manifest_rsync_uri.to_string(), Arc::<[u8]>::from([])) + { + *dirty_bytes = dirty_bytes.saturating_sub(previous.len()); } - PublicationPointCacheProjectionIndexState::Uninitialized - | PublicationPointCacheProjectionIndexState::Disabled => {} - } - } - - if invalidate_mmap_files { - self.remove_publication_point_cache_mmap_index_files()?; - let mut guard = self - .publication_point_cache_projection_index - .lock() - .map_err(|e| { - StorageError::RocksDb(format!( - "publication point cache index lock poisoned: {e}" - )) - })?; - if matches!(*guard, PublicationPointCacheProjectionIndexState::Disabled) { - *guard = PublicationPointCacheProjectionIndexState::Uninitialized; - } - } - Ok(()) - } - - fn remove_publication_point_cache_mmap_index_files(&self) -> StorageResult<()> { - let dir = &self.publication_point_cache_index_dir; - if !dir.exists() { - return Ok(()); - } - for entry in std::fs::read_dir(dir).map_err(|e| StorageError::RocksDb(e.to_string()))? { - let entry = entry.map_err(|e| StorageError::RocksDb(e.to_string()))?; - let path = entry.path(); - let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else { - continue; - }; - if file_name == "current.idx" - || (file_name.starts_with("segment-") && file_name.ends_with(".idx")) - { - std::fs::remove_file(&path).map_err(|e| StorageError::RocksDb(e.to_string()))?; } + PublicationPointCacheProjectionIndexState::Uninitialized + | PublicationPointCacheProjectionIndexState::Disabled => {} } Ok(()) } @@ -3565,6 +4051,7 @@ impl RocksStore { if !pp_cache_raw_index_enabled() { return Ok(None); } + self.try_load_publication_point_cache_mmap_index_for_update("refresh")?; enum RefreshAction { Entries { entries: Vec<(String, Vec)>, @@ -3647,6 +4134,81 @@ impl RocksStore { "write_ms": stats.write_ms, }), ); + let directory_stats = + pp_cache_index_directory_stats(&self.publication_point_cache_index_dir)?; + let compaction_reason = if directory_stats.segment_count + >= PP_CACHE_INDEX_COMPACTION_SEGMENT_THRESHOLD + { + Some(format!( + "segment_count>={PP_CACHE_INDEX_COMPACTION_SEGMENT_THRESHOLD}" + )) + } else if directory_stats.total_file_bytes >= PP_CACHE_INDEX_COMPACTION_BYTES_THRESHOLD { + Some(format!( + "total_file_bytes>={PP_CACHE_INDEX_COMPACTION_BYTES_THRESHOLD}" + )) + } else { + None + }; + if let Some(reason) = compaction_reason { + crate::progress_log::emit( + "publication_point_cache_mmap_index_compaction", + serde_json::json!({ + "state": "started", + "reason": reason, + "segment_count": directory_stats.segment_count, + "total_file_bytes": directory_stats.total_file_bytes, + }), + ); + match compact_pp_cache_index(&self.publication_point_cache_index_dir) { + Ok(mut compact_stats) => { + compact_stats.compaction_reason = Some(reason); + compact_stats.old_entries = stats.old_entries; + compact_stats.dirty_entries = stats.dirty_entries; + crate::progress_log::emit( + "publication_point_cache_mmap_index_compaction", + serde_json::json!({ + "state": "completed", + "reason": compact_stats.compaction_reason, + "segments_before": compact_stats.compaction_segments_before, + "total_file_bytes_before": compact_stats.compaction_total_file_bytes_before, + "live_entries": compact_stats.compaction_live_entries, + "file_bytes": compact_stats.compaction_file_bytes, + "reclaimed_bytes": compact_stats.compaction_reclaimed_bytes, + "deleted_segments": compact_stats.compaction_deleted_segments, + "compaction_ms": compact_stats.compaction_ms, + }), + ); + stats.compaction_triggered = compact_stats.compaction_triggered; + stats.compaction_reason = compact_stats.compaction_reason; + stats.compaction_segments_before = compact_stats.compaction_segments_before; + stats.compaction_total_file_bytes_before = + compact_stats.compaction_total_file_bytes_before; + stats.compaction_live_entries = compact_stats.compaction_live_entries; + stats.compaction_file_bytes = compact_stats.compaction_file_bytes; + stats.compaction_reclaimed_bytes = compact_stats.compaction_reclaimed_bytes; + stats.compaction_ms = compact_stats.compaction_ms; + stats.compaction_deleted_segments = compact_stats.compaction_deleted_segments; + } + Err(e) => { + let error = e.to_string(); + crate::progress_log::emit( + "publication_point_cache_mmap_index_compaction", + serde_json::json!({ + "state": "failed", + "reason": reason, + "segment_count": directory_stats.segment_count, + "total_file_bytes": directory_stats.total_file_bytes, + "error": error, + }), + ); + stats.compaction_triggered = true; + stats.compaction_reason = Some(reason); + stats.compaction_segments_before = directory_stats.segment_count; + stats.compaction_total_file_bytes_before = directory_stats.total_file_bytes; + stats.compaction_error = Some(error); + } + } + } let mut guard = self .publication_point_cache_projection_index .lock() diff --git a/src/storage/config.rs b/src/storage/config.rs index 1bec630..ffbd539 100644 --- a/src/storage/config.rs +++ b/src/storage/config.rs @@ -7,6 +7,7 @@ pub const CF_VCIR: &str = "vcir"; pub const CF_MANIFEST_REPLAY_META: &str = "manifest_replay_meta"; pub const CF_ROA_CACHE_PROJECTION: &str = "roa_cache_projection"; pub const CF_PUBLICATION_POINT_CACHE_PROJECTION: &str = "publication_point_cache_projection"; +pub const CF_CHILD_CERTIFICATE_CACHE_PROJECTION: &str = "child_certificate_cache_projection"; pub const CF_RRDP_SOURCE: &str = "rrdp_source"; pub const CF_RRDP_SOURCE_MEMBER: &str = "rrdp_source_member"; pub const CF_RRDP_URI_OWNER: &str = "rrdp_uri_owner"; @@ -20,6 +21,7 @@ pub const ALL_COLUMN_FAMILY_NAMES: &[&str] = &[ CF_MANIFEST_REPLAY_META, CF_ROA_CACHE_PROJECTION, CF_PUBLICATION_POINT_CACHE_PROJECTION, + CF_CHILD_CERTIFICATE_CACHE_PROJECTION, CF_RRDP_SOURCE, CF_RRDP_SOURCE_MEMBER, CF_RRDP_URI_OWNER, @@ -34,6 +36,8 @@ pub(super) const MANIFEST_REPLAY_META_KEY_PREFIX: &str = "manifest_replay_meta:" pub(super) const ROA_CACHE_PROJECTION_KEY_PREFIX: &str = "roa_cache_projection:"; pub(super) const PUBLICATION_POINT_CACHE_PROJECTION_KEY_PREFIX: &str = "publication_point_cache_projection:"; +pub(super) const CHILD_CERTIFICATE_CACHE_PROJECTION_KEY_PREFIX: &str = + "child_certificate_cache_projection:"; pub(super) const RRDP_SOURCE_KEY_PREFIX: &str = "rrdp_source:"; pub(super) const RRDP_SOURCE_MEMBER_KEY_PREFIX: &str = "rrdp_source_member:"; pub(super) const RRDP_URI_OWNER_KEY_PREFIX: &str = "rrdp_uri_owner:"; diff --git a/src/storage/keys.rs b/src/storage/keys.rs index eec3f0e..2469963 100644 --- a/src/storage/keys.rs +++ b/src/storage/keys.rs @@ -38,6 +38,10 @@ pub(super) fn publication_point_cache_projection_key(manifest_rsync_uri: &str) - format!("{PUBLICATION_POINT_CACHE_PROJECTION_KEY_PREFIX}{manifest_rsync_uri}") } +pub(super) fn child_certificate_cache_projection_key(cache_key_sha256_hex: &str) -> String { + format!("{CHILD_CERTIFICATE_CACHE_PROJECTION_KEY_PREFIX}{cache_key_sha256_hex}") +} + pub(super) fn publication_point_cache_projection_key_manifest_uri(key: &[u8]) -> Option { let key = std::str::from_utf8(key).ok()?; key.strip_prefix(PUBLICATION_POINT_CACHE_PROJECTION_KEY_PREFIX) diff --git a/src/storage/pp_cache_index.rs b/src/storage/pp_cache_index.rs index 73a306a..68065be 100644 --- a/src/storage/pp_cache_index.rs +++ b/src/storage/pp_cache_index.rs @@ -32,6 +32,22 @@ pub struct PpCacheIndexRefreshStats { pub new_entries: usize, pub file_bytes: u64, pub write_ms: u64, + pub compaction_triggered: bool, + pub compaction_reason: Option, + pub compaction_segments_before: usize, + pub compaction_total_file_bytes_before: u64, + pub compaction_live_entries: usize, + pub compaction_file_bytes: u64, + pub compaction_reclaimed_bytes: u64, + pub compaction_ms: u64, + pub compaction_deleted_segments: usize, + pub compaction_error: Option, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)] +pub struct PpCacheIndexDirectoryStats { + pub segment_count: usize, + pub total_file_bytes: u64, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -40,6 +56,12 @@ struct PpCacheIndexEntry { value_len: usize, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PpCacheIndexLookup<'a> { + Hit(&'a [u8]), + Deleted, +} + #[derive(Debug)] pub struct PpCacheMmapIndex { mmap: Arc, @@ -126,12 +148,23 @@ impl PpCacheMmapIndex { }) } - pub fn get(&self, manifest_rsync_uri: &str) -> Option<&[u8]> { + pub fn lookup(&self, manifest_rsync_uri: &str) -> Option> { let entry = self.entries.get(manifest_rsync_uri)?; + if entry.value_len == 0 { + return Some(PpCacheIndexLookup::Deleted); + } let blob_offset = read_u64(self.mmap.as_ref(), 40).ok()? as usize; let start = blob_offset + entry.value_offset; let end = start + entry.value_len; - Some(&self.mmap[start..end]) + Some(PpCacheIndexLookup::Hit(&self.mmap[start..end])) + } + + #[cfg(test)] + pub fn get(&self, manifest_rsync_uri: &str) -> Option<&[u8]> { + match self.lookup(manifest_rsync_uri)? { + PpCacheIndexLookup::Hit(bytes) => Some(bytes), + PpCacheIndexLookup::Deleted => None, + } } pub fn entries(&self) -> usize { @@ -156,10 +189,18 @@ pub struct PpCacheMmapIndexSet { } impl PpCacheMmapIndexSet { - pub fn get(&self, manifest_rsync_uri: &str) -> Option<&[u8]> { + pub fn lookup(&self, manifest_rsync_uri: &str) -> Option> { self.indexes .iter() - .find_map(|index| index.get(manifest_rsync_uri)) + .find_map(|index| index.lookup(manifest_rsync_uri)) + } + + #[cfg(test)] + pub fn get(&self, manifest_rsync_uri: &str) -> Option<&[u8]> { + match self.lookup(manifest_rsync_uri)? { + PpCacheIndexLookup::Hit(bytes) => Some(bytes), + PpCacheIndexLookup::Deleted => None, + } } pub fn entries(&self) -> usize { @@ -259,6 +300,84 @@ pub fn load_pp_cache_mmap_index_set( Ok((set, stats)) } +pub fn pp_cache_index_directory_stats(dir: &Path) -> StorageResult { + let mut stats = PpCacheIndexDirectoryStats::default(); + if !dir.exists() { + return Ok(stats); + } + for entry in fs::read_dir(dir).map_err(|e| StorageError::RocksDb(e.to_string()))? { + let path = entry + .map_err(|e| StorageError::RocksDb(e.to_string()))? + .path(); + let Some(name) = path.file_name().and_then(|name| name.to_str()) else { + continue; + }; + let is_index = + name == "current.idx" || (name.starts_with("segment-") && name.ends_with(".idx")); + if !is_index { + continue; + } + let len = fs::metadata(&path) + .map_err(|e| StorageError::RocksDb(e.to_string()))? + .len(); + stats.total_file_bytes = stats.total_file_bytes.saturating_add(len); + if name.starts_with("segment-") && name.ends_with(".idx") { + stats.segment_count += 1; + } + } + Ok(stats) +} + +pub fn compact_pp_cache_index(dir: &Path) -> StorageResult { + let started = Instant::now(); + fs::create_dir_all(dir).map_err(|e| StorageError::RocksDb(e.to_string()))?; + let before = pp_cache_index_directory_stats(dir)?; + let (index_set, _) = load_pp_cache_mmap_index_set(dir)?; + let mut compact_entries = Vec::with_capacity(index_set.entries()); + let mut seen = HashMap::::with_capacity(index_set.entries()); + for index in &index_set.indexes { + for (key, entry) in &index.entries { + if seen.insert(key.clone(), ()).is_some() { + continue; + } + if entry.value_len == 0 { + continue; + } + let blob_offset = read_u64(index.mmap.as_ref(), 40)? as usize; + let start = blob_offset + entry.value_offset; + let end = start + entry.value_len; + compact_entries.push((key.clone(), index.mmap[start..end].to_vec())); + } + } + + let current = dir.join("current.idx"); + let mut stats = write_pp_cache_index_atomic(¤t, compact_entries)?; + stats.state = "compacted".to_string(); + stats.compaction_triggered = true; + stats.compaction_segments_before = before.segment_count; + stats.compaction_total_file_bytes_before = before.total_file_bytes; + stats.compaction_live_entries = stats.new_entries; + stats.compaction_file_bytes = stats.file_bytes; + stats.compaction_reclaimed_bytes = before.total_file_bytes.saturating_sub(stats.file_bytes); + + let mut deleted_segments = 0usize; + for entry in fs::read_dir(dir).map_err(|e| StorageError::RocksDb(e.to_string()))? { + let path = entry + .map_err(|e| StorageError::RocksDb(e.to_string()))? + .path(); + let Some(name) = path.file_name().and_then(|name| name.to_str()) else { + continue; + }; + if name.starts_with("segment-") && name.ends_with(".idx") { + fs::remove_file(&path).map_err(|e| StorageError::RocksDb(e.to_string()))?; + deleted_segments += 1; + } + } + stats.compaction_deleted_segments = deleted_segments; + stats.compaction_ms = started.elapsed().as_millis() as u64; + Ok(stats) +} + pub fn write_pp_cache_index_segment( dir: &Path, entries: I, @@ -316,6 +435,7 @@ where new_entries: ordered.len(), file_bytes, write_ms: started.elapsed().as_millis() as u64, + ..PpCacheIndexRefreshStats::default() }) } @@ -465,4 +585,87 @@ mod tests { assert_eq!(stats.entries, 2); assert_eq!(set.get("rsync://example.test/a.mft"), Some(&b"new"[..])); } + + #[test] + fn pp_cache_index_set_tombstone_shadows_older_entry() { + let dir = tempfile::tempdir().expect("tempdir"); + let current = dir.path().join("current.idx"); + write_pp_cache_index_atomic( + ¤t, + vec![("rsync://example.test/a.mft".to_string(), b"old".to_vec())], + ) + .expect("write current index"); + write_pp_cache_index_segment( + dir.path(), + vec![("rsync://example.test/a.mft".to_string(), Vec::new())], + ) + .expect("write tombstone segment index"); + + let (set, stats) = load_pp_cache_mmap_index_set(dir.path()).expect("load index set"); + assert_eq!(stats.entries, 2); + assert_eq!( + set.lookup("rsync://example.test/a.mft"), + Some(PpCacheIndexLookup::Deleted) + ); + assert_eq!(set.get("rsync://example.test/a.mft"), None); + } + + #[test] + fn pp_cache_index_compaction_keeps_latest_values_and_removes_segments() { + let dir = tempfile::tempdir().expect("tempdir"); + let current = dir.path().join("current.idx"); + write_pp_cache_index_atomic( + ¤t, + vec![ + ("rsync://example.test/a.mft".to_string(), b"old-a".to_vec()), + ("rsync://example.test/b.mft".to_string(), b"old-b".to_vec()), + ("rsync://example.test/c.mft".to_string(), b"old-c".to_vec()), + ], + ) + .expect("write current index"); + write_pp_cache_index_segment( + dir.path(), + vec![ + ("rsync://example.test/a.mft".to_string(), b"new-a".to_vec()), + ("rsync://example.test/b.mft".to_string(), Vec::new()), + ], + ) + .expect("write first segment"); + write_pp_cache_index_segment( + dir.path(), + vec![("rsync://example.test/d.mft".to_string(), b"new-d".to_vec())], + ) + .expect("write second segment"); + + let before = pp_cache_index_directory_stats(dir.path()).expect("stats before"); + assert_eq!(before.segment_count, 2); + + let stats = compact_pp_cache_index(dir.path()).expect("compact"); + assert!(stats.compaction_triggered); + assert_eq!(stats.compaction_segments_before, 2); + assert_eq!(stats.compaction_live_entries, 3); + assert_eq!(stats.compaction_deleted_segments, 2); + assert!(stats.compaction_reclaimed_bytes > 0); + + let after = pp_cache_index_directory_stats(dir.path()).expect("stats after"); + assert_eq!(after.segment_count, 0); + let segments = fs::read_dir(dir.path()) + .expect("read dir") + .filter_map(Result::ok) + .filter(|entry| { + entry + .file_name() + .to_str() + .is_some_and(|name| name.starts_with("segment-")) + }) + .count(); + assert_eq!(segments, 0); + + let (index, _) = load_pp_cache_mmap_index(¤t).expect("load compacted current"); + assert_eq!(index.entries(), 3); + assert_eq!(index.get("rsync://example.test/a.mft"), Some(&b"new-a"[..])); + assert_eq!(index.get("rsync://example.test/b.mft"), None); + assert_eq!(index.get("rsync://example.test/c.mft"), Some(&b"old-c"[..])); + assert_eq!(index.get("rsync://example.test/d.mft"), Some(&b"new-d"[..])); + } } diff --git a/src/storage/tests.rs b/src/storage/tests.rs index 86e9d77..9a91998 100644 --- a/src/storage/tests.rs +++ b/src/storage/tests.rs @@ -14,6 +14,37 @@ fn sha256_32(input: &[u8]) -> [u8; 32] { compute_sha256_32(input) } +fn sample_child_certificate_cache_projection( + cache_key_sha256_hex: String, + child_cert_uri: &str, + child_cert_sha256_hex: &str, +) -> ChildCertificateCacheProjection { + ChildCertificateCacheProjection { + schema_version: CHILD_CERTIFICATE_CACHE_SCHEMA_VERSION, + algorithm_version: CHILD_CERTIFICATE_CACHE_ALGORITHM_VERSION, + cache_key_sha256_hex, + child_cert_uri: child_cert_uri.to_string(), + child_cert_sha256_hex: child_cert_sha256_hex.to_string(), + child_cert_serial: vec![1], + issuer_ca_sha256_hex: sha256_hex(b"issuer-ca"), + issuer_crl_uri: "rsync://example.test/repo/issuer.crl".to_string(), + issuer_crl_sha256_hex: sha256_hex(b"issuer-crl"), + parent_context_digest: sha256_32(b"parent-context"), + validation_policy_fingerprint: sha256_32(b"policy"), + effective_not_before: pack_time(0), + effective_until: pack_time(24), + payload: ChildCertificateCachePayload::ChildCa { + child_manifest_rsync_uri: format!("{child_cert_uri}.mft"), + child_ski: "11".repeat(20), + child_rsync_base_uri: "rsync://example.test/repo/child/".to_string(), + child_publication_point_rsync_uri: "rsync://example.test/repo/child/".to_string(), + child_rrdp_notification_uri: Some("https://example.test/notify.xml".to_string()), + child_effective_ip_resources: None, + child_effective_as_resources: None, + }, + } +} + #[test] fn parse_work_db_blob_mode_accepts_supported_values() { assert_eq!(default_work_db_blob_mode(), WorkDbBlobMode::Disabled); @@ -565,6 +596,128 @@ fn publication_point_cache_mmap_index_dirty_overlay_wins_and_refreshes_segment() assert_eq!(got.manifest_sha256, sha256_32(b"manifest-new")); } +#[test] +fn publication_point_cache_mmap_index_write_before_read_refreshes_segment() { + let td = tempfile::tempdir().expect("tempdir"); + let db_path = td.path().join("work-db"); + let vcir = sample_vcir("rsync://example.test/repo/current.mft"); + let mut projection = PublicationPointCacheProjection::from_vcir_with_context( + &vcir, + "rsync://example.test/repo/".to_string(), + Some("rsync://example.test/repo/ca.cer".to_string()), + sha256_32(b"ca-cert"), + sha256_32(b"manifest-old"), + sha256_32(b"ta-context"), + sha256_32(b"parent-context"), + sha256_32(b"policy"), + ) + .expect("build publication point projection"); + + { + let store = RocksStore::open(&db_path).expect("open rocksdb"); + store + .put_vcir_with_publication_point_cache_projection(&vcir, Some(&projection)) + .expect("put old projection"); + let stats = store + .refresh_publication_point_cache_mmap_index() + .expect("refresh base mmap index") + .expect("refresh stats"); + assert_eq!(stats.state, "written"); + } + + { + let store = RocksStore::open(&db_path).expect("reopen rocksdb"); + projection.manifest_sha256 = sha256_32(b"manifest-new"); + store + .put_vcir_with_publication_point_cache_projection(&vcir, Some(&projection)) + .expect("put new projection before any cached read"); + let stats = store + .refresh_publication_point_cache_mmap_index() + .expect("refresh dirty mmap segment") + .expect("refresh stats"); + assert_eq!(stats.state, "segment_written"); + assert_eq!(stats.dirty_entries, 1); + assert_eq!(stats.new_entries, 1); + } + + let store = RocksStore::open(&db_path).expect("reopen rocksdb after segment"); + let got = store + .get_publication_point_cache_projection_cached(&vcir.manifest_rsync_uri) + .expect("get refreshed projection") + .expect("projection exists"); + assert_eq!(got.manifest_sha256, sha256_32(b"manifest-new")); +} + +#[test] +fn publication_point_cache_mmap_index_delete_before_read_refreshes_tombstone_segment() { + let td = tempfile::tempdir().expect("tempdir"); + let db_path = td.path().join("work-db"); + let vcir = sample_vcir("rsync://example.test/repo/current.mft"); + let projection = PublicationPointCacheProjection::from_vcir_with_context( + &vcir, + "rsync://example.test/repo/".to_string(), + Some("rsync://example.test/repo/ca.cer".to_string()), + sha256_32(b"ca-cert"), + sha256_32(b"manifest"), + sha256_32(b"ta-context"), + sha256_32(b"parent-context"), + sha256_32(b"policy"), + ) + .expect("build publication point projection"); + + { + let store = RocksStore::open(&db_path).expect("open rocksdb"); + store + .put_vcir_with_publication_point_cache_projection(&vcir, Some(&projection)) + .expect("put projection"); + let stats = store + .refresh_publication_point_cache_mmap_index() + .expect("refresh base mmap index") + .expect("refresh stats"); + assert_eq!(stats.state, "written"); + } + + { + let store = RocksStore::open(&db_path).expect("reopen rocksdb"); + store + .replace_vcir_manifest_replay_meta_and_projection_action( + &vcir, + None, + PublicationPointCacheProjectionWriteAction::Delete { + manifest_rsync_uri: &vcir.manifest_rsync_uri, + }, + ) + .expect("delete projection before any cached read"); + assert!( + store + .get_publication_point_cache_projection_cached(&vcir.manifest_rsync_uri) + .expect("dirty tombstone lookup") + .is_none() + ); + let stats = store + .refresh_publication_point_cache_mmap_index() + .expect("refresh tombstone mmap segment") + .expect("refresh stats"); + assert_eq!(stats.state, "segment_written"); + assert_eq!(stats.dirty_entries, 1); + assert_eq!(stats.new_entries, 1); + } + + let store = RocksStore::open(&db_path).expect("reopen rocksdb after tombstone segment"); + assert!( + store + .get_publication_point_cache_projection_cached(&vcir.manifest_rsync_uri) + .expect("tombstone shadows old current index") + .is_none() + ); + assert!( + store + .get_publication_point_cache_projection(&vcir.manifest_rsync_uri) + .expect("direct db lookup") + .is_none() + ); +} + #[test] fn publication_point_cache_projection_rejects_version_mismatch() { let vcir = sample_vcir("rsync://example.test/repo/current.mft"); @@ -1364,6 +1517,196 @@ fn replace_vcir_and_manifest_replay_meta_replaces_current_entry() { ); } +#[test] +fn get_child_certificate_cache_projections_batch_preserves_order_and_misses() { + let td = tempfile::tempdir().expect("tempdir"); + let store = RocksStore::open(td.path()).expect("open rocksdb"); + + let first_key = sha256_hex(b"child-cache-key-first"); + let missing_key = sha256_hex(b"child-cache-key-missing"); + let second_key = sha256_hex(b"child-cache-key-second"); + let first_hash = sha256_hex(b"first-child-cert"); + let second_hash = sha256_hex(b"second-child-cert"); + let first = sample_child_certificate_cache_projection( + first_key.clone(), + "rsync://example.test/repo/first.cer", + &first_hash, + ); + let second = sample_child_certificate_cache_projection( + second_key.clone(), + "rsync://example.test/repo/second.cer", + &second_hash, + ); + + store + .put_child_certificate_cache_projection(&first) + .expect("put first projection"); + store + .put_child_certificate_cache_projection(&second) + .expect("put second projection"); + + let got = store + .get_child_certificate_cache_projections_batch(&[ + second_key.clone(), + missing_key, + first_key.clone(), + ]) + .expect("batch get child projections"); + + assert_eq!(got.len(), 3); + assert_eq!( + got[0] + .as_ref() + .expect("second projection") + .child_cert_sha256_hex, + second_hash + ); + assert!(got[1].is_none()); + assert_eq!( + got[2] + .as_ref() + .expect("first projection") + .child_cert_sha256_hex, + first_hash + ); +} + +#[test] +fn child_certificate_cache_mmap_segment_preserves_order_and_misses() { + let td = tempfile::tempdir().expect("tempdir"); + let store = RocksStore::open(td.path()).expect("open rocksdb"); + + let manifest_uri = "rsync://example.test/repo/parent.mft"; + let first_key = sha256_hex(b"child-cache-mmap-key-first"); + let missing_key = sha256_hex(b"child-cache-mmap-key-missing"); + let second_key = sha256_hex(b"child-cache-mmap-key-second"); + let first_hash = sha256_hex(b"first-child-cert-mmap"); + let second_hash = sha256_hex(b"second-child-cert-mmap"); + let first = sample_child_certificate_cache_projection( + first_key.clone(), + "rsync://example.test/repo/first.cer", + &first_hash, + ); + let second = sample_child_certificate_cache_projection( + second_key.clone(), + "rsync://example.test/repo/second.cer", + &second_hash, + ); + + assert!( + store + .get_child_certificate_cache_projections_mmap_segment( + manifest_uri, + &[first_key.clone()] + ) + .expect("missing segment lookup") + .is_none() + ); + + let write_stats = store + .write_child_certificate_cache_mmap_segment(manifest_uri, &[first.clone(), second.clone()]) + .expect("write child projection segment"); + assert_eq!(write_stats.new_entries, 2); + assert!(write_stats.file_bytes > 0); + + let got = store + .get_child_certificate_cache_projections_mmap_segment( + manifest_uri, + &[second_key.clone(), missing_key, first_key.clone()], + ) + .expect("lookup child projection segment") + .expect("segment exists"); + + assert_eq!(got.hits, 2); + assert_eq!(got.misses, 1); + assert!(got.file_bytes > 0); + assert_eq!(got.projections.len(), 3); + assert_eq!( + got.projections[0] + .as_ref() + .expect("second projection") + .child_cert_sha256_hex, + second_hash + ); + assert!(got.projections[1].is_none()); + assert_eq!( + got.projections[2] + .as_ref() + .expect("first projection") + .child_cert_sha256_hex, + first_hash + ); +} + +#[test] +fn child_certificate_cache_mmap_segment_overlay_preserves_existing_values() { + let td = tempfile::tempdir().expect("tempdir"); + let store = RocksStore::open(td.path()).expect("open rocksdb"); + + let manifest_uri = "rsync://example.test/repo/parent-overlay.mft"; + let first_key = sha256_hex(b"child-cache-overlay-key-first"); + let second_key = sha256_hex(b"child-cache-overlay-key-second"); + let missing_key = sha256_hex(b"child-cache-overlay-key-missing"); + let first_hash = sha256_hex(b"first-child-cert-overlay"); + let old_second_hash = sha256_hex(b"old-second-child-cert-overlay"); + let new_second_hash = sha256_hex(b"new-second-child-cert-overlay"); + let first = sample_child_certificate_cache_projection( + first_key.clone(), + "rsync://example.test/repo/first-overlay.cer", + &first_hash, + ); + let old_second = sample_child_certificate_cache_projection( + second_key.clone(), + "rsync://example.test/repo/second-overlay.cer", + &old_second_hash, + ); + let new_second = sample_child_certificate_cache_projection( + second_key.clone(), + "rsync://example.test/repo/second-overlay.cer", + &new_second_hash, + ); + + store + .write_child_certificate_cache_mmap_segment( + manifest_uri, + &[first.clone(), old_second.clone()], + ) + .expect("write initial segment"); + let stats = store + .write_child_certificate_cache_mmap_segment_overlay( + manifest_uri, + &[first_key.clone(), second_key.clone(), missing_key.clone()], + std::slice::from_ref(&new_second), + ) + .expect("write segment overlay"); + assert_eq!(stats.new_entries, 2); + + let got = store + .get_child_certificate_cache_projections_mmap_segment( + manifest_uri, + &[first_key.clone(), second_key.clone(), missing_key], + ) + .expect("lookup segment") + .expect("segment exists"); + assert_eq!(got.hits, 2); + assert_eq!(got.misses, 1); + assert_eq!( + got.projections[0] + .as_ref() + .expect("first projection") + .child_cert_sha256_hex, + first_hash + ); + assert_eq!( + got.projections[1] + .as_ref() + .expect("updated second projection") + .child_cert_sha256_hex, + new_second_hash + ); + assert!(got.projections[2].is_none()); +} + #[test] fn storage_helpers_cover_optional_validation_paths() { let withdrawn = RepositoryViewEntry { diff --git a/src/validation/manifest.rs b/src/validation/manifest.rs index e160af8..1fc0950 100644 --- a/src/validation/manifest.rs +++ b/src/validation/manifest.rs @@ -213,6 +213,9 @@ pub enum ManifestFreshError { #[error("manifest file hash mismatch: {rsync_uri} (RFC 9286 §6.5; RFC 9286 §6.6)")] HashMismatch { rsync_uri: String }, + + #[error("issuer CA certificate bytes unavailable: {detail} (RFC 6487 §4; RFC 9286 §6.2)")] + IssuerCaLoadFailed { detail: String }, } impl ManifestFreshError { diff --git a/src/validation/run.rs b/src/validation/run.rs index ae20ab8..5ac5a17 100644 --- a/src/validation/run.rs +++ b/src/validation/run.rs @@ -6,7 +6,7 @@ use crate::storage::RocksStore; use crate::sync::rrdp::Fetcher as HttpFetcher; use crate::validation::manifest::PublicationPointSource; use crate::validation::objects::ObjectsOutput; -use crate::validation::tree::{CaInstanceHandle, PublicationPointRunner}; +use crate::validation::tree::{CaCertificateRef, CaInstanceHandle, PublicationPointRunner}; use crate::validation::tree_runner::Rpkiv1PublicationPointRunner; use std::collections::HashMap; use std::sync::Mutex; @@ -49,7 +49,7 @@ pub fn run_publication_point_once( depth: 0, tal_id: "single-publication-point".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: issuer_ca_der.to_vec(), + ca_certificate: CaCertificateRef::inline_der(issuer_ca_der.to_vec()), ca_certificate_rsync_uri: issuer_ca_rsync_uri.map(str::to_string), effective_ip_resources: issuer_effective_ip.cloned(), effective_as_resources: issuer_effective_as.cloned(), @@ -80,6 +80,7 @@ pub fn run_publication_point_once( ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; diff --git a/src/validation/run_tree_from_tal.rs b/src/validation/run_tree_from_tal.rs index 037783c..c709fcc 100644 --- a/src/validation/run_tree_from_tal.rs +++ b/src/validation/run_tree_from_tal.rs @@ -33,6 +33,7 @@ use crate::validation::from_tal::{ discover_root_ca_instance_from_tal_with_fetchers_strict_name, }; use crate::validation::objects::ParallelRoaWorkerPool; +use crate::validation::tree::CaCertificateRef; use crate::validation::tree::{ CaInstanceHandle, TreeRunAuditOutput, TreeRunConfig, TreeRunError, TreeRunOutput, run_tree_serial, run_tree_serial_audit, run_tree_serial_audit_multi_root, @@ -136,6 +137,7 @@ fn make_live_runner<'a>( ccr_accumulator: Option, persist_vcir: bool, enable_roa_validation_cache: bool, + enable_child_certificate_validation_cache: bool, publication_point_cache_observe_only: bool, enable_publication_point_validation_cache: bool, ) -> Rpkiv1PublicationPointRunner<'a> { @@ -163,6 +165,7 @@ fn make_live_runner<'a>( ccr_accumulator: ccr_accumulator.map(Mutex::new), persist_vcir, enable_roa_validation_cache, + enable_child_certificate_validation_cache, publication_point_cache_observe_only, enable_publication_point_validation_cache, } @@ -537,7 +540,7 @@ pub fn root_handle_from_trust_anchor( depth: 0, tal_id, parent_manifest_rsync_uri: None, - ca_certificate_der: trust_anchor.ta_certificate.raw_der.clone(), + ca_certificate: CaCertificateRef::inline_der(trust_anchor.ta_certificate.raw_der.clone()), ca_certificate_rsync_uri, effective_ip_resources: ta_rc.tbs.extensions.ip_resources.clone(), effective_as_resources: ta_rc.tbs.extensions.as_resources.clone(), @@ -578,6 +581,7 @@ pub fn run_tree_from_tal_url_serial( None, config.persist_vcir, config.enable_roa_validation_cache, + config.enable_child_certificate_validation_cache, config.publication_point_cache_observe_only, config.enable_publication_point_validation_cache, ); @@ -624,6 +628,7 @@ pub fn run_tree_from_tal_url_serial_audit( None, config.persist_vcir, config.enable_roa_validation_cache, + config.enable_child_certificate_validation_cache, config.publication_point_cache_observe_only, config.enable_publication_point_validation_cache, ); @@ -692,6 +697,7 @@ pub fn run_tree_from_tal_url_serial_audit_with_timing( None, config.persist_vcir, config.enable_roa_validation_cache, + config.enable_child_certificate_validation_cache, config.publication_point_cache_observe_only, config.enable_publication_point_validation_cache, ); @@ -782,6 +788,7 @@ where .then(|| CcrAccumulator::new(vec![discovery.trust_anchor.clone()])), config.persist_vcir, config.enable_roa_validation_cache, + config.enable_child_certificate_validation_cache, config.publication_point_cache_observe_only, config.enable_publication_point_validation_cache, ); @@ -910,6 +917,7 @@ where }), config.persist_vcir, config.enable_roa_validation_cache, + config.enable_child_certificate_validation_cache, config.publication_point_cache_observe_only, config.enable_publication_point_validation_cache, ); @@ -1350,6 +1358,7 @@ pub fn run_tree_from_tal_and_ta_der_serial( ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: config.enable_roa_validation_cache, + enable_child_certificate_validation_cache: config.enable_child_certificate_validation_cache, publication_point_cache_observe_only: config.publication_point_cache_observe_only, enable_publication_point_validation_cache: config.enable_publication_point_validation_cache, }; @@ -1405,6 +1414,7 @@ pub fn run_tree_from_tal_bytes_serial_audit( ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: config.enable_roa_validation_cache, + enable_child_certificate_validation_cache: config.enable_child_certificate_validation_cache, publication_point_cache_observe_only: config.publication_point_cache_observe_only, enable_publication_point_validation_cache: config.enable_publication_point_validation_cache, }; @@ -1482,6 +1492,7 @@ pub fn run_tree_from_tal_bytes_serial_audit_with_timing( ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: config.enable_roa_validation_cache, + enable_child_certificate_validation_cache: config.enable_child_certificate_validation_cache, publication_point_cache_observe_only: config.publication_point_cache_observe_only, enable_publication_point_validation_cache: config.enable_publication_point_validation_cache, }; @@ -1558,6 +1569,7 @@ pub fn run_tree_from_tal_and_ta_der_serial_audit( ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: config.enable_roa_validation_cache, + enable_child_certificate_validation_cache: config.enable_child_certificate_validation_cache, publication_point_cache_observe_only: config.publication_point_cache_observe_only, enable_publication_point_validation_cache: config.enable_publication_point_validation_cache, }; @@ -1635,6 +1647,7 @@ pub fn run_tree_from_tal_and_ta_der_serial_audit_with_timing( ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: config.enable_roa_validation_cache, + enable_child_certificate_validation_cache: config.enable_child_certificate_validation_cache, publication_point_cache_observe_only: config.publication_point_cache_observe_only, enable_publication_point_validation_cache: config.enable_publication_point_validation_cache, }; @@ -1719,6 +1732,7 @@ pub fn run_tree_from_tal_and_ta_der_payload_replay_serial( ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: config.enable_roa_validation_cache, + enable_child_certificate_validation_cache: config.enable_child_certificate_validation_cache, publication_point_cache_observe_only: config.publication_point_cache_observe_only, enable_publication_point_validation_cache: config.enable_publication_point_validation_cache, }; @@ -1784,6 +1798,7 @@ pub fn run_tree_from_tal_and_ta_der_payload_replay_serial_audit( ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: config.enable_roa_validation_cache, + enable_child_certificate_validation_cache: config.enable_child_certificate_validation_cache, publication_point_cache_observe_only: config.publication_point_cache_observe_only, enable_publication_point_validation_cache: config.enable_publication_point_validation_cache, }; @@ -1871,6 +1886,7 @@ pub fn run_tree_from_tal_and_ta_der_payload_replay_serial_audit_with_timing( ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: config.enable_roa_validation_cache, + enable_child_certificate_validation_cache: config.enable_child_certificate_validation_cache, publication_point_cache_observe_only: config.publication_point_cache_observe_only, enable_publication_point_validation_cache: config.enable_publication_point_validation_cache, }; @@ -1938,6 +1954,7 @@ fn build_payload_replay_runner<'a>( ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, } @@ -1975,6 +1992,7 @@ fn build_payload_delta_replay_runner<'a>( ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, } @@ -2012,6 +2030,7 @@ fn build_payload_delta_replay_current_store_runner<'a>( ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, } @@ -2577,6 +2596,7 @@ mod replay_api_tests { persist_vcir: true, build_ccr_accumulator: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, enable_transport_request_prefetch: false, @@ -2616,6 +2636,7 @@ mod replay_api_tests { persist_vcir: true, build_ccr_accumulator: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, enable_transport_request_prefetch: false, @@ -2665,6 +2686,7 @@ mod replay_api_tests { persist_vcir: true, build_ccr_accumulator: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, enable_transport_request_prefetch: false, @@ -2715,6 +2737,7 @@ mod replay_api_tests { persist_vcir: true, build_ccr_accumulator: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, enable_transport_request_prefetch: false, @@ -2775,6 +2798,7 @@ mod replay_api_tests { persist_vcir: true, build_ccr_accumulator: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, enable_transport_request_prefetch: false, @@ -2811,6 +2835,7 @@ mod replay_api_tests { persist_vcir: true, build_ccr_accumulator: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, enable_transport_request_prefetch: false, @@ -2867,6 +2892,7 @@ mod replay_api_tests { persist_vcir: true, build_ccr_accumulator: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, enable_transport_request_prefetch: false, @@ -2932,6 +2958,7 @@ mod replay_api_tests { persist_vcir: true, build_ccr_accumulator: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, enable_transport_request_prefetch: false, diff --git a/src/validation/tree.rs b/src/validation/tree.rs index ef4a646..bcefa94 100644 --- a/src/validation/tree.rs +++ b/src/validation/tree.rs @@ -7,6 +7,8 @@ use crate::validation::objects::{ AspaAttestation, ObjectsOutput, RoaValidationCacheStats, RouterKeyPayload, Vrp, }; use crate::validation::publication_point::PublicationPointSnapshot; +use std::borrow::Cow; +use std::sync::{Arc, OnceLock}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct TreeRunConfig { @@ -22,6 +24,8 @@ pub struct TreeRunConfig { pub build_ccr_accumulator: bool, /// Reuse accepted ROA validation outputs from previous VCIR when explicitly enabled. pub enable_roa_validation_cache: bool, + /// Reuse validated child certificate discovery results when explicitly enabled. + pub enable_child_certificate_validation_cache: bool, /// Evaluate publication-point cache eligibility without changing validation results. pub publication_point_cache_observe_only: bool, /// Reuse complete publication-point validation projections when explicitly enabled. @@ -39,6 +43,7 @@ impl Default for TreeRunConfig { persist_vcir: true, build_ccr_accumulator: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, enable_transport_request_prefetch: false, @@ -46,13 +51,85 @@ impl Default for TreeRunConfig { } } +#[derive(Clone, Debug)] +pub enum CaCertificateRef { + InlineDer(Vec), + RepoBytes { + sha256_hex: String, + cached_der: Arc>>, + }, +} + +impl PartialEq for CaCertificateRef { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::InlineDer(left), Self::InlineDer(right)) => left == right, + ( + Self::RepoBytes { + sha256_hex: left, .. + }, + Self::RepoBytes { + sha256_hex: right, .. + }, + ) => left == right, + _ => false, + } + } +} + +impl Eq for CaCertificateRef {} + +impl CaCertificateRef { + pub fn inline_der(bytes: Vec) -> Self { + Self::InlineDer(bytes) + } + + pub fn repo_bytes(sha256_hex: String) -> Self { + Self::RepoBytes { + sha256_hex, + cached_der: Arc::new(OnceLock::new()), + } + } + + pub fn sha256_hex(&self) -> Option<&str> { + match self { + Self::InlineDer(_) => None, + Self::RepoBytes { sha256_hex, .. } => Some(sha256_hex.as_str()), + } + } + + pub fn der<'a>(&'a self, store: &crate::storage::RocksStore) -> Result, String> { + match self { + Self::InlineDer(bytes) => Ok(Cow::Borrowed(bytes.as_slice())), + Self::RepoBytes { + sha256_hex, + cached_der, + } => { + if cached_der.get().is_none() { + let bytes = store + .get_blob_bytes(sha256_hex) + .map_err(|e| format!("load CA certificate bytes failed: {e}"))? + .ok_or_else(|| { + format!("missing CA certificate repo bytes for sha256={sha256_hex}") + })?; + let _ = cached_der.set(Arc::from(bytes)); + } + let bytes = cached_der.get().ok_or_else(|| { + format!("missing cached CA certificate bytes for sha256={sha256_hex}") + })?; + Ok(Cow::Borrowed(bytes.as_ref())) + } + } + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct CaInstanceHandle { pub depth: usize, pub tal_id: String, pub parent_manifest_rsync_uri: Option, - /// DER bytes of the CA certificate for this CA instance. - pub ca_certificate_der: Vec, + /// CA certificate bytes or a lazy repo-bytes reference for this CA instance. + pub ca_certificate: CaCertificateRef, /// rsync URI of this CA certificate object (where it is published). /// /// This is used for strict AIA binding checks (RFC 6487 §4.8.7) when validating @@ -74,6 +151,32 @@ impl CaInstanceHandle { self.depth = depth; self } + + pub fn ca_certificate_der<'a>( + &'a self, + store: &crate::storage::RocksStore, + ) -> Result, String> { + self.ca_certificate.der(store) + } + + pub fn ca_certificate_sha256_hex(&self) -> Option<&str> { + self.ca_certificate.sha256_hex() + } + + pub fn ca_certificate_sha256_32(&self) -> Option<[u8; 32]> { + match &self.ca_certificate { + CaCertificateRef::InlineDer(bytes) => { + use sha2::Digest as _; + let digest = sha2::Sha256::digest(bytes); + Some(digest.into()) + } + CaCertificateRef::RepoBytes { sha256_hex, .. } => { + let mut out = [0u8; 32]; + hex::decode_to_slice(sha256_hex, &mut out).ok()?; + Some(out) + } + } + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -337,7 +440,7 @@ mod tests { use crate::validation::objects::{ObjectsOutput, ObjectsStats}; use super::{ - CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult, + CaCertificateRef, CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult, PublicationPointRunner, TreeRunConfig, run_tree_serial_audit, run_tree_serial_audit_multi_root, }; @@ -347,7 +450,7 @@ mod tests { depth: 0, tal_id: "arin".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: vec![1, 2, 3], + ca_certificate: CaCertificateRef::inline_der(vec![1, 2, 3]), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, diff --git a/src/validation/tree_parallel.rs b/src/validation/tree_parallel.rs index 352b8ca..0458e54 100644 --- a/src/validation/tree_parallel.rs +++ b/src/validation/tree_parallel.rs @@ -994,6 +994,11 @@ fn stage_ready_publication_point( result.discovered_children.clone(), ); metrics.child_enqueue_ms = elapsed_ms(child_enqueue_started); + runner.record_publication_point_step_ms( + metrics.manifest_rsync_uri.as_deref().unwrap_or_default(), + "publication_point_cache_child_enqueue", + metrics.child_enqueue_ms, + ); finished.push(FinishedPublicationPoint { node: FinishedPublicationPointNode::from_queued(ready.node), result: compact_phase2_finished_result(result, compact_audit), @@ -1182,7 +1187,7 @@ fn stage_ready_publication_point( ready.node.id, &fresh_stage.fresh_point, runner.policy, - &ready.node.handle.ca_certificate_der, + fresh_stage.issuer_ca_der.as_ref(), ready.node.handle.ca_certificate_rsync_uri.as_deref(), ready.node.handle.effective_ip_resources.as_ref(), ready.node.handle.effective_as_resources.as_ref(), @@ -2420,7 +2425,7 @@ mod tests { .push(crate::validation::tree::DiscoveredChildCaInstance { handle: crate::validation::tree::CaInstanceHandle { tal_id: "test".to_string(), - ca_certificate_der: vec![1], + ca_certificate: crate::validation::tree::CaCertificateRef::inline_der(vec![1]), ca_certificate_rsync_uri: Some( "rsync://example.test/repo/child.cer".to_string(), ), diff --git a/src/validation/tree_runner.rs b/src/validation/tree_runner.rs index dce4d74..afb210f 100644 --- a/src/validation/tree_runner.rs +++ b/src/validation/tree_runner.rs @@ -9,6 +9,7 @@ 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::common::BigUnsigned; use crate::data_model::crl::RpkixCrl; use crate::data_model::manifest::ManifestObject; use crate::data_model::rc::{ResourceCertificate, SubjectInfoAccess}; @@ -25,12 +26,15 @@ 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, + CHILD_CERTIFICATE_CACHE_ALGORITHM_VERSION, CHILD_CERTIFICATE_CACHE_SCHEMA_VERSION, + ChildCertificateCachePayload, ChildCertificateCacheProjection, + ChildCertificateCacheRouterKeyProjection, 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, @@ -53,7 +57,8 @@ use crate::validation::objects::{ }; use crate::validation::publication_point::PublicationPointSnapshot; use crate::validation::tree::{ - CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult, PublicationPointRunner, + CaCertificateRef, CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult, + PublicationPointRunner, }; use sha2::Digest; use std::collections::{HashMap, HashSet}; @@ -67,6 +72,7 @@ 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; +const CHILD_CERTIFICATE_CACHE_MMAP_MIN_CER_COUNT: usize = 2048; fn sha256_hex_to_32(hex_value: &str) -> [u8; 32] { let mut out = [0u8; 32]; @@ -98,6 +104,7 @@ pub(crate) struct PersistVcirTimingBreakdown { #[derive(Clone, Debug)] pub(crate) struct FreshPublicationPointStage { pub(crate) fresh_point: FreshValidatedPublicationPoint, + pub(crate) issuer_ca_der: Arc<[u8]>, pub(crate) snapshot_prepare_timing: FreshPublicationPointTimingBreakdown, pub(crate) snapshot_prepare_ms: u64, pub(crate) discovered_children: Vec, @@ -160,6 +167,7 @@ pub struct Rpkiv1PublicationPointRunner<'a> { /// the resulting DB to be reused by a later delta run. pub persist_vcir: bool, pub enable_roa_validation_cache: bool, + pub enable_child_certificate_validation_cache: bool, pub publication_point_cache_observe_only: bool, pub enable_publication_point_validation_cache: bool, } @@ -483,8 +491,11 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> { 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), + .or_else(|| ca.ca_certificate_sha256_32()) + .ok_or_else(|| "current_ca_certificate_hash_missing".to_string())?, + None => ca + .ca_certificate_sha256_32() + .ok_or_else(|| "current_ca_certificate_hash_missing".to_string())?, }; let manifest_sha256 = self .current_hash_for_uri(&ca.manifest_rsync_uri) @@ -538,6 +549,11 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> { "publication_point_cache_build_objects_total", build_objects_started, ); + self.record_publication_point_step_ms( + &ca.manifest_rsync_uri, + "publication_point_cache_build_objects", + build_objects_ms, + ); 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( @@ -553,6 +569,11 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> { "publication_point_cache_restore_children_total", restore_children_started, ); + self.record_publication_point_step_ms( + &ca.manifest_rsync_uri, + "publication_point_cache_restore_children", + restore_children_ms, + ); let ccr_projection = projection.ccr_manifest_projection.clone(); let ccr_append_started = std::time::Instant::now(); self.append_ccr_manifest_projection(&ccr_projection)?; @@ -560,6 +581,11 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> { "publication_point_cache_ccr_append_total", ccr_append_started, ); + self.record_publication_point_step_ms( + &ca.manifest_rsync_uri, + "publication_point_cache_ccr_append", + ccr_append_ms, + ); let audit_build_started = std::time::Instant::now(); let audit = build_publication_point_audit_from_publication_point_cache_projection( ca, @@ -578,13 +604,24 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> { "publication_point_cache_audit_build_total", audit_build_started, ); + self.record_publication_point_step_ms( + &ca.manifest_rsync_uri, + "publication_point_cache_audit_build", + audit_build_ms, + ); let audit_object_count = audit.objects.len() as u64; let cir_cached_objects = audit.objects.clone(); + let cir_cached_objects_count = cir_cached_objects.len() as u64; objects.local_outputs_cache.clear(); let total_ms = self.record_publication_point_cache_phase_ms( "publication_point_cache_reuse_build_total", build_started, ); + self.record_publication_point_step_ms( + &ca.manifest_rsync_uri, + "publication_point_cache_reuse_build", + total_ms, + ); 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); @@ -596,6 +633,10 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> { "publication_point_cache_audit_objects_reused", audit_object_count, ); + if total_ms > 0 { + timing.record_count("publication_point_cache_reuse_nonzero_ms_hits", 1); + timing.record_count("publication_point_cache_reuse_nonzero_ms_total", total_ms); + } } if total_ms >= crate::progress_log::pp_cache_slow_threshold_ms() { crate::progress_log::emit( @@ -617,6 +658,7 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> { "ccr_append_ms": ccr_append_ms, "audit_build_ms": audit_build_ms, "total_ms": total_ms, + "cir_cached_objects": cir_cached_objects_count, "slow_threshold_ms": crate::progress_log::pp_cache_slow_threshold_ms(), }), ); @@ -766,6 +808,12 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> { repo_sync_err: Option<&str>, ) -> Result { let snapshot_prepare_started = std::time::Instant::now(); + let issuer_ca_der = ca_certificate_der_for_validation(ca, self.store, self.timing.as_ref()) + .map_err(|detail| FreshPublicationPointStageError { + error: ManifestFreshError::IssuerCaLoadFailed { detail }, + snapshot_prepare_ms: snapshot_prepare_started.elapsed().as_millis() as u64, + })?; + let issuer_ca_der: Arc<[u8]> = Arc::from(issuer_ca_der.as_ref()); let fresh_publication_point = { let _manifest_total = self .timing @@ -776,7 +824,7 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> { &ca.manifest_rsync_uri, &ca.publication_point_rsync_uri, self.current_repo_index.as_ref(), - &ca.ca_certificate_der, + issuer_ca_der.as_ref(), ca.ca_certificate_rsync_uri.as_deref(), self.validation_time, repo_sync_ok, @@ -878,11 +926,22 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> { .timing .as_ref() .map(|t| t.span_phase("child_discovery_total")); - discover_children_from_fresh_snapshot_with_audit( + discover_children_from_fresh_snapshot_with_audit_cached_with_issuer_der( ca, + issuer_ca_der.as_ref(), &fresh_point, self.validation_time, self.timing.as_ref(), + if self.enable_child_certificate_validation_cache { + Some(ChildCertificateValidationCacheContext { + store: self.store, + issuer_ca_sha256: sha256_digest_32(issuer_ca_der.as_ref()), + parent_context_digest: parent_context_digest_for_ca(ca), + policy_fingerprint: publication_point_cache_policy_fingerprint(self.policy), + }) + } else { + None + }, ) }; let (discovered_children, child_audits, discovered_router_keys, warnings) = match out { @@ -922,6 +981,7 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> { Ok(FreshPublicationPointStage { fresh_point, + issuer_ca_der, snapshot_prepare_timing, snapshot_prepare_ms, discovered_children, @@ -1052,7 +1112,7 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> { 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)?; + build_vcir_child_entries(self.store, &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; @@ -1430,6 +1490,7 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> { Ok(stage) => { let FreshPublicationPointStage { fresh_point, + issuer_ca_der, snapshot_prepare_timing, snapshot_prepare_ms, discovered_children, @@ -1483,7 +1544,7 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> { process_publication_point_for_issuer_parallel_roa_with_pool_cache_options( &fresh_point, self.policy, - &ca.ca_certificate_der, + issuer_ca_der.as_ref(), ca.ca_certificate_rsync_uri.as_deref(), ca.effective_ip_resources.as_ref(), ca.effective_as_resources.as_ref(), @@ -1497,7 +1558,7 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> { process_publication_point_for_issuer_parallel_roa_with_cache_options( &fresh_point, self.policy, - &ca.ca_certificate_der, + issuer_ca_der.as_ref(), ca.ca_certificate_rsync_uri.as_deref(), ca.effective_ip_resources.as_ref(), ca.effective_as_resources.as_ref(), @@ -1511,7 +1572,7 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> { crate::validation::objects::process_publication_point_for_issuer_with_cache_options( &fresh_point, self.policy, - &ca.ca_certificate_der, + issuer_ca_der.as_ref(), ca.ca_certificate_rsync_uri.as_deref(), ca.effective_ip_resources.as_ref(), ca.effective_as_resources.as_ref(), @@ -1850,14 +1911,34 @@ struct ChildDiscoveryOutput { struct VerifiedIssuerCrl { crl: crate::data_model::crl::RpkixCrl, revoked_serials: std::collections::HashSet>, + sha256_hex: String, } #[derive(Clone, Debug)] enum CachedIssuerCrl { - Pending(Vec), + Pending { + bytes: Vec, + sha256_hex: Option, + }, Ok(VerifiedIssuerCrl), } +impl CachedIssuerCrl { + fn current_sha256_hex(&mut self) -> &str { + match self { + CachedIssuerCrl::Pending { bytes, sha256_hex } => { + if sha256_hex.is_none() { + *sha256_hex = Some(crate::audit::sha256_hex(bytes)); + } + sha256_hex + .as_deref() + .expect("pending CRL sha256 must be populated") + } + CachedIssuerCrl::Ok(verified) => verified.sha256_hex.as_str(), + } + } +} + struct PublicationPointCacheIdentity { ca_cert_sha256: [u8; 32], manifest_sha256: [u8; 32], @@ -1970,6 +2051,190 @@ fn pack_time_window_contains( validation_time >= not_before && validation_time < until } +#[derive(Clone, Copy)] +struct ChildCertificateValidationCacheContext<'a> { + store: &'a RocksStore, + issuer_ca_sha256: [u8; 32], + parent_context_digest: [u8; 32], + policy_fingerprint: [u8; 32], +} + +fn child_certificate_cache_key_sha256_hex( + child_cert_uri: &str, + child_cert_sha256: &[u8; 32], + issuer_ca_sha256: &[u8; 32], + parent_context_digest: &[u8; 32], + policy_fingerprint: &[u8; 32], +) -> String { + let digest = hash_serialized_parts(&[ + ("version", b"child-certificate-cache-key-v1".to_vec()), + ("child_cert_uri", child_cert_uri.as_bytes().to_vec()), + ("child_cert_sha256", child_cert_sha256.to_vec()), + ("issuer_ca_sha256", issuer_ca_sha256.to_vec()), + ("parent_context_digest", parent_context_digest.to_vec()), + ("policy_fingerprint", policy_fingerprint.to_vec()), + ]); + sha256_hex_from_32(&digest) +} + +#[derive(Clone, Debug)] +struct ChildCertificateCacheCandidate { + projection: Option, +} + +fn remember_child_certificate_cache_dirty_projection( + dirty_projections: &mut Option>, + projection: &ChildCertificateCacheProjection, +) { + if let Some(dirty_projections) = dirty_projections { + dirty_projections.insert(projection.cache_key_sha256_hex.clone(), projection.clone()); + } +} + +fn load_child_certificate_der_for_discovery<'a>( + file: &'a PackFile, + elapsed_nanos: &mut u64, + count: &mut u64, +) -> Result<&'a [u8], String> { + let started = std::time::Instant::now(); + let bytes = file + .bytes() + .map_err(|e| format!("child certificate bytes load failed: {e}"))?; + *elapsed_nanos = + elapsed_nanos.saturating_add(started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64); + *count = count.saturating_add(1); + Ok(bytes) +} + +fn ca_certificate_der_for_validation<'a>( + ca: &'a CaInstanceHandle, + store: &RocksStore, + timing: Option<&TimingHandle>, +) -> Result, String> { + let started = std::time::Instant::now(); + let was_lazy = ca.ca_certificate_sha256_hex().is_some(); + let der = ca.ca_certificate_der(store)?; + if was_lazy { + let elapsed = started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64; + if let Some(timing) = timing { + timing.record_count("ca_certificate_lazy_load_count", 1); + timing.record_count("ca_certificate_lazy_load_bytes", der.len() as u64); + timing.record_phase_nanos("ca_certificate_lazy_load_total", elapsed); + } + } + Ok(der) +} + +fn child_certificate_cache_certificate_window( + child_not_before: time::OffsetDateTime, + child_not_after: time::OffsetDateTime, + issuer_not_before: time::OffsetDateTime, + issuer_not_after: time::OffsetDateTime, +) -> (PackTime, PackTime) { + let effective_not_before = child_not_before.max(issuer_not_before); + let effective_until = child_not_after.min(issuer_not_after); + ( + PackTime::from_utc_offset_datetime(effective_not_before), + PackTime::from_utc_offset_datetime(effective_until), + ) +} + +fn get_current_crl_sha256_hex( + crl_rsync_uri: &str, + crl_cache: &mut std::collections::HashMap, +) -> Option { + crl_cache + .get_mut(crl_rsync_uri) + .map(|entry| entry.current_sha256_hex().to_string()) +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ChildCertificateCacheCrlGate { + Unchanged, + ChangedValid, + Expired, + Invalid, + Missing, +} + +#[derive(Default)] +struct ChildCertificateCacheCrlGateSet { + gates_by_uri_and_expected_hash: + std::collections::HashMap<(String, String), ChildCertificateCacheCrlGate>, +} + +impl ChildCertificateCacheCrlGateSet { + fn evaluate( + &mut self, + projection: &ChildCertificateCacheProjection, + crl_cache: &mut std::collections::HashMap, + issuer_ca_der: &[u8], + validation_time: time::OffsetDateTime, + ) -> (ChildCertificateCacheCrlGate, bool) { + let key = ( + projection.issuer_crl_uri.clone(), + projection.issuer_crl_sha256_hex.clone(), + ); + if let Some(gate) = self.gates_by_uri_and_expected_hash.get(&key) { + return (*gate, true); + } + + let gate = evaluate_child_certificate_cache_crl_gate( + projection, + crl_cache, + issuer_ca_der, + validation_time, + ); + self.gates_by_uri_and_expected_hash.insert(key, gate); + (gate, false) + } +} + +fn evaluate_child_certificate_cache_crl_gate( + projection: &ChildCertificateCacheProjection, + crl_cache: &mut std::collections::HashMap, + issuer_ca_der: &[u8], + validation_time: time::OffsetDateTime, +) -> ChildCertificateCacheCrlGate { + let Some(current_crl_hash) = get_current_crl_sha256_hex(&projection.issuer_crl_uri, crl_cache) + else { + return ChildCertificateCacheCrlGate::Missing; + }; + if current_crl_hash == projection.issuer_crl_sha256_hex { + let verified_crl = match ensure_issuer_crl_verified( + &projection.issuer_crl_uri, + crl_cache, + issuer_ca_der, + ) { + Ok(verified_crl) => verified_crl, + Err(_) => return ChildCertificateCacheCrlGate::Invalid, + }; + if !crl_valid_at_time_for_cache(&verified_crl.crl, validation_time) { + return ChildCertificateCacheCrlGate::Expired; + } + return ChildCertificateCacheCrlGate::Unchanged; + } + + let verified_crl = + match ensure_issuer_crl_verified(&projection.issuer_crl_uri, crl_cache, issuer_ca_der) { + Ok(verified_crl) => verified_crl, + Err(_) => return ChildCertificateCacheCrlGate::Invalid, + }; + if !crl_valid_at_time_for_cache(&verified_crl.crl, validation_time) { + return ChildCertificateCacheCrlGate::Expired; + } + ChildCertificateCacheCrlGate::ChangedValid +} + +fn crl_valid_at_time_for_cache( + crl: &crate::data_model::crl::RpkixCrl, + validation_time: time::OffsetDateTime, +) -> bool { + let this_update = crl.this_update.utc.to_offset(time::UtcOffset::UTC); + let next_update = crl.next_update.utc.to_offset(time::UtcOffset::UTC); + validation_time >= this_update && validation_time < next_update +} + #[derive(Clone, Debug)] struct VcirReuseProjection { source: PublicationPointSource, @@ -1987,9 +2252,57 @@ fn discover_children_from_fresh_snapshot_with_audit( publication_point: &P, validation_time: time::OffsetDateTime, timing: Option<&TimingHandle>, +) -> Result { + let issuer_ca_der = match &issuer.ca_certificate { + CaCertificateRef::InlineDer(bytes) => bytes.as_slice(), + CaCertificateRef::RepoBytes { .. } => { + return Err("lazy CA certificate requires store-backed child discovery".to_string()); + } + }; + discover_children_from_fresh_snapshot_with_audit_cached_with_issuer_der( + issuer, + issuer_ca_der, + publication_point, + validation_time, + timing, + None, + ) +} + +fn discover_children_from_fresh_snapshot_with_audit_cached( + issuer: &CaInstanceHandle, + publication_point: &P, + validation_time: time::OffsetDateTime, + timing: Option<&TimingHandle>, + cache_context: Option>, +) -> Result { + let issuer_ca_der = match &issuer.ca_certificate { + CaCertificateRef::InlineDer(bytes) => bytes.as_slice(), + CaCertificateRef::RepoBytes { .. } => { + return Err("lazy CA certificate requires store-backed child discovery".to_string()); + } + }; + discover_children_from_fresh_snapshot_with_audit_cached_with_issuer_der( + issuer, + issuer_ca_der, + publication_point, + validation_time, + timing, + cache_context, + ) +} + +fn discover_children_from_fresh_snapshot_with_audit_cached_with_issuer_der< + P: PublicationPointData, +>( + issuer: &CaInstanceHandle, + issuer_ca_der: &[u8], + publication_point: &P, + validation_time: time::OffsetDateTime, + timing: Option<&TimingHandle>, + cache_context: Option>, ) -> Result { let locked_files = publication_point.files(); - let issuer_ca_der = issuer.ca_certificate_der.as_slice(); // Issuer CA is only required when we actually attempt to validate a subordinate CA. For some // audit-only error paths (e.g., missing CRL in the snapshot), we still want discovery to succeed. let issuer_ca_decode_error: Option; @@ -2039,7 +2352,13 @@ fn discover_children_from_fresh_snapshot_with_audit( let bytes = f .bytes_cloned() .map_err(|e| format!("snapshot CRL bytes load failed: {e}"))?; - Ok((f.rsync_uri.clone(), CachedIssuerCrl::Pending(bytes))) + Ok(( + f.rsync_uri.clone(), + CachedIssuerCrl::Pending { + bytes, + sha256_hex: Some(sha256_hex_from_32(&f.sha256)), + }, + )) }) .collect::>()?; @@ -2061,6 +2380,21 @@ fn discover_children_from_fresh_snapshot_with_audit( let mut router_skipped_non_router: u64 = 0; let mut crl_select_error: u64 = 0; let mut uri_discovery_error: u64 = 0; + let mut child_cert_cache_lookup: u64 = 0; + let mut child_cert_cache_hit: u64 = 0; + let mut child_cert_cache_hit_ca: u64 = 0; + let mut child_cert_cache_hit_router: u64 = 0; + let mut child_cert_cache_miss_not_found: u64 = 0; + let mut child_cert_cache_miss_time_gate: u64 = 0; + let mut child_cert_cache_miss_crl_missing: u64 = 0; + let mut child_cert_cache_miss_crl_invalid: u64 = 0; + let mut child_cert_cache_miss_crl_expired: u64 = 0; + let mut child_cert_cache_miss_revoked: u64 = 0; + let mut child_cert_cache_crl_recheck_hit: u64 = 0; + let mut child_cert_cache_crl_gate_reused: u64 = 0; + let mut child_cert_cache_load_error: u64 = 0; + let mut child_cert_cache_write_ok: u64 = 0; + let mut child_cert_cache_write_error: u64 = 0; let mut select_crl_nanos: u64 = 0; let mut child_decode_nanos: u64 = 0; @@ -2068,6 +2402,12 @@ fn discover_children_from_fresh_snapshot_with_audit( let mut validate_router_nanos: u64 = 0; let mut uri_discovery_nanos: u64 = 0; let mut enqueue_nanos: u64 = 0; + let mut child_cert_cache_lookup_nanos: u64 = 0; + let mut child_cert_cache_write_nanos: u64 = 0; + let child_cert_der_load_cache_hit_nanos: u64 = 0; + let mut child_cert_der_load_fresh_nanos: u64 = 0; + let child_cert_der_load_cache_hit_count: u64 = 0; + let mut child_cert_der_load_fresh_count: u64 = 0; let mut eff_ip_items_bucket_le_10: u64 = 0; let mut eff_ip_items_bucket_le_100: u64 = 0; @@ -2076,6 +2416,152 @@ fn discover_children_from_fresh_snapshot_with_audit( let mut eff_as_items_bucket_le_100: u64 = 0; let mut eff_as_items_bucket_gt_100: u64 = 0; + let mut child_cache_candidates: HashMap = + HashMap::new(); + let mut child_cache_segment_keys = Vec::::new(); + let mut child_cache_segment_dirty_projections: Option< + HashMap, + > = None; + let mut child_cert_cache_batch_lookup_publication_points: u64 = 0; + let mut child_cert_cache_batch_lookup_entries: u64 = 0; + let mut child_cert_cache_batch_lookup_errors: u64 = 0; + let mut child_cert_cache_batch_lookup_nanos: u64 = 0; + let mut child_cert_cache_mmap_lookup_publication_points: u64 = 0; + let mut child_cert_cache_mmap_lookup_entries: u64 = 0; + let mut child_cert_cache_mmap_lookup_hits: u64 = 0; + let mut child_cert_cache_mmap_lookup_misses: u64 = 0; + let mut child_cert_cache_mmap_lookup_missing_segments: u64 = 0; + let mut child_cert_cache_mmap_lookup_errors: u64 = 0; + let mut child_cert_cache_mmap_lookup_file_bytes: u64 = 0; + let mut child_cert_cache_mmap_lookup_nanos: u64 = 0; + let mut child_cert_cache_mmap_write_entries: u64 = 0; + let mut child_cert_cache_mmap_write_errors: u64 = 0; + let mut child_cert_cache_mmap_write_file_bytes: u64 = 0; + let mut child_cert_cache_mmap_write_nanos: u64 = 0; + + if let Some(cache) = cache_context { + let mut uris = Vec::new(); + let mut keys = Vec::new(); + for f in locked_files + .iter() + .filter(|file| file.rsync_uri.ends_with(".cer")) + { + uris.push(f.rsync_uri.clone()); + keys.push(child_certificate_cache_key_sha256_hex( + &f.rsync_uri, + &f.sha256, + &cache.issuer_ca_sha256, + &cache.parent_context_digest, + &cache.policy_fingerprint, + )); + } + let use_mmap_segment = keys.len() >= CHILD_CERTIFICATE_CACHE_MMAP_MIN_CER_COUNT; + if use_mmap_segment { + child_cache_segment_keys = keys.clone(); + child_cache_segment_dirty_projections = Some(HashMap::new()); + } + + let mut db_lookup_indices: Vec = Vec::new(); + if !keys.is_empty() { + if use_mmap_segment { + let mmap_lookup_started = std::time::Instant::now(); + child_cert_cache_mmap_lookup_publication_points = 1; + child_cert_cache_mmap_lookup_entries = keys.len() as u64; + match cache + .store + .get_child_certificate_cache_projections_mmap_segment( + publication_point.manifest_rsync_uri(), + &keys, + ) { + Ok(Some(lookup)) => { + child_cert_cache_mmap_lookup_hits = lookup.hits as u64; + child_cert_cache_mmap_lookup_misses = lookup.misses as u64; + child_cert_cache_mmap_lookup_file_bytes = lookup.file_bytes; + for (idx, ((uri, _key), projection)) in uris + .iter() + .zip(keys.iter()) + .zip(lookup.projections.into_iter()) + .enumerate() + { + if let Some(projection) = projection { + child_cache_candidates.insert( + uri.clone(), + ChildCertificateCacheCandidate { + projection: Some(projection), + }, + ); + } else { + db_lookup_indices.push(idx); + } + } + } + Ok(None) => { + child_cert_cache_mmap_lookup_missing_segments = 1; + db_lookup_indices.extend(0..keys.len()); + } + Err(_) => { + child_cert_cache_mmap_lookup_errors = + child_cert_cache_mmap_lookup_errors.saturating_add(keys.len() as u64); + db_lookup_indices.extend(0..keys.len()); + } + } + child_cert_cache_mmap_lookup_nanos = mmap_lookup_started + .elapsed() + .as_nanos() + .min(u128::from(u64::MAX)) + as u64; + } else { + db_lookup_indices.extend(0..keys.len()); + } + + if !db_lookup_indices.is_empty() { + let batch_lookup_started = std::time::Instant::now(); + child_cert_cache_batch_lookup_publication_points = 1; + child_cert_cache_batch_lookup_entries = db_lookup_indices.len() as u64; + let db_keys = db_lookup_indices + .iter() + .map(|idx| keys[*idx].clone()) + .collect::>(); + match cache + .store + .get_child_certificate_cache_projections_batch(&db_keys) + { + Ok(projections) => { + for (idx, projection) in db_lookup_indices + .iter() + .copied() + .zip(projections.into_iter()) + { + if let Some(projection) = projection { + remember_child_certificate_cache_dirty_projection( + &mut child_cache_segment_dirty_projections, + &projection, + ); + child_cache_candidates.insert( + uris[idx].clone(), + ChildCertificateCacheCandidate { + projection: Some(projection), + }, + ); + } + } + } + Err(_) => { + child_cert_cache_batch_lookup_errors = child_cert_cache_batch_lookup_errors + .saturating_add(child_cert_cache_batch_lookup_entries); + } + } + child_cert_cache_batch_lookup_nanos = child_cert_cache_batch_lookup_nanos + .saturating_add( + batch_lookup_started + .elapsed() + .as_nanos() + .min(u128::from(u64::MAX)) as u64, + ); + } + } + } + fn bucketize(v: usize) -> u8 { if v <= 10 { 0 @@ -2113,14 +2599,224 @@ fn discover_children_from_fresh_snapshot_with_audit( n } + let mut child_cert_crl_gate_set = ChildCertificateCacheCrlGateSet::default(); + 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 child_cert_sha256_hex = sha256_hex_from_32(&f.sha256); + + if cache_context.is_some() { + let lookup_started = std::time::Instant::now(); + child_cert_cache_lookup = child_cert_cache_lookup.saturating_add(1); + let cache_lookup_failed = child_cert_cache_batch_lookup_errors > 0 + && !child_cache_candidates.contains_key(&f.rsync_uri); + if cache_lookup_failed { + child_cert_cache_load_error = child_cert_cache_load_error.saturating_add(1); + } else { + match child_cache_candidates + .get(&f.rsync_uri) + .and_then(|candidate| candidate.projection.as_ref()) + { + Some(projection) => { + let time_gate_ok = pack_time_window_contains( + &projection.effective_not_before, + &projection.effective_until, + validation_time, + ); + if !time_gate_ok { + child_cert_cache_miss_time_gate = + child_cert_cache_miss_time_gate.saturating_add(1); + } else { + let (crl_gate, gate_reused) = child_cert_crl_gate_set.evaluate( + projection, + &mut crl_cache, + issuer_ca_der, + validation_time, + ); + if gate_reused { + child_cert_cache_crl_gate_reused = + child_cert_cache_crl_gate_reused.saturating_add(1); + } + let mut crl_gate_allows_reuse = false; + match crl_gate { + ChildCertificateCacheCrlGate::Unchanged => { + crl_gate_allows_reuse = true; + } + ChildCertificateCacheCrlGate::ChangedValid => { + match ensure_issuer_crl_verified( + &projection.issuer_crl_uri, + &mut crl_cache, + issuer_ca_der, + ) { + Ok(verified_crl) => { + if verified_crl + .revoked_serials + .contains(&projection.child_cert_serial) + { + child_cert_cache_miss_revoked = + child_cert_cache_miss_revoked.saturating_add(1); + } else { + child_cert_cache_crl_recheck_hit = + child_cert_cache_crl_recheck_hit + .saturating_add(1); + crl_gate_allows_reuse = true; + } + } + Err(_) => { + child_cert_cache_miss_crl_invalid = + child_cert_cache_miss_crl_invalid.saturating_add(1); + } + } + } + ChildCertificateCacheCrlGate::Expired => { + child_cert_cache_miss_crl_expired = + child_cert_cache_miss_crl_expired.saturating_add(1); + } + ChildCertificateCacheCrlGate::Invalid => { + child_cert_cache_miss_crl_invalid = + child_cert_cache_miss_crl_invalid.saturating_add(1); + } + ChildCertificateCacheCrlGate::Missing => { + child_cert_cache_miss_crl_missing = + child_cert_cache_miss_crl_missing.saturating_add(1); + } + } + + if crl_gate_allows_reuse { + match &projection.payload { + ChildCertificateCachePayload::ChildCa { + child_manifest_rsync_uri, + child_rsync_base_uri, + child_publication_point_rsync_uri, + child_rrdp_notification_uri, + child_effective_ip_resources, + child_effective_as_resources, + .. + } => { + child_cert_cache_lookup_nanos = + child_cert_cache_lookup_nanos.saturating_add( + lookup_started + .elapsed() + .as_nanos() + .min(u128::from(u64::MAX)) + as u64, + ); + 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: CaCertificateRef::repo_bytes( + child_cert_sha256_hex.clone(), + ), + ca_certificate_rsync_uri: Some(f.rsync_uri.clone()), + effective_ip_resources: + child_effective_ip_resources.clone(), + effective_as_resources: + child_effective_as_resources.clone(), + rsync_base_uri: child_rsync_base_uri.clone(), + manifest_rsync_uri: child_manifest_rsync_uri + .clone(), + publication_point_rsync_uri: + child_publication_point_rsync_uri.clone(), + rrdp_notification_uri: child_rrdp_notification_uri + .clone(), + }, + 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: + child_cert_sha256_hex.clone(), + }, + }); + ca_ok = ca_ok.saturating_add(1); + child_cert_cache_hit = + child_cert_cache_hit.saturating_add(1); + child_cert_cache_hit_ca = + child_cert_cache_hit_ca.saturating_add(1); + audits.push(ObjectAuditEntry { + rsync_uri: f.rsync_uri.clone(), + sha256_hex: child_cert_sha256_hex.clone(), + kind: AuditObjectKind::Certificate, + result: AuditObjectResult::Ok, + detail: Some( + "restored subordinate CA discovery from child certificate validation cache" + .to_string(), + ), + }); + continue; + } + ChildCertificateCachePayload::Router { + router_keys: cached_keys, + } => { + for cached_key in cached_keys { + router_keys.push(RouterKeyPayload { + as_id: cached_key.as_id, + ski: cached_key.ski.clone(), + spki_der: cached_key.spki_der.clone(), + source_object_uri: f.rsync_uri.clone(), + source_object_hash: child_cert_sha256_hex.clone(), + source_ee_cert_hash: child_cert_sha256_hex.clone(), + item_effective_until: cached_key + .item_effective_until + .clone(), + }); + } + router_ok = router_ok.saturating_add(1); + child_cert_cache_hit = + child_cert_cache_hit.saturating_add(1); + child_cert_cache_hit_router = + child_cert_cache_hit_router.saturating_add(1); + audits.push(ObjectAuditEntry { + rsync_uri: f.rsync_uri.clone(), + sha256_hex: child_cert_sha256_hex.clone(), + kind: AuditObjectKind::RouterCertificate, + result: AuditObjectResult::Ok, + detail: Some( + "restored BGPsec router certificate from child certificate validation cache" + .to_string(), + ), + }); + child_cert_cache_lookup_nanos = + child_cert_cache_lookup_nanos.saturating_add( + lookup_started + .elapsed() + .as_nanos() + .min(u128::from(u64::MAX)) + as u64, + ); + continue; + } + } + } + } + } + None => { + child_cert_cache_miss_not_found = + child_cert_cache_miss_not_found.saturating_add(1); + } + } + } + child_cert_cache_lookup_nanos = child_cert_cache_lookup_nanos.saturating_add( + lookup_started + .elapsed() + .as_nanos() + .min(u128::from(u64::MAX)) as u64, + ); + } + + let child_der = load_child_certificate_der_for_discovery( + f, + &mut child_cert_der_load_fresh_nanos, + &mut child_cert_der_load_fresh_count, + )?; let tdecode = std::time::Instant::now(); let child_cert = match crate::data_model::rc::ResourceCertificate::decode_der(child_der) { @@ -2263,6 +2959,84 @@ fn discover_children_from_fresh_snapshot_with_audit( item_effective_until: item_effective_until.clone(), }); } + if let Some(cache) = cache_context { + let write_started = std::time::Instant::now(); + if let Ok(verified_crl) = ensure_issuer_crl_verified( + issuer_crl_uri.as_str(), + &mut crl_cache, + issuer_ca_der, + ) { + let (effective_not_before, effective_until) = + child_certificate_cache_certificate_window( + router.resource_cert.tbs.validity_not_before, + router.resource_cert.tbs.validity_not_after, + issuer_ca_ref.tbs.validity_not_before, + issuer_ca_ref.tbs.validity_not_after, + ); + let cache_key_sha256_hex = child_certificate_cache_key_sha256_hex( + &f.rsync_uri, + &f.sha256, + &cache.issuer_ca_sha256, + &cache.parent_context_digest, + &cache.policy_fingerprint, + ); + let projection = ChildCertificateCacheProjection { + schema_version: CHILD_CERTIFICATE_CACHE_SCHEMA_VERSION, + algorithm_version: CHILD_CERTIFICATE_CACHE_ALGORITHM_VERSION, + cache_key_sha256_hex, + child_cert_uri: f.rsync_uri.clone(), + child_cert_sha256_hex: child_cert_sha256_hex.clone(), + child_cert_serial: BigUnsigned::from_biguint( + &router.resource_cert.tbs.serial_number, + ) + .bytes_be, + issuer_ca_sha256_hex: sha256_hex_from_32( + &cache.issuer_ca_sha256, + ), + issuer_crl_uri: issuer_crl_uri.clone(), + issuer_crl_sha256_hex: verified_crl.sha256_hex.clone(), + parent_context_digest: cache.parent_context_digest, + validation_policy_fingerprint: cache.policy_fingerprint, + effective_not_before, + effective_until, + payload: ChildCertificateCachePayload::Router { + router_keys: router + .asns + .iter() + .map(|as_id| ChildCertificateCacheRouterKeyProjection { + as_id: *as_id, + ski: router.subject_key_identifier.clone(), + spki_der: router.spki_der.clone(), + item_effective_until: item_effective_until.clone(), + }) + .collect(), + }, + }; + if cache + .store + .put_child_certificate_cache_projection(&projection) + .is_ok() + { + remember_child_certificate_cache_dirty_projection( + &mut child_cache_segment_dirty_projections, + &projection, + ); + child_cert_cache_write_ok = + child_cert_cache_write_ok.saturating_add(1); + } else { + child_cert_cache_write_error = + child_cert_cache_write_error.saturating_add(1); + } + } else { + child_cert_cache_write_error = + child_cert_cache_write_error.saturating_add(1); + } + child_cert_cache_write_nanos = child_cert_cache_write_nanos + .saturating_add( + write_started.elapsed().as_nanos().min(u128::from(u64::MAX)) + as u64, + ); + } audits.push(ObjectAuditEntry { rsync_uri: f.rsync_uri.clone(), sha256_hex: sha256_hex_from_32(&f.sha256), @@ -2348,39 +3122,149 @@ fn discover_children_from_fresh_snapshot_with_audit( .saturating_add(t2.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64); let t3 = std::time::Instant::now(); + let child_rsync_base_uri = uris.rsync_base_uri.clone(); + let child_manifest_rsync_uri = uris.manifest_rsync_uri.clone(); + let child_publication_point_rsync_uri = uris.publication_point_rsync_uri.clone(); + let child_rrdp_notification_uri = uris.rrdp_notification_uri.clone(); 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: CaCertificateRef::inline_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, + rsync_base_uri: child_rsync_base_uri.clone(), + manifest_rsync_uri: child_manifest_rsync_uri.clone(), + publication_point_rsync_uri: child_publication_point_rsync_uri.clone(), + rrdp_notification_uri: child_rrdp_notification_uri.clone(), }, 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), + child_ca_certificate_sha256_hex: child_cert_sha256_hex.clone(), }, }); enqueue_nanos = enqueue_nanos.saturating_add(t3.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64); + if let Some(cache) = cache_context { + let write_started = std::time::Instant::now(); + if let Ok(verified_crl) = + ensure_issuer_crl_verified(issuer_crl_uri.as_str(), &mut crl_cache, issuer_ca_der) + { + if let Some(child_ski) = validated + .child_ca + .tbs + .extensions + .subject_key_identifier + .as_ref() + { + let (effective_not_before, effective_until) = + child_certificate_cache_certificate_window( + validated.child_ca.tbs.validity_not_before, + validated.child_ca.tbs.validity_not_after, + issuer_ca_ref.tbs.validity_not_before, + issuer_ca_ref.tbs.validity_not_after, + ); + let cache_key_sha256_hex = child_certificate_cache_key_sha256_hex( + &f.rsync_uri, + &f.sha256, + &cache.issuer_ca_sha256, + &cache.parent_context_digest, + &cache.policy_fingerprint, + ); + let projection = ChildCertificateCacheProjection { + schema_version: CHILD_CERTIFICATE_CACHE_SCHEMA_VERSION, + algorithm_version: CHILD_CERTIFICATE_CACHE_ALGORITHM_VERSION, + cache_key_sha256_hex, + child_cert_uri: f.rsync_uri.clone(), + child_cert_sha256_hex: child_cert_sha256_hex.clone(), + child_cert_serial: BigUnsigned::from_biguint( + &validated.child_ca.tbs.serial_number, + ) + .bytes_be, + issuer_ca_sha256_hex: sha256_hex_from_32(&cache.issuer_ca_sha256), + issuer_crl_uri: issuer_crl_uri.clone(), + issuer_crl_sha256_hex: verified_crl.sha256_hex.clone(), + parent_context_digest: cache.parent_context_digest, + validation_policy_fingerprint: cache.policy_fingerprint, + effective_not_before, + effective_until, + payload: ChildCertificateCachePayload::ChildCa { + child_manifest_rsync_uri, + child_ski: hex::encode(child_ski), + child_rsync_base_uri, + child_publication_point_rsync_uri, + child_rrdp_notification_uri, + child_effective_ip_resources: validated.effective_ip_resources.clone(), + child_effective_as_resources: validated.effective_as_resources.clone(), + }, + }; + if cache + .store + .put_child_certificate_cache_projection(&projection) + .is_ok() + { + remember_child_certificate_cache_dirty_projection( + &mut child_cache_segment_dirty_projections, + &projection, + ); + child_cert_cache_write_ok = child_cert_cache_write_ok.saturating_add(1); + } else { + child_cert_cache_write_error = + child_cert_cache_write_error.saturating_add(1); + } + } else { + child_cert_cache_write_error = child_cert_cache_write_error.saturating_add(1); + } + } else { + child_cert_cache_write_error = child_cert_cache_write_error.saturating_add(1); + } + child_cert_cache_write_nanos = + child_cert_cache_write_nanos.saturating_add( + write_started.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), + sha256_hex: child_cert_sha256_hex.clone(), kind: AuditObjectKind::Certificate, result: AuditObjectResult::Ok, detail: Some("validated subordinate CA certificate; enqueued CA instance".to_string()), }); } + if let (Some(cache), Some(dirty_projections)) = + (cache_context, child_cache_segment_dirty_projections.take()) + { + if !dirty_projections.is_empty() { + let write_started = std::time::Instant::now(); + let projections = dirty_projections.into_values().collect::>(); + child_cert_cache_mmap_write_entries = projections.len() as u64; + match cache + .store + .write_child_certificate_cache_mmap_segment_overlay( + publication_point.manifest_rsync_uri(), + &child_cache_segment_keys, + &projections, + ) { + Ok(stats) => { + child_cert_cache_mmap_write_file_bytes = stats.file_bytes; + } + Err(_) => { + child_cert_cache_mmap_write_errors = + child_cert_cache_mmap_write_errors.saturating_add(1); + } + } + child_cert_cache_mmap_write_nanos = + write_started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64; + } + } + if let Some(t) = timing { t.record_count("child_cer_seen", cer_seen); t.record_count("child_ca_ok", ca_ok); @@ -2391,6 +3275,117 @@ fn discover_children_from_fresh_snapshot_with_audit( 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_certificate_cache_lookup", child_cert_cache_lookup); + t.record_count("child_certificate_cache_hit", child_cert_cache_hit); + t.record_count("child_certificate_cache_hit_ca", child_cert_cache_hit_ca); + t.record_count( + "child_certificate_cache_hit_router", + child_cert_cache_hit_router, + ); + t.record_count( + "child_certificate_cache_miss_not_found", + child_cert_cache_miss_not_found, + ); + t.record_count( + "child_certificate_cache_miss_time_gate", + child_cert_cache_miss_time_gate, + ); + t.record_count( + "child_certificate_cache_miss_crl_missing", + child_cert_cache_miss_crl_missing, + ); + t.record_count( + "child_certificate_cache_miss_crl_invalid", + child_cert_cache_miss_crl_invalid, + ); + t.record_count( + "child_certificate_cache_miss_crl_expired", + child_cert_cache_miss_crl_expired, + ); + t.record_count( + "child_certificate_cache_miss_revoked", + child_cert_cache_miss_revoked, + ); + t.record_count( + "child_certificate_cache_crl_recheck_hit", + child_cert_cache_crl_recheck_hit, + ); + t.record_count( + "child_certificate_cache_crl_gate_reused", + child_cert_cache_crl_gate_reused, + ); + t.record_count( + "child_certificate_cache_load_error", + child_cert_cache_load_error, + ); + t.record_count( + "child_certificate_cache_write_ok", + child_cert_cache_write_ok, + ); + t.record_count( + "child_certificate_cache_write_error", + child_cert_cache_write_error, + ); + t.record_count( + "child_certificate_cache_batch_lookup_publication_points", + child_cert_cache_batch_lookup_publication_points, + ); + t.record_count( + "child_certificate_cache_batch_lookup_entries", + child_cert_cache_batch_lookup_entries, + ); + t.record_count( + "child_certificate_cache_batch_lookup_errors", + child_cert_cache_batch_lookup_errors, + ); + t.record_count( + "child_certificate_cache_mmap_lookup_publication_points", + child_cert_cache_mmap_lookup_publication_points, + ); + t.record_count( + "child_certificate_cache_mmap_lookup_entries", + child_cert_cache_mmap_lookup_entries, + ); + t.record_count( + "child_certificate_cache_mmap_lookup_hits", + child_cert_cache_mmap_lookup_hits, + ); + t.record_count( + "child_certificate_cache_mmap_lookup_misses", + child_cert_cache_mmap_lookup_misses, + ); + t.record_count( + "child_certificate_cache_mmap_lookup_missing_segments", + child_cert_cache_mmap_lookup_missing_segments, + ); + t.record_count( + "child_certificate_cache_mmap_lookup_errors", + child_cert_cache_mmap_lookup_errors, + ); + t.record_count( + "child_certificate_cache_mmap_lookup_file_bytes", + child_cert_cache_mmap_lookup_file_bytes, + ); + t.record_count( + "child_certificate_cache_mmap_write_entries", + child_cert_cache_mmap_write_entries, + ); + t.record_count( + "child_certificate_cache_mmap_write_errors", + child_cert_cache_mmap_write_errors, + ); + t.record_count( + "child_certificate_cache_mmap_write_file_bytes", + child_cert_cache_mmap_write_file_bytes, + ); + t.record_count( + "child_certificate_der_load_cache_hit_count", + child_cert_der_load_cache_hit_count, + ); + t.record_count( + "child_certificate_der_load_fresh_count", + child_cert_der_load_fresh_count, + ); t.record_count("child_effective_ip_items_le_10", eff_ip_items_bucket_le_10); t.record_count( @@ -2420,6 +3415,38 @@ fn discover_children_from_fresh_snapshot_with_audit( ); t.record_phase_nanos("child_ca_instance_uri_discovery_total", uri_discovery_nanos); t.record_phase_nanos("child_enqueue_total", enqueue_nanos); + t.record_phase_nanos( + "child_certificate_cache_lookup_total", + child_cert_cache_lookup_nanos, + ); + t.record_phase_nanos( + "child_certificate_cache_write_total", + child_cert_cache_write_nanos, + ); + t.record_phase_nanos( + "child_certificate_cache_batch_lookup_total", + child_cert_cache_batch_lookup_nanos, + ); + t.record_phase_nanos( + "child_certificate_cache_mmap_lookup_total", + child_cert_cache_mmap_lookup_nanos, + ); + t.record_phase_nanos( + "child_certificate_cache_mmap_write_total", + child_cert_cache_mmap_write_nanos, + ); + t.record_phase_nanos( + "child_certificate_der_load_cache_hit_total", + child_cert_der_load_cache_hit_nanos, + ); + t.record_phase_nanos( + "child_certificate_der_load_fresh_total", + child_cert_der_load_fresh_nanos, + ); + t.record_phase_nanos( + "child_certificate_der_load_total", + child_cert_der_load_cache_hit_nanos.saturating_add(child_cert_der_load_fresh_nanos), + ); } Ok(ChildDiscoveryOutput { @@ -2483,10 +3510,16 @@ fn ensure_issuer_crl_verified<'a>( .expect("CRL must exist in cache"); match entry { CachedIssuerCrl::Ok(v) => Ok(v), - CachedIssuerCrl::Pending(bytes) => { + CachedIssuerCrl::Pending { + bytes, + sha256_hex: cached_sha256_hex, + } => { 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 sha256_hex = cached_sha256_hex + .take() + .unwrap_or_else(|| crate::audit::sha256_hex(&der)); let mut revoked_serials: std::collections::HashSet> = std::collections::HashSet::with_capacity(crl.revoked_certs.len()); @@ -2497,6 +3530,7 @@ fn ensure_issuer_crl_verified<'a>( *entry = CachedIssuerCrl::Ok(VerifiedIssuerCrl { crl, revoked_serials, + sha256_hex, }); match entry { CachedIssuerCrl::Ok(v) => Ok(v), @@ -3796,79 +4830,41 @@ fn parse_publication_point_cache_router_key_output( } fn restore_children_from_vcir( - store: &RocksStore, + _store: &RocksStore, ca: &CaInstanceHandle, vcir: &ValidatedCaInstanceResult, - warnings: &mut Vec, + _warnings: &mut Vec, ) -> (Vec, Vec) { let mut children = Vec::new(); let mut audits = Vec::new(); for child in &vcir.child_entries { - match store.get_blob_bytes(&child.child_cert_hash) { - Ok(Some(bytes)) => { - children.push(DiscoveredChildCaInstance { - handle: CaInstanceHandle { - depth: 0, - tal_id: ca.tal_id.clone(), - parent_manifest_rsync_uri: Some(ca.manifest_rsync_uri.clone()), - ca_certificate_der: bytes, - ca_certificate_rsync_uri: Some(child.child_cert_rsync_uri.clone()), - effective_ip_resources: child.child_effective_ip_resources.clone(), - effective_as_resources: child.child_effective_as_resources.clone(), - rsync_base_uri: child.child_rsync_base_uri.clone(), - manifest_rsync_uri: child.child_manifest_rsync_uri.clone(), - publication_point_rsync_uri: child - .child_publication_point_rsync_uri - .clone(), - rrdp_notification_uri: child.child_rrdp_notification_uri.clone(), - }, - discovered_from: crate::audit::DiscoveredFrom { - parent_manifest_rsync_uri: ca.manifest_rsync_uri.clone(), - child_ca_certificate_rsync_uri: child.child_cert_rsync_uri.clone(), - child_ca_certificate_sha256_hex: child.child_cert_hash.clone(), - }, - }); - audits.push(ObjectAuditEntry { - rsync_uri: child.child_cert_rsync_uri.clone(), - sha256_hex: child.child_cert_hash.clone(), - kind: AuditObjectKind::Certificate, - result: AuditObjectResult::Ok, - detail: Some("restored child CA instance from VCIR".to_string()), - }); - } - Ok(None) => { - warnings.push( - Warning::new("child certificate bytes missing for VCIR child restoration") - .with_context(&child.child_cert_rsync_uri), - ); - audits.push(ObjectAuditEntry { - rsync_uri: child.child_cert_rsync_uri.clone(), - sha256_hex: child.child_cert_hash.clone(), - kind: AuditObjectKind::Certificate, - result: AuditObjectResult::Error, - detail: Some( - "child certificate bytes missing for VCIR child restoration".to_string(), - ), - }); - } - Err(e) => { - warnings.push( - Warning::new(format!( - "child certificate bytes load failed for VCIR child restoration: {e}" - )) - .with_context(&child.child_cert_rsync_uri), - ); - audits.push(ObjectAuditEntry { - rsync_uri: child.child_cert_rsync_uri.clone(), - sha256_hex: child.child_cert_hash.clone(), - kind: AuditObjectKind::Certificate, - result: AuditObjectResult::Error, - detail: Some(format!( - "child certificate bytes load failed for VCIR child restoration: {e}" - )), - }); - } - } + children.push(DiscoveredChildCaInstance { + handle: CaInstanceHandle { + depth: 0, + tal_id: ca.tal_id.clone(), + parent_manifest_rsync_uri: Some(ca.manifest_rsync_uri.clone()), + ca_certificate: CaCertificateRef::repo_bytes(child.child_cert_hash.clone()), + 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()), + }); } (children, audits) } @@ -3931,7 +4927,7 @@ fn restore_children_from_publication_point_cache( } fn restore_publication_point_cache_children_parallel( - store: &RocksStore, + _store: &RocksStore, ca: &CaInstanceHandle, children: &[PublicationPointCacheChild], validation_time: time::OffsetDateTime, @@ -3943,7 +4939,7 @@ fn restore_publication_point_cache_children_parallel( 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) + restore_publication_point_cache_children_chunk(_store, ca, chunk, validation_time) })); } for handle in handles { @@ -3958,14 +4954,12 @@ fn restore_publication_point_cache_children_parallel( } fn restore_publication_point_cache_children_chunk( - store: &RocksStore, + _store: &RocksStore, ca: &CaInstanceHandle, children: &[PublicationPointCacheChild], validation_time: time::OffsetDateTime, ) -> Vec { let mut outcomes = vec![None; children.len()]; - let mut valid_positions = Vec::new(); - let mut hashes = Vec::new(); for (position, child) in children.iter().enumerate() { let effective_not_before = match parse_snapshot_time_value(&child.child_effective_not_before) { @@ -4012,68 +5006,15 @@ fn restore_publication_point_cache_children_chunk( }); 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[position] = Some(PublicationPointCacheChildRestoreOutcome { + child: Some(publication_point_cache_discovered_child(ca, child)), + warning: None, + audit: Some(publication_point_cache_child_audit( + child, + AuditObjectResult::Ok, + Some("restored child CA instance from publication-point cache".to_string()), + )), + }); } outcomes @@ -4112,14 +5053,13 @@ struct PublicationPointCacheChildRestoreOutcome { fn publication_point_cache_discovered_child( ca: &CaInstanceHandle, child: &PublicationPointCacheChild, - bytes: Vec, ) -> DiscoveredChildCaInstance { DiscoveredChildCaInstance { handle: CaInstanceHandle { depth: 0, tal_id: ca.tal_id.clone(), parent_manifest_rsync_uri: Some(ca.manifest_rsync_uri.clone()), - ca_certificate_der: bytes, + ca_certificate: CaCertificateRef::repo_bytes(child.child_cert_hash.clone()), 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(), @@ -4175,6 +5115,7 @@ fn persist_vcir_for_fresh_result_with_timing( let build_vcir_started = std::time::Instant::now(); let (vcir, build_vcir_timing) = build_vcir_from_fresh_result_with_timing( + store, ca, pack, objects, @@ -4240,7 +5181,9 @@ fn build_publication_point_cache_projection_from_fresh( pack: &PublicationPointSnapshot, vcir: &ValidatedCaInstanceResult, ) -> Result { - let ca_cert_sha256 = sha256_digest_32(&ca.ca_certificate_der); + let ca_cert_sha256 = ca + .ca_certificate_sha256_32() + .ok_or_else(|| "current CA certificate hash unavailable".to_string())?; let manifest_sha256 = sha256_digest_32(&pack.manifest_bytes); PublicationPointCacheProjection::from_vcir_with_context( vcir, @@ -4358,6 +5301,7 @@ fn signed_object_ee_not_before( } fn build_vcir_from_fresh_result_with_timing( + store: &RocksStore, ca: &CaInstanceHandle, pack: &PublicationPointSnapshot, objects: &mut crate::validation::objects::ObjectsOutput, @@ -4373,7 +5317,8 @@ fn build_vcir_from_fresh_result_with_timing( 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) + let ca_der = ca.ca_certificate_der(store)?; + let ca_cert = ResourceCertificate::decode_der(ca_der.as_ref()) .map_err(|e| format!("decode current CA certificate failed: {e}"))?; timing.current_ca_decode_ms = current_ca_decode_started.elapsed().as_millis() as u64; @@ -4382,11 +5327,12 @@ fn build_vcir_from_fresh_result_with_timing( 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)?; + let child_entries = build_vcir_child_entries(store, 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( + store, ca, pack, current_crl.file.rsync_uri.as_str(), @@ -4761,12 +5707,14 @@ pub(crate) fn build_router_key_local_outputs( } fn build_vcir_child_entries( + store: &RocksStore, discovered_children: &[DiscoveredChildCaInstance], validation_time: time::OffsetDateTime, ) -> Result, String> { let mut out = Vec::with_capacity(discovered_children.len()); for child in discovered_children { - let child_cert = ResourceCertificate::decode_der(&child.handle.ca_certificate_der) + let child_der = child.handle.ca_certificate_der(store)?; + let child_cert = ResourceCertificate::decode_der(child_der.as_ref()) .map_err(|e| format!("decode child certificate for VCIR failed: {e}"))?; let child_ski = child_cert .tbs @@ -4797,9 +5745,9 @@ 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()); + let ca_der = ca.ca_certificate_der(store)?; + let current_ca_hash = sha256_hex(ca_der.as_ref()); + let mut current_ca_entry = RawByHashEntry::from_bytes(current_ca_hash, ca_der.to_vec()); if let Some(uri) = ca.ca_certificate_rsync_uri.as_ref() { current_ca_entry.origin_uris.push(uri.clone()); } @@ -4853,6 +5801,7 @@ fn upsert_raw_by_hash_entry(store: &RocksStore, entry: RawByHashEntry) -> Result } fn build_vcir_related_artifacts( + store: &RocksStore, ca: &CaInstanceHandle, pack: &PublicationPointSnapshot, current_crl_rsync_uri: &str, @@ -4881,7 +5830,14 @@ fn build_vcir_related_artifacts( }, artifact_kind: VcirArtifactKind::Cer, uri: ca.ca_certificate_rsync_uri.clone(), - sha256: sha256_hex(&ca.ca_certificate_der), + sha256: ca + .ca_certificate_sha256_hex() + .map(str::to_string) + .unwrap_or_else(|| { + ca.ca_certificate_der(store) + .map(|bytes| sha256_hex(bytes.as_ref())) + .unwrap_or_default() + }), object_type: Some("cer".to_string()), validation_status: VcirArtifactValidationStatus::Accepted, }); diff --git a/src/validation/tree_runner/tests.rs b/src/validation/tree_runner/tests.rs index 22037de..c426240 100644 --- a/src/validation/tree_runner/tests.rs +++ b/src/validation/tree_runner/tests.rs @@ -71,6 +71,7 @@ fn sample_runner_with_ccr_accumulator<'a>( ccr_accumulator: Some(Mutex::new(CcrAccumulator::new(Vec::new()))), persist_vcir: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, } @@ -88,6 +89,7 @@ struct Generated { issuer_ca_der: Vec, child_ca_der: Vec, issuer_crl_der: Vec, + issuer_crl_der_next: Vec, } fn run(cmd: &mut Command) { @@ -254,11 +256,27 @@ authorityKeyIdentifier = keyid:always .arg("DER") .arg("-out") .arg(dir.join("issuer.crl"))); + run(Command::new("openssl") + .arg("ca") + .arg("-gencrl") + .arg("-config") + .arg(dir.join("openssl.cnf")) + .arg("-out") + .arg(dir.join("issuer-next.crl.pem"))); + run(Command::new("openssl") + .arg("crl") + .arg("-in") + .arg(dir.join("issuer-next.crl.pem")) + .arg("-outform") + .arg("DER") + .arg("-out") + .arg(dir.join("issuer-next.crl"))); Generated { issuer_ca_der: std::fs::read(dir.join("issuer.cer")).expect("read issuer der"), child_ca_der: std::fs::read(dir.join("child.cer")).expect("read child der"), issuer_crl_der: std::fs::read(dir.join("issuer.crl")).expect("read crl der"), + issuer_crl_der_next: std::fs::read(dir.join("issuer-next.crl")).expect("read next crl der"), } } @@ -731,7 +749,7 @@ fn build_vcir_local_outputs_prefers_cached_outputs() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: vec![1], + ca_certificate: CaCertificateRef::inline_der(vec![1]), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, @@ -800,7 +818,7 @@ fn persist_vcir_non_repository_evidence_stores_current_ca_cert_only() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: issuer_ca_der.clone(), + ca_certificate: CaCertificateRef::inline_der(issuer_ca_der.clone()), ca_certificate_rsync_uri: Some( "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string(), ), @@ -842,7 +860,7 @@ fn build_router_key_local_outputs_encodes_router_key_payloads() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: Vec::new(), + ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, @@ -896,7 +914,7 @@ fn build_vcir_local_outputs_falls_back_to_decoding_accepted_objects_when_cache_i depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: issuer_ca_der, + ca_certificate: CaCertificateRef::inline_der(issuer_ca_der), ca_certificate_rsync_uri: Some( "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string(), ), @@ -963,6 +981,7 @@ fn finalize_fresh_publication_point_releases_local_outputs_cache_after_persist() ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; @@ -970,7 +989,7 @@ fn finalize_fresh_publication_point_releases_local_outputs_cache_after_persist() depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: issuer_ca_der.clone(), + ca_certificate: CaCertificateRef::inline_der(issuer_ca_der.clone()), ca_certificate_rsync_uri: Some( "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string(), ), @@ -1052,7 +1071,7 @@ fn persist_vcir_for_fresh_result_stores_vcir_and_replay_meta_for_real_snapshot() depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: issuer_ca_der.clone(), + ca_certificate: CaCertificateRef::inline_der(issuer_ca_der.clone()), ca_certificate_rsync_uri: Some( "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string(), ), @@ -1122,7 +1141,7 @@ fn build_vcir_ccr_manifest_projection_from_fresh_real_snapshot_matches_manifest_ depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: issuer_ca_der, + ca_certificate: CaCertificateRef::inline_der(issuer_ca_der), ca_certificate_rsync_uri: Some( "rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string(), ), @@ -1136,8 +1155,11 @@ fn build_vcir_ccr_manifest_projection_from_fresh_real_snapshot_matches_manifest_ let child_discovery = discover_children_from_fresh_snapshot_with_audit(&ca, &pack, validation_time, None) .expect("discover children"); - let child_entries = build_vcir_child_entries(&child_discovery.children, validation_time) - .expect("build child entries"); + let store_dir = tempfile::tempdir().expect("store dir"); + let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); + let child_entries = + build_vcir_child_entries(&store, &child_discovery.children, validation_time) + .expect("build child entries"); let projection = build_vcir_ccr_manifest_projection_from_fresh(&ca, &pack, &child_entries) .expect("build ccr manifest projection"); @@ -1244,7 +1266,7 @@ fn build_vcir_related_artifacts_classifies_snapshot_files_and_audit_statuses() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: vec![0x11, 0x22], + ca_certificate: CaCertificateRef::inline_der(vec![0x11, 0x22]), ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()), effective_ip_resources: None, effective_as_resources: None, @@ -1253,6 +1275,8 @@ fn build_vcir_related_artifacts_classifies_snapshot_files_and_audit_statuses() { publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(), rrdp_notification_uri: None, }; + let store_dir = tempfile::tempdir().expect("store dir"); + let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let objects = crate::validation::objects::ObjectsOutput { vrps: Vec::new(), aspas: Vec::new(), @@ -1280,6 +1304,7 @@ fn build_vcir_related_artifacts_classifies_snapshot_files_and_audit_statuses() { roa_cache_object_meta: Vec::new(), }; let artifacts = build_vcir_related_artifacts( + &store, &ca, &pack, "rsync://example.test/repo/issuer/issuer.crl", @@ -1383,7 +1408,7 @@ fn discover_children_from_fresh_pack_discovers_child_ca() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: g.issuer_ca_der.clone(), + ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()), ca_certificate_rsync_uri: None, effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), @@ -1424,6 +1449,387 @@ fn discover_children_from_fresh_pack_discovers_child_ca() { ); } +#[test] +fn discover_children_child_certificate_cache_reuses_successful_child_ca() { + let g = generate_chain_and_crl(); + let pack = dummy_pack_with_files(vec![ + PackFile::from_bytes_compute_sha256( + "rsync://example.test/repo/issuer/issuer.crl", + g.issuer_crl_der.clone(), + ), + PackFile::from_bytes_compute_sha256( + "rsync://example.test/repo/issuer/child.cer", + g.child_ca_der.clone(), + ), + ]); + let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer"); + let issuer = CaInstanceHandle { + depth: 0, + tal_id: "test-tal".to_string(), + parent_manifest_rsync_uri: None, + ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()), + ca_certificate_rsync_uri: None, + effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), + effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), + rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), + manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(), + publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), + rrdp_notification_uri: None, + }; + let store_dir = tempfile::tempdir().expect("store dir"); + let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); + let policy = Policy::default(); + let cache_context = ChildCertificateValidationCacheContext { + store: &store, + issuer_ca_sha256: issuer.ca_certificate_sha256_32().unwrap(), + parent_context_digest: parent_context_digest_for_ca(&issuer), + policy_fingerprint: publication_point_cache_policy_fingerprint(&policy), + }; + let validation_time = time::OffsetDateTime::now_utc(); + + let first = discover_children_from_fresh_snapshot_with_audit_cached( + &issuer, + &pack, + validation_time, + None, + Some(cache_context), + ) + .expect("first discovery writes cache"); + assert_eq!(first.children.len(), 1); + + let child_file = pack + .files + .iter() + .find(|file| file.rsync_uri.ends_with("child.cer")) + .expect("child file"); + let cache_key = child_certificate_cache_key_sha256_hex( + &child_file.rsync_uri, + &child_file.sha256, + &cache_context.issuer_ca_sha256, + &cache_context.parent_context_digest, + &cache_context.policy_fingerprint, + ); + assert!( + store + .get_child_certificate_cache_projection(&cache_key) + .expect("get projection") + .is_some() + ); + + let timing = TimingHandle::new(crate::analysis::timing::TimingMeta { + recorded_at_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(), + validation_time_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(), + tal_url: None, + db_path: None, + }); + let second = discover_children_from_fresh_snapshot_with_audit_cached( + &issuer, + &pack, + validation_time, + Some(&timing), + Some(cache_context), + ) + .expect("second discovery reuses cache"); + assert_eq!(second.children.len(), 1); + assert_eq!( + second.children[0].handle.manifest_rsync_uri, + first.children[0].handle.manifest_rsync_uri + ); + assert!(second.audits.iter().any(|audit| { + audit + .detail + .as_deref() + .unwrap_or("") + .contains("child certificate validation cache") + })); + let counts = timing.counts_snapshot(); + assert_eq!( + counts.get("child_certificate_cache_hit_ca").copied(), + Some(1) + ); + assert_eq!( + counts + .get("child_certificate_cache_batch_lookup_publication_points") + .copied(), + Some(1) + ); + assert_eq!( + counts + .get("child_certificate_cache_batch_lookup_entries") + .copied(), + Some(1) + ); + assert_eq!( + counts + .get("child_certificate_der_load_fresh_count") + .copied(), + Some(0) + ); +} + +#[test] +fn discover_children_child_certificate_cache_rechecks_changed_valid_crl() { + let g = generate_chain_and_crl(); + let pack = dummy_pack_with_files(vec![ + PackFile::from_bytes_compute_sha256( + "rsync://example.test/repo/issuer/issuer.crl", + g.issuer_crl_der.clone(), + ), + PackFile::from_bytes_compute_sha256( + "rsync://example.test/repo/issuer/child.cer", + g.child_ca_der.clone(), + ), + ]); + let mut changed_pack = pack.clone(); + changed_pack.files[0] = PackFile::from_bytes_compute_sha256( + "rsync://example.test/repo/issuer/issuer.crl", + g.issuer_crl_der_next.clone(), + ); + let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer"); + let issuer = CaInstanceHandle { + depth: 0, + tal_id: "test-tal".to_string(), + parent_manifest_rsync_uri: None, + ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()), + ca_certificate_rsync_uri: None, + effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), + effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), + rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), + manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(), + publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), + rrdp_notification_uri: None, + }; + let store_dir = tempfile::tempdir().expect("store dir"); + let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); + let policy = Policy::default(); + let cache_context = ChildCertificateValidationCacheContext { + store: &store, + issuer_ca_sha256: issuer.ca_certificate_sha256_32().unwrap(), + parent_context_digest: parent_context_digest_for_ca(&issuer), + policy_fingerprint: publication_point_cache_policy_fingerprint(&policy), + }; + let validation_time = time::OffsetDateTime::now_utc(); + discover_children_from_fresh_snapshot_with_audit_cached( + &issuer, + &pack, + validation_time, + None, + Some(cache_context), + ) + .expect("populate cache"); + let child_file = pack + .files + .iter() + .find(|file| file.rsync_uri.ends_with("child.cer")) + .expect("child file"); + let cache_key = child_certificate_cache_key_sha256_hex( + &child_file.rsync_uri, + &child_file.sha256, + &cache_context.issuer_ca_sha256, + &cache_context.parent_context_digest, + &cache_context.policy_fingerprint, + ); + let projection = store + .get_child_certificate_cache_projection(&cache_key) + .expect("get child certificate cache projection") + .expect("child certificate cache projection present"); + let projected_until = + parse_snapshot_time_value(&projection.effective_until).expect("parse projected until"); + let initial_crl = crate::data_model::crl::RpkixCrl::decode_der(&g.issuer_crl_der) + .expect("decode initial crl"); + assert!( + projected_until > initial_crl.next_update.utc, + "child certificate cache projection must not be hard-capped by the issuing CRL nextUpdate" + ); + + let timing = TimingHandle::new(crate::analysis::timing::TimingMeta { + recorded_at_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(), + validation_time_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(), + tal_url: None, + db_path: None, + }); + let out = discover_children_from_fresh_snapshot_with_audit_cached( + &issuer, + &changed_pack, + validation_time, + Some(&timing), + Some(cache_context), + ) + .expect("changed valid CRL should recheck revocation and reuse cache"); + assert_eq!(out.children.len(), 1); + assert!( + out.audits + .iter() + .any(|audit| audit.detail.as_deref().unwrap_or("").contains("cache")) + ); + let counts = timing.counts_snapshot(); + assert_eq!( + counts + .get("child_certificate_cache_crl_recheck_hit") + .copied(), + Some(1) + ); + assert_eq!( + counts.get("child_certificate_cache_hit_ca").copied(), + Some(1) + ); +} + +#[test] +fn discover_children_child_certificate_cache_misses_when_current_crl_invalid() { + let g = generate_chain_and_crl(); + let pack = dummy_pack_with_files(vec![ + PackFile::from_bytes_compute_sha256( + "rsync://example.test/repo/issuer/issuer.crl", + g.issuer_crl_der.clone(), + ), + PackFile::from_bytes_compute_sha256( + "rsync://example.test/repo/issuer/child.cer", + g.child_ca_der.clone(), + ), + ]); + let mut changed_pack = pack.clone(); + changed_pack.files[0] = PackFile::from_bytes_compute_sha256( + "rsync://example.test/repo/issuer/issuer.crl", + b"not-a-valid-crl".to_vec(), + ); + let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer"); + let issuer = CaInstanceHandle { + depth: 0, + tal_id: "test-tal".to_string(), + parent_manifest_rsync_uri: None, + ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()), + ca_certificate_rsync_uri: None, + effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), + effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), + rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), + manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(), + publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), + rrdp_notification_uri: None, + }; + let store_dir = tempfile::tempdir().expect("store dir"); + let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); + let policy = Policy::default(); + let cache_context = ChildCertificateValidationCacheContext { + store: &store, + issuer_ca_sha256: issuer.ca_certificate_sha256_32().unwrap(), + parent_context_digest: parent_context_digest_for_ca(&issuer), + policy_fingerprint: publication_point_cache_policy_fingerprint(&policy), + }; + let validation_time = time::OffsetDateTime::now_utc(); + discover_children_from_fresh_snapshot_with_audit_cached( + &issuer, + &pack, + validation_time, + None, + Some(cache_context), + ) + .expect("populate cache"); + + let timing = TimingHandle::new(crate::analysis::timing::TimingMeta { + recorded_at_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(), + validation_time_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(), + tal_url: None, + db_path: None, + }); + let out = discover_children_from_fresh_snapshot_with_audit_cached( + &issuer, + &changed_pack, + validation_time, + Some(&timing), + Some(cache_context), + ) + .expect("invalid CRL should miss cache and continue with audit error"); + assert!(out.children.is_empty()); + assert!( + out.audits + .iter() + .all(|audit| !audit.detail.as_deref().unwrap_or("").contains("cache")) + ); + let counts = timing.counts_snapshot(); + assert_eq!( + counts + .get("child_certificate_cache_miss_crl_invalid") + .copied(), + Some(1) + ); +} + +#[test] +fn discover_children_child_certificate_cache_misses_when_unchanged_crl_expired() { + let g = generate_chain_and_crl(); + let pack = dummy_pack_with_files(vec![ + PackFile::from_bytes_compute_sha256( + "rsync://example.test/repo/issuer/issuer.crl", + g.issuer_crl_der.clone(), + ), + PackFile::from_bytes_compute_sha256( + "rsync://example.test/repo/issuer/child.cer", + g.child_ca_der.clone(), + ), + ]); + let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer"); + let issuer = CaInstanceHandle { + depth: 0, + tal_id: "test-tal".to_string(), + parent_manifest_rsync_uri: None, + ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()), + ca_certificate_rsync_uri: None, + effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), + effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), + rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(), + manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(), + publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), + rrdp_notification_uri: None, + }; + let store_dir = tempfile::tempdir().expect("store dir"); + let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); + let policy = Policy::default(); + let cache_context = ChildCertificateValidationCacheContext { + store: &store, + issuer_ca_sha256: issuer.ca_certificate_sha256_32().unwrap(), + parent_context_digest: parent_context_digest_for_ca(&issuer), + policy_fingerprint: publication_point_cache_policy_fingerprint(&policy), + }; + let validation_time = time::OffsetDateTime::now_utc(); + discover_children_from_fresh_snapshot_with_audit_cached( + &issuer, + &pack, + validation_time, + None, + Some(cache_context), + ) + .expect("populate cache"); + + let timing = TimingHandle::new(crate::analysis::timing::TimingMeta { + recorded_at_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(), + validation_time_utc_rfc3339: "2026-06-24T00:00:00Z".to_string(), + tal_url: None, + db_path: None, + }); + let out = discover_children_from_fresh_snapshot_with_audit_cached( + &issuer, + &pack, + validation_time + time::Duration::days(2), + Some(&timing), + Some(cache_context), + ) + .expect("expired unchanged CRL should miss cache and continue with audit error"); + assert!(out.children.is_empty()); + assert!( + out.audits + .iter() + .all(|audit| !audit.detail.as_deref().unwrap_or("").contains("cache")) + ); + let counts = timing.counts_snapshot(); + assert_eq!( + counts + .get("child_certificate_cache_miss_crl_expired") + .copied(), + Some(1) + ); +} + #[test] fn discover_children_with_audit_records_missing_crl_for_child_certificate() { let now = time::OffsetDateTime::now_utc(); @@ -1445,7 +1851,7 @@ fn discover_children_with_audit_records_missing_crl_for_child_certificate() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: vec![1], + ca_certificate: CaCertificateRef::inline_der(vec![1]), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, @@ -1530,6 +1936,7 @@ fn runner_offline_rsync_fixture_produces_pack_and_warnings() { ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; @@ -1548,7 +1955,7 @@ fn runner_offline_rsync_fixture_produces_pack_and_warnings() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: issuer_ca_der, + ca_certificate: CaCertificateRef::inline_der(issuer_ca_der), ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), @@ -1632,7 +2039,7 @@ fn runner_roa_validation_cache_reuses_vcir_outputs_on_second_fixture_run() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: issuer_ca_der, + ca_certificate: CaCertificateRef::inline_der(issuer_ca_der), ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), @@ -1663,6 +2070,7 @@ fn runner_roa_validation_cache_reuses_vcir_outputs_on_second_fixture_run() { ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; @@ -1693,6 +2101,7 @@ fn runner_roa_validation_cache_reuses_vcir_outputs_on_second_fixture_run() { ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: true, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; @@ -1749,7 +2158,7 @@ fn runner_publication_point_cache_observe_and_reuse_path() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: issuer_ca_der, + ca_certificate: CaCertificateRef::inline_der(issuer_ca_der), ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), @@ -1780,6 +2189,7 @@ fn runner_publication_point_cache_observe_and_reuse_path() { ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: true, enable_publication_point_validation_cache: false, }; @@ -1821,6 +2231,7 @@ fn runner_publication_point_cache_observe_and_reuse_path() { ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: true, enable_publication_point_validation_cache: false, }; @@ -1858,6 +2269,7 @@ fn runner_publication_point_cache_observe_and_reuse_path() { ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: true, }; @@ -1902,7 +2314,7 @@ fn seed_publication_point_cache_projection( &vcir, ca.publication_point_rsync_uri.clone(), ca.ca_certificate_rsync_uri.clone(), - sha256_32(&ca.ca_certificate_der), + ca.ca_certificate_sha256_32().unwrap(), sha256_32(b"manifest-bytes"), ta_context_digest_for_ca(ca), parent_context_digest_for_ca(ca), @@ -2003,7 +2415,7 @@ fn publication_point_cache_fixture_ca() -> CaInstanceHandle { depth: 1, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: b"ca-cert".to_vec(), + ca_certificate: CaCertificateRef::inline_der(b"ca-cert".to_vec()), ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()), effective_ip_resources: None, effective_as_resources: None, @@ -2049,6 +2461,7 @@ fn runner_publication_point_cache_reuses_projection_outputs_children_and_ccr() { ccr_accumulator: Some(Mutex::new(CcrAccumulator::new(Vec::new()))), persist_vcir: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: true, }; @@ -2174,11 +2587,15 @@ fn publication_point_cache_restore_children_parallel_keeps_order_and_audit() { assert_eq!(restored_children.len(), 300); assert_eq!(audits.len(), 300); assert_eq!( - restored_children[0].handle.ca_certificate_der, - b"child-cert-0" + restored_children[0].handle.ca_certificate_sha256_hex(), + Some(sha256_hex(b"child-cert-0").as_str()) ); assert_eq!( - restored_children[299].handle.ca_certificate_der, + restored_children[299] + .handle + .ca_certificate_der(&store) + .unwrap() + .as_ref(), b"child-cert-299" ); assert_eq!( @@ -2211,6 +2628,47 @@ fn publication_point_cache_restore_children_parallel_keeps_order_and_audit() { ); } +#[test] +fn publication_point_cache_restore_children_does_not_require_child_der_bytes() { + let store_dir = tempfile::tempdir().expect("store dir"); + let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); + let policy = Policy::default(); + let validation_time = time::OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(1); + let ca = publication_point_cache_fixture_ca(); + seed_publication_point_cache_projection(&store, &policy, &ca, validation_time); + let mut projection = store + .get_publication_point_cache_projection(&ca.manifest_rsync_uri) + .expect("load projection") + .expect("projection"); + projection.children[0].child_cert_hash = "22".repeat(32); + let mut warnings = Vec::new(); + + let (restored_children, audits) = restore_children_from_publication_point_cache( + &store, + &ca, + &projection, + validation_time, + &mut warnings, + 1, + None, + ); + + assert!(warnings.is_empty()); + assert!(!restored_children.is_empty()); + assert_eq!(audits.len(), restored_children.len()); + assert_eq!( + restored_children[0].handle.ca_certificate_sha256_hex(), + Some(projection.children[0].child_cert_hash.as_str()) + ); + assert!( + restored_children[0] + .handle + .ca_certificate_der(&store) + .is_err(), + "lazy child handle should not require repo bytes until a fresh path asks for DER" + ); +} + #[test] fn runner_publication_point_cache_blocks_parent_policy_and_output_time_mismatch() { let store_dir = tempfile::tempdir().expect("store dir"); @@ -2250,6 +2708,7 @@ fn runner_publication_point_cache_blocks_parent_policy_and_output_time_mismatch( ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: true, }; @@ -2298,6 +2757,7 @@ fn runner_publication_point_cache_blocks_parent_policy_and_output_time_mismatch( ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: true, }; @@ -2342,6 +2802,7 @@ fn runner_publication_point_cache_blocks_parent_policy_and_output_time_mismatch( ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: true, }; @@ -2402,7 +2863,7 @@ fn runner_rsync_dedup_skips_second_sync_for_same_base() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: issuer_ca_der, + ca_certificate: CaCertificateRef::inline_der(issuer_ca_der), ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), @@ -2453,6 +2914,7 @@ fn runner_rsync_dedup_skips_second_sync_for_same_base() { ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; @@ -2509,7 +2971,7 @@ fn runner_rsync_dedup_skips_second_sync_for_same_module_scope() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: issuer_ca_der, + ca_certificate: CaCertificateRef::inline_der(issuer_ca_der), ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), @@ -2569,6 +3031,7 @@ fn runner_rsync_dedup_skips_second_sync_for_same_module_scope() { ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; @@ -2628,7 +3091,7 @@ fn runner_rsync_dedup_works_in_rsync_only_mode_even_when_rrdp_notify_exists() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: issuer_ca_der, + ca_certificate: CaCertificateRef::inline_der(issuer_ca_der), ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), @@ -2688,6 +3151,7 @@ fn runner_rsync_dedup_works_in_rsync_only_mode_even_when_rrdp_notify_exists() { ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; @@ -2746,7 +3210,7 @@ fn runner_when_repo_sync_fails_uses_current_instance_vcir_and_keeps_children_emp depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: issuer_ca_der, + ca_certificate: CaCertificateRef::inline_der(issuer_ca_der), ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), @@ -2778,6 +3242,7 @@ fn runner_when_repo_sync_fails_uses_current_instance_vcir_and_keeps_children_emp ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; @@ -2812,6 +3277,7 @@ fn runner_when_repo_sync_fails_uses_current_instance_vcir_and_keeps_children_emp ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; @@ -2844,7 +3310,7 @@ fn build_publication_point_audit_emits_no_audit_entry_for_duplicate_pack_uri() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: vec![1], + ca_certificate: CaCertificateRef::inline_der(vec![1]), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, @@ -2907,7 +3373,7 @@ fn build_publication_point_audit_marks_invalid_crl_as_error_and_overlays_roa_aud depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: vec![1], + ca_certificate: CaCertificateRef::inline_der(vec![1]), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, @@ -2994,7 +3460,7 @@ fn discover_children_with_router_certificate_records_ok_audit_and_no_child() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: g.issuer_ca_der.clone(), + ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()), ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), @@ -3042,7 +3508,7 @@ fn discover_children_with_non_router_ee_certificate_records_skipped_audit() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: g.issuer_ca_der.clone(), + ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()), ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), @@ -3090,7 +3556,7 @@ fn discover_children_with_invalid_router_certificate_records_error_audit() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: g.issuer_ca_der.clone(), + ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()), ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), @@ -3139,7 +3605,7 @@ fn discover_children_with_audit_records_decode_error_for_corrupt_cer() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: g.issuer_ca_der.clone(), + ca_certificate: CaCertificateRef::inline_der(g.issuer_ca_der.clone()), ca_certificate_rsync_uri: None, effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), @@ -3175,7 +3641,10 @@ fn select_issuer_crl_uri_for_child_covers_missing_and_not_found_paths() { let mut cache = std::collections::HashMap::new(); cache.insert( "rsync://example.test/repo/issuer/issuer.crl".to_string(), - CachedIssuerCrl::Pending(g.issuer_crl_der.clone()), + CachedIssuerCrl::Pending { + bytes: g.issuer_crl_der.clone(), + sha256_hex: None, + }, ); let err = select_issuer_crl_uri_for_child(&ta, &cache).unwrap_err(); assert!(err.contains("CRLDistributionPoints missing"), "{err}"); @@ -3183,7 +3652,10 @@ fn select_issuer_crl_uri_for_child_covers_missing_and_not_found_paths() { let mut wrong = std::collections::HashMap::new(); wrong.insert( "rsync://example.test/repo/issuer/other.crl".to_string(), - CachedIssuerCrl::Pending(g.issuer_crl_der), + CachedIssuerCrl::Pending { + bytes: g.issuer_crl_der, + sha256_hex: None, + }, ); let err = select_issuer_crl_uri_for_child(&child, &wrong).unwrap_err(); assert!( @@ -3199,7 +3671,10 @@ fn ensure_issuer_crl_verified_promotes_pending_cache_entry() { let crl_uri = "rsync://example.test/repo/issuer/issuer.crl".to_string(); cache.insert( crl_uri.clone(), - CachedIssuerCrl::Pending(g.issuer_crl_der.clone()), + CachedIssuerCrl::Pending { + bytes: g.issuer_crl_der.clone(), + sha256_hex: None, + }, ); let first = ensure_issuer_crl_verified(&crl_uri, &mut cache, &g.issuer_ca_der) @@ -3230,7 +3705,7 @@ fn discover_children_with_invalid_issuer_der_records_error_audit() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: vec![0u8], + ca_certificate: CaCertificateRef::inline_der(vec![0u8]), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, @@ -3287,7 +3762,7 @@ fn project_current_instance_vcir_reuses_local_outputs_and_restores_children() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: Vec::new(), + ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, @@ -3378,7 +3853,7 @@ fn project_current_instance_vcir_returns_no_output_when_instance_gate_expired() depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: Vec::new(), + ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, @@ -3428,7 +3903,7 @@ fn project_current_instance_vcir_keeps_real_fresh_validation_warning() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: Vec::new(), + ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, @@ -3476,7 +3951,7 @@ fn project_current_instance_vcir_returns_no_output_when_latest_result_missing() depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: Vec::new(), + ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, @@ -3527,7 +4002,7 @@ fn project_current_instance_vcir_returns_no_output_when_latest_result_is_ineligi depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: Vec::new(), + ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, @@ -3578,7 +4053,7 @@ fn project_current_instance_vcir_rejects_mismatched_ccr_projection_uri() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: Vec::new(), + ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, @@ -3611,7 +4086,7 @@ fn fresh_and_reuse_paths_produce_equivalent_ccr_manifest_projection() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: issuer_ca_der, + ca_certificate: CaCertificateRef::inline_der(issuer_ca_der), ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()), effective_ip_resources: None, effective_as_resources: None, @@ -3624,7 +4099,10 @@ fn fresh_and_reuse_paths_produce_equivalent_ccr_manifest_projection() { discover_children_from_fresh_snapshot_with_audit(&ca, &pack, validation_time, None) .expect("discover children"); let mut objects = empty_objects_output(); + let store_dir = tempfile::tempdir().expect("store dir"); + let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let (fresh_vcir, _timing) = build_vcir_from_fresh_result_with_timing( + &store, &ca, &pack, &mut objects, @@ -3765,6 +4243,7 @@ fn runner_roa_validation_cache_uses_projection_not_full_vcir_fallback() { ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: true, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; @@ -3956,7 +4435,7 @@ fn build_publication_point_audit_from_vcir_uses_vcir_metadata_and_overlays_child depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: Vec::new(), + ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, @@ -4055,7 +4534,7 @@ fn build_publication_point_audit_from_vcir_failed_no_cache_keeps_current_reject_ depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: Vec::new(), + ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, @@ -4148,7 +4627,7 @@ fn rejected_manifest_audit_entry_for_failed_fetch_uses_current_repo_hash() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: Vec::new(), + ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, @@ -4188,7 +4667,7 @@ fn build_publication_point_audit_from_vcir_without_cached_inputs_returns_empty_l depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: Vec::new(), + ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, @@ -4283,7 +4762,7 @@ fn reconstruct_snapshot_from_vcir_reports_missing_manifest_and_related_raw_bytes depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: Vec::new(), + ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, @@ -4380,7 +4859,7 @@ fn reconstruct_snapshot_from_vcir_reads_repo_bytes_without_raw_entries() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: Vec::new(), + ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, @@ -4465,7 +4944,7 @@ fn runner_dedup_paths_execute_with_timing_enabled() { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: issuer_ca_der, + ca_certificate: CaCertificateRef::inline_der(issuer_ca_der), ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()), effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(), effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(), @@ -4502,6 +4981,7 @@ fn runner_dedup_paths_execute_with_timing_enabled() { ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; @@ -4539,6 +5019,7 @@ fn runner_dedup_paths_execute_with_timing_enabled() { ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; diff --git a/tests/test_apnic_stats_live_stage2.rs b/tests/test_apnic_stats_live_stage2.rs index 929c82a..a526fdb 100644 --- a/tests/test_apnic_stats_live_stage2.rs +++ b/tests/test_apnic_stats_live_stage2.rs @@ -190,6 +190,7 @@ fn apnic_tree_full_stats_serial() { ccr_accumulator: None, persist_vcir: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, }; @@ -224,6 +225,7 @@ fn apnic_tree_full_stats_serial() { persist_vcir: true, build_ccr_accumulator: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, enable_transport_request_prefetch: false, diff --git a/tests/test_apnic_tree_live_m15.rs b/tests/test_apnic_tree_live_m15.rs index d8d7cc1..d1e7f95 100644 --- a/tests/test_apnic_tree_live_m15.rs +++ b/tests/test_apnic_tree_live_m15.rs @@ -40,6 +40,7 @@ fn apnic_tree_depth1_processes_more_than_root() { persist_vcir: true, build_ccr_accumulator: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, enable_transport_request_prefetch: false, @@ -85,6 +86,7 @@ fn apnic_tree_root_only_processes_root_with_long_timeouts() { persist_vcir: true, build_ccr_accumulator: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, enable_transport_request_prefetch: false, diff --git a/tests/test_deterministic_semantics_m4.rs b/tests/test_deterministic_semantics_m4.rs index 00bf75f..3745479 100644 --- a/tests/test_deterministic_semantics_m4.rs +++ b/tests/test_deterministic_semantics_m4.rs @@ -5,8 +5,8 @@ use rpki::validation::manifest::PublicationPointSource; use rpki::validation::objects::process_publication_point_snapshot_for_issuer; use rpki::validation::publication_point::PublicationPointSnapshot; use rpki::validation::tree::{ - CaInstanceHandle, PublicationPointRunResult, PublicationPointRunner, TreeRunConfig, - run_tree_serial_audit, + CaCertificateRef, CaInstanceHandle, PublicationPointRunResult, PublicationPointRunner, + TreeRunConfig, run_tree_serial_audit, }; fn fixture_bytes(path: &str) -> Vec { @@ -46,7 +46,10 @@ impl PublicationPointRunner for SinglePackRunner { let objects = process_publication_point_snapshot_for_issuer( &self.snapshot, &self.policy, - &ca.ca_certificate_der, + match &ca.ca_certificate { + CaCertificateRef::InlineDer(bytes) => bytes.as_slice(), + other => panic!("test runner expects inline CA DER, got {other:?}"), + }, ca.ca_certificate_rsync_uri.as_deref(), ca.effective_ip_resources.as_ref(), ca.effective_as_resources.as_ref(), @@ -92,7 +95,9 @@ fn crl_mismatch_drops_publication_point_and_cites_rfc_sections() { parent_manifest_rsync_uri: None, // Use a real, parseable CA certificate DER so objects processing can reach CRL selection. // The test only asserts CRLDP/locked-pack error handling, not signature chaining. - ca_certificate_der: fixture_bytes("tests/fixtures/ta/apnic-ta.cer"), + ca_certificate: CaCertificateRef::inline_der(fixture_bytes( + "tests/fixtures/ta/apnic-ta.cer", + )), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, @@ -114,6 +119,7 @@ fn crl_mismatch_drops_publication_point_and_cites_rfc_sections() { persist_vcir: true, build_ccr_accumulator: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, enable_transport_request_prefetch: false, diff --git a/tests/test_run_tree_from_tal_offline_m17.rs b/tests/test_run_tree_from_tal_offline_m17.rs index cc83c99..d2f4741 100644 --- a/tests/test_run_tree_from_tal_offline_m17.rs +++ b/tests/test_run_tree_from_tal_offline_m17.rs @@ -6,7 +6,7 @@ use rpki::validation::run_tree_from_tal::{ run_tree_from_tal_and_ta_der_serial_audit_with_timing, run_tree_from_tal_url_serial, run_tree_from_tal_url_serial_audit, run_tree_from_tal_url_serial_audit_with_timing, }; -use rpki::validation::tree::TreeRunConfig; +use rpki::validation::tree::{CaCertificateRef, TreeRunConfig}; use std::collections::HashMap; @@ -71,10 +71,12 @@ fn root_handle_is_constructible_from_fixture_tal_and_ta() { discovery.ca_instance.manifest_rsync_uri ); assert_eq!(root.rsync_base_uri, discovery.ca_instance.rsync_base_uri); - assert!( - root.ca_certificate_der.len() > 100, - "TA der should be non-empty" - ); + match &root.ca_certificate { + CaCertificateRef::InlineDer(bytes) => { + assert!(bytes.len() > 100, "TA der should be non-empty"); + } + other => panic!("TA root certificate should be inline DER, got {other:?}"), + } } #[test] @@ -118,6 +120,7 @@ fn run_tree_from_tal_url_entry_executes_and_records_failure_when_repo_empty() { persist_vcir: true, build_ccr_accumulator: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, enable_transport_request_prefetch: false, @@ -169,6 +172,7 @@ fn run_tree_from_tal_and_ta_der_entry_executes_and_records_failure_when_repo_emp persist_vcir: true, build_ccr_accumulator: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, enable_transport_request_prefetch: false, @@ -228,6 +232,7 @@ fn run_tree_from_tal_url_audit_entry_collects_no_publication_points_when_repo_em persist_vcir: true, build_ccr_accumulator: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, enable_transport_request_prefetch: false, @@ -275,6 +280,7 @@ fn run_tree_from_tal_and_ta_der_audit_entry_collects_no_publication_points_when_ persist_vcir: true, build_ccr_accumulator: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, enable_transport_request_prefetch: false, @@ -330,6 +336,7 @@ fn run_tree_from_tal_url_audit_with_timing_records_phases_when_repo_empty() { persist_vcir: true, build_ccr_accumulator: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, enable_transport_request_prefetch: false, @@ -384,6 +391,7 @@ fn run_tree_from_tal_and_ta_der_audit_with_timing_records_phases_when_repo_empty persist_vcir: true, build_ccr_accumulator: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, enable_transport_request_prefetch: false, diff --git a/tests/test_tree_failure_handling.rs b/tests/test_tree_failure_handling.rs index 000c83d..2765701 100644 --- a/tests/test_tree_failure_handling.rs +++ b/tests/test_tree_failure_handling.rs @@ -7,8 +7,8 @@ use rpki::validation::manifest::PublicationPointSource; use rpki::validation::objects::{ObjectsOutput, ObjectsStats}; use rpki::validation::publication_point::PublicationPointSnapshot; use rpki::validation::tree::{ - CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult, PublicationPointRunner, - TreeRunConfig, run_tree_serial, + CaCertificateRef, CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult, + PublicationPointRunner, TreeRunConfig, run_tree_serial, }; fn empty_snapshot(manifest_uri: &str, pp_uri: &str) -> PublicationPointSnapshot { @@ -36,7 +36,7 @@ fn ca_handle(manifest_uri: &str) -> CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: Vec::new(), + ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, diff --git a/tests/test_tree_traversal_m14.rs b/tests/test_tree_traversal_m14.rs index 59b93e1..7a37464 100644 --- a/tests/test_tree_traversal_m14.rs +++ b/tests/test_tree_traversal_m14.rs @@ -7,8 +7,8 @@ use rpki::validation::manifest::PublicationPointSource; use rpki::validation::objects::{ObjectsOutput, ObjectsStats, RouterKeyPayload}; use rpki::validation::publication_point::PublicationPointSnapshot; use rpki::validation::tree::{ - CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult, PublicationPointRunner, - TreeRunConfig, run_tree_serial, run_tree_serial_audit, + CaCertificateRef, CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult, + PublicationPointRunner, TreeRunConfig, run_tree_serial, run_tree_serial_audit, }; #[derive(Default)] @@ -69,7 +69,7 @@ fn ca_handle(manifest_uri: &str) -> CaInstanceHandle { depth: 0, tal_id: "test-tal".to_string(), parent_manifest_rsync_uri: None, - ca_certificate_der: Vec::new(), + ca_certificate: CaCertificateRef::inline_der(Vec::new()), ca_certificate_rsync_uri: None, effective_ip_resources: None, effective_as_resources: None, @@ -314,6 +314,7 @@ fn tree_respects_max_depth_and_max_instances() { persist_vcir: true, build_ccr_accumulator: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, enable_transport_request_prefetch: false, @@ -333,6 +334,7 @@ fn tree_respects_max_depth_and_max_instances() { persist_vcir: true, build_ccr_accumulator: true, enable_roa_validation_cache: false, + enable_child_certificate_validation_cache: false, publication_point_cache_observe_only: false, enable_publication_point_validation_cache: false, enable_transport_request_prefetch: false,