Files
rpki/src/ccr/verify.rs

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);
}
}