20260324_2 增加ccr & router key support
This commit is contained in:
569
src/ccr/verify.rs
Normal file
569
src/ccr/verify.rs
Normal file
@@ -0,0 +1,569 @@
|
||||
use crate::ccr::decode::{CcrDecodeError, decode_content_info};
|
||||
use crate::ccr::encode::{
|
||||
encode_aspa_payload_state_payload_der, encode_manifest_state_payload_der,
|
||||
encode_roa_payload_state_payload_der, encode_router_key_state_payload_der,
|
||||
encode_trust_anchor_state_payload_der,
|
||||
};
|
||||
use crate::ccr::hash::verify_state_hash;
|
||||
use crate::ccr::model::{CcrContentInfo, RouterKeyState, TrustAnchorState};
|
||||
use crate::storage::{RocksStore, VcirArtifactRole};
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct CcrVerifySummary {
|
||||
pub content_type_oid: String,
|
||||
pub version: u32,
|
||||
pub produced_at_rfc3339_utc: String,
|
||||
pub state_hashes_ok: bool,
|
||||
pub manifest_instances: usize,
|
||||
pub roa_payload_sets: usize,
|
||||
pub roa_vrp_count: usize,
|
||||
pub aspa_payload_sets: usize,
|
||||
pub trust_anchor_ski_count: usize,
|
||||
pub router_key_sets: usize,
|
||||
pub router_key_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CcrVerifyError {
|
||||
#[error("CCR decode failed: {0}")]
|
||||
Decode(#[from] CcrDecodeError),
|
||||
|
||||
#[error("ManifestState hash mismatch")]
|
||||
ManifestHashMismatch,
|
||||
|
||||
#[error("ROAPayloadState hash mismatch")]
|
||||
RoaHashMismatch,
|
||||
|
||||
#[error("ASPAPayloadState hash mismatch")]
|
||||
AspaHashMismatch,
|
||||
|
||||
#[error("TrustAnchorState hash mismatch")]
|
||||
TrustAnchorHashMismatch,
|
||||
|
||||
#[error("RouterKeyState hash mismatch")]
|
||||
RouterKeyHashMismatch,
|
||||
|
||||
#[error("read report json failed: {0}: {1}")]
|
||||
ReportRead(String, String),
|
||||
|
||||
#[error("parse report json failed: {0}")]
|
||||
ReportParse(String),
|
||||
|
||||
#[error("VRP set mismatch: only_in_ccr={only_in_ccr} only_in_report={only_in_report}")]
|
||||
ReportVrpMismatch {
|
||||
only_in_ccr: usize,
|
||||
only_in_report: usize,
|
||||
},
|
||||
|
||||
#[error("ASPA set mismatch: only_in_ccr={only_in_ccr} only_in_report={only_in_report}")]
|
||||
ReportAspaMismatch {
|
||||
only_in_ccr: usize,
|
||||
only_in_report: usize,
|
||||
},
|
||||
|
||||
#[error("open RocksDB failed: {0}")]
|
||||
OpenStore(String),
|
||||
|
||||
#[error("list VCIRs failed: {0}")]
|
||||
ListVcirs(String),
|
||||
|
||||
#[error("VCIR manifest set mismatch: only_in_ccr={only_in_ccr} only_in_vcir={only_in_vcir}")]
|
||||
VcirManifestMismatch {
|
||||
only_in_ccr: usize,
|
||||
only_in_vcir: usize,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn verify_content_info_bytes(der: &[u8]) -> Result<CcrVerifySummary, CcrVerifyError> {
|
||||
let content_info = decode_content_info(der)?;
|
||||
verify_content_info(&content_info)
|
||||
}
|
||||
|
||||
pub fn verify_content_info(content_info: &CcrContentInfo) -> Result<CcrVerifySummary, CcrVerifyError> {
|
||||
content_info.validate().map_err(CcrDecodeError::Validate)?;
|
||||
let state_hashes_ok = true;
|
||||
let mut manifest_instances = 0usize;
|
||||
let mut roa_payload_sets = 0usize;
|
||||
let mut roa_vrp_count = 0usize;
|
||||
let mut aspa_payload_sets = 0usize;
|
||||
let mut trust_anchor_ski_count = 0usize;
|
||||
let mut router_key_sets = 0usize;
|
||||
let mut router_key_count = 0usize;
|
||||
|
||||
if let Some(mfts) = &content_info.content.mfts {
|
||||
let payload_der = encode_manifest_state_payload_der(&mfts.mis)
|
||||
.map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Validate(e.to_string())))?;
|
||||
if !verify_state_hash(&mfts.hash, &payload_der) {
|
||||
return Err(CcrVerifyError::ManifestHashMismatch);
|
||||
}
|
||||
manifest_instances = mfts.mis.len();
|
||||
}
|
||||
if let Some(vrps) = &content_info.content.vrps {
|
||||
let payload_der = encode_roa_payload_state_payload_der(&vrps.rps)
|
||||
.map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Validate(e.to_string())))?;
|
||||
if !verify_state_hash(&vrps.hash, &payload_der) {
|
||||
return Err(CcrVerifyError::RoaHashMismatch);
|
||||
}
|
||||
roa_payload_sets = vrps.rps.len();
|
||||
roa_vrp_count = vrps.rps.iter().map(|set| count_roa_block_entries(&set.ip_addr_blocks)).sum();
|
||||
}
|
||||
if let Some(vaps) = &content_info.content.vaps {
|
||||
let payload_der = encode_aspa_payload_state_payload_der(&vaps.aps)
|
||||
.map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Validate(e.to_string())))?;
|
||||
if !verify_state_hash(&vaps.hash, &payload_der) {
|
||||
return Err(CcrVerifyError::AspaHashMismatch);
|
||||
}
|
||||
aspa_payload_sets = vaps.aps.len();
|
||||
}
|
||||
if let Some(tas) = &content_info.content.tas {
|
||||
verify_trust_anchor_state_hash(tas)?;
|
||||
trust_anchor_ski_count = tas.skis.len();
|
||||
}
|
||||
if let Some(rks) = &content_info.content.rks {
|
||||
verify_router_key_state_hash(rks)?;
|
||||
router_key_sets = rks.rksets.len();
|
||||
router_key_count = rks.rksets.iter().map(|set| set.router_keys.len()).sum();
|
||||
}
|
||||
|
||||
let produced_at_rfc3339_utc = content_info
|
||||
.content
|
||||
.produced_at
|
||||
.to_offset(time::UtcOffset::UTC)
|
||||
.format(&time::format_description::well_known::Rfc3339)
|
||||
.map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Validate(e.to_string())))?;
|
||||
|
||||
Ok(CcrVerifySummary {
|
||||
content_type_oid: content_info.content_type_oid.clone(),
|
||||
version: content_info.content.version,
|
||||
produced_at_rfc3339_utc,
|
||||
state_hashes_ok,
|
||||
manifest_instances,
|
||||
roa_payload_sets,
|
||||
roa_vrp_count,
|
||||
aspa_payload_sets,
|
||||
trust_anchor_ski_count,
|
||||
router_key_sets,
|
||||
router_key_count,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn verify_against_report_json_path(
|
||||
content_info: &CcrContentInfo,
|
||||
report_json_path: &Path,
|
||||
) -> Result<(), CcrVerifyError> {
|
||||
let bytes = std::fs::read(report_json_path)
|
||||
.map_err(|e| CcrVerifyError::ReportRead(report_json_path.display().to_string(), e.to_string()))?;
|
||||
let json: serde_json::Value = serde_json::from_slice(&bytes)
|
||||
.map_err(|e| CcrVerifyError::ReportParse(e.to_string()))?;
|
||||
|
||||
let report_vrps = report_vrp_keys(&json)?;
|
||||
let ccr_vrps = extract_vrp_rows(content_info)?;
|
||||
let only_in_ccr = ccr_vrps.difference(&report_vrps).count();
|
||||
let only_in_report = report_vrps.difference(&ccr_vrps).count();
|
||||
if only_in_ccr != 0 || only_in_report != 0 {
|
||||
return Err(CcrVerifyError::ReportVrpMismatch {
|
||||
only_in_ccr,
|
||||
only_in_report,
|
||||
});
|
||||
}
|
||||
|
||||
let report_aspas = report_aspa_keys(&json)?;
|
||||
let ccr_aspas = ccr_aspa_keys(content_info)?;
|
||||
let only_in_ccr = ccr_aspas.difference(&report_aspas).count();
|
||||
let only_in_report = report_aspas.difference(&ccr_aspas).count();
|
||||
if only_in_ccr != 0 || only_in_report != 0 {
|
||||
return Err(CcrVerifyError::ReportAspaMismatch {
|
||||
only_in_ccr,
|
||||
only_in_report,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn verify_against_vcir_store_path(
|
||||
content_info: &CcrContentInfo,
|
||||
db_path: &Path,
|
||||
) -> Result<(), CcrVerifyError> {
|
||||
let store = RocksStore::open(db_path).map_err(|e| CcrVerifyError::OpenStore(e.to_string()))?;
|
||||
verify_against_vcir_store(content_info, &store)
|
||||
}
|
||||
|
||||
pub fn verify_against_vcir_store(
|
||||
content_info: &CcrContentInfo,
|
||||
store: &RocksStore,
|
||||
) -> Result<(), CcrVerifyError> {
|
||||
let Some(mfts) = &content_info.content.mfts else {
|
||||
return Ok(());
|
||||
};
|
||||
let vcirs = store.list_vcirs().map_err(|e| CcrVerifyError::ListVcirs(e.to_string()))?;
|
||||
let mut vcir_hashes = BTreeSet::new();
|
||||
for vcir in vcirs {
|
||||
if let Some(artifact) = vcir.related_artifacts.iter().find(|artifact| {
|
||||
artifact.artifact_role == VcirArtifactRole::Manifest
|
||||
&& artifact.uri.as_deref() == Some(vcir.current_manifest_rsync_uri.as_str())
|
||||
}) {
|
||||
if let Ok(bytes) = hex::decode(&artifact.sha256) {
|
||||
vcir_hashes.insert(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
let ccr_hashes = mfts
|
||||
.mis
|
||||
.iter()
|
||||
.map(|mi| mi.hash.clone())
|
||||
.collect::<BTreeSet<_>>();
|
||||
let only_in_ccr = ccr_hashes.difference(&vcir_hashes).count();
|
||||
let only_in_vcir = vcir_hashes.difference(&ccr_hashes).count();
|
||||
if only_in_ccr != 0 || only_in_vcir != 0 {
|
||||
return Err(CcrVerifyError::VcirManifestMismatch {
|
||||
only_in_ccr,
|
||||
only_in_vcir,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_trust_anchor_state_hash(state: &TrustAnchorState) -> Result<(), CcrVerifyError> {
|
||||
let payload_der = encode_trust_anchor_state_payload_der(&state.skis)
|
||||
.map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Validate(e.to_string())))?;
|
||||
if !verify_state_hash(&state.hash, &payload_der) {
|
||||
return Err(CcrVerifyError::TrustAnchorHashMismatch);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_router_key_state_hash(state: &RouterKeyState) -> Result<(), CcrVerifyError> {
|
||||
let payload_der = encode_router_key_state_payload_der(&state.rksets)
|
||||
.map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Validate(e.to_string())))?;
|
||||
if !verify_state_hash(&state.hash, &payload_der) {
|
||||
return Err(CcrVerifyError::RouterKeyHashMismatch);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn report_vrp_keys(json: &serde_json::Value) -> Result<BTreeSet<(u32, String, u16)>, CcrVerifyError> {
|
||||
let mut out = BTreeSet::new();
|
||||
let Some(items) = json.get("vrps").and_then(|v| v.as_array()) else {
|
||||
return Ok(out);
|
||||
};
|
||||
for item in items {
|
||||
let asn = item
|
||||
.get("asn")
|
||||
.and_then(|v| v.as_u64())
|
||||
.ok_or_else(|| CcrVerifyError::ReportParse("vrps[].asn missing".into()))? as u32;
|
||||
let prefix = item
|
||||
.get("prefix")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| CcrVerifyError::ReportParse("vrps[].prefix missing".into()))?
|
||||
.to_string();
|
||||
let max_length = item
|
||||
.get("max_length")
|
||||
.and_then(|v| v.as_u64())
|
||||
.ok_or_else(|| CcrVerifyError::ReportParse("vrps[].max_length missing".into()))?
|
||||
as u16;
|
||||
out.insert((asn, prefix, max_length));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn report_aspa_keys(json: &serde_json::Value) -> Result<BTreeSet<(u32, Vec<u32>)>, CcrVerifyError> {
|
||||
let mut out = BTreeSet::new();
|
||||
let Some(items) = json.get("aspas").and_then(|v| v.as_array()) else {
|
||||
return Ok(out);
|
||||
};
|
||||
for item in items {
|
||||
let customer = item
|
||||
.get("customer_as_id")
|
||||
.and_then(|v| v.as_u64())
|
||||
.ok_or_else(|| CcrVerifyError::ReportParse("aspas[].customer_as_id missing".into()))?
|
||||
as u32;
|
||||
let mut providers = item
|
||||
.get("provider_as_ids")
|
||||
.and_then(|v| v.as_array())
|
||||
.ok_or_else(|| CcrVerifyError::ReportParse("aspas[].provider_as_ids missing".into()))?
|
||||
.iter()
|
||||
.map(|v| v.as_u64().ok_or_else(|| CcrVerifyError::ReportParse("provider_as_ids[] invalid".into())).map(|v| v as u32))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
providers.sort_unstable();
|
||||
providers.dedup();
|
||||
out.insert((customer, providers));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn extract_vrp_rows(content_info: &CcrContentInfo) -> Result<BTreeSet<(u32, String, u16)>, CcrVerifyError> {
|
||||
let mut out = BTreeSet::new();
|
||||
let Some(vrps) = &content_info.content.vrps else {
|
||||
return Ok(out);
|
||||
};
|
||||
for set in &vrps.rps {
|
||||
for block in &set.ip_addr_blocks {
|
||||
let (afi, entries) = decode_roa_family_block(block)?;
|
||||
for (prefix_len, addr_bytes, max_len) in entries {
|
||||
let prefix = format_prefix(afi, &addr_bytes, prefix_len)?;
|
||||
out.insert((set.as_id, prefix, max_len.unwrap_or(prefix_len as u16)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn ccr_aspa_keys(content_info: &CcrContentInfo) -> Result<BTreeSet<(u32, Vec<u32>)>, CcrVerifyError> {
|
||||
let mut out = BTreeSet::new();
|
||||
let Some(vaps) = &content_info.content.vaps else {
|
||||
return Ok(out);
|
||||
};
|
||||
for set in &vaps.aps {
|
||||
out.insert((set.customer_as_id, set.providers.clone()));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn decode_roa_family_block(block: &[u8]) -> Result<(u16, Vec<(u8, Vec<u8>, Option<u16>)>), CcrVerifyError> {
|
||||
let mut top = crate::data_model::common::DerReader::new(block);
|
||||
let mut seq = top.take_sequence().map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Parse(e)))?;
|
||||
if !top.is_empty() {
|
||||
return Err(CcrVerifyError::Decode(CcrDecodeError::Parse("trailing bytes after ROAIPAddressFamily".into())));
|
||||
}
|
||||
let afi_bytes = seq.take_octet_string().map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Parse(e)))?;
|
||||
let afi = u16::from_be_bytes([afi_bytes[0], afi_bytes[1]]);
|
||||
let mut addrs = seq.take_sequence().map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Parse(e)))?;
|
||||
let mut entries = Vec::new();
|
||||
while !addrs.is_empty() {
|
||||
let mut addr_seq = addrs.take_sequence().map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Parse(e)))?;
|
||||
let (unused_bits, content) = addr_seq.take_bit_string().map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Parse(e)))?;
|
||||
let prefix_len = (content.len() * 8) as u8 - unused_bits;
|
||||
let max_len = if addr_seq.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(addr_seq.take_uint_u64().map_err(|e| CcrVerifyError::Decode(CcrDecodeError::Parse(e)))? as u16)
|
||||
};
|
||||
entries.push((prefix_len, content.to_vec(), max_len));
|
||||
}
|
||||
Ok((afi, entries))
|
||||
}
|
||||
|
||||
fn format_prefix(afi: u16, addr_bytes: &[u8], prefix_len: u8) -> Result<String, CcrVerifyError> {
|
||||
match afi {
|
||||
1 => {
|
||||
let mut full = [0u8; 4];
|
||||
full[..addr_bytes.len()].copy_from_slice(addr_bytes);
|
||||
Ok(format!("{}/{prefix_len}", std::net::Ipv4Addr::from(full)))
|
||||
}
|
||||
2 => {
|
||||
let mut full = [0u8; 16];
|
||||
full[..addr_bytes.len()].copy_from_slice(addr_bytes);
|
||||
Ok(format!("{}/{prefix_len}", std::net::Ipv6Addr::from(full)))
|
||||
}
|
||||
other => Err(CcrVerifyError::Decode(CcrDecodeError::Parse(format!("unsupported AFI {other}")))),
|
||||
}
|
||||
}
|
||||
|
||||
fn count_roa_block_entries(blocks: &[Vec<u8>]) -> usize {
|
||||
blocks
|
||||
.iter()
|
||||
.map(|block| decode_roa_family_block(block).map(|(_, entries)| entries.len()).unwrap_or(0))
|
||||
.sum()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ccr::encode::{
|
||||
encode_manifest_state_payload_der, encode_roa_payload_state_payload_der,
|
||||
encode_router_key_state_payload_der, encode_trust_anchor_state_payload_der,
|
||||
};
|
||||
use crate::ccr::build::{build_aspa_payload_state, build_roa_payload_state};
|
||||
use crate::ccr::model::{
|
||||
CcrDigestAlgorithm, ManifestInstance, ManifestState,
|
||||
RouterKey, RouterKeySet, RpkiCanonicalCacheRepresentation,
|
||||
};
|
||||
use crate::data_model::roa::{IpPrefix, RoaAfi};
|
||||
use crate::validation::objects::{AspaAttestation, Vrp};
|
||||
use crate::data_model::common::BigUnsigned;
|
||||
use crate::storage::{
|
||||
PackTime, ValidatedCaInstanceResult, ValidatedManifestMeta, VcirArtifactKind,
|
||||
VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary, VcirChildEntry,
|
||||
VcirInstanceGate, VcirRelatedArtifact, VcirSummary,
|
||||
};
|
||||
|
||||
fn sample_time() -> time::OffsetDateTime {
|
||||
time::OffsetDateTime::parse(
|
||||
"2026-03-24T00:00:00Z",
|
||||
&time::format_description::well_known::Rfc3339,
|
||||
)
|
||||
.expect("time")
|
||||
}
|
||||
|
||||
fn sample_content_info() -> CcrContentInfo {
|
||||
let mis = vec![ManifestInstance {
|
||||
hash: vec![0x10; 32],
|
||||
size: 2048,
|
||||
aki: vec![0x20; 20],
|
||||
manifest_number: BigUnsigned { bytes_be: vec![1] },
|
||||
this_update: sample_time(),
|
||||
locations: vec![vec![0x30, 0x00]],
|
||||
subordinates: vec![vec![0x30; 20]],
|
||||
}];
|
||||
let mfts = ManifestState {
|
||||
most_recent_update: sample_time(),
|
||||
hash: crate::ccr::compute_state_hash(&encode_manifest_state_payload_der(&mis).unwrap()),
|
||||
mis,
|
||||
};
|
||||
let vrps = build_roa_payload_state(&[Vrp {
|
||||
asn: 64496,
|
||||
prefix: IpPrefix { afi: RoaAfi::Ipv4, prefix_len: 0, addr: [0; 16] },
|
||||
max_length: 0,
|
||||
}]).expect("build roa state");
|
||||
let vaps = build_aspa_payload_state(&[AspaAttestation {
|
||||
customer_as_id: 64496,
|
||||
provider_as_ids: vec![64497],
|
||||
}]).expect("build aspa state");
|
||||
let skis = vec![vec![0x11; 20]];
|
||||
let tas = TrustAnchorState {
|
||||
hash: crate::ccr::compute_state_hash(&encode_trust_anchor_state_payload_der(&skis).unwrap()),
|
||||
skis,
|
||||
};
|
||||
let rksets = vec![RouterKeySet { as_id: 64496, router_keys: vec![RouterKey { ski: vec![0x22;20], spki_der: vec![0x30,0x00] }] }];
|
||||
let rks = RouterKeyState {
|
||||
hash: crate::ccr::compute_state_hash(&encode_router_key_state_payload_der(&rksets).unwrap()),
|
||||
rksets,
|
||||
};
|
||||
CcrContentInfo::new(RpkiCanonicalCacheRepresentation {
|
||||
version: 0,
|
||||
hash_alg: CcrDigestAlgorithm::Sha256,
|
||||
produced_at: sample_time(),
|
||||
mfts: Some(mfts),
|
||||
vrps: Some(vrps),
|
||||
vaps: Some(vaps),
|
||||
tas: Some(tas),
|
||||
rks: Some(rks),
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_detects_each_state_hash_mismatch() {
|
||||
let mut ci = sample_content_info();
|
||||
ci.content.vrps.as_mut().unwrap().hash[0] ^= 0x01;
|
||||
assert!(matches!(verify_content_info(&ci), Err(CcrVerifyError::RoaHashMismatch)));
|
||||
|
||||
let mut ci = sample_content_info();
|
||||
ci.content.vaps.as_mut().unwrap().hash[0] ^= 0x01;
|
||||
assert!(matches!(verify_content_info(&ci), Err(CcrVerifyError::AspaHashMismatch)));
|
||||
|
||||
let mut ci = sample_content_info();
|
||||
ci.content.tas.as_mut().unwrap().hash[0] ^= 0x01;
|
||||
assert!(matches!(verify_content_info(&ci), Err(CcrVerifyError::TrustAnchorHashMismatch)));
|
||||
|
||||
let mut ci = sample_content_info();
|
||||
ci.content.rks.as_mut().unwrap().hash[0] ^= 0x01;
|
||||
assert!(matches!(verify_content_info(&ci), Err(CcrVerifyError::RouterKeyHashMismatch)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_against_report_json_accepts_matching_report_and_rejects_parse_errors() {
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let report = serde_json::json!({
|
||||
"vrps": [{"asn": 64496, "prefix": "0.0.0.0/0", "max_length": 0}],
|
||||
"aspas": [{"customer_as_id": 64496, "provider_as_ids": [64497]}]
|
||||
});
|
||||
let report_path = td.path().join("report.json");
|
||||
std::fs::write(&report_path, serde_json::to_vec(&report).unwrap()).unwrap();
|
||||
verify_against_report_json_path(&sample_content_info(), &report_path).expect("matching report");
|
||||
|
||||
let bad_path = td.path().join("bad.json");
|
||||
std::fs::write(&bad_path, b"not-json").unwrap();
|
||||
assert!(matches!(verify_against_report_json_path(&sample_content_info(), &bad_path), Err(CcrVerifyError::ReportParse(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_against_report_json_rejects_missing_fields_and_aspa_mismatch() {
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let missing = serde_json::json!({"vrps":[{"prefix":"0.0.0.0/0","max_length":0}],"aspas":[]});
|
||||
let missing_path = td.path().join("missing.json");
|
||||
std::fs::write(&missing_path, serde_json::to_vec(&missing).unwrap()).unwrap();
|
||||
assert!(matches!(verify_against_report_json_path(&sample_content_info(), &missing_path), Err(CcrVerifyError::ReportParse(_))));
|
||||
|
||||
let mismatch = serde_json::json!({
|
||||
"vrps": [{"asn": 64496, "prefix": "0.0.0.0/0", "max_length": 0}],
|
||||
"aspas": [{"customer_as_id": 64496, "provider_as_ids": [65000]}]
|
||||
});
|
||||
let mismatch_path = td.path().join("mismatch.json");
|
||||
std::fs::write(&mismatch_path, serde_json::to_vec(&mismatch).unwrap()).unwrap();
|
||||
assert!(matches!(verify_against_report_json_path(&sample_content_info(), &mismatch_path), Err(CcrVerifyError::ReportAspaMismatch { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_against_vcir_store_rejects_mismatched_manifest_hashes() {
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(td.path()).expect("open db");
|
||||
let vcir = ValidatedCaInstanceResult {
|
||||
manifest_rsync_uri: "rsync://example.test/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(sample_time()),
|
||||
current_manifest_rsync_uri: "rsync://example.test/current.mft".to_string(),
|
||||
current_crl_rsync_uri: "rsync://example.test/current.crl".to_string(),
|
||||
validated_manifest_meta: ValidatedManifestMeta {
|
||||
validated_manifest_number: vec![1],
|
||||
validated_manifest_this_update: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
validated_manifest_next_update: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
},
|
||||
instance_gate: VcirInstanceGate {
|
||||
manifest_next_update: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
current_crl_next_update: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
self_ca_not_after: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
instance_effective_until: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
},
|
||||
child_entries: vec![VcirChildEntry {
|
||||
child_manifest_rsync_uri: "rsync://example.test/child.mft".to_string(),
|
||||
child_cert_rsync_uri: "rsync://example.test/child.cer".to_string(),
|
||||
child_cert_hash: "aa".repeat(32),
|
||||
child_ski: "33".repeat(20),
|
||||
child_rsync_base_uri: "rsync://example.test/".to_string(),
|
||||
child_publication_point_rsync_uri: "rsync://example.test/".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(sample_time()),
|
||||
}],
|
||||
local_outputs: Vec::new(),
|
||||
related_artifacts: vec![VcirRelatedArtifact {
|
||||
artifact_role: VcirArtifactRole::Manifest,
|
||||
artifact_kind: VcirArtifactKind::Mft,
|
||||
uri: Some("rsync://example.test/current.mft".to_string()),
|
||||
sha256: "ff".repeat(32),
|
||||
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() },
|
||||
};
|
||||
store.put_vcir(&vcir).unwrap();
|
||||
assert!(matches!(verify_against_vcir_store(&sample_content_info(), &store), Err(CcrVerifyError::VcirManifestMismatch { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_vrp_helpers_reject_bad_afi_and_count_invalid_block_as_zero() {
|
||||
let block = vec![0x30, 0x08, 0x04, 0x02, 0x00, 0x63, 0x30, 0x02, 0x03, 0x00];
|
||||
let ci = CcrContentInfo::new(RpkiCanonicalCacheRepresentation {
|
||||
version: 0,
|
||||
hash_alg: CcrDigestAlgorithm::Sha256,
|
||||
produced_at: sample_time(),
|
||||
mfts: None,
|
||||
vrps: Some(crate::ccr::model::RoaPayloadState { rps: vec![crate::ccr::model::RoaPayloadSet { as_id: 64496, ip_addr_blocks: vec![block.clone()] }], hash: crate::ccr::compute_state_hash(&encode_roa_payload_state_payload_der(&[crate::ccr::model::RoaPayloadSet { as_id: 64496, ip_addr_blocks: vec![block] }]).unwrap()) }),
|
||||
vaps: None,
|
||||
tas: None,
|
||||
rks: None,
|
||||
});
|
||||
assert!(matches!(extract_vrp_rows(&ci), Err(CcrVerifyError::Decode(_))));
|
||||
let bad_count = count_roa_block_entries(&[vec![0x04, 0x00]]);
|
||||
assert_eq!(bad_count, 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user