use std::collections::BTreeMap; use crate::ccr::build::{ build_aspa_payload_state, build_roa_payload_state, build_router_key_state_from_runtime, build_trust_anchor_state, }; use crate::ccr::encode::encode_manifest_state_payload_der; use crate::ccr::hash::compute_state_hash; use crate::ccr::model::{ CcrDigestAlgorithm, ManifestInstance, ManifestState, RpkiCanonicalCacheRepresentation, }; use crate::data_model::common::BigUnsigned; use crate::data_model::ta::TrustAnchor; use crate::storage::VcirCcrManifestProjection; use crate::validation::objects::{AspaAttestation, RouterKeyPayload, Vrp}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct CcrManifestContribution { pub manifest_rsync_uri: String, pub hash: Vec, pub size: u64, pub aki: Vec, pub manifest_number_be: Vec, pub this_update: time::OffsetDateTime, pub locations_der: Vec>, pub subordinate_skis: Vec>, } #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct CcrAccumulatorMemoryStats { pub trust_anchor_count: u64, pub manifest_count: u64, pub estimated_heap_bytes: u64, pub string_bytes: u64, pub string_capacity_bytes: u64, pub vec_payload_bytes: u64, pub vec_capacity_bytes: u64, pub locations_der_count: u64, pub subordinate_ski_count: u64, pub btree_key_capacity_bytes: u64, pub btree_entry_shallow_bytes: u64, } impl CcrManifestContribution { fn from_projection(projection: &VcirCcrManifestProjection) -> Result { let this_update = projection .manifest_this_update .parse() .map_err(|e| format!("parse projection manifest_this_update failed: {e}"))?; Ok(Self { manifest_rsync_uri: projection.manifest_rsync_uri.clone(), hash: projection.manifest_sha256.clone(), size: projection.manifest_size, aki: projection.manifest_ee_aki.clone(), manifest_number_be: projection.manifest_number_be.clone(), this_update, locations_der: projection.manifest_sia_locations_der.clone(), subordinate_skis: projection.subordinate_skis.clone(), }) } fn to_manifest_instance(&self) -> ManifestInstance { ManifestInstance { hash: self.hash.clone(), size: self.size, aki: self.aki.clone(), manifest_number: BigUnsigned { bytes_be: self.manifest_number_be.clone(), }, this_update: self.this_update, locations: self.locations_der.clone(), subordinates: self.subordinate_skis.clone(), } } fn add_memory_stats(&self, stats: &mut CcrAccumulatorMemoryStats) { stats.string_bytes += self.manifest_rsync_uri.len() as u64; stats.string_capacity_bytes += self.manifest_rsync_uri.capacity() as u64; stats.estimated_heap_bytes += self.manifest_rsync_uri.capacity() as u64; add_vec_stats(&self.hash, stats); add_vec_stats(&self.aki, stats); add_vec_stats(&self.manifest_number_be, stats); add_vec_of_vec_stats(&self.locations_der, stats); add_vec_of_vec_stats(&self.subordinate_skis, stats); stats.locations_der_count += self.locations_der.len() as u64; stats.subordinate_ski_count += self.subordinate_skis.len() as u64; } } fn add_vec_stats(value: &Vec, stats: &mut CcrAccumulatorMemoryStats) { stats.vec_payload_bytes += value.len() as u64; stats.vec_capacity_bytes += value.capacity() as u64; stats.estimated_heap_bytes += value.capacity() as u64; } fn add_vec_of_vec_stats(values: &Vec>, stats: &mut CcrAccumulatorMemoryStats) { let outer_capacity = values.capacity() * std::mem::size_of::>(); stats.vec_payload_bytes += (values.len() * std::mem::size_of::>()) as u64; stats.vec_capacity_bytes += outer_capacity as u64; stats.estimated_heap_bytes += outer_capacity as u64; for value in values { add_vec_stats(value, stats); } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct CcrAccumulator { trust_anchors: Vec, manifests_by_hash: BTreeMap, CcrManifestContribution>, most_recent_update: time::OffsetDateTime, } impl CcrAccumulator { pub fn new(trust_anchors: Vec) -> Self { Self { trust_anchors, manifests_by_hash: BTreeMap::new(), most_recent_update: time::OffsetDateTime::UNIX_EPOCH, } } pub fn append_manifest_projection( &mut self, projection: &VcirCcrManifestProjection, ) -> Result<(), String> { let contribution = CcrManifestContribution::from_projection(projection)?; match self.manifests_by_hash.get(contribution.hash.as_slice()) { Some(existing) if existing != &contribution => { return Err(format!( "duplicate manifest hash with conflicting content for URI: {}", contribution.manifest_rsync_uri )); } Some(_) => {} None => { self.manifests_by_hash .insert(contribution.hash.clone(), contribution.clone()); } } if contribution.this_update > self.most_recent_update { self.most_recent_update = contribution.this_update; } Ok(()) } pub fn finish( &self, produced_at: time::OffsetDateTime, vrps: &[Vrp], aspas: &[AspaAttestation], router_keys: &[RouterKeyPayload], ) -> Result { let manifest_instances = self .manifests_by_hash .values() .map(CcrManifestContribution::to_manifest_instance) .collect::>(); let manifest_payload_der = encode_manifest_state_payload_der(&manifest_instances) .map_err(|e| format!("manifest state encoding failed: {e}"))?; let manifest_state = ManifestState { mis: manifest_instances, most_recent_update: self.most_recent_update, hash: compute_state_hash(&manifest_payload_der), }; let vrp_state = build_roa_payload_state(vrps).map_err(|e| e.to_string())?; let aspa_state = build_aspa_payload_state(aspas).map_err(|e| e.to_string())?; let ta_state = build_trust_anchor_state(&self.trust_anchors).map_err(|e| e.to_string())?; let router_key_state = build_router_key_state_from_runtime(router_keys).map_err(|e| e.to_string())?; Ok(RpkiCanonicalCacheRepresentation { version: 0, hash_alg: CcrDigestAlgorithm::Sha256, produced_at, mfts: Some(manifest_state), vrps: Some(vrp_state), vaps: Some(aspa_state), tas: Some(ta_state), rks: Some(router_key_state), }) } pub fn manifest_count(&self) -> usize { self.manifests_by_hash.len() } pub fn memory_stats(&self) -> CcrAccumulatorMemoryStats { let mut stats = CcrAccumulatorMemoryStats { trust_anchor_count: self.trust_anchors.len() as u64, manifest_count: self.manifests_by_hash.len() as u64, ..CcrAccumulatorMemoryStats::default() }; stats.estimated_heap_bytes += (self.trust_anchors.capacity() * std::mem::size_of::()) as u64; for trust_anchor in &self.trust_anchors { add_vec_stats(&trust_anchor.tal.raw, &mut stats); add_vec_of_string_stats(&trust_anchor.tal.comments, &mut stats); stats.vec_payload_bytes += (trust_anchor.tal.ta_uris.len() * std::mem::size_of::()) as u64; stats.vec_capacity_bytes += (trust_anchor.tal.ta_uris.capacity() * std::mem::size_of::()) as u64; stats.estimated_heap_bytes += (trust_anchor.tal.ta_uris.capacity() * std::mem::size_of::()) as u64; for uri in &trust_anchor.tal.ta_uris { stats.string_bytes += uri.as_str().len() as u64; stats.string_capacity_bytes += uri.as_str().len() as u64; stats.estimated_heap_bytes += uri.as_str().len() as u64; } add_vec_stats(&trust_anchor.tal.subject_public_key_info_der, &mut stats); add_vec_stats(&trust_anchor.ta_certificate.raw_der, &mut stats); if let Some(uri) = &trust_anchor.resolved_ta_uri { stats.string_bytes += uri.as_str().len() as u64; stats.string_capacity_bytes += uri.as_str().len() as u64; stats.estimated_heap_bytes += uri.as_str().len() as u64; } } stats.btree_entry_shallow_bytes = (self.manifests_by_hash.len() * (std::mem::size_of::>() + std::mem::size_of::())) as u64; stats.estimated_heap_bytes += stats.btree_entry_shallow_bytes; for (key, contribution) in &self.manifests_by_hash { stats.btree_key_capacity_bytes += key.capacity() as u64; stats.estimated_heap_bytes += key.capacity() as u64; contribution.add_memory_stats(&mut stats); } stats } } fn add_vec_of_string_stats(values: &Vec, stats: &mut CcrAccumulatorMemoryStats) { let outer_capacity = values.capacity() * std::mem::size_of::(); stats.vec_payload_bytes += (values.len() * std::mem::size_of::()) as u64; stats.vec_capacity_bytes += outer_capacity as u64; stats.estimated_heap_bytes += outer_capacity as u64; for value in values { stats.string_bytes += value.len() as u64; stats.string_capacity_bytes += value.capacity() as u64; stats.estimated_heap_bytes += value.capacity() as u64; } } #[cfg(test)] mod tests { use super::*; use crate::ccr::export::build_ccr_from_run; use crate::ccr::verify::verify_content_info; use crate::data_model::manifest::ManifestObject; use crate::data_model::rc::SubjectInfoAccess; use crate::data_model::roa::{IpPrefix, RoaAfi}; use crate::data_model::ta::TrustAnchor; use crate::data_model::tal::Tal; use crate::storage::{ PackTime, RawByHashEntry, RocksStore, ValidatedCaInstanceResult, ValidatedManifestMeta, VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary, VcirCcrManifestProjection, VcirChildEntry, VcirInstanceGate, VcirRelatedArtifact, VcirSummary, }; use sha2::Digest; fn sample_trust_anchor() -> TrustAnchor { let base = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); let tal_bytes = std::fs::read(base.join("tests/fixtures/tal/apnic-rfc7730-https.tal")) .expect("read tal"); let ta_der = std::fs::read(base.join("tests/fixtures/ta/apnic-ta.cer")).expect("read ta"); let tal = Tal::decode_bytes(&tal_bytes).expect("decode tal"); TrustAnchor::bind_der(tal, &ta_der, None).expect("bind ta") } fn sample_vcir_and_manifest( store: &RocksStore, ) -> ( ValidatedCaInstanceResult, Vec, Vec, Vec, ) { let base = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); let manifest_der = std::fs::read( base.join( "tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft", ), ) .expect("read manifest"); let manifest = ManifestObject::decode_der(&manifest_der).expect("decode manifest"); let manifest_hash = hex::encode(sha2::Sha256::digest(&manifest_der)); let mut raw = RawByHashEntry::from_bytes(manifest_hash.clone(), manifest_der.clone()); raw.origin_uris .push("rsync://example.test/repo/current.mft".to_string()); raw.object_type = Some("mft".to_string()); raw.encoding = Some("der".to_string()); store.put_raw_by_hash_entry(&raw).expect("put raw"); let projection = VcirCcrManifestProjection { manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(), manifest_sha256: sha2::Sha256::digest(&manifest_der).to_vec(), manifest_size: manifest_der.len() as u64, manifest_ee_aki: manifest.signed_object.signed_data.certificates[0] .resource_cert .tbs .extensions .authority_key_identifier .clone() .expect("manifest aki"), manifest_number_be: manifest.manifest.manifest_number.bytes_be.clone(), manifest_this_update: PackTime::from_utc_offset_datetime(manifest.manifest.this_update), manifest_sia_locations_der: match manifest.signed_object.signed_data.certificates[0] .resource_cert .tbs .extensions .subject_info_access .as_ref() .expect("manifest sia") { SubjectInfoAccess::Ee(ee_sia) => ee_sia .access_descriptions .iter() .map(|ad| { let oid = encode_oid_for_test(&ad.access_method_oid) .expect("encode access method oid"); let uri = encode_tlv_for_test(0x86, ad.access_location.as_bytes().to_vec()); encode_sequence_for_test(&[oid, uri]) }) .collect(), SubjectInfoAccess::Ca(_) => panic!("manifest ee sia should not be CA variant"), }, subordinate_skis: vec![vec![0x33; 20]], }; let vcir = ValidatedCaInstanceResult { manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(), parent_manifest_rsync_uri: None, tal_id: "apnic".to_string(), ca_subject_name: "CN=test".to_string(), ca_ski: "11".repeat(20), issuer_ski: "22".repeat(20), last_successful_validation_time: PackTime::from_utc_offset_datetime( manifest.manifest.this_update, ), current_manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(), current_crl_rsync_uri: "rsync://example.test/repo/current.crl".to_string(), validated_manifest_meta: ValidatedManifestMeta { validated_manifest_number: manifest.manifest.manifest_number.bytes_be.clone(), validated_manifest_this_update: PackTime::from_utc_offset_datetime( manifest.manifest.this_update, ), validated_manifest_next_update: PackTime::from_utc_offset_datetime( manifest.manifest.next_update, ), }, ccr_manifest_projection: projection.clone(), instance_gate: VcirInstanceGate { manifest_next_update: PackTime::from_utc_offset_datetime( manifest.manifest.next_update, ), current_crl_next_update: PackTime::from_utc_offset_datetime( manifest.manifest.next_update, ), self_ca_not_after: PackTime::from_utc_offset_datetime( manifest.manifest.next_update, ), instance_effective_until: PackTime::from_utc_offset_datetime( manifest.manifest.next_update, ), }, child_entries: vec![VcirChildEntry { child_manifest_rsync_uri: "rsync://example.test/repo/child.mft".to_string(), child_cert_rsync_uri: "rsync://example.test/repo/child.cer".to_string(), child_cert_hash: "aa".repeat(32), child_ski: "33".repeat(20), child_rsync_base_uri: "rsync://example.test/repo/".to_string(), child_publication_point_rsync_uri: "rsync://example.test/repo/".to_string(), child_rrdp_notification_uri: None, child_effective_ip_resources: None, child_effective_as_resources: None, accepted_at_validation_time: PackTime::from_utc_offset_datetime( manifest.manifest.this_update, ), }], local_outputs: Vec::new(), related_artifacts: vec![VcirRelatedArtifact { artifact_role: VcirArtifactRole::Manifest, artifact_kind: VcirArtifactKind::Mft, uri: Some("rsync://example.test/repo/current.mft".to_string()), sha256: manifest_hash, object_type: Some("mft".to_string()), validation_status: VcirArtifactValidationStatus::Accepted, }], summary: VcirSummary { local_vrp_count: 0, local_aspa_count: 0, local_router_key_count: 0, child_count: 1, accepted_object_count: 1, rejected_object_count: 0, }, audit_summary: VcirAuditSummary { failed_fetch_eligible: true, last_failed_fetch_reason: None, warning_count: 0, audit_flags: Vec::new(), }, }; let vrps = vec![Vrp { asn: 64496, prefix: IpPrefix { afi: RoaAfi::Ipv4, prefix_len: 8, addr: [10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], }, max_length: 8, }]; let aspas = vec![AspaAttestation { customer_as_id: 64496, provider_as_ids: vec![64497], }]; let router_keys = vec![RouterKeyPayload { as_id: 64496, ski: vec![0x11; 20], spki_der: vec![0x30, 0x00], source_object_uri: "rsync://example.test/repo/router.cer".to_string(), source_object_hash: hex::encode([0x11; 32]), source_ee_cert_hash: hex::encode([0x11; 32]), item_effective_until: PackTime::from_utc_offset_datetime( time::OffsetDateTime::now_utc() + time::Duration::hours(1), ), }]; (vcir, vrps, aspas, router_keys) } fn encode_oid_for_test(oid: &str) -> Result, String> { let arcs = oid .split('.') .map(|part| part.parse::().map_err(|_| format!("bad oid: {oid}"))) .collect::, _>>()?; if arcs.len() < 2 { return Err(format!("bad oid: {oid}")); } let mut body = Vec::new(); body.push((arcs[0] * 40 + arcs[1]) as u8); for arc in &arcs[2..] { encode_base128_for_test(*arc, &mut body); } Ok(encode_tlv_for_test(0x06, body)) } fn encode_base128_for_test(mut value: u64, out: &mut Vec) { let mut tmp = vec![(value & 0x7F) as u8]; value >>= 7; while value > 0 { tmp.push(((value & 0x7F) as u8) | 0x80); value >>= 7; } tmp.reverse(); out.extend_from_slice(&tmp); } fn encode_sequence_for_test(elements: &[Vec]) -> Vec { let total_len: usize = elements.iter().map(Vec::len).sum(); let mut buf = Vec::with_capacity(total_len); for element in elements { buf.extend_from_slice(element); } encode_tlv_for_test(0x30, buf) } fn encode_tlv_for_test(tag: u8, value: Vec) -> Vec { let mut out = Vec::with_capacity(1 + 9 + value.len()); out.push(tag); if value.len() < 0x80 { out.push(value.len() as u8); } else { out.push(0x81); out.push(value.len() as u8); } out.extend_from_slice(&value); out } #[test] fn accumulator_finish_matches_builder_on_fresh_vcir_inputs() { let td = tempfile::tempdir().expect("tempdir"); let store = RocksStore::open(td.path()).expect("open rocksdb"); let (vcir, vrps, aspas, router_keys) = sample_vcir_and_manifest(&store); store.put_vcir(&vcir).expect("put vcir"); let trust_anchor = sample_trust_anchor(); let builder_ccr = build_ccr_from_run( &store, &[trust_anchor.clone()], &vrps, &aspas, &router_keys, time::OffsetDateTime::now_utc(), ) .expect("build ccr from run"); let mut accumulator = CcrAccumulator::new(vec![trust_anchor]); accumulator .append_manifest_projection(&vcir.ccr_manifest_projection) .expect("append manifest projection"); let accumulated_ccr = accumulator .finish(time::OffsetDateTime::now_utc(), &vrps, &aspas, &router_keys) .expect("finish accumulator"); assert_eq!(builder_ccr.mfts, accumulated_ccr.mfts); assert_eq!(builder_ccr.vrps, accumulated_ccr.vrps); assert_eq!(builder_ccr.vaps, accumulated_ccr.vaps); assert_eq!(builder_ccr.tas, accumulated_ccr.tas); assert_eq!(builder_ccr.rks, accumulated_ccr.rks); let ci = crate::ccr::model::CcrContentInfo::new(accumulated_ccr); verify_content_info(&ci).expect("verify accumulated ccr"); } }