705 lines
26 KiB
Rust
705 lines
26 KiB
Rust
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::build::{build_aspa_payload_state, build_roa_payload_state};
|
|
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::model::{
|
|
CcrDigestAlgorithm, ManifestInstance, ManifestState, RouterKey, RouterKeySet,
|
|
RpkiCanonicalCacheRepresentation,
|
|
};
|
|
use crate::data_model::common::BigUnsigned;
|
|
use crate::data_model::roa::{IpPrefix, RoaAfi};
|
|
use crate::storage::{
|
|
PackTime, ValidatedCaInstanceResult, ValidatedManifestMeta, VcirArtifactKind,
|
|
VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary,
|
|
VcirCcrManifestProjection, VcirChildEntry, VcirInstanceGate, VcirRelatedArtifact,
|
|
VcirSummary,
|
|
};
|
|
use crate::validation::objects::{AspaAttestation, Vrp};
|
|
|
|
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),
|
|
})
|
|
}
|
|
|
|
fn sample_ccr_manifest_projection(manifest_rsync_uri: &str) -> VcirCcrManifestProjection {
|
|
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(sample_time()),
|
|
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]],
|
|
}
|
|
}
|
|
|
|
#[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()),
|
|
},
|
|
ccr_manifest_projection: sample_ccr_manifest_projection(
|
|
"rsync://example.test/current.mft",
|
|
),
|
|
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,
|
|
reject_reason: None,
|
|
}],
|
|
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);
|
|
}
|
|
}
|