894 lines
35 KiB
Rust
894 lines
35 KiB
Rust
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<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub object_type: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub byte_len: Option<usize>,
|
|
}
|
|
|
|
#[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<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub object_type: Option<String>,
|
|
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<String>,
|
|
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<AuditTraceArtifact>,
|
|
}
|
|
|
|
#[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<String>,
|
|
}
|
|
|
|
#[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<AuditTraceChainNode>,
|
|
}
|
|
|
|
pub fn trace_rule_to_root(
|
|
store: &RocksStore,
|
|
kind: AuditRuleKind,
|
|
rule_hash: &str,
|
|
) -> Result<Option<AuditRuleTrace>, 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<Option<Vec<AuditTraceChainNode>>, 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<AuditTraceChainNode, AuditTraceError> {
|
|
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<AuditTraceRawRef, AuditTraceError> {
|
|
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<AuditTraceRawRef, AuditTraceError> {
|
|
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<VcirLocalOutput>,
|
|
related_artifacts: Vec<VcirRelatedArtifact>,
|
|
) -> ValidatedCaInstanceResult {
|
|
let now = time::OffsetDateTime::now_utc();
|
|
let next = PackTime::from_utc_offset_datetime(now + time::Duration::hours(1));
|
|
let local_outputs: Vec<VcirLocalOutput> = 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<VcirRelatedArtifact> {
|
|
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
|
|
));
|
|
}
|
|
}
|