use crate::data_model::aspa::AspaObject; use crate::data_model::manifest::ManifestObject; use crate::data_model::roa::RoaObject; use crate::storage::{ AuditRuleIndexEntry, AuditRuleKind, RawByHashEntry, RocksStore, ValidatedCaInstanceResult, VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirLocalOutput, VcirOutputType, }; use serde::Serialize; use std::collections::HashSet; #[derive(Debug, thiserror::Error)] pub enum AuditTraceError { #[error("storage error: {0}")] Storage(#[from] crate::storage::StorageError), #[error("audit rule index points to missing VCIR: {manifest_rsync_uri}")] MissingVcir { manifest_rsync_uri: String }, #[error( "audit rule index points to missing local output: rule_hash={rule_hash}, output_id={output_id}, manifest={manifest_rsync_uri}" )] MissingLocalOutput { rule_hash: String, output_id: String, manifest_rsync_uri: String, }, #[error("detected VCIR parent cycle at {manifest_rsync_uri}")] ParentCycle { manifest_rsync_uri: String }, } #[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub struct AuditTraceRawRef { pub sha256_hex: String, pub raw_present: bool, #[serde(skip_serializing_if = "Vec::is_empty", default)] pub origin_uris: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub object_type: Option, #[serde(skip_serializing_if = "Option::is_none")] pub byte_len: Option, } #[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub struct AuditTraceArtifact { pub artifact_role: VcirArtifactRole, pub artifact_kind: VcirArtifactKind, #[serde(skip_serializing_if = "Option::is_none")] pub uri: Option, #[serde(skip_serializing_if = "Option::is_none")] pub object_type: Option, pub validation_status: VcirArtifactValidationStatus, pub raw: AuditTraceRawRef, } #[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub struct AuditTraceChainNode { pub manifest_rsync_uri: String, #[serde(skip_serializing_if = "Option::is_none")] pub parent_manifest_rsync_uri: Option, pub tal_id: String, pub ca_subject_name: String, pub ca_ski: String, pub issuer_ski: String, pub current_manifest_rsync_uri: String, pub current_crl_rsync_uri: String, pub last_successful_validation_time_rfc3339_utc: String, pub local_output_count: usize, pub child_count: usize, pub related_artifacts: Vec, } #[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub struct AuditTraceResolvedOutput { pub output_id: String, pub output_type: VcirOutputType, pub rule_hash: String, pub source_object_uri: String, pub source_object_type: String, pub source_object_hash: String, pub source_ee_cert_hash: String, pub item_effective_until_rfc3339_utc: String, pub payload_json: String, pub validation_path_hint: Vec, } #[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub struct AuditRuleTrace { pub rule: AuditRuleIndexEntry, pub resolved_output: AuditTraceResolvedOutput, pub source_object_raw: AuditTraceRawRef, pub source_ee_cert_raw: AuditTraceRawRef, pub chain_leaf_to_root: Vec, } pub fn trace_rule_to_root( store: &RocksStore, kind: AuditRuleKind, rule_hash: &str, ) -> Result, AuditTraceError> { let Some(rule) = store.get_audit_rule_index_entry(kind, rule_hash)? else { return Ok(None); }; let Some(leaf_vcir) = store.get_vcir(&rule.manifest_rsync_uri)? else { return Err(AuditTraceError::MissingVcir { manifest_rsync_uri: rule.manifest_rsync_uri.clone(), }); }; let Some(local_output) = leaf_vcir .local_outputs .iter() .find(|output| output.output_id == rule.output_id && output.rule_hash == rule.rule_hash) .or_else(|| { leaf_vcir .local_outputs .iter() .find(|output| output.rule_hash == rule.rule_hash) }) .cloned() else { return Err(AuditTraceError::MissingLocalOutput { rule_hash: rule.rule_hash.clone(), output_id: rule.output_id.clone(), manifest_rsync_uri: rule.manifest_rsync_uri.clone(), }); }; let chain = trace_vcir_chain_to_root(store, &leaf_vcir.manifest_rsync_uri)? .expect("leaf VCIR already loaded must exist"); Ok(Some(AuditRuleTrace { rule, resolved_output: resolved_output_from_local(&local_output), source_object_raw: resolve_raw_ref( store, &local_output.source_object_hash, Some(&local_output.source_object_uri), Some(local_output.source_object_type.as_str()), )?, source_ee_cert_raw: resolve_source_ee_cert_raw_ref(store, &local_output)?, chain_leaf_to_root: chain, })) } pub fn trace_vcir_chain_to_root( store: &RocksStore, manifest_rsync_uri: &str, ) -> Result>, AuditTraceError> { let Some(mut current) = store.get_vcir(manifest_rsync_uri)? else { return Ok(None); }; let mut seen = HashSet::new(); let mut chain = Vec::new(); loop { if !seen.insert(current.manifest_rsync_uri.clone()) { return Err(AuditTraceError::ParentCycle { manifest_rsync_uri: current.manifest_rsync_uri, }); } let parent = current.parent_manifest_rsync_uri.clone(); chain.push(trace_chain_node(store, ¤t)?); let Some(parent_manifest_rsync_uri) = parent else { break; }; let Some(parent_vcir) = store.get_vcir(&parent_manifest_rsync_uri)? else { return Err(AuditTraceError::MissingVcir { manifest_rsync_uri: parent_manifest_rsync_uri, }); }; current = parent_vcir; } Ok(Some(chain)) } fn trace_chain_node( store: &RocksStore, vcir: &ValidatedCaInstanceResult, ) -> Result { let mut related_artifacts = Vec::with_capacity(vcir.related_artifacts.len()); for artifact in &vcir.related_artifacts { related_artifacts.push(AuditTraceArtifact { artifact_role: artifact.artifact_role, artifact_kind: artifact.artifact_kind, uri: artifact.uri.clone(), object_type: artifact.object_type.clone(), validation_status: artifact.validation_status, raw: resolve_raw_ref( store, &artifact.sha256, artifact.uri.as_deref(), artifact.object_type.as_deref(), )?, }); } Ok(AuditTraceChainNode { manifest_rsync_uri: vcir.manifest_rsync_uri.clone(), parent_manifest_rsync_uri: vcir.parent_manifest_rsync_uri.clone(), tal_id: vcir.tal_id.clone(), ca_subject_name: vcir.ca_subject_name.clone(), ca_ski: vcir.ca_ski.clone(), issuer_ski: vcir.issuer_ski.clone(), current_manifest_rsync_uri: vcir.current_manifest_rsync_uri.clone(), current_crl_rsync_uri: vcir.current_crl_rsync_uri.clone(), last_successful_validation_time_rfc3339_utc: vcir .last_successful_validation_time .rfc3339_utc .clone(), local_output_count: vcir.local_outputs.len(), child_count: vcir.child_entries.len(), related_artifacts, }) } fn resolved_output_from_local(local: &VcirLocalOutput) -> AuditTraceResolvedOutput { AuditTraceResolvedOutput { output_id: local.output_id.clone(), output_type: local.output_type, rule_hash: local.rule_hash.clone(), source_object_uri: local.source_object_uri.clone(), source_object_type: local.source_object_type.clone(), source_object_hash: local.source_object_hash.clone(), source_ee_cert_hash: local.source_ee_cert_hash.clone(), item_effective_until_rfc3339_utc: local.item_effective_until.rfc3339_utc.clone(), payload_json: local.payload_json.clone(), validation_path_hint: local.validation_path_hint.clone(), } } fn resolve_raw_ref( store: &RocksStore, sha256_hex: &str, fallback_uri: Option<&str>, fallback_object_type: Option<&str>, ) -> Result { let raw = store.get_raw_by_hash_entry(sha256_hex)?; if raw.is_some() { return Ok(raw_ref_from_entry(sha256_hex, raw.as_ref())); } let blob = store.get_blob_bytes(sha256_hex)?; match blob { Some(bytes) => Ok(AuditTraceRawRef { sha256_hex: sha256_hex.to_string(), raw_present: true, origin_uris: fallback_uri .map(|uri| vec![uri.to_string()]) .unwrap_or_default(), object_type: fallback_object_type.map(str::to_string), byte_len: Some(bytes.len()), }), None => Ok(raw_ref_from_entry(sha256_hex, None)), } } fn resolve_source_ee_cert_raw_ref( store: &RocksStore, local: &VcirLocalOutput, ) -> Result { let raw = store.get_raw_by_hash_entry(&local.source_ee_cert_hash)?; if raw.is_some() { return Ok(raw_ref_from_entry(&local.source_ee_cert_hash, raw.as_ref())); } let source_bytes = store.get_blob_bytes(&local.source_object_hash)?; let Some(source_bytes) = source_bytes else { return Ok(raw_ref_from_entry(&local.source_ee_cert_hash, None)); }; let derived = match local.source_object_type.as_str() { "roa" => RoaObject::decode_der(&source_bytes).ok().and_then(|roa| { roa.signed_object .signed_data .certificates .first() .map(|cert| cert.raw_der.to_vec()) }), "aspa" => AspaObject::decode_der(&source_bytes).ok().and_then(|aspa| { aspa.signed_object .signed_data .certificates .first() .map(|cert| cert.raw_der.to_vec()) }), "mft" => ManifestObject::decode_der(&source_bytes) .ok() .and_then(|manifest| { manifest .signed_object .signed_data .certificates .first() .map(|cert| cert.raw_der.to_vec()) }), "router_key" => Some(source_bytes), _ => None, }; let Some(ee_der) = derived else { return Ok(raw_ref_from_entry(&local.source_ee_cert_hash, None)); }; if crate::audit::sha256_hex(ee_der.as_slice()) != local.source_ee_cert_hash { return Ok(raw_ref_from_entry(&local.source_ee_cert_hash, None)); } Ok(AuditTraceRawRef { sha256_hex: local.source_ee_cert_hash.clone(), raw_present: true, origin_uris: Vec::new(), object_type: Some("cer".to_string()), byte_len: Some(ee_der.len()), }) } fn raw_ref_from_entry(sha256_hex: &str, entry: Option<&RawByHashEntry>) -> AuditTraceRawRef { match entry { Some(entry) => AuditTraceRawRef { sha256_hex: sha256_hex.to_string(), raw_present: true, origin_uris: entry.origin_uris.clone(), object_type: entry.object_type.clone(), byte_len: Some(entry.bytes.len()), }, None => AuditTraceRawRef { sha256_hex: sha256_hex.to_string(), raw_present: false, origin_uris: Vec::new(), object_type: None, byte_len: None, }, } } #[cfg(test)] mod tests { use super::*; use crate::audit::sha256_hex; use crate::data_model::roa::RoaObject; use crate::storage::{ PackTime, ValidatedManifestMeta, VcirAuditSummary, VcirCcrManifestProjection, VcirChildEntry, VcirInstanceGate, VcirRelatedArtifact, VcirSummary, }; use base64::Engine as _; fn sample_vcir( manifest_rsync_uri: &str, parent_manifest_rsync_uri: Option<&str>, tal_id: &str, local_output: Option, related_artifacts: Vec, ) -> ValidatedCaInstanceResult { let now = time::OffsetDateTime::now_utc(); let next = PackTime::from_utc_offset_datetime(now + time::Duration::hours(1)); let local_outputs: Vec = local_output.into_iter().collect(); let ccr_manifest_projection = VcirCcrManifestProjection { manifest_rsync_uri: manifest_rsync_uri.to_string(), manifest_sha256: vec![0x44; 32], manifest_size: 2048, manifest_ee_aki: vec![0x55; 20], manifest_number_be: vec![1], manifest_this_update: PackTime::from_utc_offset_datetime(now), 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: manifest_rsync_uri.to_string(), parent_manifest_rsync_uri: parent_manifest_rsync_uri.map(str::to_string), tal_id: tal_id.to_string(), ca_subject_name: format!("CN={manifest_rsync_uri}"), ca_ski: "11".repeat(20), issuer_ski: "22".repeat(20), last_successful_validation_time: PackTime::from_utc_offset_datetime(now), current_manifest_rsync_uri: manifest_rsync_uri.to_string(), current_crl_rsync_uri: manifest_rsync_uri.replace(".mft", ".crl"), validated_manifest_meta: ValidatedManifestMeta { validated_manifest_number: vec![1], validated_manifest_this_update: PackTime::from_utc_offset_datetime(now), validated_manifest_next_update: next.clone(), }, ccr_manifest_projection, instance_gate: VcirInstanceGate { manifest_next_update: next.clone(), current_crl_next_update: next.clone(), self_ca_not_after: PackTime::from_utc_offset_datetime( now + time::Duration::hours(2), ), instance_effective_until: next, }, child_entries: vec![VcirChildEntry { child_manifest_rsync_uri: "rsync://example.test/child/child.mft".to_string(), child_cert_rsync_uri: "rsync://example.test/parent/child.cer".to_string(), child_cert_hash: sha256_hex(b"child-cert"), child_ski: "33".repeat(20), child_rsync_base_uri: "rsync://example.test/child/".to_string(), child_publication_point_rsync_uri: "rsync://example.test/child/".to_string(), child_rrdp_notification_uri: Some( "https://example.test/child/notify.xml".to_string(), ), child_effective_ip_resources: None, child_effective_as_resources: None, accepted_at_validation_time: PackTime::from_utc_offset_datetime(now), }], summary: VcirSummary { local_vrp_count: local_outputs .iter() .filter(|output| output.output_type == VcirOutputType::Vrp) .count() as u32, local_aspa_count: local_outputs .iter() .filter(|output| output.output_type == VcirOutputType::Aspa) .count() as u32, local_router_key_count: local_outputs .iter() .filter(|output| output.output_type == VcirOutputType::RouterKey) .count() as u32, child_count: 1, accepted_object_count: related_artifacts.len() as u32, rejected_object_count: 0, }, local_outputs, related_artifacts, audit_summary: VcirAuditSummary { failed_fetch_eligible: true, last_failed_fetch_reason: None, warning_count: 0, audit_flags: Vec::new(), }, } } fn sample_local_output(manifest_rsync_uri: &str) -> VcirLocalOutput { let now = time::OffsetDateTime::now_utc(); VcirLocalOutput { output_id: sha256_hex(b"vrp-output"), output_type: VcirOutputType::Vrp, item_effective_until: PackTime::from_utc_offset_datetime( now + time::Duration::minutes(30), ), source_object_uri: "rsync://example.test/leaf/a.roa".to_string(), source_object_type: "roa".to_string(), source_object_hash: sha256_hex(b"roa-raw"), source_ee_cert_hash: sha256_hex(b"roa-ee"), payload_json: serde_json::json!({"asn": 64496, "prefix": "203.0.113.0/24", "max_length": 24}) .to_string(), rule_hash: sha256_hex(b"roa-rule"), validation_path_hint: vec![ manifest_rsync_uri.to_string(), "rsync://example.test/leaf/a.roa".to_string(), sha256_hex(b"roa-raw"), ], } } fn sample_artifacts(manifest_rsync_uri: &str, roa_hash: &str) -> Vec { vec![ VcirRelatedArtifact { artifact_role: VcirArtifactRole::Manifest, artifact_kind: VcirArtifactKind::Mft, uri: Some(manifest_rsync_uri.to_string()), sha256: sha256_hex(manifest_rsync_uri.as_bytes()), object_type: Some("mft".to_string()), validation_status: VcirArtifactValidationStatus::Accepted, }, VcirRelatedArtifact { artifact_role: VcirArtifactRole::CurrentCrl, artifact_kind: VcirArtifactKind::Crl, uri: Some(manifest_rsync_uri.replace(".mft", ".crl")), sha256: sha256_hex(format!("{}-crl", manifest_rsync_uri).as_bytes()), object_type: Some("crl".to_string()), validation_status: VcirArtifactValidationStatus::Accepted, }, VcirRelatedArtifact { artifact_role: VcirArtifactRole::SignedObject, artifact_kind: VcirArtifactKind::Roa, uri: Some("rsync://example.test/leaf/a.roa".to_string()), sha256: roa_hash.to_string(), object_type: Some("roa".to_string()), validation_status: VcirArtifactValidationStatus::Accepted, }, ] } fn put_raw_evidence(store: &RocksStore, bytes: &[u8], uri: &str, object_type: &str) { let mut entry = RawByHashEntry::from_bytes(sha256_hex(bytes), bytes.to_vec()); entry.origin_uris.push(uri.to_string()); entry.object_type = Some(object_type.to_string()); entry.encoding = Some("der".to_string()); store .put_raw_by_hash_entry(&entry) .expect("put raw evidence"); } fn put_blob_only(store: &RocksStore, bytes: &[u8]) { store .put_blob_bytes_batch(&[(sha256_hex(bytes), bytes.to_vec())]) .expect("put blob bytes"); } #[test] fn trace_rule_to_root_returns_leaf_to_root_chain_and_evidence_refs() { let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let root_manifest = "rsync://example.test/root/root.mft"; let leaf_manifest = "rsync://example.test/leaf/leaf.mft"; let local = sample_local_output(leaf_manifest); let leaf_vcir = sample_vcir( leaf_manifest, Some(root_manifest), "test-tal", Some(local.clone()), sample_artifacts(leaf_manifest, &local.source_object_hash), ); let root_vcir = sample_vcir( root_manifest, None, "test-tal", None, sample_artifacts(root_manifest, &sha256_hex(b"root-object")), ); store.put_vcir(&leaf_vcir).expect("put leaf vcir"); store.put_vcir(&root_vcir).expect("put root vcir"); let rule_entry = AuditRuleIndexEntry { kind: AuditRuleKind::Roa, rule_hash: local.rule_hash.clone(), manifest_rsync_uri: leaf_manifest.to_string(), source_object_uri: local.source_object_uri.clone(), source_object_hash: local.source_object_hash.clone(), output_id: local.output_id.clone(), item_effective_until: local.item_effective_until.clone(), }; store .put_audit_rule_index_entry(&rule_entry) .expect("put rule index"); put_raw_evidence(&store, leaf_manifest.as_bytes(), leaf_manifest, "mft"); put_raw_evidence( &store, format!("{}-crl", leaf_manifest).as_bytes(), &leaf_manifest.replace(".mft", ".crl"), "crl", ); put_raw_evidence(&store, b"roa-raw", &local.source_object_uri, "roa"); put_raw_evidence(&store, b"roa-ee", "rsync://example.test/leaf/a.ee", "cer"); put_raw_evidence(&store, root_manifest.as_bytes(), root_manifest, "mft"); put_raw_evidence( &store, format!("{}-crl", root_manifest).as_bytes(), &root_manifest.replace(".mft", ".crl"), "crl", ); let trace = trace_rule_to_root(&store, AuditRuleKind::Roa, &local.rule_hash) .expect("trace rule") .expect("trace exists"); assert_eq!(trace.rule, rule_entry); assert_eq!(trace.resolved_output.output_id, local.output_id); assert_eq!(trace.chain_leaf_to_root.len(), 2); assert_eq!( trace.chain_leaf_to_root[0].manifest_rsync_uri, leaf_manifest ); assert_eq!( trace.chain_leaf_to_root[1].manifest_rsync_uri, root_manifest ); assert_eq!( trace.chain_leaf_to_root[0] .parent_manifest_rsync_uri .as_deref(), Some(root_manifest) ); assert!(trace.source_object_raw.raw_present); assert!(trace.source_ee_cert_raw.raw_present); assert!( trace.chain_leaf_to_root[0] .related_artifacts .iter() .any(|artifact| { artifact.uri.as_deref() == Some(leaf_manifest) && artifact.raw.raw_present }) ); } #[test] fn trace_rule_to_root_supports_router_key_rules() { let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let manifest = "rsync://example.test/router/leaf.mft"; let mut local = sample_local_output(manifest); local.output_type = VcirOutputType::RouterKey; local.source_object_uri = "rsync://example.test/router/router.cer".to_string(); local.source_object_type = "router_key".to_string(); local.payload_json = serde_json::json!({ "as_id": 64496, "ski_hex": "11".repeat(20), "spki_der_base64": base64::engine::general_purpose::STANDARD.encode([0x30u8, 0x00]), }) .to_string(); let mut vcir = sample_vcir( manifest, None, "test-tal", Some(local), sample_artifacts(manifest, &sha256_hex(b"router-object")), ); vcir.local_outputs[0].output_type = VcirOutputType::RouterKey; vcir.local_outputs[0].source_object_uri = "rsync://example.test/router/router.cer".to_string(); vcir.local_outputs[0].source_object_type = "router_key".to_string(); vcir.local_outputs[0].payload_json = serde_json::json!({ "as_id": 64496, "ski_hex": "11".repeat(20), "spki_der_base64": base64::engine::general_purpose::STANDARD.encode([0x30u8, 0x00]), }) .to_string(); vcir.summary.local_vrp_count = 0; vcir.summary.local_router_key_count = 1; store.put_vcir(&vcir).expect("put vcir"); let rule_entry = AuditRuleIndexEntry { kind: AuditRuleKind::RouterKey, rule_hash: vcir.local_outputs[0].rule_hash.clone(), manifest_rsync_uri: manifest.to_string(), source_object_uri: vcir.local_outputs[0].source_object_uri.clone(), source_object_hash: vcir.local_outputs[0].source_object_hash.clone(), output_id: vcir.local_outputs[0].output_id.clone(), item_effective_until: vcir.local_outputs[0].item_effective_until.clone(), }; store .put_audit_rule_index_entry(&rule_entry) .expect("put rule"); let trace = trace_rule_to_root(&store, AuditRuleKind::RouterKey, &rule_entry.rule_hash) .expect("trace rule") .expect("trace exists"); assert_eq!(trace.rule.kind, AuditRuleKind::RouterKey); assert_eq!(trace.resolved_output.output_type, VcirOutputType::RouterKey); } #[test] fn trace_rule_to_root_lazily_derives_source_ee_cert_when_raw_is_missing() { let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let manifest = "rsync://example.test/leaf/leaf.mft"; let roa_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142071.roa"); let roa_bytes = std::fs::read(&roa_path).expect("read ROA fixture"); let roa = RoaObject::decode_der(&roa_bytes).expect("decode ROA fixture"); let local = VcirLocalOutput { output_id: sha256_hex(b"lazy-vrp-output"), output_type: VcirOutputType::Vrp, item_effective_until: PackTime::from_utc_offset_datetime( time::OffsetDateTime::now_utc() + time::Duration::minutes(30), ), source_object_uri: "rsync://example.test/leaf/a.roa".to_string(), source_object_type: "roa".to_string(), source_object_hash: sha256_hex(&roa_bytes), source_ee_cert_hash: sha256_hex( roa.signed_object.signed_data.certificates[0] .raw_der .as_slice(), ), payload_json: serde_json::json!({"asn": 64496, "prefix": "203.0.113.0/24", "max_length": 24}) .to_string(), rule_hash: sha256_hex(b"lazy-roa-rule"), validation_path_hint: vec![manifest.to_string()], }; let vcir = sample_vcir( manifest, None, "test-tal", Some(local.clone()), sample_artifacts(manifest, &local.source_object_hash), ); store.put_vcir(&vcir).expect("put vcir"); let rule_entry = AuditRuleIndexEntry { kind: AuditRuleKind::Roa, rule_hash: local.rule_hash.clone(), manifest_rsync_uri: manifest.to_string(), source_object_uri: local.source_object_uri.clone(), source_object_hash: local.source_object_hash.clone(), output_id: local.output_id.clone(), item_effective_until: local.item_effective_until.clone(), }; store .put_audit_rule_index_entry(&rule_entry) .expect("put rule index"); put_raw_evidence(&store, manifest.as_bytes(), manifest, "mft"); put_raw_evidence( &store, format!("{}-crl", manifest).as_bytes(), &manifest.replace(".mft", ".crl"), "crl", ); put_raw_evidence(&store, &roa_bytes, &local.source_object_uri, "roa"); let trace = trace_rule_to_root(&store, AuditRuleKind::Roa, &local.rule_hash) .expect("trace rule") .expect("trace exists"); assert!(trace.source_object_raw.raw_present); assert!(trace.source_ee_cert_raw.raw_present); assert_eq!(trace.source_ee_cert_raw.object_type.as_deref(), Some("cer")); } #[test] fn trace_rule_to_root_uses_blob_only_fallback_for_source_object_raw() { let store_dir = tempfile::tempdir().expect("store dir"); let main_db = store_dir.path().join("main-db"); let raw_db = store_dir.path().join("raw-store.db"); let store = RocksStore::open_with_external_raw_store(&main_db, &raw_db).expect("open rocksdb"); let manifest = "rsync://example.test/leaf/leaf.mft"; let roa_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142071.roa"); let roa_bytes = std::fs::read(&roa_path).expect("read ROA fixture"); let roa = RoaObject::decode_der(&roa_bytes).expect("decode ROA fixture"); let local = VcirLocalOutput { output_id: sha256_hex(b"blob-only-vrp-output"), output_type: VcirOutputType::Vrp, item_effective_until: PackTime::from_utc_offset_datetime( time::OffsetDateTime::now_utc() + time::Duration::minutes(30), ), source_object_uri: "rsync://example.test/leaf/blob-only.roa".to_string(), source_object_type: "roa".to_string(), source_object_hash: sha256_hex(&roa_bytes), source_ee_cert_hash: sha256_hex( roa.signed_object.signed_data.certificates[0] .raw_der .as_slice(), ), payload_json: serde_json::json!({"asn": 64496, "prefix": "203.0.113.0/24", "max_length": 24}) .to_string(), rule_hash: sha256_hex(b"blob-only-roa-rule"), validation_path_hint: vec![manifest.to_string()], }; let vcir = sample_vcir( manifest, None, "test-tal", Some(local.clone()), sample_artifacts(manifest, &local.source_object_hash), ); store.put_vcir(&vcir).expect("put vcir"); let rule_entry = AuditRuleIndexEntry { kind: AuditRuleKind::Roa, rule_hash: local.rule_hash.clone(), manifest_rsync_uri: manifest.to_string(), source_object_uri: local.source_object_uri.clone(), source_object_hash: local.source_object_hash.clone(), output_id: local.output_id.clone(), item_effective_until: local.item_effective_until.clone(), }; store .put_audit_rule_index_entry(&rule_entry) .expect("put rule index"); put_raw_evidence(&store, manifest.as_bytes(), manifest, "mft"); put_raw_evidence( &store, format!("{}-crl", manifest).as_bytes(), &manifest.replace(".mft", ".crl"), "crl", ); put_blob_only(&store, &roa_bytes); let trace = trace_rule_to_root(&store, AuditRuleKind::Roa, &local.rule_hash) .expect("trace rule") .expect("trace exists"); assert!(trace.source_object_raw.raw_present); assert_eq!( trace.source_object_raw.origin_uris, vec![local.source_object_uri.clone()] ); assert_eq!(trace.source_object_raw.object_type.as_deref(), Some("roa")); assert_eq!(trace.source_object_raw.byte_len, Some(roa_bytes.len())); assert!(trace.source_ee_cert_raw.raw_present); } #[test] fn trace_rule_to_root_returns_none_for_missing_rule_index() { let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); assert!( trace_rule_to_root(&store, AuditRuleKind::Roa, &sha256_hex(b"missing")) .expect("missing trace ok") .is_none() ); } #[test] fn trace_rule_to_root_errors_when_index_points_to_missing_vcir() { let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let rule_hash = sha256_hex(b"missing-vcir-rule"); store .put_audit_rule_index_entry(&AuditRuleIndexEntry { kind: AuditRuleKind::Roa, rule_hash: rule_hash.clone(), manifest_rsync_uri: "rsync://example.test/missing.mft".to_string(), source_object_uri: "rsync://example.test/missing.roa".to_string(), source_object_hash: sha256_hex(b"missing-source"), output_id: sha256_hex(b"missing-output"), item_effective_until: PackTime::from_utc_offset_datetime( time::OffsetDateTime::now_utc() + time::Duration::minutes(1), ), }) .expect("put rule index"); let err = trace_rule_to_root(&store, AuditRuleKind::Roa, &rule_hash).unwrap_err(); assert!(matches!( err, AuditTraceError::MissingVcir { manifest_rsync_uri } if manifest_rsync_uri == "rsync://example.test/missing.mft" )); } #[test] fn trace_rule_to_root_errors_when_vcir_local_output_is_missing() { let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let manifest = "rsync://example.test/leaf/leaf.mft"; let vcir = sample_vcir( manifest, None, "test-tal", None, sample_artifacts(manifest, &sha256_hex(b"leaf-object")), ); store.put_vcir(&vcir).expect("put vcir"); let rule_hash = sha256_hex(b"missing-output-rule"); store .put_audit_rule_index_entry(&AuditRuleIndexEntry { kind: AuditRuleKind::Roa, rule_hash: rule_hash.clone(), manifest_rsync_uri: manifest.to_string(), source_object_uri: "rsync://example.test/leaf/a.roa".to_string(), source_object_hash: sha256_hex(b"leaf-object"), output_id: sha256_hex(b"missing-output"), item_effective_until: PackTime::from_utc_offset_datetime( time::OffsetDateTime::now_utc() + time::Duration::minutes(1), ), }) .expect("put rule index"); let err = trace_rule_to_root(&store, AuditRuleKind::Roa, &rule_hash).unwrap_err(); assert!(matches!(err, AuditTraceError::MissingLocalOutput { .. })); } #[test] fn trace_vcir_chain_to_root_detects_parent_cycle() { let store_dir = tempfile::tempdir().expect("store dir"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let a_manifest = "rsync://example.test/a.mft"; let b_manifest = "rsync://example.test/b.mft"; let a_vcir = sample_vcir( a_manifest, Some(b_manifest), "test-tal", None, sample_artifacts(a_manifest, &sha256_hex(b"a-object")), ); let b_vcir = sample_vcir( b_manifest, Some(a_manifest), "test-tal", None, sample_artifacts(b_manifest, &sha256_hex(b"b-object")), ); store.put_vcir(&a_vcir).expect("put a"); store.put_vcir(&b_vcir).expect("put b"); let err = trace_vcir_chain_to_root(&store, a_manifest).unwrap_err(); assert!(matches!( err, AuditTraceError::ParentCycle { manifest_rsync_uri } if manifest_rsync_uri == a_manifest )); } }