From 4047214ddfdc55d8c2f94b6194468a0842b76415 Mon Sep 17 00:00:00 2001 From: yuyr Date: Thu, 11 Jun 2026 19:13:48 +0800 Subject: [PATCH] =?UTF-8?q?20260609=20CIR=E5=8C=BA=E5=88=86fresh=E5=92=8Cc?= =?UTF-8?q?ached=E9=AA=8C=E8=AF=81=E8=BE=93=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/audit.rs | 4 + src/bin/cir_drop_report.rs | 2 +- src/bin/cir_dump_reject_list.rs | 33 ++- src/bin/cir_probe_rpki_client_cache.rs | 26 +-- src/bin/cir_state_compare.rs | 67 +++--- src/bin/cir_ta_only_fixture.rs | 20 +- src/bin/triage_ccr_cir_pair.rs | 61 ++--- src/cir/decode.rs | 81 +++++-- src/cir/encode.rs | 30 ++- src/cir/export.rs | 190 +++++++++------ src/cir/materialize.rs | 148 +++++------- src/cir/mod.rs | 196 ++++++++-------- src/cir/model.rs | 247 ++++++++++++++++++-- src/tools/rpki_artifact_metrics.rs | 189 +++++++++++++-- src/tools/sequence_triage_ccr_cir.rs | 46 +++- src/tools/sequence_triage_ccr_cir/loader.rs | 12 +- src/validation/tree_runner.rs | 51 ++-- src/validation/tree_runner/tests.rs | 3 + tests/test_cir_delta_export_m1.rs | 41 ++-- tests/test_cir_drop_report_m5.rs | 22 +- tests/test_cir_matrix_m9.rs | 21 +- tests/test_cir_peer_replay_m8.rs | 21 +- tests/test_cir_sequence_m2.rs | 50 ++-- tests/test_cir_sequence_peer_replay_m4.rs | 21 +- tests/test_cir_sequence_replay_m3.rs | 21 +- tests/test_cli_run_offline_m18.rs | 3 +- 26 files changed, 1054 insertions(+), 552 deletions(-) diff --git a/src/audit.rs b/src/audit.rs index cdbd29e..e466b07 100644 --- a/src/audit.rs +++ b/src/audit.rs @@ -87,6 +87,10 @@ pub struct PublicationPointAudit { pub warnings: Vec, pub objects: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub cir_fresh_objects: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub cir_cached_objects: Vec, } #[derive(Clone, Debug, PartialEq, Eq, Serialize)] diff --git a/src/bin/cir_drop_report.rs b/src/bin/cir_drop_report.rs index d15f811..229a5f6 100644 --- a/src/bin/cir_drop_report.rs +++ b/src/bin/cir_drop_report.rs @@ -127,7 +127,7 @@ fn main() -> Result<(), String> { .map_err(|e| format!("open repo bytes db failed: {e}"))?; let mut object_hash_by_uri = BTreeMap::new(); - for object in &cir.objects { + for object in cir.validated_objects() { object_hash_by_uri.insert(object.rsync_uri.clone(), hex::encode(&object.sha256)); } diff --git a/src/bin/cir_dump_reject_list.rs b/src/bin/cir_dump_reject_list.rs index b02fe90..506b3d7 100644 --- a/src/bin/cir_dump_reject_list.rs +++ b/src/bin/cir_dump_reject_list.rs @@ -56,9 +56,24 @@ fn real_main() -> Result<(), String> { .map_err(|e| format!("read cir failed: {}: {e}", cir_path.display()))?; let cir = rpki::cir::decode_cir(&bytes).map_err(|e| format!("decode cir failed: {e}"))?; - println!("object_count={}", cir.objects.len()); + println!("version={}", cir.version); + println!("object_count={}", cir.validated_object_count()); + println!("fresh_object_count={}", cir.fresh_validated_objects.len()); + println!("cached_object_count={}", cir.cached_validated_objects.len()); + println!( + "object_list_sha256={}", + hex::encode(&cir.object_list_sha256) + ); + println!( + "fresh_object_list_sha256={}", + hex::encode(&cir.fresh_object_list_sha256) + ); + println!( + "cached_object_list_sha256={}", + hex::encode(&cir.cached_object_list_sha256) + ); println!("trust_anchor_count={}", cir.trust_anchors.len()); - for (index, item) in cir.objects.iter().take(args.limit).enumerate() { + for (index, item) in cir.validated_objects().take(args.limit).enumerate() { println!( "{:04} object={} sha256={}", index + 1, @@ -70,8 +85,18 @@ fn real_main() -> Result<(), String> { "reject_list_sha256={}", hex::encode(&cir.reject_list_sha256) ); - println!("reject_count={}", cir.rejected_objects.len()); - for (index, item) in cir.rejected_objects.iter().take(args.limit).enumerate() { + println!("reject_count={}", cir.rejected_object_count()); + println!("fresh_reject_count={}", cir.fresh_rejected_objects.len()); + println!("cached_reject_count={}", cir.cached_rejected_objects.len()); + println!( + "fresh_reject_list_sha256={}", + hex::encode(&cir.fresh_reject_list_sha256) + ); + println!( + "cached_reject_list_sha256={}", + hex::encode(&cir.cached_reject_list_sha256) + ); + for (index, item) in cir.rejected_objects().take(args.limit).enumerate() { println!( "{:04} uri={} reason={}", index + 1, diff --git a/src/bin/cir_probe_rpki_client_cache.rs b/src/bin/cir_probe_rpki_client_cache.rs index f42a64d..bbe440e 100644 --- a/src/bin/cir_probe_rpki_client_cache.rs +++ b/src/bin/cir_probe_rpki_client_cache.rs @@ -108,13 +108,11 @@ fn run(args: Args) -> Result<(), String> { let peer = decode_cir(&read_file(peer_cir_path)?) .map_err(|e| format!("decode rpki-client CIR failed: {e}"))?; let peer_objects = peer - .objects - .iter() + .validated_objects() .map(|item| item.rsync_uri.as_str()) .collect::>(); let only_in_ours = ours - .objects - .iter() + .validated_objects() .filter(|item| !peer_objects.contains(item.rsync_uri.as_str())) .map(|item| ProbeObject { uri: item.rsync_uri.clone(), @@ -442,10 +440,7 @@ fn uri_extension(uri: &str) -> String { #[cfg(test)] mod tests { use super::*; - use rpki::cir::{ - CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, - CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, encode_cir, - }; + use rpki::cir::{CanonicalInputRepresentation, CirObject, CirTrustAnchor, encode_cir}; #[test] fn parse_args_accepts_required_flags() { @@ -663,15 +658,14 @@ mod tests { }) .collect::>(); objects.sort_by(|left, right| left.rsync_uri.cmp(&right.rsync_uri)); - CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: time::OffsetDateTime::UNIX_EPOCH, + CanonicalInputRepresentation::new_v4( + time::OffsetDateTime::UNIX_EPOCH, objects, - trust_anchors: vec![sample_trust_anchor()], - reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), - rejected_objects: Vec::::new(), - } + Vec::new(), + vec![sample_trust_anchor()], + Vec::new(), + Vec::new(), + ) } fn sample_trust_anchor() -> CirTrustAnchor { diff --git a/src/bin/cir_state_compare.rs b/src/bin/cir_state_compare.rs index 0e11225..0e68190 100644 --- a/src/bin/cir_state_compare.rs +++ b/src/bin/cir_state_compare.rs @@ -95,23 +95,19 @@ fn run(args: Args) -> Result<(), String> { .map_err(|e| format!("decode rpki-client CIR failed: {e}"))?; let ours_objects = ours - .objects - .iter() + .validated_objects() .map(|item| (item.rsync_uri.clone(), hex::encode(&item.sha256))) .collect::>(); let peer_objects = peer - .objects - .iter() + .validated_objects() .map(|item| (item.rsync_uri.clone(), hex::encode(&item.sha256))) .collect::>(); let ours_rejects = ours - .rejected_objects - .iter() + .rejected_objects() .map(|item| item.object_uri.clone()) .collect::>(); let peer_rejects = peer - .rejected_objects - .iter() + .rejected_objects() .map(|item| item.object_uri.clone()) .collect::>(); let ours_trust_anchors = ours @@ -153,18 +149,18 @@ fn run(args: Args) -> Result<(), String> { "trust_anchors": trust_anchor_summary.to_json(), "rejectListSha256Match": reject_hash_match, "ours": { - "objectCount": ours.objects.len(), + "objectCount": ours.validated_object_count(), "talCount": ours.trust_anchors.len(), "trustAnchorCount": ours.trust_anchors.len(), - "rejectCount": ours.rejected_objects.len(), + "rejectCount": ours.rejected_object_count(), "rejectListSha256": hex::encode(&ours.reject_list_sha256), "validationTime": ours.validation_time.to_string(), }, "rpkiClient": { - "objectCount": peer.objects.len(), + "objectCount": peer.validated_object_count(), "talCount": peer.trust_anchors.len(), "trustAnchorCount": peer.trust_anchors.len(), - "rejectCount": peer.rejected_objects.len(), + "rejectCount": peer.rejected_object_count(), "rejectListSha256": hex::encode(&peer.reject_list_sha256), "validationTime": peer.validation_time.to_string(), } @@ -428,8 +424,8 @@ fn uri_extension(uri: &str) -> String { mod tests { use super::*; use rpki::cir::{ - CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, - CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, encode_cir, sha256, + CanonicalInputRepresentation, CirObject, CirRejectedObject, CirTrustAnchor, encode_cir, + sha256, }; #[test] @@ -533,7 +529,7 @@ mod tests { assert_eq!(summary["rejects"]["match"], true); assert_eq!(summary["trustAnchors"]["match"], true); assert_eq!(summary["rejectListSha256Match"], true); - assert_eq!(summary["ours"]["objectCount"], 2); + assert_eq!(summary["ours"]["objectCount"], 3); assert!( std::fs::read_to_string(out_md) .expect("read markdown") @@ -721,6 +717,24 @@ mod tests { trust_anchors: &[&str], rejected_objects: &[(&str, Option<&str>)], ) -> CanonicalInputRepresentation { + let mut cir_objects = objects + .iter() + .map(|(rsync_uri, fill)| CirObject { + rsync_uri: (*rsync_uri).to_string(), + sha256: vec![*fill; 32], + }) + .collect::>(); + for (object_uri, _) in rejected_objects { + if cir_objects + .iter() + .all(|object| object.rsync_uri != *object_uri) + { + cir_objects.push(CirObject { + rsync_uri: (*object_uri).to_string(), + sha256: vec![0xee; 32], + }); + } + } let rejected_objects = rejected_objects .iter() .map(|(object_uri, reason)| CirRejectedObject { @@ -728,18 +742,11 @@ mod tests { reason: reason.map(str::to_string), }) .collect::>(); - CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: sample_time(), - objects: objects - .iter() - .map(|(rsync_uri, fill)| CirObject { - rsync_uri: (*rsync_uri).to_string(), - sha256: vec![*fill; 32], - }) - .collect(), - trust_anchors: trust_anchors + CanonicalInputRepresentation::new_v4( + sample_time(), + cir_objects, + Vec::new(), + trust_anchors .iter() .map(|tal_uri| { let name = tal_uri @@ -758,11 +765,9 @@ mod tests { } }) .collect(), - reject_list_sha256: compute_reject_list_sha256( - rejected_objects.iter().map(|item| item.object_uri.as_str()), - ), rejected_objects, - } + Vec::new(), + ) } fn write_cir(path: &Path, cir: &CanonicalInputRepresentation) { diff --git a/src/bin/cir_ta_only_fixture.rs b/src/bin/cir_ta_only_fixture.rs index 322facd..b0dc458 100644 --- a/src/bin/cir_ta_only_fixture.rs +++ b/src/bin/cir_ta_only_fixture.rs @@ -1,9 +1,6 @@ use std::path::PathBuf; -use rpki::cir::{ - CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirTrustAnchor, - compute_reject_list_sha256, encode_cir, sha256, -}; +use rpki::cir::{CanonicalInputRepresentation, CirTrustAnchor, encode_cir, sha256}; const USAGE: &str = "Usage: cir_ta_only_fixture --tal-path --ta-path --tal-uri --validation-time --cir-out --repo-bytes-db "; @@ -102,21 +99,20 @@ fn main() -> Result<(), String> { let ta_certificate_sha256 = sha256(&ta_bytes); - let cir = CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, + let cir = CanonicalInputRepresentation::new_v4( validation_time, - objects: Vec::new(), - trust_anchors: vec![CirTrustAnchor { + Vec::new(), + Vec::new(), + vec![CirTrustAnchor { ta_rsync_uri, tal_uri, tal_bytes, ta_certificate_der: ta_bytes, ta_certificate_sha256, }], - reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), - rejected_objects: Vec::new(), - }; + Vec::new(), + Vec::new(), + ); let der = encode_cir(&cir).map_err(|e| format!("encode cir failed: {e}"))?; if let Some(parent) = cir_out.parent() { std::fs::create_dir_all(parent).map_err(|e| format!("create cir parent failed: {e}"))?; diff --git a/src/bin/triage_ccr_cir_pair.rs b/src/bin/triage_ccr_cir_pair.rs index 5bdfa38..6ab2d4d 100644 --- a/src/bin/triage_ccr_cir_pair.rs +++ b/src/bin/triage_ccr_cir_pair.rs @@ -273,23 +273,19 @@ fn build_cir_summary(args: &Args) -> Result { let left = decode_cir(&read_file(&args.left_cir)?).map_err(|e| e.to_string())?; let right = decode_cir(&read_file(&args.right_cir)?).map_err(|e| e.to_string())?; let left_objects = left - .objects - .iter() + .validated_objects() .map(|item| (item.rsync_uri.clone(), hex::encode(&item.sha256))) .collect::>(); let right_objects = right - .objects - .iter() + .validated_objects() .map(|item| (item.rsync_uri.clone(), hex::encode(&item.sha256))) .collect::>(); let left_rejects = left - .rejected_objects - .iter() + .rejected_objects() .map(|item| item.object_uri.clone()) .collect::>(); let right_rejects = right - .rejected_objects - .iter() + .rejected_objects() .map(|item| item.object_uri.clone()) .collect::>(); let left_trust_anchors = left @@ -338,15 +334,15 @@ fn build_cir_summary(args: &Args) -> Result { "rejectListSha256Match": reject_hash_match, "validationTimeMatch": validation_time_match, "left": { - "objectCount": left.objects.len(), - "rejectCount": left.rejected_objects.len(), + "objectCount": left.validated_object_count(), + "rejectCount": left.rejected_object_count(), "trustAnchorCount": left.trust_anchors.len(), "rejectListSha256": hex::encode(&left.reject_list_sha256), "validationTime": format_time(left.validation_time)?, }, "right": { - "objectCount": right.objects.len(), - "rejectCount": right.rejected_objects.len(), + "objectCount": right.validated_object_count(), + "rejectCount": right.rejected_object_count(), "trustAnchorCount": right.trust_anchors.len(), "rejectListSha256": hex::encode(&right.reject_list_sha256), "validationTime": format_time(right.validation_time)?, @@ -977,8 +973,8 @@ mod tests { build_aspa_payload_state, build_roa_payload_state, encode_content_info, }; use rpki::cir::{ - CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, - CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, encode_cir, sha256, + CanonicalInputRepresentation, CirObject, CirRejectedObject, CirTrustAnchor, encode_cir, + sha256, }; use rpki::data_model::roa::{IpPrefix, RoaAfi}; use rpki::validation::objects::{AspaAttestation, Vrp}; @@ -1193,6 +1189,7 @@ mod tests { codes, vec![ "P1_TRUST_ANCHOR_DIFFERENCE", + "P2_OBJECT_URI_SET_DIFFERENCE", "P3_OBJECT_CONTENT_HASH_DIFFERENCE", "P4_REJECT_DECISION_DIFFERENCE", "P5_VALIDATION_TIME_DIFFERENCE", @@ -1392,20 +1389,30 @@ mod tests { }] }) .unwrap_or_default(); - CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time, - objects: vec![CirObject { - rsync_uri: "rsync://example.net/repo/a.roa".to_string(), - sha256: vec![object_hash_fill; 32], - }], - trust_anchors: vec![trust_anchor], - reject_list_sha256: compute_reject_list_sha256( - rejected_objects.iter().map(|item| item.object_uri.as_str()), - ), - rejected_objects, + let mut objects = vec![CirObject { + rsync_uri: "rsync://example.net/repo/a.roa".to_string(), + sha256: vec![object_hash_fill; 32], + }]; + for rejected in &rejected_objects { + if !objects + .iter() + .any(|object| object.rsync_uri == rejected.object_uri) + { + objects.push(CirObject { + rsync_uri: rejected.object_uri.clone(), + sha256: vec![object_hash_fill; 32], + }); + } } + objects.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri)); + CanonicalInputRepresentation::new_v4( + validation_time, + objects, + Vec::new(), + vec![trust_anchor], + rejected_objects, + Vec::new(), + ) } fn sample_trust_anchor(name: &str) -> CirTrustAnchor { diff --git a/src/cir/decode.rs b/src/cir/decode.rs index cc27001..b99e6f1 100644 --- a/src/cir/decode.rs +++ b/src/cir/decode.rs @@ -1,5 +1,5 @@ use crate::cir::model::{ - CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirRejectedObject, + CIR_VERSION_V4, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirRejectedObject, CirTrustAnchor, }; use crate::data_model::common::DerReader; @@ -32,9 +32,9 @@ pub fn decode_cir(der: &[u8]) -> Result Result Result Result Result, CirEncodeError> { cir.validate().map_err(CirEncodeError::Validate)?; Ok(encode_sequence(&[ - encode_integer_u32(cir.version), + encode_integer_u32(CIR_VERSION_V4), encode_oid(match cir.hash_alg { CirHashAlgorithm::Sha256 => OID_SHA256_RAW, }), encode_generalized_time(cir.validation_time), encode_sequence( - &cir.objects + &cir.fresh_validated_objects + .iter() + .map(encode_object) + .collect::, _>>()?, + ), + encode_sequence( + &cir.cached_validated_objects .iter() .map(encode_object) .collect::, _>>()?, @@ -30,9 +36,20 @@ pub fn encode_cir(cir: &CanonicalInputRepresentation) -> Result, CirEnco .map(encode_trust_anchor) .collect::, _>>()?, ), + encode_octet_string(&cir.object_list_sha256), + encode_octet_string(&cir.fresh_object_list_sha256), + encode_octet_string(&cir.cached_object_list_sha256), encode_octet_string(&cir.reject_list_sha256), + encode_octet_string(&cir.fresh_reject_list_sha256), + encode_octet_string(&cir.cached_reject_list_sha256), encode_sequence( - &cir.rejected_objects + &cir.fresh_rejected_objects + .iter() + .map(encode_rejected_object) + .collect::, _>>()?, + ), + encode_sequence( + &cir.cached_rejected_objects .iter() .map(encode_rejected_object) .collect::, _>>()?, @@ -160,8 +177,3 @@ fn encode_len_into(len: usize, out: &mut Vec) { out.push(0x80 | (len_bytes.len() as u8)); out.extend_from_slice(len_bytes); } - -#[allow(dead_code)] -const _: () = { - let _ = CIR_VERSION_V3; -}; diff --git a/src/cir/export.rs b/src/cir/export.rs index b1057c3..f5d0084 100644 --- a/src/cir/export.rs +++ b/src/cir/export.rs @@ -2,11 +2,10 @@ use std::collections::BTreeMap; use std::collections::BTreeSet; use std::path::Path; -use crate::audit::{AuditObjectResult, PublicationPointAudit}; +use crate::audit::{AuditObjectResult, ObjectAuditEntry, PublicationPointAudit}; use crate::cir::encode::{CirEncodeError, encode_cir}; use crate::cir::model::{ - CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirRejectedObject, - CirTrustAnchor, compute_reject_list_sha256, + CanonicalInputRepresentation, CirObject, CirRejectedObject, CirTrustAnchor, }; use crate::cir::static_pool::{ CirStaticPoolError, CirStaticPoolExportSummary, export_hashes_from_store, @@ -99,12 +98,45 @@ fn insert_consumed_object_hash( Ok(()) } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum CirObjectSection { + Fresh, + Cached, +} + +fn publication_point_uses_explicit_cir_sections(pp: &PublicationPointAudit) -> bool { + !pp.cir_fresh_objects.is_empty() || !pp.cir_cached_objects.is_empty() +} + +fn publication_point_is_cached_fallback(pp: &PublicationPointAudit) -> bool { + pp.source == "vcir_current_instance" || pp.repo_terminal_state == "fallback_current_instance" +} + +fn publication_point_cir_entries<'a>( + pp: &'a PublicationPointAudit, + section: CirObjectSection, +) -> &'a [ObjectAuditEntry] { + if publication_point_uses_explicit_cir_sections(pp) { + return match section { + CirObjectSection::Fresh => &pp.cir_fresh_objects, + CirObjectSection::Cached => &pp.cir_cached_objects, + }; + } + + match (section, publication_point_is_cached_fallback(pp)) { + (CirObjectSection::Fresh, false) => &pp.objects, + (CirObjectSection::Cached, true) => &pp.objects, + _ => &[], + } +} + fn collect_cir_objects_from_validation_audit( publication_points: &[PublicationPointAudit], + section: CirObjectSection, ) -> Result, CirExportError> { let mut objects = BTreeMap::new(); for pp in publication_points { - for obj in &pp.objects { + for obj in publication_point_cir_entries(pp, section) { if !matches!(obj.result, AuditObjectResult::Ok | AuditObjectResult::Error) { continue; } @@ -114,6 +146,35 @@ fn collect_cir_objects_from_validation_audit( Ok(objects) } +fn collect_rejected_objects_from_validation_audit( + publication_points: &[PublicationPointAudit], + section: CirObjectSection, +) -> Vec { + let mut rejected_objects = publication_points + .iter() + .flat_map(|pp| publication_point_cir_entries(pp, section).iter()) + .filter(|item| item.result == AuditObjectResult::Error) + .filter(|item| item.rsync_uri.starts_with("rsync://")) + .map(|item| CirRejectedObject { + object_uri: item.rsync_uri.clone(), + reason: item.detail.clone(), + }) + .collect::>(); + rejected_objects.sort_by(|a, b| a.object_uri.cmp(&b.object_uri)); + rejected_objects.dedup_by(|a, b| a.object_uri == b.object_uri); + rejected_objects +} + +fn cir_objects_from_hash_map(objects: BTreeMap) -> Vec { + objects + .into_iter() + .map(|(rsync_uri, sha256_hex)| CirObject { + rsync_uri, + sha256: hex::decode(sha256_hex).expect("validated hex"), + }) + .collect() +} + fn canonical_ta_rsync_uri(trust_anchor: &TrustAnchor) -> Result { if let Some(uri) = &trust_anchor.resolved_ta_uri && uri.scheme() == "rsync" @@ -162,7 +223,10 @@ pub fn build_cir_from_run_multi( } } - let objects = collect_cir_objects_from_validation_audit(publication_points)?; + let fresh_objects = + collect_cir_objects_from_validation_audit(publication_points, CirObjectSection::Fresh)?; + let cached_objects = + collect_cir_objects_from_validation_audit(publication_points, CirObjectSection::Cached)?; let mut trust_anchors = Vec::with_capacity(tal_bindings.len()); for binding in tal_bindings { @@ -178,41 +242,21 @@ pub fn build_cir_from_run_multi( } trust_anchors.sort_by(|a, b| a.ta_rsync_uri.cmp(&b.ta_rsync_uri)); - let cir = CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: validation_time.to_offset(time::UtcOffset::UTC), - objects: objects - .into_iter() - .map(|(rsync_uri, sha256_hex)| CirObject { - rsync_uri, - sha256: hex::decode(sha256_hex).expect("validated hex"), - }) - .collect(), - trust_anchors, - reject_list_sha256: Vec::new(), - rejected_objects: Vec::new(), - }; - let mut rejected_objects = publication_points - .iter() - .flat_map(|pp| pp.objects.iter()) - .filter(|item| item.result == AuditObjectResult::Error) - .filter(|item| item.rsync_uri.starts_with("rsync://")) - .map(|item| CirRejectedObject { - object_uri: item.rsync_uri.clone(), - reason: item.detail.clone(), - }) - .collect::>(); - rejected_objects.sort_by(|a, b| a.object_uri.cmp(&b.object_uri)); - rejected_objects.dedup_by(|a, b| a.object_uri == b.object_uri); + let fresh_rejected_objects = + collect_rejected_objects_from_validation_audit(publication_points, CirObjectSection::Fresh); + let cached_rejected_objects = collect_rejected_objects_from_validation_audit( + publication_points, + CirObjectSection::Cached, + ); - let cir = CanonicalInputRepresentation { - reject_list_sha256: compute_reject_list_sha256( - rejected_objects.iter().map(|item| item.object_uri.as_str()), - ), - rejected_objects, - ..cir - }; + let cir = CanonicalInputRepresentation::new_v4( + validation_time, + cir_objects_from_hash_map(fresh_objects), + cir_objects_from_hash_map(cached_objects), + trust_anchors, + fresh_rejected_objects, + cached_rejected_objects, + ); cir.validate().map_err(CirExportError::Validate)?; Ok(cir) } @@ -239,8 +283,7 @@ pub fn export_cir_static_pool( ) -> Result { let _ = trust_anchors; let hashes = cir - .objects - .iter() + .validated_objects() .map(|item| hex::encode(&item.sha256)) .collect::>(); export_hashes_from_store(store, static_root, capture_date_utc, &hashes).map_err(Into::into) @@ -254,8 +297,7 @@ pub fn export_cir_raw_store( ) -> Result { let _ = trust_anchors; let unique: BTreeSet = cir - .objects - .iter() + .validated_objects() .map(|item| hex::encode(&item.sha256)) .collect(); @@ -337,7 +379,7 @@ pub fn export_cir_from_run_multi( let write_cir_ms = started.elapsed().as_millis() as u64; Ok(CirExportSummary { - object_count: cir.objects.len(), + object_count: cir.validated_object_count(), trust_anchor_count: cir.trust_anchors.len(), timing: CirExportTiming { build_cir_ms, @@ -437,19 +479,19 @@ mod tests { &publication_points, ) .expect("build cir"); - assert_eq!(cir.version, CIR_VERSION_V3); + assert_eq!(cir.version, crate::cir::model::CIR_VERSION_V4); assert_eq!(cir.trust_anchors.len(), 1); assert_eq!( cir.trust_anchors[0].tal_uri, "https://example.test/root.tal" ); assert!( - cir.objects + cir.fresh_validated_objects .iter() .any(|item| item.rsync_uri == "rsync://example.test/repo/a.cer") ); assert!( - !cir.objects + !cir.fresh_validated_objects .iter() .any(|item| item.rsync_uri == cir.trust_anchors[0].ta_rsync_uri) ); @@ -610,12 +652,12 @@ mod tests { .expect("build cir"); assert!( - cir.objects + cir.cached_validated_objects .iter() .any(|item| item.rsync_uri == "rsync://example.test/repo/fallback.mft") ); assert!( - cir.objects + cir.cached_validated_objects .iter() .any(|item| item.rsync_uri == "rsync://example.test/repo/fallback.roa") ); @@ -664,19 +706,19 @@ mod tests { assert_eq!(cir.trust_anchors.len(), 2); assert!( - cir.objects + cir.fresh_validated_objects .iter() .any(|item| item.rsync_uri == "rsync://example.test/repo/consumed.roa") ); assert!( - !cir.objects + !cir.fresh_validated_objects .iter() .any(|item| item.rsync_uri == "rsync://example.test/repo/superfluous.roa"), "current repo objects must not be included unless validation consumed them", ); for trust_anchor in &cir.trust_anchors { assert!( - !cir.objects + !cir.fresh_validated_objects .iter() .any(|item| item.rsync_uri == trust_anchor.ta_rsync_uri), "trust anchor rsync objects must not be included in CIR.objects", @@ -765,33 +807,33 @@ mod tests { ) .expect("build cir"); - assert_eq!(cir.rejected_objects.len(), 2); + assert_eq!(cir.rejected_object_count(), 2); assert_eq!( - cir.rejected_objects[0].object_uri, + cir.fresh_rejected_objects[0].object_uri, "rsync://example.test/repo/a.roa" ); assert_eq!( - cir.rejected_objects[0].reason.as_deref(), + cir.fresh_rejected_objects[0].reason.as_deref(), Some("invalid roa") ); assert_eq!( - cir.rejected_objects[1].object_uri, + cir.fresh_rejected_objects[1].object_uri, "rsync://example.test/repo/c.roa" ); assert!( - cir.objects + cir.fresh_validated_objects .iter() .any(|item| item.rsync_uri == "rsync://example.test/repo/a.roa"), "rejected audit objects were still consumed as validation input", ); assert!( - cir.objects + cir.fresh_validated_objects .iter() .any(|item| item.rsync_uri == "rsync://example.test/repo/c.roa"), "rejected audit objects were still consumed as validation input", ); assert!( - !cir.objects + !cir.fresh_validated_objects .iter() .any(|item| item.rsync_uri == "rsync://example.test/repo/b.asa"), "skipped audit objects are not considered consumed input", @@ -829,20 +871,20 @@ mod tests { .expect("build cir"); assert!( - cir.objects + cir.fresh_validated_objects .iter() .any(|item| item.rsync_uri == manifest_uri), "manifest rejected because issuer CRL is expired was still read as validation input", ); - assert_eq!(cir.rejected_objects.len(), 1); - assert_eq!(cir.rejected_objects[0].object_uri, manifest_uri); + assert_eq!(cir.rejected_object_count(), 1); + assert_eq!(cir.fresh_rejected_objects[0].object_uri, manifest_uri); assert_eq!( - cir.rejected_objects[0].reason.as_deref(), + cir.fresh_rejected_objects[0].reason.as_deref(), Some(reject_reason) ); assert_eq!( cir.reject_list_sha256, - compute_reject_list_sha256([manifest_uri].into_iter()) + crate::cir::model::compute_reject_list_sha256([manifest_uri].into_iter()) ); } @@ -894,21 +936,25 @@ mod tests { .expect("build cir"); assert!( - cir.objects + cir.fresh_validated_objects .iter() .any(|item| item.rsync_uri == manifest_uri), "rejected manifest is still a current-run validation input", ); assert!( - !cir.objects.iter().any(|item| item.rsync_uri == roa_uri), + !cir.fresh_validated_objects + .iter() + .any(|item| item.rsync_uri == roa_uri), "ROA listed by a rejected manifest must not enter CIR objects", ); assert!( - !cir.objects.iter().any(|item| item.rsync_uri == crl_uri), + !cir.fresh_validated_objects + .iter() + .any(|item| item.rsync_uri == crl_uri), "CRL listed by a rejected manifest must not enter CIR objects", ); - assert_eq!(cir.rejected_objects.len(), 1); - assert_eq!(cir.rejected_objects[0].object_uri, manifest_uri); + assert_eq!(cir.rejected_object_count(), 1); + assert_eq!(cir.fresh_rejected_objects[0].object_uri, manifest_uri); } #[test] @@ -953,8 +999,8 @@ mod tests { assert_eq!(cir_a.reject_list_sha256, cir_b.reject_list_sha256); assert_ne!( - cir_a.rejected_objects[0].reason, - cir_b.rejected_objects[0].reason + cir_a.fresh_rejected_objects[0].reason, + cir_b.fresh_rejected_objects[0].reason ); } @@ -1083,7 +1129,7 @@ mod tests { assert_eq!(summary.reused_entries, 0); let mut cir_missing_object = cir_only_tas.clone(); - cir_missing_object.objects.push(CirObject { + cir_missing_object.fresh_validated_objects.push(CirObject { rsync_uri: "rsync://example.test/repo/missing.roa".to_string(), sha256: vec![0x44; 32], }); diff --git a/src/cir/materialize.rs b/src/cir/materialize.rs index 9c06952..f637eed 100644 --- a/src/cir/materialize.rs +++ b/src/cir/materialize.rs @@ -86,7 +86,7 @@ pub fn materialize_cir( let mut linked_files = 0usize; let mut copied_files = 0usize; - for object in &cir.objects { + for object in cir.validated_objects() { let sha256_hex = hex::encode(&object.sha256); let source = resolve_static_pool_file(static_root, &sha256_hex)?; let relative = mirror_relative_path_for_rsync_uri(&object.rsync_uri)?; @@ -140,7 +140,7 @@ pub fn materialize_cir( } Ok(CirMaterializeSummary { - object_count: cir.objects.len(), + object_count: cir.validated_object_count(), trust_anchor_count: cir.trust_anchors.len(), materialized_file_count: expected.len(), linked_files, @@ -165,7 +165,7 @@ pub fn materialize_cir_from_raw_store( })?; let mut copied_files = 0usize; - for object in &cir.objects { + for object in cir.validated_objects() { let sha256_hex = hex::encode(&object.sha256); let bytes = raw_store .get_blob_bytes(&sha256_hex) @@ -206,7 +206,7 @@ pub fn materialize_cir_from_raw_store( } Ok(CirMaterializeSummary { - object_count: cir.objects.len(), + object_count: cir.validated_object_count(), trust_anchor_count: cir.trust_anchors.len(), materialized_file_count: expected.len(), linked_files: 0, @@ -232,7 +232,7 @@ pub fn materialize_cir_from_repo_bytes( })?; let mut copied_files = 0usize; - for object in &cir.objects { + for object in cir.validated_objects() { let sha256_hex = hex::encode(&object.sha256); let bytes = repo_bytes .get_blob_bytes(&sha256_hex) @@ -273,7 +273,7 @@ pub fn materialize_cir_from_repo_bytes( } Ok(CirMaterializeSummary { - object_count: cir.objects.len(), + object_count: cir.validated_object_count(), trust_anchor_count: cir.trust_anchors.len(), materialized_file_count: expected.len(), linked_files: 0, @@ -325,8 +325,7 @@ fn write_bytes_to_mirror_uri( } fn expected_materialized_uris(cir: &CanonicalInputRepresentation) -> BTreeSet { - cir.objects - .iter() + cir.validated_objects() .map(|item| item.rsync_uri.clone()) .chain( cir.trust_anchors @@ -435,10 +434,7 @@ mod tests { resolve_static_pool_file, }; use crate::blob_store::{ExternalRawStoreDb, ExternalRepoBytesDb}; - use crate::cir::model::{ - CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, - CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, sha256, - }; + use crate::cir::model::{CanonicalInputRepresentation, CirObject, CirTrustAnchor, sha256}; use sha2::Digest; use std::path::{Path, PathBuf}; @@ -463,15 +459,9 @@ mod tests { } fn sample_cir() -> CanonicalInputRepresentation { - let rejected_objects = vec![CirRejectedObject { - object_uri: "rsync://example.net/repo/rejected-a.roa".to_string(), - reason: Some("invalid roa".to_string()), - }]; - CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: sample_time(), - objects: vec![ + CanonicalInputRepresentation::new_v4( + sample_time(), + vec![ CirObject { rsync_uri: "rsync://example.net/repo/a.cer".to_string(), sha256: hex::decode( @@ -487,20 +477,17 @@ mod tests { .unwrap(), }, ], - trust_anchors: vec![sample_trust_anchor()], - reject_list_sha256: compute_reject_list_sha256( - rejected_objects.iter().map(|item| item.object_uri.as_str()), - ), - rejected_objects, - } + Vec::new(), + vec![sample_trust_anchor()], + Vec::new(), + Vec::new(), + ) } fn cir_with_real_hashes(a: &[u8], b: &[u8]) -> CanonicalInputRepresentation { - CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: sample_time(), - objects: vec![ + CanonicalInputRepresentation::new_v4( + sample_time(), + vec![ CirObject { rsync_uri: "rsync://example.net/repo/a.cer".to_string(), sha256: sha2::Sha256::digest(a).to_vec(), @@ -510,10 +497,11 @@ mod tests { sha256: sha2::Sha256::digest(b).to_vec(), }, ], - trust_anchors: vec![sample_trust_anchor()], - reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), - rejected_objects: Vec::new(), - } + Vec::new(), + vec![sample_trust_anchor()], + Vec::new(), + Vec::new(), + ) } #[test] @@ -661,31 +649,14 @@ mod tests { let mirror_root = td.path().join("mirror"); let a = b"a".to_vec(); let b = b"b".to_vec(); - let cir = CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: sample_time(), - objects: vec![ - CirObject { - rsync_uri: "rsync://example.net/repo/a.cer".to_string(), - sha256: sha2::Sha256::digest(&a).to_vec(), - }, - CirObject { - rsync_uri: "rsync://example.net/repo/nested/b.roa".to_string(), - sha256: sha2::Sha256::digest(&b).to_vec(), - }, - ], - trust_anchors: vec![sample_trust_anchor()], - reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), - rejected_objects: Vec::new(), - }; + let cir = cir_with_real_hashes(&a, &b); { let raw_store = ExternalRawStoreDb::open(&raw_store_path).unwrap(); raw_store .put_blob_bytes_batch(&[ - (hex::encode(&cir.objects[0].sha256), a), - (hex::encode(&cir.objects[1].sha256), b), + (hex::encode(&cir.fresh_validated_objects[0].sha256), a), + (hex::encode(&cir.fresh_validated_objects[1].sha256), b), ]) .unwrap(); } @@ -720,7 +691,10 @@ mod tests { { let raw_store = ExternalRawStoreDb::open(&raw_store_path).unwrap(); raw_store - .put_blob_bytes_batch(&[(hex::encode(&cir.objects[0].sha256), b"a".to_vec())]) + .put_blob_bytes_batch(&[( + hex::encode(&cir.fresh_validated_objects[0].sha256), + b"a".to_vec(), + )]) .unwrap(); } @@ -742,8 +716,14 @@ mod tests { let raw_store = ExternalRawStoreDb::open(&raw_store_path).unwrap(); raw_store .put_blob_bytes_batch(&[ - (hex::encode(&cir.objects[0].sha256), b"a".to_vec()), - (hex::encode(&cir.objects[1].sha256), b"b".to_vec()), + ( + hex::encode(&cir.fresh_validated_objects[0].sha256), + b"a".to_vec(), + ), + ( + hex::encode(&cir.fresh_validated_objects[1].sha256), + b"b".to_vec(), + ), ]) .unwrap(); } @@ -767,8 +747,14 @@ mod tests { let raw_store = ExternalRawStoreDb::open(&raw_store_path).unwrap(); raw_store .put_blob_bytes_batch(&[ - (hex::encode(&cir.objects[0].sha256), a.clone()), - (hex::encode(&cir.objects[1].sha256), b.clone()), + ( + hex::encode(&cir.fresh_validated_objects[0].sha256), + a.clone(), + ), + ( + hex::encode(&cir.fresh_validated_objects[1].sha256), + b.clone(), + ), ]) .unwrap(); } @@ -787,21 +773,20 @@ mod tests { let td = tempfile::tempdir().unwrap(); let raw_store_path = td.path().join("raw-store.db"); let mirror_root = td.path().join("mirror"); - let cir = CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: sample_time(), - objects: vec![CirObject { + let cir = CanonicalInputRepresentation::new_v4( + sample_time(), + vec![CirObject { rsync_uri: "rsync://example.net/repo/a.cer".to_string(), sha256: hex::decode( "1111111111111111111111111111111111111111111111111111111111111111", ) .unwrap(), }], - trust_anchors: vec![sample_trust_anchor()], - reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), - rejected_objects: Vec::new(), - }; + Vec::new(), + vec![sample_trust_anchor()], + Vec::new(), + Vec::new(), + ); { let raw_store = ExternalRawStoreDb::open(&raw_store_path).unwrap(); raw_store @@ -841,31 +826,14 @@ mod tests { let mirror_root = td.path().join("mirror"); let a = b"a".to_vec(); let b = b"b".to_vec(); - let cir = CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: sample_time(), - objects: vec![ - CirObject { - rsync_uri: "rsync://example.net/repo/a.cer".to_string(), - sha256: sha2::Sha256::digest(&a).to_vec(), - }, - CirObject { - rsync_uri: "rsync://example.net/repo/nested/b.roa".to_string(), - sha256: sha2::Sha256::digest(&b).to_vec(), - }, - ], - trust_anchors: vec![sample_trust_anchor()], - reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), - rejected_objects: Vec::new(), - }; + let cir = cir_with_real_hashes(&a, &b); { let repo_bytes = ExternalRepoBytesDb::open(&repo_bytes_db).unwrap(); repo_bytes .put_blob_bytes_batch(&[ - (hex::encode(&cir.objects[0].sha256), a), - (hex::encode(&cir.objects[1].sha256), b), + (hex::encode(&cir.fresh_validated_objects[0].sha256), a), + (hex::encode(&cir.fresh_validated_objects[1].sha256), b), ]) .unwrap(); } diff --git a/src/cir/mod.rs b/src/cir/mod.rs index e4e62de..97eceba 100644 --- a/src/cir/mod.rs +++ b/src/cir/mod.rs @@ -22,8 +22,8 @@ pub use materialize::{ materialize_cir_from_repo_bytes, mirror_relative_path_for_rsync_uri, resolve_static_pool_file, }; pub use model::{ - CIR_VERSION_V1, CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, - CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, sha256, + CIR_VERSION_V1, CIR_VERSION_V3, CIR_VERSION_V4, CanonicalInputRepresentation, CirHashAlgorithm, + CirObject, CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, sha256, }; pub use sequence::{CirSequenceManifest, CirSequenceStep, CirSequenceStepKind}; #[cfg(feature = "full")] @@ -36,8 +36,8 @@ pub use static_pool::{ #[cfg(test)] mod tests { use super::{ - CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, - CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, decode_cir, encode_cir, + CIR_VERSION_V4, CanonicalInputRepresentation, CirObject, CirRejectedObject, CirTrustAnchor, + decode_cir, encode_cir, }; fn sample_time() -> time::OffsetDateTime { @@ -69,11 +69,9 @@ mod tests { reason: None, }, ]; - CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: sample_time(), - objects: vec![ + CanonicalInputRepresentation::new_v4( + sample_time(), + vec![ CirObject { rsync_uri: "rsync://example.net/repo/a.cer".to_string(), sha256: vec![0x11; 32], @@ -82,17 +80,24 @@ mod tests { rsync_uri: "rsync://example.net/repo/b.roa".to_string(), sha256: vec![0x22; 32], }, + CirObject { + rsync_uri: "rsync://example.net/repo/rejected-a.roa".to_string(), + sha256: vec![0x33; 32], + }, + CirObject { + rsync_uri: "rsync://example.net/repo/rejected-b.asa".to_string(), + sha256: vec![0x44; 32], + }, ], - trust_anchors: vec![sample_trust_anchor( + Vec::new(), + vec![sample_trust_anchor( "rsync://example.net/repo/ta.cer", "https://tal.example.net/root.tal", b"ta-der", )], - reject_list_sha256: compute_reject_list_sha256( - rejected_objects.iter().map(|item| item.object_uri.as_str()), - ), rejected_objects, - } + Vec::new(), + ) } fn test_encode_tlv(tag: u8, value: &[u8]) -> Vec { @@ -125,19 +130,18 @@ mod tests { #[test] fn cir_roundtrip_minimal_succeeds() { - let cir = CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: sample_time(), - objects: Vec::new(), - trust_anchors: vec![sample_trust_anchor( + let cir = CanonicalInputRepresentation::new_v4( + sample_time(), + Vec::new(), + Vec::new(), + vec![sample_trust_anchor( "rsync://example.net/repo/minimal-ta.cer", "https://tal.example.net/minimal.tal", b"minimal-ta-der", )], - reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), - rejected_objects: Vec::new(), - }; + Vec::new(), + Vec::new(), + ); let der = encode_cir(&cir).expect("encode minimal cir"); let decoded = decode_cir(&der).expect("decode minimal cir"); assert_eq!(decoded, cir); @@ -145,11 +149,9 @@ mod tests { #[test] fn cir_model_rejects_unsorted_duplicate_objects() { - let cir = CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: sample_time(), - objects: vec![ + let cir = CanonicalInputRepresentation::new_v4( + sample_time(), + vec![ CirObject { rsync_uri: "rsync://example.net/repo/z.roa".to_string(), sha256: vec![0x11; 32], @@ -159,26 +161,29 @@ mod tests { sha256: vec![0x22; 32], }, ], - trust_anchors: vec![sample_trust_anchor( + Vec::new(), + vec![sample_trust_anchor( "rsync://example.net/repo/ta.cer", "https://tal.example.net/root.tal", b"ta-der", )], - reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), - rejected_objects: Vec::new(), - }; + Vec::new(), + Vec::new(), + ); let err = encode_cir(&cir).expect_err("unsorted objects must fail"); - assert!(err.to_string().contains("CIR.objects"), "{err}"); + assert!( + err.to_string().contains("CIR.freshValidatedObjects"), + "{err}" + ); } #[test] fn cir_model_rejects_duplicate_tals() { - let cir = CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: sample_time(), - objects: Vec::new(), - trust_anchors: vec![ + let cir = CanonicalInputRepresentation::new_v4( + sample_time(), + Vec::new(), + Vec::new(), + vec![ sample_trust_anchor( "rsync://example.net/repo/ta.cer", "https://tal.example.net/root.tal", @@ -190,9 +195,9 @@ mod tests { b"ta-der-b", ), ], - reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), - rejected_objects: Vec::new(), - }; + Vec::new(), + Vec::new(), + ); let err = encode_cir(&cir).expect_err("duplicate trust_anchors must fail"); assert!(err.to_string().contains("CIR.trustAnchors"), "{err}"); } @@ -202,10 +207,10 @@ mod tests { let mut der = encode_cir(&sample_cir()).expect("encode cir"); let pos = der .windows(3) - .position(|window| window == [0x02, 0x01, CIR_VERSION_V3 as u8]) + .position(|window| window == [0x02, 0x01, CIR_VERSION_V4 as u8]) .or_else(|| { der.windows(3) - .position(|window| window == [0x02, 0x01, CIR_VERSION_V3 as u8]) + .position(|window| window == [0x02, 0x01, CIR_VERSION_V4 as u8]) }) .expect("find version integer"); der[pos + 2] = 2; @@ -243,34 +248,32 @@ mod tests { #[test] fn cir_model_rejects_non_rsync_object_uri_and_empty_tals() { - let bad_object = CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: sample_time(), - objects: vec![CirObject { + let bad_object = CanonicalInputRepresentation::new_v4( + sample_time(), + vec![CirObject { rsync_uri: "https://example.net/repo/a.roa".to_string(), sha256: vec![0x11; 32], }], - trust_anchors: vec![sample_trust_anchor( + Vec::new(), + vec![sample_trust_anchor( "rsync://example.net/repo/ta.cer", "https://tal.example.net/root.tal", b"ta-der", )], - reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), - rejected_objects: Vec::new(), - }; + Vec::new(), + Vec::new(), + ); let err = encode_cir(&bad_object).expect_err("non-rsync object uri must fail"); assert!(err.to_string().contains("rsync://"), "{err}"); - let no_tals = CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: sample_time(), - objects: Vec::new(), - trust_anchors: Vec::new(), - reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), - rejected_objects: Vec::new(), - }; + let no_tals = CanonicalInputRepresentation::new_v4( + sample_time(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + ); let err = encode_cir(&no_tals).expect_err("empty trust_anchors must fail"); assert!( err.to_string() @@ -281,54 +284,55 @@ mod tests { #[test] fn cir_model_rejects_non_utc_time_bad_hash_len_and_non_http_tal_uri() { - let bad_time = CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: sample_time().to_offset(time::UtcOffset::from_hms(8, 0, 0).unwrap()), - objects: Vec::new(), - trust_anchors: vec![sample_trust_anchor( + let bad_time = CanonicalInputRepresentation::new_v4( + sample_time().to_offset(time::UtcOffset::from_hms(8, 0, 0).unwrap()), + Vec::new(), + Vec::new(), + vec![sample_trust_anchor( "rsync://example.net/repo/ta.cer", "https://tal.example.net/root.tal", b"ta-der", )], - reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), - rejected_objects: Vec::new(), + Vec::new(), + Vec::new(), + ); + let bad_time = CanonicalInputRepresentation { + validation_time: sample_time().to_offset(time::UtcOffset::from_hms(8, 0, 0).unwrap()), + ..bad_time }; let err = encode_cir(&bad_time).expect_err("non-utc validation time must fail"); assert!(err.to_string().contains("UTC"), "{err}"); - let bad_hash = CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: sample_time(), - objects: vec![CirObject { + let bad_hash = CanonicalInputRepresentation::new_v4( + sample_time(), + vec![CirObject { rsync_uri: "rsync://example.net/repo/a.roa".to_string(), sha256: vec![0x11; 31], }], - trust_anchors: vec![sample_trust_anchor( + Vec::new(), + vec![sample_trust_anchor( "rsync://example.net/repo/ta.cer", "https://tal.example.net/root.tal", b"ta-der", )], - reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), - rejected_objects: Vec::new(), - }; + Vec::new(), + Vec::new(), + ); let err = encode_cir(&bad_hash).expect_err("bad digest len must fail"); assert!(err.to_string().contains("32 bytes"), "{err}"); - let bad_tal_uri = CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: sample_time(), - objects: Vec::new(), - trust_anchors: vec![sample_trust_anchor( + let bad_tal_uri = CanonicalInputRepresentation::new_v4( + sample_time(), + Vec::new(), + Vec::new(), + vec![sample_trust_anchor( "rsync://example.net/repo/ta.cer", "ftp://tal.example.net/root.tal", b"ta-der", )], - reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), - rejected_objects: Vec::new(), - }; + Vec::new(), + Vec::new(), + ); let err = encode_cir(&bad_tal_uri).expect_err("bad tal uri must fail"); assert!(err.to_string().contains("http:// or https://"), "{err}"); } @@ -367,12 +371,19 @@ mod tests { let bad = test_encode_tlv( 0x30, &[ - test_encode_tlv(0x02, &[CIR_VERSION_V3 as u8]), + test_encode_tlv(0x02, &[CIR_VERSION_V4 as u8]), test_encode_tlv(0x06, crate::data_model::oid::OID_SHA256_RAW), test_encode_tlv(0x18, b"20260407123456Z"), test_encode_tlv(0x30, &object), + test_encode_tlv(0x30, &[]), test_encode_tlv(0x30, &trust_anchor), test_encode_tlv(0x04, &[0x33; 32]), + test_encode_tlv(0x04, &[0x33; 32]), + test_encode_tlv(0x04, &[0x33; 32]), + test_encode_tlv(0x04, &[0x33; 32]), + test_encode_tlv(0x04, &[0x33; 32]), + test_encode_tlv(0x04, &[0x33; 32]), + test_encode_tlv(0x30, &[]), test_encode_tlv(0x30, &[]), ] .concat(), @@ -412,9 +423,12 @@ mod tests { #[test] fn cir_model_rejects_unsorted_rejected_objects_and_bad_digest() { let mut cir = sample_cir(); - cir.rejected_objects.swap(0, 1); + cir.fresh_rejected_objects.swap(0, 1); let err = encode_cir(&cir).expect_err("unsorted rejected objects must fail"); - assert!(err.to_string().contains("CIR.rejectedObjects"), "{err}"); + assert!( + err.to_string().contains("CIR.freshRejectedObjects"), + "{err}" + ); let mut cir = sample_cir(); cir.reject_list_sha256 = vec![0x55; 32]; diff --git a/src/cir/model.rs b/src/cir/model.rs index 362be54..bb35667 100644 --- a/src/cir/model.rs +++ b/src/cir/model.rs @@ -3,6 +3,7 @@ use crate::data_model::oid::OID_SHA256; pub const CIR_VERSION_V1: u32 = 1; pub const CIR_VERSION_V2: u32 = 2; pub const CIR_VERSION_V3: u32 = 3; +pub const CIR_VERSION_V4: u32 = 4; pub const DIGEST_LEN_SHA256: usize = 32; #[derive(Clone, Debug, PartialEq, Eq)] @@ -23,17 +24,81 @@ pub struct CanonicalInputRepresentation { pub version: u32, pub hash_alg: CirHashAlgorithm, pub validation_time: time::OffsetDateTime, - pub objects: Vec, + pub fresh_validated_objects: Vec, + pub cached_validated_objects: Vec, pub trust_anchors: Vec, + pub object_list_sha256: Vec, + pub fresh_object_list_sha256: Vec, + pub cached_object_list_sha256: Vec, pub reject_list_sha256: Vec, - pub rejected_objects: Vec, + pub fresh_reject_list_sha256: Vec, + pub cached_reject_list_sha256: Vec, + pub fresh_rejected_objects: Vec, + pub cached_rejected_objects: Vec, } impl CanonicalInputRepresentation { + pub fn new_v4( + validation_time: time::OffsetDateTime, + fresh_validated_objects: Vec, + cached_validated_objects: Vec, + trust_anchors: Vec, + fresh_rejected_objects: Vec, + cached_rejected_objects: Vec, + ) -> Self { + let fresh_object_list_sha256 = compute_object_list_sha256(fresh_validated_objects.iter()); + let cached_object_list_sha256 = compute_object_list_sha256(cached_validated_objects.iter()); + let object_list_sha256 = compute_sectioned_object_list_sha256( + fresh_validated_objects + .iter() + .map(|object| ("fresh", object)) + .chain( + cached_validated_objects + .iter() + .map(|object| ("cached", object)), + ), + ); + let fresh_reject_list_sha256 = compute_reject_list_sha256( + fresh_rejected_objects + .iter() + .map(|item| item.object_uri.as_str()), + ); + let cached_reject_list_sha256 = compute_reject_list_sha256( + cached_rejected_objects + .iter() + .map(|item| item.object_uri.as_str()), + ); + let mut all_rejected_uris = fresh_rejected_objects + .iter() + .chain(cached_rejected_objects.iter()) + .map(|item| item.object_uri.as_str()) + .collect::>(); + all_rejected_uris.sort_unstable(); + all_rejected_uris.dedup(); + let reject_list_sha256 = compute_reject_list_sha256(all_rejected_uris.iter().copied()); + + Self { + version: CIR_VERSION_V4, + hash_alg: CirHashAlgorithm::Sha256, + validation_time: validation_time.to_offset(time::UtcOffset::UTC), + fresh_validated_objects, + cached_validated_objects, + trust_anchors, + object_list_sha256, + fresh_object_list_sha256, + cached_object_list_sha256, + reject_list_sha256, + fresh_reject_list_sha256, + cached_reject_list_sha256, + fresh_rejected_objects, + cached_rejected_objects, + } + } + pub fn validate(&self) -> Result<(), String> { - if self.version != CIR_VERSION_V3 { + if self.version != CIR_VERSION_V4 { return Err(format!( - "CIR version must be {CIR_VERSION_V3}, got {}", + "CIR version must be {CIR_VERSION_V4}, got {}", self.version )); } @@ -44,8 +109,16 @@ impl CanonicalInputRepresentation { return Err("CIR validationTime must be UTC".into()); } validate_sorted_unique_strings( - self.objects.iter().map(|item| item.rsync_uri.as_str()), - "CIR.objects must be sorted by rsyncUri and unique", + self.fresh_validated_objects + .iter() + .map(|item| item.rsync_uri.as_str()), + "CIR.freshValidatedObjects must be sorted by rsyncUri and unique", + )?; + validate_sorted_unique_strings( + self.cached_validated_objects + .iter() + .map(|item| item.rsync_uri.as_str()), + "CIR.cachedValidatedObjects must be sorted by rsyncUri and unique", )?; validate_sorted_unique_strings( self.trust_anchors @@ -54,14 +127,21 @@ impl CanonicalInputRepresentation { "CIR.trustAnchors must be sorted by taRsyncUri and unique", )?; validate_sorted_unique_strings( - self.rejected_objects + self.fresh_rejected_objects .iter() .map(|item| item.object_uri.as_str()), - "CIR.rejectedObjects must be sorted by objectUri and unique", + "CIR.freshRejectedObjects must be sorted by objectUri and unique", + )?; + validate_sorted_unique_strings( + self.cached_rejected_objects + .iter() + .map(|item| item.object_uri.as_str()), + "CIR.cachedRejectedObjects must be sorted by objectUri and unique", )?; let object_uris = self - .objects + .fresh_validated_objects .iter() + .chain(self.cached_validated_objects.iter()) .map(|item| item.rsync_uri.as_str()) .collect::>(); for trust_anchor in &self.trust_anchors { @@ -72,34 +152,136 @@ impl CanonicalInputRepresentation { )); } } + let fresh_object_uris = self + .fresh_validated_objects + .iter() + .map(|item| item.rsync_uri.as_str()) + .collect::>(); + for item in &self.fresh_rejected_objects { + if !fresh_object_uris.contains(item.object_uri.as_str()) { + return Err(format!( + "CIR.freshRejectedObjects URI must exist in freshValidatedObjects: {}", + item.object_uri + )); + } + } + let cached_object_uris = self + .cached_validated_objects + .iter() + .map(|item| item.rsync_uri.as_str()) + .collect::>(); + for item in &self.cached_rejected_objects { + if !cached_object_uris.contains(item.object_uri.as_str()) { + return Err(format!( + "CIR.cachedRejectedObjects URI must exist in cachedValidatedObjects: {}", + item.object_uri + )); + } + } if self.trust_anchors.is_empty() { return Err("CIR.trustAnchors must be non-empty".into()); } - if self.reject_list_sha256.len() != DIGEST_LEN_SHA256 { - return Err(format!( - "CIR.rejectListSha256 must be {DIGEST_LEN_SHA256} bytes, got {}", - self.reject_list_sha256.len() - )); + for (label, digest) in [ + ("CIR.objectListSha256", &self.object_list_sha256), + ("CIR.freshObjectListSha256", &self.fresh_object_list_sha256), + ( + "CIR.cachedObjectListSha256", + &self.cached_object_list_sha256, + ), + ("CIR.rejectListSha256", &self.reject_list_sha256), + ("CIR.freshRejectListSha256", &self.fresh_reject_list_sha256), + ( + "CIR.cachedRejectListSha256", + &self.cached_reject_list_sha256, + ), + ] { + if digest.len() != DIGEST_LEN_SHA256 { + return Err(format!( + "{label} must be {DIGEST_LEN_SHA256} bytes, got {}", + digest.len() + )); + } } - for object in &self.objects { + for object in self.validated_objects() { object.validate()?; } for trust_anchor in &self.trust_anchors { trust_anchor.validate()?; } - for item in &self.rejected_objects { + for item in self.rejected_objects() { item.validate()?; } - let expected_digest = compute_reject_list_sha256( - self.rejected_objects + let expected_fresh_object_digest = + compute_object_list_sha256(self.fresh_validated_objects.iter()); + if self.fresh_object_list_sha256 != expected_fresh_object_digest { + return Err("CIR.freshObjectListSha256 does not match freshValidatedObjects".into()); + } + let expected_cached_object_digest = + compute_object_list_sha256(self.cached_validated_objects.iter()); + if self.cached_object_list_sha256 != expected_cached_object_digest { + return Err("CIR.cachedObjectListSha256 does not match cachedValidatedObjects".into()); + } + let expected_object_digest = compute_sectioned_object_list_sha256( + self.fresh_validated_objects + .iter() + .map(|object| ("fresh", object)) + .chain( + self.cached_validated_objects + .iter() + .map(|object| ("cached", object)), + ), + ); + if self.object_list_sha256 != expected_object_digest { + return Err("CIR.objectListSha256 does not match fresh/cached objects".into()); + } + let expected_fresh_reject_digest = compute_reject_list_sha256( + self.fresh_rejected_objects .iter() .map(|item| item.object_uri.as_str()), ); + if self.fresh_reject_list_sha256 != expected_fresh_reject_digest { + return Err("CIR.freshRejectListSha256 does not match freshRejectedObjects".into()); + } + let expected_cached_reject_digest = compute_reject_list_sha256( + self.cached_rejected_objects + .iter() + .map(|item| item.object_uri.as_str()), + ); + if self.cached_reject_list_sha256 != expected_cached_reject_digest { + return Err("CIR.cachedRejectListSha256 does not match cachedRejectedObjects".into()); + } + let mut all_rejected_uris = self + .rejected_objects() + .map(|item| item.object_uri.as_str()) + .collect::>(); + all_rejected_uris.sort_unstable(); + all_rejected_uris.dedup(); + let expected_digest = compute_reject_list_sha256(all_rejected_uris.iter().copied()); if self.reject_list_sha256 != expected_digest { - return Err("CIR.rejectListSha256 does not match rejectedObjects".into()); + return Err("CIR.rejectListSha256 does not match fresh/cached rejectedObjects".into()); } Ok(()) } + + pub fn validated_objects(&self) -> impl Iterator { + self.fresh_validated_objects + .iter() + .chain(self.cached_validated_objects.iter()) + } + + pub fn rejected_objects(&self) -> impl Iterator { + self.fresh_rejected_objects + .iter() + .chain(self.cached_rejected_objects.iter()) + } + + pub fn validated_object_count(&self) -> usize { + self.fresh_validated_objects.len() + self.cached_validated_objects.len() + } + + pub fn rejected_object_count(&self) -> usize { + self.fresh_rejected_objects.len() + self.cached_rejected_objects.len() + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -209,12 +391,39 @@ pub fn compute_reject_list_sha256<'a>(uris: impl IntoIterator) - sha256(&body) } +pub fn compute_object_list_sha256<'a>(objects: impl IntoIterator) -> Vec { + let mut body = Vec::new(); + for object in objects { + append_digest_string(&mut body, &object.rsync_uri); + body.extend_from_slice(&object.sha256); + } + sha256(&body) +} + +pub fn compute_sectioned_object_list_sha256<'a>( + objects: impl IntoIterator, +) -> Vec { + let mut body = Vec::new(); + for (section, object) in objects { + append_digest_string(&mut body, section); + append_digest_string(&mut body, &object.rsync_uri); + body.extend_from_slice(&object.sha256); + } + sha256(&body) +} + pub fn sha256(bytes: &[u8]) -> Vec { use sha2::Digest; sha2::Sha256::digest(bytes).to_vec() } +fn append_digest_string(body: &mut Vec, value: &str) { + let bytes = value.as_bytes(); + body.extend_from_slice(&(bytes.len() as u32).to_be_bytes()); + body.extend_from_slice(bytes); +} + fn validate_sorted_unique_strings<'a>( items: impl IntoIterator, message: &str, diff --git a/src/tools/rpki_artifact_metrics.rs b/src/tools/rpki_artifact_metrics.rs index e8ccc21..1c15ad1 100644 --- a/src/tools/rpki_artifact_metrics.rs +++ b/src/tools/rpki_artifact_metrics.rs @@ -352,11 +352,24 @@ impl Histogram { struct CirMetrics { version: u32, objects: u64, + fresh_objects: u64, + cached_objects: u64, trust_anchors: u64, rejected_objects: u64, + fresh_rejected_objects: u64, + cached_rejected_objects: u64, + object_list_sha256: String, + fresh_object_list_sha256: String, + cached_object_list_sha256: String, reject_list_sha256: String, + fresh_reject_list_sha256: String, + cached_reject_list_sha256: String, objects_by_type: BTreeMap, + fresh_objects_by_type: BTreeMap, + cached_objects_by_type: BTreeMap, rejected_objects_by_type: BTreeMap, + fresh_rejected_objects_by_type: BTreeMap, + cached_rejected_objects_by_type: BTreeMap, } #[derive(Clone, Debug, Default, Serialize)] @@ -973,25 +986,62 @@ fn parse_cir(path: &Path, snapshot: &mut MetricsSnapshot) { { Ok(cir) => { let mut objects_by_type = BTreeMap::new(); - for object in &cir.objects { + for object in cir.validated_objects() { *objects_by_type .entry(object_type_from_uri(&object.rsync_uri)) .or_default() += 1; } + let mut fresh_objects_by_type = BTreeMap::new(); + for object in &cir.fresh_validated_objects { + *fresh_objects_by_type + .entry(object_type_from_uri(&object.rsync_uri)) + .or_default() += 1; + } + let mut cached_objects_by_type = BTreeMap::new(); + for object in &cir.cached_validated_objects { + *cached_objects_by_type + .entry(object_type_from_uri(&object.rsync_uri)) + .or_default() += 1; + } let mut rejected_objects_by_type = BTreeMap::new(); - for object in &cir.rejected_objects { + for object in cir.rejected_objects() { *rejected_objects_by_type .entry(object_type_from_uri(&object.object_uri)) .or_default() += 1; } + let mut fresh_rejected_objects_by_type = BTreeMap::new(); + for object in &cir.fresh_rejected_objects { + *fresh_rejected_objects_by_type + .entry(object_type_from_uri(&object.object_uri)) + .or_default() += 1; + } + let mut cached_rejected_objects_by_type = BTreeMap::new(); + for object in &cir.cached_rejected_objects { + *cached_rejected_objects_by_type + .entry(object_type_from_uri(&object.object_uri)) + .or_default() += 1; + } snapshot.cir = Some(CirMetrics { version: cir.version, - objects: cir.objects.len() as u64, + objects: cir.validated_object_count() as u64, + fresh_objects: cir.fresh_validated_objects.len() as u64, + cached_objects: cir.cached_validated_objects.len() as u64, trust_anchors: cir.trust_anchors.len() as u64, - rejected_objects: cir.rejected_objects.len() as u64, + rejected_objects: cir.rejected_object_count() as u64, + fresh_rejected_objects: cir.fresh_rejected_objects.len() as u64, + cached_rejected_objects: cir.cached_rejected_objects.len() as u64, + object_list_sha256: hex::encode(&cir.object_list_sha256), + fresh_object_list_sha256: hex::encode(&cir.fresh_object_list_sha256), + cached_object_list_sha256: hex::encode(&cir.cached_object_list_sha256), reject_list_sha256: hex::encode(&cir.reject_list_sha256), + fresh_reject_list_sha256: hex::encode(&cir.fresh_reject_list_sha256), + cached_reject_list_sha256: hex::encode(&cir.cached_reject_list_sha256), objects_by_type, + fresh_objects_by_type, + cached_objects_by_type, rejected_objects_by_type, + fresh_rejected_objects_by_type, + cached_rejected_objects_by_type, }); } Err(err) => snapshot @@ -1580,6 +1630,14 @@ fn render_cir_metrics(writer: &mut PromWriter<'_>, instance: &str, cir: &CirMetr &[label("instance", instance)], cir.objects as f64, ); + for (source, count) in [("fresh", cir.fresh_objects), ("cached", cir.cached_objects)] { + writer.gauge( + "ours_rp_cir_objects_by_source", + "CIR object count by validation input source", + &[label("instance", instance), label("source", source)], + count as f64, + ); + } writer.gauge( "ours_rp_cir_trust_anchors", "CIR trust anchor count", @@ -1592,6 +1650,29 @@ fn render_cir_metrics(writer: &mut PromWriter<'_>, instance: &str, cir: &CirMetr &[label("instance", instance)], cir.rejected_objects as f64, ); + for (source, count) in [ + ("fresh", cir.fresh_rejected_objects), + ("cached", cir.cached_rejected_objects), + ] { + writer.gauge( + "ours_rp_cir_rejected_objects_by_source", + "CIR rejected object count by validation input source", + &[label("instance", instance), label("source", source)], + count as f64, + ); + } + for (source, digest) in [ + ("merged", cir.object_list_sha256.as_str()), + ("fresh", cir.fresh_object_list_sha256.as_str()), + ("cached", cir.cached_object_list_sha256.as_str()), + ] { + writer.gauge( + "ours_rp_cir_object_list_digest_present", + "CIR object list digest is present by validation input source", + &[label("instance", instance), label("source", source)], + if digest.len() == 64 { 1.0 } else { 0.0 }, + ); + } writer.gauge( "ours_rp_cir_reject_list_digest_present", "CIR reject list digest is present", @@ -1602,6 +1683,18 @@ fn render_cir_metrics(writer: &mut PromWriter<'_>, instance: &str, cir: &CirMetr 0.0 }, ); + for (source, digest) in [ + ("merged", cir.reject_list_sha256.as_str()), + ("fresh", cir.fresh_reject_list_sha256.as_str()), + ("cached", cir.cached_reject_list_sha256.as_str()), + ] { + writer.gauge( + "ours_rp_cir_reject_list_digest_present_by_source", + "CIR reject list digest is present by validation input source", + &[label("instance", instance), label("source", source)], + if digest.len() == 64 { 1.0 } else { 0.0 }, + ); + } for (object_type, count) in &cir.objects_by_type { writer.gauge( "ours_rp_cir_objects_by_type", @@ -1613,6 +1706,23 @@ fn render_cir_metrics(writer: &mut PromWriter<'_>, instance: &str, cir: &CirMetr *count as f64, ); } + for (source, counts) in [ + ("fresh", &cir.fresh_objects_by_type), + ("cached", &cir.cached_objects_by_type), + ] { + for (object_type, count) in counts { + writer.gauge( + "ours_rp_cir_objects_by_source_type", + "CIR object count by validation input source and file type", + &[ + label("instance", instance), + label("source", source), + label("object_type", object_type), + ], + *count as f64, + ); + } + } for (object_type, count) in &cir.rejected_objects_by_type { writer.gauge( "ours_rp_cir_rejected_objects_by_type", @@ -1624,6 +1734,23 @@ fn render_cir_metrics(writer: &mut PromWriter<'_>, instance: &str, cir: &CirMetr *count as f64, ); } + for (source, counts) in [ + ("fresh", &cir.fresh_rejected_objects_by_type), + ("cached", &cir.cached_rejected_objects_by_type), + ] { + for (object_type, count) in counts { + writer.gauge( + "ours_rp_cir_rejected_objects_by_source_type", + "CIR rejected object count by validation input source and file type", + &[ + label("instance", instance), + label("source", source), + label("object_type", object_type), + ], + *count as f64, + ); + } + } } fn render_ccr_metrics(writer: &mut PromWriter<'_>, instance: &str, ccr: &CcrMetrics) { @@ -2092,8 +2219,8 @@ mod tests { encode_content_info, }; use crate::cir::{ - CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirRejectedObject, - CirTrustAnchor, compute_reject_list_sha256, encode_cir, sha256, + CanonicalInputRepresentation, CirObject, CirRejectedObject, CirTrustAnchor, encode_cir, + sha256, }; use tempfile::TempDir; @@ -2154,7 +2281,7 @@ mod tests { assert!(snapshot.repo_stats[0].sync_success); assert_eq!(snapshot.repo_stats[0].download_bytes, 333); assert_eq!(snapshot.top_pp_by_object_count[0].object_count, 2); - assert_eq!(snapshot.cir.as_ref().unwrap().objects, 1); + assert_eq!(snapshot.cir.as_ref().unwrap().objects, 2); assert_eq!(snapshot.ccr.as_ref().unwrap().state_items["tas"], 1); let metrics = render_metrics(&snapshot); assert!(metrics.contains("ours_rp_repository_info")); @@ -2162,12 +2289,29 @@ mod tests { assert!(metrics.contains("ours_rp_repository_download_bytes")); assert!(metrics.contains("ours_rp_large_publication_points")); assert!(metrics.contains("ours_rp_cir_objects")); + assert!(metrics.contains("ours_rp_cir_objects_by_source")); + assert!(metrics.contains("ours_rp_cir_rejected_objects_by_source")); + assert!(metrics.contains("ours_rp_cir_objects_by_source_type")); + assert!(metrics.contains("ours_rp_cir_rejected_objects_by_source_type")); + assert!(metrics.contains("ours_rp_cir_object_list_digest_present")); + assert!(metrics.contains("ours_rp_cir_reject_list_digest_present_by_source")); assert!(metrics.contains("ours_rp_ccr_state_items")); + assert!( + metrics.contains(r#"ours_rp_cir_objects_by_source{instance="test",source="fresh"} 2"#) + ); + assert!( + metrics.contains(r#"ours_rp_cir_objects_by_source{instance="test",source="cached"} 0"#) + ); + assert!(metrics.contains( + r#"ours_rp_cir_rejected_objects_by_source{instance="test",source="fresh"} 1"# + )); assert!(metrics.contains(r#"ours_rp_vrps{instance="test",kind="total"} 3"#)); assert!(metrics.contains(r#"ours_rp_vrps{instance="test",kind="unique"} 2"#)); let status = render_status_json(&snapshot).expect("status"); assert!(status.contains("topPublicationPointsByObjectCount")); assert!(status.contains(r#""vrpsUnique": 2"#)); + assert!(status.contains(r#""freshObjects": 2"#)); + assert!(status.contains(r#""cachedObjects": 0"#)); } #[test] @@ -2186,30 +2330,33 @@ mod tests { object_uri: "rsync://repo.example/a/bad.roa".to_string(), reason: Some("bad".to_string()), }]; - let cir = CanonicalInputRepresentation { - version: crate::cir::CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: time::OffsetDateTime::parse( + let cir = CanonicalInputRepresentation::new_v4( + time::OffsetDateTime::parse( "2026-05-25T00:00:00Z", &time::format_description::well_known::Rfc3339, ) .unwrap(), - objects: vec![CirObject { - rsync_uri: "rsync://repo.example/a/a.roa".to_string(), - sha256: vec![1; 32], - }], - trust_anchors: vec![CirTrustAnchor { + vec![ + CirObject { + rsync_uri: "rsync://repo.example/a/a.roa".to_string(), + sha256: vec![1; 32], + }, + CirObject { + rsync_uri: "rsync://repo.example/a/bad.roa".to_string(), + sha256: vec![2; 32], + }, + ], + Vec::new(), + vec![CirTrustAnchor { ta_rsync_uri: "rsync://repo.example/ta.cer".to_string(), tal_uri: "https://tal.example/tal.tal".to_string(), tal_bytes: b"rsync://repo.example/ta.cer\n\nAQID\n".to_vec(), ta_certificate_der: b"ta".to_vec(), ta_certificate_sha256: sha256(b"ta"), }], - reject_list_sha256: compute_reject_list_sha256( - rejected.iter().map(|item| item.object_uri.as_str()), - ), - rejected_objects: rejected, - }; + rejected, + Vec::new(), + ); encode_cir(&cir).expect("encode cir") } diff --git a/src/tools/sequence_triage_ccr_cir.rs b/src/tools/sequence_triage_ccr_cir.rs index 9ef4c8a..b8ad71d 100644 --- a/src/tools/sequence_triage_ccr_cir.rs +++ b/src/tools/sequence_triage_ccr_cir.rs @@ -48,8 +48,8 @@ mod tests { build_aspa_payload_state, build_roa_payload_state, encode_content_info, }; use crate::cir::{ - CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, - CirRejectedObject, CirTrustAnchor, compute_reject_list_sha256, encode_cir, sha256, + CanonicalInputRepresentation, CirObject, CirRejectedObject, CirTrustAnchor, encode_cir, + sha256, }; use crate::data_model::roa::{IpPrefix, RoaAfi}; use crate::validation::objects::{AspaAttestation, Vrp}; @@ -100,6 +100,7 @@ mod tests { &[ object("rsync://example.net/a.roa", 0x11), object("rsync://example.net/persistent.roa", 0x44), + object("rsync://example.net/reject-old.roa", 0xee), ], &[], 64496, @@ -485,7 +486,18 @@ mod tests { 9, 64496, ); - write_sample_with_ccr_seq(root, "right2", 2, &[peer_mismatch], &[], 10, 64497); + write_sample_with_ccr_seq( + root, + "right2", + 2, + &[ + peer_mismatch, + object("rsync://example.net/pp/rejected.roa", 0xee), + ], + &[], + 10, + 64497, + ); std::fs::write( root.join("left.jsonl"), jsonl(&[ @@ -845,7 +857,6 @@ mod tests { rejected: &[&str], ) -> CanonicalInputRepresentation { let mut objects = objects.to_vec(); - objects.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri)); let rejected_objects = rejected .iter() .map(|uri| CirRejectedObject { @@ -853,15 +864,26 @@ mod tests { reason: Some("test".to_string()), }) .collect::>(); - CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: sample_time(seq), - objects, - trust_anchors: vec![sample_trust_anchor()], - reject_list_sha256: compute_reject_list_sha256(rejected.iter().copied()), - rejected_objects, + for rejected_uri in rejected { + if !objects + .iter() + .any(|object| object.rsync_uri == *rejected_uri) + { + objects.push(CirObject { + rsync_uri: (*rejected_uri).to_string(), + sha256: vec![0xee; 32], + }); + } } + objects.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri)); + CanonicalInputRepresentation::new_v4( + sample_time(seq), + objects, + Vec::new(), + vec![sample_trust_anchor()], + rejected_objects, + Vec::new(), + ) } fn sample_trust_anchor() -> CirTrustAnchor { diff --git a/src/tools/sequence_triage_ccr_cir/loader.rs b/src/tools/sequence_triage_ccr_cir/loader.rs index e67730d..bff665c 100644 --- a/src/tools/sequence_triage_ccr_cir/loader.rs +++ b/src/tools/sequence_triage_ccr_cir/loader.rs @@ -42,10 +42,12 @@ pub(super) fn load_sequence_meta(path: &Path, side: Side) -> Result>(); object_hashes = objects @@ -98,8 +99,7 @@ fn load_sample_parts( .map(|(uri, hash)| object_hash_key(uri, hash)) .collect::>(); rejects = cir - .rejected_objects - .iter() + .rejected_objects() .map(|item| item.object_uri.clone()) .collect::>(); } diff --git a/src/validation/tree_runner.rs b/src/validation/tree_runner.rs index a5ca9bc..3c3aa9d 100644 --- a/src/validation/tree_runner.rs +++ b/src/validation/tree_runner.rs @@ -281,6 +281,19 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> { }) } + fn fresh_failure_audit_entries_for_cir( + &self, + ca: &CaInstanceHandle, + fresh_err: &ManifestFreshError, + ) -> Vec { + if !fresh_err.should_warn_when_current_instance_reused() { + return Vec::new(); + } + self.rejected_manifest_audit_entry_for_failed_fetch(ca, fresh_err) + .into_iter() + .collect() + } + pub(crate) fn stage_fresh_publication_point_after_repo_ready( &self, ca: &CaInstanceHandle, @@ -955,21 +968,15 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> { } crate::policy::CaFailedFetchPolicy::ReuseCurrentInstanceVcir => { let projection_started = std::time::Instant::now(); - let mut projection = project_current_instance_vcir_on_failed_fetch( + let projection = project_current_instance_vcir_on_failed_fetch( self.store, ca, &fresh_err, self.validation_time, ) .map_err(|e| format!("failed fetch VCIR projection failed: {e}"))?; - if matches!( - projection.source, - PublicationPointSource::FailedFetchNoCache - ) && let Some(rejected_manifest) = - self.rejected_manifest_audit_entry_for_failed_fetch(ca, &fresh_err) - { - projection.objects.audit.push(rejected_manifest); - } + let fresh_failure_audits = + self.fresh_failure_audit_entries_for_cir(ca, &fresh_err); self.append_ccr_manifest_projection_from_reuse(&projection)?; let projection_ms = projection_started.elapsed().as_millis() as u64; warnings.extend(projection.warnings.clone()); @@ -986,6 +993,7 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> { &warnings, &projection.objects, &projection.child_audits, + &fresh_failure_audits, ); let audit_build_ms = audit_build_started.elapsed().as_millis() as u64; let result = PublicationPointRunResult { @@ -1922,7 +1930,9 @@ fn build_publication_point_audit_from_snapshot( next_update_rfc3339_utc: pack.next_update.rfc3339_utc.clone(), verified_at_rfc3339_utc: pack.verified_at.rfc3339_utc.clone(), warnings, - objects: objects_out, + objects: objects_out.clone(), + cir_fresh_objects: objects_out, + cir_cached_objects: Vec::new(), } } @@ -1938,6 +1948,7 @@ fn build_publication_point_audit_from_vcir( runner_warnings: &[Warning], objects: &crate::validation::objects::ObjectsOutput, child_audits: &[ObjectAuditEntry], + fresh_failure_audits: &[ObjectAuditEntry], ) -> PublicationPointAudit { if let Some(pack) = pack { return build_publication_point_audit_from_snapshot( @@ -1977,14 +1988,19 @@ fn build_publication_point_audit_from_vcir( next_update_rfc3339_utc: String::new(), verified_at_rfc3339_utc: String::new(), warnings, - objects: Vec::new(), + objects: fresh_failure_audits.to_vec(), + cir_fresh_objects: fresh_failure_audits.to_vec(), + cir_cached_objects: Vec::new(), }; }; if source == PublicationPointSource::FailedFetchNoCache { - let mut objects_out = Vec::with_capacity(objects.audit.len() + child_audits.len()); + let mut objects_out = Vec::with_capacity( + objects.audit.len() + child_audits.len() + fresh_failure_audits.len(), + ); objects_out.extend(child_audits.iter().cloned()); objects_out.extend(objects.audit.iter().cloned()); + objects_out.extend(fresh_failure_audits.iter().cloned()); return PublicationPointAudit { node_id: None, parent_node_id: None, @@ -2011,7 +2027,9 @@ fn build_publication_point_audit_from_vcir( .clone(), verified_at_rfc3339_utc: vcir.last_successful_validation_time.rfc3339_utc.clone(), warnings, - objects: objects_out, + objects: objects_out.clone(), + cir_fresh_objects: objects_out, + cir_cached_objects: Vec::new(), }; } @@ -2076,6 +2094,9 @@ fn build_publication_point_audit_from_vcir( } } + let mut audit_objects = objects_out.clone(); + audit_objects.extend(fresh_failure_audits.iter().cloned()); + PublicationPointAudit { node_id: None, parent_node_id: None, @@ -2102,7 +2123,9 @@ fn build_publication_point_audit_from_vcir( .clone(), verified_at_rfc3339_utc: vcir.last_successful_validation_time.rfc3339_utc.clone(), warnings, - objects: objects_out, + objects: audit_objects, + cir_fresh_objects: fresh_failure_audits.to_vec(), + cir_cached_objects: objects_out, } } diff --git a/src/validation/tree_runner/tests.rs b/src/validation/tree_runner/tests.rs index f015373..b80f81d 100644 --- a/src/validation/tree_runner/tests.rs +++ b/src/validation/tree_runner/tests.rs @@ -3324,6 +3324,7 @@ fn build_publication_point_audit_from_vcir_uses_vcir_metadata_and_overlays_child &runner_warnings, &objects, &child_audits, + &[], ); assert_eq!(audit.source, "vcir_current_instance"); @@ -3413,6 +3414,7 @@ fn build_publication_point_audit_from_vcir_failed_no_cache_keeps_current_reject_ &[Warning::new("latest VCIR instance_gate expired")], &objects, &[], + &[], ); assert_eq!(audit.source, "failed_fetch_no_cache"); @@ -3537,6 +3539,7 @@ fn build_publication_point_audit_from_vcir_without_cached_inputs_returns_empty_l roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(), }, &[], + &[], ); assert_eq!(audit.source, "failed_fetch_no_cache"); diff --git a/tests/test_cir_delta_export_m1.rs b/tests/test_cir_delta_export_m1.rs index 1005870..edfa633 100644 --- a/tests/test_cir_delta_export_m1.rs +++ b/tests/test_cir_delta_export_m1.rs @@ -5,10 +5,7 @@ use rpki::ccr::{ CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation, TrustAnchorState, encode_content_info, }; -use rpki::cir::{ - CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTrustAnchor, - compute_reject_list_sha256, encode_cir, sha256, -}; +use rpki::cir::{CanonicalInputRepresentation, CirObject, CirTrustAnchor, encode_cir, sha256}; fn skip_heavy_blackbox_test() -> bool { std::env::var_os("RPKI_SKIP_HEAVY_BLACKBOX_TESTS").is_some() @@ -44,31 +41,28 @@ fn cir_full_and_delta_pair_reuses_shared_repo_bytes_db() { }; let trust_anchors = vec![test_trust_anchor()]; - let full_cir = CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: time::OffsetDateTime::parse( + let full_cir = CanonicalInputRepresentation::new_v4( + time::OffsetDateTime::parse( "2026-03-16T11:49:15Z", &time::format_description::well_known::Rfc3339, ) .unwrap(), - objects: vec![CirObject { + vec![CirObject { rsync_uri: "rsync://example.net/repo/full.roa".to_string(), sha256: hex::decode(&full_obj_hash).unwrap(), }], - trust_anchors: trust_anchors.clone(), - reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), - rejected_objects: Vec::new(), - }; - let delta_cir = CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: time::OffsetDateTime::parse( + Vec::new(), + trust_anchors.clone(), + Vec::new(), + Vec::new(), + ); + let delta_cir = CanonicalInputRepresentation::new_v4( + time::OffsetDateTime::parse( "2026-03-16T11:50:15Z", &time::format_description::well_known::Rfc3339, ) .unwrap(), - objects: { + { let mut objects = vec![ CirObject { rsync_uri: "rsync://example.net/repo/full.roa".to_string(), @@ -82,10 +76,11 @@ fn cir_full_and_delta_pair_reuses_shared_repo_bytes_db() { objects.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri)); objects }, + Vec::new(), trust_anchors, - reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), - rejected_objects: Vec::new(), - }; + Vec::new(), + Vec::new(), + ); let empty_ccr = CcrContentInfo::new(RpkiCanonicalCacheRepresentation { version: 0, hash_alg: CcrDigestAlgorithm::Sha256, @@ -214,8 +209,8 @@ fi rpki::cir::decode_cir(&std::fs::read(out.join("delta-001").join("input.cir")).unwrap()) .expect("decode delta cir"); - assert!(!full_cir.objects.is_empty()); - assert!(!delta_cir.objects.is_empty()); + assert!(full_cir.validated_object_count() > 0); + assert!(delta_cir.validated_object_count() > 0); let summary: serde_json::Value = serde_json::from_slice(&std::fs::read(out.join("summary.json")).unwrap()).unwrap(); diff --git a/tests/test_cir_drop_report_m5.rs b/tests/test_cir_drop_report_m5.rs index a32e834..f960646 100644 --- a/tests/test_cir_drop_report_m5.rs +++ b/tests/test_cir_drop_report_m5.rs @@ -6,10 +6,7 @@ use rpki::ccr::{ CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation, TrustAnchorState, encode_content_info, }; -use rpki::cir::{ - CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTrustAnchor, - compute_reject_list_sha256, encode_cir, sha256, -}; +use rpki::cir::{CanonicalInputRepresentation, CirObject, CirTrustAnchor, encode_cir, sha256}; #[test] fn cir_drop_report_counts_dropped_roa_objects_and_vrps() { @@ -33,22 +30,21 @@ fn cir_drop_report_counts_dropped_roa_objects_and_vrps() { .put_blob_bytes_batch(&[(hash.clone(), roa_bytes.clone())]) .expect("write repo bytes"); - let cir = CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: time::OffsetDateTime::parse( + let cir = CanonicalInputRepresentation::new_v4( + time::OffsetDateTime::parse( "2026-04-09T00:00:00Z", &time::format_description::well_known::Rfc3339, ) .unwrap(), - objects: vec![CirObject { + vec![CirObject { rsync_uri: "rsync://example.net/repo/AS4538.roa".to_string(), sha256: hex::decode(&hash).unwrap(), }], - trust_anchors: vec![test_trust_anchor()], - reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), - rejected_objects: Vec::new(), - }; + Vec::new(), + vec![test_trust_anchor()], + Vec::new(), + Vec::new(), + ); std::fs::write(&cir_path, encode_cir(&cir).unwrap()).unwrap(); let ccr = CcrContentInfo::new(RpkiCanonicalCacheRepresentation { diff --git a/tests/test_cir_matrix_m9.rs b/tests/test_cir_matrix_m9.rs index c82ad78..fc55521 100644 --- a/tests/test_cir_matrix_m9.rs +++ b/tests/test_cir_matrix_m9.rs @@ -3,8 +3,8 @@ use std::process::Command; use rpki::blob_store::ExternalRepoBytesDb; use rpki::cir::{ - CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirTrustAnchor, - compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, sha256, + CanonicalInputRepresentation, CirTrustAnchor, encode_cir, materialize_cir_from_repo_bytes, + sha256, }; fn skip_heavy_script_replay_test() -> bool { @@ -31,25 +31,24 @@ fn build_ta_only_cir() -> (CanonicalInputRepresentation, Vec) { .as_str() .to_string(); ( - CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: time::OffsetDateTime::parse( + CanonicalInputRepresentation::new_v4( + time::OffsetDateTime::parse( "2026-04-07T00:00:00Z", &time::format_description::well_known::Rfc3339, ) .unwrap(), - objects: Vec::new(), - trust_anchors: vec![CirTrustAnchor { + Vec::new(), + Vec::new(), + vec![CirTrustAnchor { ta_rsync_uri, tal_uri: "https://example.test/root.tal".to_string(), tal_bytes, ta_certificate_sha256: sha256(&ta_bytes), ta_certificate_der: ta_bytes.clone(), }], - reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), - rejected_objects: Vec::new(), - }, + Vec::new(), + Vec::new(), + ), ta_bytes, ) } diff --git a/tests/test_cir_peer_replay_m8.rs b/tests/test_cir_peer_replay_m8.rs index 85e343c..b6d62d4 100644 --- a/tests/test_cir_peer_replay_m8.rs +++ b/tests/test_cir_peer_replay_m8.rs @@ -3,8 +3,8 @@ use std::process::Command; use rpki::blob_store::ExternalRepoBytesDb; use rpki::cir::{ - CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirTrustAnchor, - compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, sha256, + CanonicalInputRepresentation, CirTrustAnchor, encode_cir, materialize_cir_from_repo_bytes, + sha256, }; fn skip_heavy_script_replay_test() -> bool { @@ -31,25 +31,24 @@ fn build_ta_only_cir() -> (CanonicalInputRepresentation, Vec) { .as_str() .to_string(); ( - CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: time::OffsetDateTime::parse( + CanonicalInputRepresentation::new_v4( + time::OffsetDateTime::parse( "2026-04-07T00:00:00Z", &time::format_description::well_known::Rfc3339, ) .unwrap(), - objects: Vec::new(), - trust_anchors: vec![CirTrustAnchor { + Vec::new(), + Vec::new(), + vec![CirTrustAnchor { ta_rsync_uri, tal_uri: "https://example.test/root.tal".to_string(), tal_bytes, ta_certificate_sha256: sha256(&ta_bytes), ta_certificate_der: ta_bytes.clone(), }], - reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), - rejected_objects: Vec::new(), - }, + Vec::new(), + Vec::new(), + ), ta_bytes, ) } diff --git a/tests/test_cir_sequence_m2.rs b/tests/test_cir_sequence_m2.rs index b8fbea7..24d7d4e 100644 --- a/tests/test_cir_sequence_m2.rs +++ b/tests/test_cir_sequence_m2.rs @@ -5,10 +5,7 @@ use rpki::ccr::{ CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation, TrustAnchorState, encode_content_info, }; -use rpki::cir::{ - CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTrustAnchor, - compute_reject_list_sha256, encode_cir, sha256, -}; +use rpki::cir::{CanonicalInputRepresentation, CirObject, CirTrustAnchor, encode_cir, sha256}; fn skip_heavy_blackbox_test() -> bool { std::env::var_os("RPKI_SKIP_HEAVY_BLACKBOX_TESTS").is_some() @@ -35,21 +32,19 @@ fn cir_offline_sequence_writes_parseable_sequence_json_and_steps() { ) .unwrap(); - let mk_cir = |uri: &str, hash_hex: &str, vt: &str| CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: time::OffsetDateTime::parse( - vt, - &time::format_description::well_known::Rfc3339, + let mk_cir = |uri: &str, hash_hex: &str, vt: &str| { + CanonicalInputRepresentation::new_v4( + time::OffsetDateTime::parse(vt, &time::format_description::well_known::Rfc3339) + .unwrap(), + vec![CirObject { + rsync_uri: uri.to_string(), + sha256: hex::decode(hash_hex).unwrap(), + }], + Vec::new(), + vec![test_trust_anchor()], + Vec::new(), + Vec::new(), ) - .unwrap(), - objects: vec![CirObject { - rsync_uri: uri.to_string(), - sha256: hex::decode(hash_hex).unwrap(), - }], - trust_anchors: vec![test_trust_anchor()], - reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), - rejected_objects: Vec::new(), }; let full_hash = { use sha2::{Digest, Sha256}; @@ -64,17 +59,15 @@ fn cir_offline_sequence_writes_parseable_sequence_json_and_steps() { &full_hash, "2026-03-16T11:49:15Z", ); - let delta_cir = CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: time::OffsetDateTime::parse( + let delta_cir = CanonicalInputRepresentation::new_v4( + time::OffsetDateTime::parse( "2026-03-16T11:50:15Z", &time::format_description::well_known::Rfc3339, ) .unwrap(), - objects: { + { let mut objects = vec![ - full_cir.objects[0].clone(), + full_cir.fresh_validated_objects[0].clone(), CirObject { rsync_uri: "rsync://example.net/repo/delta.roa".to_string(), sha256: hex::decode(&delta_hash).unwrap(), @@ -83,10 +76,11 @@ fn cir_offline_sequence_writes_parseable_sequence_json_and_steps() { objects.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri)); objects }, - trust_anchors: full_cir.trust_anchors.clone(), - reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), - rejected_objects: Vec::new(), - }; + Vec::new(), + full_cir.trust_anchors.clone(), + Vec::new(), + Vec::new(), + ); let empty_ccr = CcrContentInfo::new(RpkiCanonicalCacheRepresentation { version: 0, hash_alg: CcrDigestAlgorithm::Sha256, diff --git a/tests/test_cir_sequence_peer_replay_m4.rs b/tests/test_cir_sequence_peer_replay_m4.rs index 0f7df15..5422918 100644 --- a/tests/test_cir_sequence_peer_replay_m4.rs +++ b/tests/test_cir_sequence_peer_replay_m4.rs @@ -3,8 +3,8 @@ use std::process::Command; use rpki::blob_store::ExternalRepoBytesDb; use rpki::cir::{ - CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirTrustAnchor, - compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, sha256, + CanonicalInputRepresentation, CirTrustAnchor, encode_cir, materialize_cir_from_repo_bytes, + sha256, }; fn skip_heavy_script_replay_test() -> bool { @@ -31,25 +31,24 @@ fn build_ta_only_cir() -> (CanonicalInputRepresentation, Vec) { .as_str() .to_string(); ( - CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: time::OffsetDateTime::parse( + CanonicalInputRepresentation::new_v4( + time::OffsetDateTime::parse( "2026-04-07T00:00:00Z", &time::format_description::well_known::Rfc3339, ) .unwrap(), - objects: Vec::new(), - trust_anchors: vec![CirTrustAnchor { + Vec::new(), + Vec::new(), + vec![CirTrustAnchor { ta_rsync_uri, tal_uri: "https://example.test/root.tal".to_string(), tal_bytes, ta_certificate_sha256: sha256(&ta_bytes), ta_certificate_der: ta_bytes.clone(), }], - reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), - rejected_objects: Vec::new(), - }, + Vec::new(), + Vec::new(), + ), ta_bytes, ) } diff --git a/tests/test_cir_sequence_replay_m3.rs b/tests/test_cir_sequence_replay_m3.rs index 5e75abe..8329094 100644 --- a/tests/test_cir_sequence_replay_m3.rs +++ b/tests/test_cir_sequence_replay_m3.rs @@ -3,8 +3,8 @@ use std::process::Command; use rpki::blob_store::ExternalRepoBytesDb; use rpki::cir::{ - CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirTrustAnchor, - compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, sha256, + CanonicalInputRepresentation, CirTrustAnchor, encode_cir, materialize_cir_from_repo_bytes, + sha256, }; fn skip_heavy_script_replay_test() -> bool { @@ -31,25 +31,24 @@ fn build_ta_only_cir() -> (CanonicalInputRepresentation, Vec) { .as_str() .to_string(); ( - CanonicalInputRepresentation { - version: CIR_VERSION_V3, - hash_alg: CirHashAlgorithm::Sha256, - validation_time: time::OffsetDateTime::parse( + CanonicalInputRepresentation::new_v4( + time::OffsetDateTime::parse( "2026-04-07T00:00:00Z", &time::format_description::well_known::Rfc3339, ) .unwrap(), - objects: Vec::new(), - trust_anchors: vec![CirTrustAnchor { + Vec::new(), + Vec::new(), + vec![CirTrustAnchor { ta_rsync_uri, tal_uri: "https://example.test/root.tal".to_string(), tal_bytes, ta_certificate_sha256: sha256(&ta_bytes), ta_certificate_der: ta_bytes.clone(), }], - reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), - rejected_objects: Vec::new(), - }, + Vec::new(), + Vec::new(), + ), ta_bytes, ) } diff --git a/tests/test_cli_run_offline_m18.rs b/tests/test_cli_run_offline_m18.rs index 33d1947..f7775e0 100644 --- a/tests/test_cli_run_offline_m18.rs +++ b/tests/test_cli_run_offline_m18.rs @@ -134,8 +134,7 @@ fn cli_run_offline_mode_writes_cir_and_static_pool() { ); assert!(!cir.trust_anchors[0].ta_certificate_der.is_empty()); assert!( - !cir.objects - .iter() + !cir.validated_objects() .any(|item| item.rsync_uri == cir.trust_anchors[0].ta_rsync_uri) ); }