use crate::ccr::build::{ build_aspa_payload_state, build_manifest_state_from_vcirs_with_breakdown, build_roa_payload_state, build_router_key_state_from_runtime, build_trust_anchor_state, CcrBuildError, ManifestStateBuildBreakdown, }; use crate::ccr::encode::{encode_content_info, CcrEncodeError}; use crate::ccr::model::{CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation}; use crate::data_model::ta::TrustAnchor; use crate::storage::RocksStore; use crate::validation::objects::{AspaAttestation, RouterKeyPayload, Vrp}; use serde::Serialize; use std::path::Path; #[derive(Debug, thiserror::Error)] pub enum CcrExportError { #[error("list VCIRs failed: {0}")] ListVcirs(String), #[error("build CCR state failed: {0}")] Build(#[from] CcrBuildError), #[error("encode CCR failed: {0}")] Encode(#[from] CcrEncodeError), #[error("write CCR file failed: {0}: {1}")] Write(String, String), } #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)] pub struct CcrBuildBreakdown { pub vcir_count: usize, pub list_vcirs_ms: u64, pub manifest_state_ms: u64, pub manifest_state_breakdown: ManifestStateBuildBreakdown, pub roa_payload_state_ms: u64, pub aspa_payload_state_ms: u64, pub trust_anchor_state_ms: u64, pub router_key_state_ms: u64, pub total_ms: u64, } pub fn build_ccr_from_run( store: &RocksStore, trust_anchors: &[TrustAnchor], vrps: &[Vrp], aspas: &[AspaAttestation], router_keys: &[RouterKeyPayload], produced_at: time::OffsetDateTime, ) -> Result { build_ccr_from_run_with_breakdown(store, trust_anchors, vrps, aspas, router_keys, produced_at) .map(|(ccr, _)| ccr) } pub fn build_ccr_from_run_with_breakdown( store: &RocksStore, trust_anchors: &[TrustAnchor], vrps: &[Vrp], aspas: &[AspaAttestation], router_keys: &[RouterKeyPayload], produced_at: time::OffsetDateTime, ) -> Result<(RpkiCanonicalCacheRepresentation, CcrBuildBreakdown), CcrExportError> { let total_started = std::time::Instant::now(); let mut breakdown = CcrBuildBreakdown::default(); let started = std::time::Instant::now(); let vcirs = store .list_vcirs() .map_err(|e| CcrExportError::ListVcirs(e.to_string()))?; breakdown.list_vcirs_ms = started.elapsed().as_millis() as u64; breakdown.vcir_count = vcirs.len(); let started = std::time::Instant::now(); let (mfts, manifest_state_breakdown) = build_manifest_state_from_vcirs_with_breakdown(store, &vcirs)?; breakdown.manifest_state_ms = started.elapsed().as_millis() as u64; breakdown.manifest_state_breakdown = manifest_state_breakdown; let started = std::time::Instant::now(); let vrps = build_roa_payload_state(vrps)?; breakdown.roa_payload_state_ms = started.elapsed().as_millis() as u64; let started = std::time::Instant::now(); let vaps = build_aspa_payload_state(aspas)?; breakdown.aspa_payload_state_ms = started.elapsed().as_millis() as u64; let started = std::time::Instant::now(); let tas = build_trust_anchor_state(trust_anchors)?; breakdown.trust_anchor_state_ms = started.elapsed().as_millis() as u64; let started = std::time::Instant::now(); let rks = build_router_key_state_from_runtime(router_keys)?; breakdown.router_key_state_ms = started.elapsed().as_millis() as u64; let ccr = RpkiCanonicalCacheRepresentation { version: 0, hash_alg: CcrDigestAlgorithm::Sha256, produced_at, mfts: Some(mfts), vrps: Some(vrps), vaps: Some(vaps), tas: Some(tas), rks: Some(rks), }; breakdown.total_ms = total_started.elapsed().as_millis() as u64; Ok((ccr, breakdown)) } pub fn write_ccr_file( path: &Path, ccr: &RpkiCanonicalCacheRepresentation, ) -> Result<(), CcrExportError> { let der = encode_content_info(&CcrContentInfo::new(ccr.clone()))?; if let Some(parent) = path.parent() { std::fs::create_dir_all(parent) .map_err(|e| CcrExportError::Write(path.display().to_string(), e.to_string()))?; } std::fs::write(path, der) .map_err(|e| CcrExportError::Write(path.display().to_string(), e.to_string())) } #[cfg(test)] mod tests { use super::*; use crate::ccr::decode::decode_content_info; use crate::data_model::manifest::ManifestObject; 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 crate::validation::objects::{AspaAttestation, RouterKeyPayload, Vrp}; 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_raw(store: &RocksStore) -> ValidatedCaInstanceResult { 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 hash = hex::encode(sha2::Sha256::digest(&manifest_der)); let mut raw = RawByHashEntry::from_bytes(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: vec![vec![ 0x30, 0x11, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x05, 0x86, 0x05, b'r', b's', b'y', b'n', b'c', ]], subordinate_skis: vec![vec![0x33; 20]], }; 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, 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: 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(), }, } } #[test] fn build_and_write_ccr_from_run_exports_der_content_info() { let td = tempfile::tempdir().expect("tempdir"); let db_path = td.path().join("db"); let store = RocksStore::open(&db_path).expect("open rocksdb"); let vcir = sample_vcir_and_raw(&store); store.put_vcir(&vcir).expect("put vcir"); let trust_anchor = sample_trust_anchor(); 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), ), }]; let ccr = build_ccr_from_run( &store, &[trust_anchor], &vrps, &aspas, &router_keys, time::OffsetDateTime::now_utc(), ) .expect("build ccr"); assert!(ccr.mfts.is_some()); assert!(ccr.vrps.is_some()); assert!(ccr.vaps.is_some()); assert!(ccr.tas.is_some()); assert!(ccr.rks.is_some()); assert_eq!(ccr.rks.as_ref().unwrap().rksets.len(), 1); let out = td.path().join("out/example.ccr"); write_ccr_file(&out, &ccr).expect("write ccr"); let der = std::fs::read(&out).expect("read ccr file"); let decoded = decode_content_info(&der).expect("decode ccr"); assert_eq!(decoded.content.version, 0); assert!(decoded.content.mfts.is_some()); } }