20260410 完成五个rir 基于cir的三方回放,raw by hash 独立db,发现内存占用大,连续大rir 录制发生oom

This commit is contained in:
2026-04-11 11:24:32 +08:00
parent e083fe4daa
commit 77fc2f1a41
96 changed files with 5159 additions and 1720 deletions

View File

@@ -12,12 +12,13 @@ use crate::ccr::model::{
AspaPayloadSet, AspaPayloadState, ManifestInstance, ManifestState, RoaPayloadSet,
RoaPayloadState, RouterKey, RouterKeySet, RouterKeyState, TrustAnchorState,
};
use crate::blob_store::RawObjectStore;
use crate::data_model::manifest::ManifestObject;
use crate::data_model::rc::{AccessDescription, SubjectInfoAccess};
use crate::data_model::roa::RoaAfi;
use crate::data_model::router_cert::BgpsecRouterCertificate;
use crate::data_model::ta::TrustAnchor;
use crate::storage::{RocksStore, VcirArtifactRole, ValidatedCaInstanceResult};
use crate::storage::{RocksStore, ValidatedCaInstanceResult, VcirArtifactRole};
use crate::validation::objects::{AspaAttestation, Vrp};
#[derive(Debug, thiserror::Error)]
@@ -41,13 +42,22 @@ pub enum CcrBuildError {
MissingManifestArtifact(String),
#[error("manifest raw bytes missing in store for {manifest_rsync_uri}: {sha256_hex}")]
MissingManifestRawBytes { manifest_rsync_uri: String, sha256_hex: String },
MissingManifestRawBytes {
manifest_rsync_uri: String,
sha256_hex: String,
},
#[error("manifest raw bytes load failed for {manifest_rsync_uri}: {detail}")]
LoadManifestRawBytes { manifest_rsync_uri: String, detail: String },
LoadManifestRawBytes {
manifest_rsync_uri: String,
detail: String,
},
#[error("manifest decode failed for {manifest_rsync_uri}: {detail}")]
ManifestDecode { manifest_rsync_uri: String, detail: String },
ManifestDecode {
manifest_rsync_uri: String,
detail: String,
},
#[error("manifest EE certificate missing AuthorityKeyIdentifier: {0}")]
ManifestEeMissingAki(String),
@@ -99,8 +109,8 @@ pub fn build_roa_payload_state(vrps: &[Vrp]) -> Result<RoaPayloadState, CcrBuild
});
}
let payload_der =
encode_roa_payload_state_payload_der(&rps).map_err(|e| CcrBuildError::RoaEncode(e.to_string()))?;
let payload_der = encode_roa_payload_state_payload_der(&rps)
.map_err(|e| CcrBuildError::RoaEncode(e.to_string()))?;
Ok(RoaPayloadState {
rps,
hash: compute_state_hash(&payload_der),
@@ -176,10 +186,12 @@ pub fn build_manifest_state_from_vcirs(
artifact.artifact_role == VcirArtifactRole::Manifest
&& artifact.uri.as_deref() == Some(vcir.current_manifest_rsync_uri.as_str())
})
.ok_or_else(|| CcrBuildError::MissingManifestArtifact(vcir.current_manifest_rsync_uri.clone()))?;
.ok_or_else(|| {
CcrBuildError::MissingManifestArtifact(vcir.current_manifest_rsync_uri.clone())
})?;
let raw_entry = store
.get_raw_by_hash_entry(&manifest_artifact.sha256)
let raw_bytes = store
.get_blob_bytes(&manifest_artifact.sha256)
.map_err(|e| CcrBuildError::LoadManifestRawBytes {
manifest_rsync_uri: vcir.current_manifest_rsync_uri.clone(),
detail: e.to_string(),
@@ -189,9 +201,11 @@ pub fn build_manifest_state_from_vcirs(
sha256_hex: manifest_artifact.sha256.clone(),
})?;
let manifest = ManifestObject::decode_der(&raw_entry.bytes).map_err(|e| CcrBuildError::ManifestDecode {
manifest_rsync_uri: vcir.current_manifest_rsync_uri.clone(),
detail: e.to_string(),
let manifest = ManifestObject::decode_der(&raw_bytes).map_err(|e| {
CcrBuildError::ManifestDecode {
manifest_rsync_uri: vcir.current_manifest_rsync_uri.clone(),
detail: e.to_string(),
}
})?;
let ee = &manifest.signed_object.signed_data.certificates[0].resource_cert;
@@ -200,13 +214,17 @@ pub fn build_manifest_state_from_vcirs(
.extensions
.authority_key_identifier
.clone()
.ok_or_else(|| CcrBuildError::ManifestEeMissingAki(vcir.current_manifest_rsync_uri.clone()))?;
.ok_or_else(|| {
CcrBuildError::ManifestEeMissingAki(vcir.current_manifest_rsync_uri.clone())
})?;
let sia = ee
.tbs
.extensions
.subject_info_access
.as_ref()
.ok_or_else(|| CcrBuildError::ManifestEeMissingSia(vcir.current_manifest_rsync_uri.clone()))?;
.ok_or_else(|| {
CcrBuildError::ManifestEeMissingSia(vcir.current_manifest_rsync_uri.clone())
})?;
let locations = match sia {
SubjectInfoAccess::Ee(ee_sia) => ee_sia
.access_descriptions
@@ -216,7 +234,7 @@ pub fn build_manifest_state_from_vcirs(
SubjectInfoAccess::Ca(_) => {
return Err(CcrBuildError::ManifestEeSiaWrongVariant(
vcir.current_manifest_rsync_uri.clone(),
))
));
}
};
@@ -234,11 +252,14 @@ pub fn build_manifest_state_from_vcirs(
}
let instance = ManifestInstance {
hash: sha2::Sha256::digest(&raw_entry.bytes).to_vec(),
size: raw_entry.bytes.len() as u64,
hash: sha2::Sha256::digest(&raw_bytes).to_vec(),
size: raw_bytes.len() as u64,
aki,
manifest_number: crate::data_model::common::BigUnsigned {
bytes_be: vcir.validated_manifest_meta.validated_manifest_number.clone(),
bytes_be: vcir
.validated_manifest_meta
.validated_manifest_number
.clone(),
},
this_update,
locations,
@@ -292,7 +313,10 @@ fn encode_access_description_der(ad: &AccessDescription) -> Result<Vec<u8>, CcrB
fn encode_oid_from_string(oid: &str) -> Result<Vec<u8>, CcrBuildError> {
let arcs = oid
.split('.')
.map(|part| part.parse::<u64>().map_err(|_| CcrBuildError::UnsupportedAccessMethodOid(oid.to_string())))
.map(|part| {
part.parse::<u64>()
.map_err(|_| CcrBuildError::UnsupportedAccessMethodOid(oid.to_string()))
})
.collect::<Result<Vec<_>, _>>()?;
if arcs.len() < 2 {
return Err(CcrBuildError::UnsupportedAccessMethodOid(oid.to_string()));
@@ -471,7 +495,9 @@ fn encode_length(len: usize, out: &mut Vec<u8>) {
mod tests {
use super::*;
use crate::ccr::decode::decode_content_info;
use crate::ccr::encode::{encode_aspa_payload_state, encode_content_info, encode_trust_anchor_state};
use crate::ccr::encode::{
encode_aspa_payload_state, encode_content_info, encode_trust_anchor_state,
};
use crate::ccr::model::{CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation};
use crate::data_model::roa::{IpPrefix, RoaAfi};
use crate::data_model::ta::TrustAnchor;
@@ -561,13 +587,19 @@ mod tests {
ca_subject_name: "CN=test".to_string(),
ca_ski: "11".repeat(20),
issuer_ski: "22".repeat(20),
last_successful_validation_time: crate::storage::PackTime::from_utc_offset_datetime(now),
last_successful_validation_time: crate::storage::PackTime::from_utc_offset_datetime(
now,
),
current_manifest_rsync_uri: manifest_uri.to_string(),
current_crl_rsync_uri: format!("{manifest_uri}.crl"),
validated_manifest_meta: crate::storage::ValidatedManifestMeta {
validated_manifest_number: manifest.manifest.manifest_number.bytes_be.clone(),
validated_manifest_this_update: crate::storage::PackTime::from_utc_offset_datetime(now),
validated_manifest_next_update: crate::storage::PackTime::from_utc_offset_datetime(next),
validated_manifest_this_update: crate::storage::PackTime::from_utc_offset_datetime(
now,
),
validated_manifest_next_update: crate::storage::PackTime::from_utc_offset_datetime(
next,
),
},
instance_gate: crate::storage::VcirInstanceGate {
manifest_next_update: crate::storage::PackTime::from_utc_offset_datetime(next),
@@ -585,7 +617,9 @@ mod tests {
child_rrdp_notification_uri: None,
child_effective_ip_resources: None,
child_effective_as_resources: None,
accepted_at_validation_time: crate::storage::PackTime::from_utc_offset_datetime(now),
accepted_at_validation_time: crate::storage::PackTime::from_utc_offset_datetime(
now,
),
}],
local_outputs: Vec::new(),
related_artifacts: vec![crate::storage::VcirRelatedArtifact {
@@ -617,21 +651,29 @@ mod tests {
fn build_manifest_state_from_vcirs_collects_current_manifests_and_hashes_payload() {
let base = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let manifest_a = std::fs::read(base.join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft")).expect("read manifest a");
let manifest_b = std::fs::read(base.join("tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.mft")).expect("read manifest b");
let vcir_a = sample_manifest_vcir("rsync://example.test/a.mft", &manifest_a, &"33".repeat(20));
let vcir_b = sample_manifest_vcir("rsync://example.test/b.mft", &manifest_b, &"44".repeat(20));
let manifest_b = std::fs::read(base.join(
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.mft",
))
.expect("read manifest b");
let vcir_a =
sample_manifest_vcir("rsync://example.test/a.mft", &manifest_a, &"33".repeat(20));
let vcir_b =
sample_manifest_vcir("rsync://example.test/b.mft", &manifest_b, &"44".repeat(20));
let store_dir = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
for (vcir, bytes) in [(&vcir_a, &manifest_a), (&vcir_b, &manifest_b)] {
let artifact = &vcir.related_artifacts[0];
let mut raw = crate::storage::RawByHashEntry::from_bytes(artifact.sha256.clone(), bytes.to_vec());
raw.origin_uris.push(vcir.current_manifest_rsync_uri.clone());
let mut raw =
crate::storage::RawByHashEntry::from_bytes(artifact.sha256.clone(), bytes.to_vec());
raw.origin_uris
.push(vcir.current_manifest_rsync_uri.clone());
raw.object_type = Some("mft".to_string());
raw.encoding = Some("der".to_string());
store.put_raw_by_hash_entry(&raw).expect("put raw manifest");
}
let state = build_manifest_state_from_vcirs(&store, &[vcir_a.clone(), vcir_b.clone()]).expect("build manifest state");
let state = build_manifest_state_from_vcirs(&store, &[vcir_a.clone(), vcir_b.clone()])
.expect("build manifest state");
assert_eq!(state.mis.len(), 2);
assert!(state.mis[0].hash < state.mis[1].hash);
let expected_subordinates = [
@@ -647,13 +689,24 @@ mod tests {
mi.subordinates[0].clone()
})
.collect::<std::collections::BTreeSet<_>>();
let expected_subordinates = expected_subordinates.into_iter().collect::<std::collections::BTreeSet<_>>();
let expected_subordinates = expected_subordinates
.into_iter()
.collect::<std::collections::BTreeSet<_>>();
assert_eq!(actual_subordinates, expected_subordinates);
let payload_der = encode_manifest_state_payload_der(&state.mis).expect("encode mis payload");
let payload_der =
encode_manifest_state_payload_der(&state.mis).expect("encode mis payload");
assert!(crate::ccr::verify_state_hash(&state.hash, &payload_der));
let max_time = [
vcir_a.validated_manifest_meta.validated_manifest_this_update.parse().unwrap(),
vcir_b.validated_manifest_meta.validated_manifest_this_update.parse().unwrap(),
vcir_a
.validated_manifest_meta
.validated_manifest_this_update
.parse()
.unwrap(),
vcir_b
.validated_manifest_meta
.validated_manifest_this_update
.parse()
.unwrap(),
]
.into_iter()
.max()
@@ -668,7 +721,8 @@ mod tests {
let state = build_manifest_state_from_vcirs(&store, &[]).expect("empty manifest state");
assert!(state.mis.is_empty());
assert_eq!(state.most_recent_update, time::OffsetDateTime::UNIX_EPOCH);
let payload_der = encode_manifest_state_payload_der(&state.mis).expect("encode mis payload");
let payload_der =
encode_manifest_state_payload_der(&state.mis).expect("encode mis payload");
assert!(crate::ccr::verify_state_hash(&state.hash, &payload_der));
}
@@ -687,7 +741,8 @@ mod tests {
assert_eq!(state.rksets[0].router_keys.len(), 2);
assert_eq!(state.rksets[1].router_keys.len(), 2);
assert!(state.rksets[0].router_keys[0].ski <= state.rksets[0].router_keys[1].ski);
let payload_der = encode_router_key_state_payload_der(&state.rksets).expect("encode rk payload");
let payload_der =
encode_router_key_state_payload_der(&state.rksets).expect("encode rk payload");
assert!(crate::ccr::verify_state_hash(&state.hash, &payload_der));
}
@@ -695,7 +750,8 @@ mod tests {
fn build_router_key_state_empty_is_valid_and_hashes_empty_sequence() {
let state = build_router_key_state(&[]).expect("empty router key state");
assert!(state.rksets.is_empty());
let payload_der = encode_router_key_state_payload_der(&state.rksets).expect("encode rk payload");
let payload_der =
encode_router_key_state_payload_der(&state.rksets).expect("encode rk payload");
assert!(crate::ccr::verify_state_hash(&state.hash, &payload_der));
}
@@ -706,7 +762,12 @@ mod tests {
sample_vrp_v4(64496, [10, 0, 0, 0], 8, 8),
sample_vrp_v4(64496, [10, 0, 0, 0], 8, 8),
sample_vrp_v4(64496, [10, 1, 0, 0], 16, 24),
sample_vrp_v6(64496, [0x20,0x01,0x0d,0xb8,0,0,0,0,0,0,0,0,0,0,0,0], 32, 48),
sample_vrp_v6(
64496,
[0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
32,
48,
),
];
let state = build_roa_payload_state(&vrps).expect("build roa state");
assert_eq!(state.rps.len(), 2);
@@ -721,7 +782,7 @@ mod tests {
assert_eq!(entries4[0], (8, vec![10], None));
assert_eq!(entries4[1], (16, vec![10, 1], Some(24)));
assert_eq!(entries6.len(), 1);
assert_eq!(entries6[0], (32, vec![0x20,0x01,0x0d,0xb8], Some(48)));
assert_eq!(entries6[0], (32, vec![0x20, 0x01, 0x0d, 0xb8], Some(48)));
let payload_der = encode_roa_payload_state_payload_der(&state.rps).expect("encode payload");
assert!(crate::ccr::verify_state_hash(&state.hash, &payload_der));
}
@@ -737,9 +798,18 @@ mod tests {
#[test]
fn build_aspa_payload_state_merges_and_sorts() {
let aspas = vec![
AspaAttestation { customer_as_id: 64497, provider_as_ids: vec![65002] },
AspaAttestation { customer_as_id: 64496, provider_as_ids: vec![65003, 65001] },
AspaAttestation { customer_as_id: 64496, provider_as_ids: vec![65002, 65001] },
AspaAttestation {
customer_as_id: 64497,
provider_as_ids: vec![65002],
},
AspaAttestation {
customer_as_id: 64496,
provider_as_ids: vec![65003, 65001],
},
AspaAttestation {
customer_as_id: 64496,
provider_as_ids: vec![65002, 65001],
},
];
let state = build_aspa_payload_state(&aspas).expect("build aspa state");
assert_eq!(state.aps.len(), 2);
@@ -747,8 +817,8 @@ mod tests {
assert_eq!(state.aps[0].providers, vec![65001, 65002, 65003]);
assert_eq!(state.aps[1].customer_as_id, 64497);
let encoded = encode_aspa_payload_state(&state).expect("encode aspa state");
let decoded = decode_content_info(&encode_content_info(&CcrContentInfo::new(
RpkiCanonicalCacheRepresentation {
let decoded = decode_content_info(
&encode_content_info(&CcrContentInfo::new(RpkiCanonicalCacheRepresentation {
version: 0,
hash_alg: CcrDigestAlgorithm::Sha256,
produced_at: time::OffsetDateTime::now_utc(),
@@ -757,9 +827,10 @@ mod tests {
vaps: Some(state.clone()),
tas: None,
rks: None,
},
))
.expect("encode ccr")).expect("decode ccr");
}))
.expect("encode ccr"),
)
.expect("decode ccr");
assert_eq!(decoded.content.vaps, Some(state));
assert!(!encoded.is_empty());
}
@@ -778,7 +849,8 @@ mod tests {
.expect("build ta state");
assert_eq!(state.skis.len(), 2);
assert!(state.skis[0] < state.skis[1]);
let payload_der = encode_trust_anchor_state_payload_der(&state.skis).expect("encode ta payload");
let payload_der =
encode_trust_anchor_state_payload_der(&state.skis).expect("encode ta payload");
assert!(crate::ccr::verify_state_hash(&state.hash, &payload_der));
let encoded = encode_trust_anchor_state(&state).expect("encode ta state");
assert!(!encoded.is_empty());
@@ -793,7 +865,11 @@ mod tests {
"tests/fixtures/tal/apnic-rfc7730-https.tal",
"tests/fixtures/ta/apnic-ta.cer",
);
ta.ta_certificate.rc_ca.tbs.extensions.subject_key_identifier = None;
ta.ta_certificate
.rc_ca
.tbs
.extensions
.subject_key_identifier = None;
let err = build_trust_anchor_state(&[ta]).expect_err("missing ski must fail");
assert!(err.to_string().contains("SubjectKeyIdentifier"), "{err}");
}

View File

@@ -13,10 +13,16 @@ pub enum CcrDecodeError {
Parse(String),
#[error("unexpected contentType OID: expected {expected}, got {actual}")]
UnexpectedContentType { expected: &'static str, actual: String },
UnexpectedContentType {
expected: &'static str,
actual: String,
},
#[error("unexpected digest algorithm OID: expected {expected}, got {actual}")]
UnexpectedDigestAlgorithm { expected: &'static str, actual: String },
UnexpectedDigestAlgorithm {
expected: &'static str,
actual: String,
},
#[error("CCR model validation failed after decode: {0}")]
Validate(String),
@@ -26,7 +32,9 @@ pub fn decode_content_info(der: &[u8]) -> Result<CcrContentInfo, CcrDecodeError>
let mut top = DerReader::new(der);
let mut seq = top.take_sequence().map_err(CcrDecodeError::Parse)?;
if !top.is_empty() {
return Err(CcrDecodeError::Parse("trailing bytes after ContentInfo".into()));
return Err(CcrDecodeError::Parse(
"trailing bytes after ContentInfo".into(),
));
}
let content_type_raw = seq.take_tag(0x06).map_err(CcrDecodeError::Parse)?;
if content_type_raw != OID_CT_RPKI_CCR_RAW {
@@ -37,7 +45,9 @@ pub fn decode_content_info(der: &[u8]) -> Result<CcrContentInfo, CcrDecodeError>
}
let inner = seq.take_tag(0xA0).map_err(CcrDecodeError::Parse)?;
if !seq.is_empty() {
return Err(CcrDecodeError::Parse("trailing fields in ContentInfo".into()));
return Err(CcrDecodeError::Parse(
"trailing fields in ContentInfo".into(),
));
}
let content = decode_ccr(inner)?;
let ci = CcrContentInfo::new(content);
@@ -87,7 +97,7 @@ pub fn decode_ccr(der: &[u8]) -> Result<RpkiCanonicalCacheRepresentation, CcrDec
_ => {
return Err(CcrDecodeError::Parse(format!(
"unexpected CCR field tag 0x{tag:02X}"
)))
)));
}
}
}
@@ -110,7 +120,9 @@ fn decode_manifest_state(explicit_der: &[u8]) -> Result<ManifestState, CcrDecode
let mut outer = DerReader::new(explicit_der);
let mut seq = outer.take_sequence().map_err(CcrDecodeError::Parse)?;
if !outer.is_empty() {
return Err(CcrDecodeError::Parse("trailing bytes after ManifestState".into()));
return Err(CcrDecodeError::Parse(
"trailing bytes after ManifestState".into(),
));
}
let mis_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
let mut mis_reader = DerReader::new(mis_der);
@@ -119,30 +131,50 @@ fn decode_manifest_state(explicit_der: &[u8]) -> Result<ManifestState, CcrDecode
let (_tag, full, _value) = mis_reader.take_any_full().map_err(CcrDecodeError::Parse)?;
mis.push(decode_manifest_instance(full)?);
}
let most_recent_update = parse_generalized_time(seq.take_tag(0x18).map_err(CcrDecodeError::Parse)?)?;
let hash = seq.take_octet_string().map_err(CcrDecodeError::Parse)?.to_vec();
let most_recent_update =
parse_generalized_time(seq.take_tag(0x18).map_err(CcrDecodeError::Parse)?)?;
let hash = seq
.take_octet_string()
.map_err(CcrDecodeError::Parse)?
.to_vec();
if !seq.is_empty() {
return Err(CcrDecodeError::Parse("trailing fields in ManifestState".into()));
return Err(CcrDecodeError::Parse(
"trailing fields in ManifestState".into(),
));
}
Ok(ManifestState { mis, most_recent_update, hash })
Ok(ManifestState {
mis,
most_recent_update,
hash,
})
}
fn decode_manifest_instance(der: &[u8]) -> Result<ManifestInstance, CcrDecodeError> {
let mut top = DerReader::new(der);
let mut seq = top.take_sequence().map_err(CcrDecodeError::Parse)?;
if !top.is_empty() {
return Err(CcrDecodeError::Parse("trailing bytes after ManifestInstance".into()));
return Err(CcrDecodeError::Parse(
"trailing bytes after ManifestInstance".into(),
));
}
let hash = seq.take_octet_string().map_err(CcrDecodeError::Parse)?.to_vec();
let hash = seq
.take_octet_string()
.map_err(CcrDecodeError::Parse)?
.to_vec();
let size = seq.take_uint_u64().map_err(CcrDecodeError::Parse)?;
let aki = seq.take_octet_string().map_err(CcrDecodeError::Parse)?.to_vec();
let aki = seq
.take_octet_string()
.map_err(CcrDecodeError::Parse)?
.to_vec();
let manifest_number = decode_big_unsigned(seq.take_tag(0x02).map_err(CcrDecodeError::Parse)?)?;
let this_update = parse_generalized_time(seq.take_tag(0x18).map_err(CcrDecodeError::Parse)?)?;
let locations_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
let mut locations_reader = DerReader::new(locations_der);
let mut locations = Vec::new();
while !locations_reader.is_empty() {
let (_tag, full, _value) = locations_reader.take_any_full().map_err(CcrDecodeError::Parse)?;
let (_tag, full, _value) = locations_reader
.take_any_full()
.map_err(CcrDecodeError::Parse)?;
locations.push(full.to_vec());
}
let subordinates = if !seq.is_empty() {
@@ -150,20 +182,35 @@ fn decode_manifest_instance(der: &[u8]) -> Result<ManifestInstance, CcrDecodeErr
let mut reader = DerReader::new(subordinate_der);
let mut out = Vec::new();
while !reader.is_empty() {
out.push(reader.take_octet_string().map_err(CcrDecodeError::Parse)?.to_vec());
out.push(
reader
.take_octet_string()
.map_err(CcrDecodeError::Parse)?
.to_vec(),
);
}
out
} else {
Vec::new()
};
Ok(ManifestInstance { hash, size, aki, manifest_number, this_update, locations, subordinates })
Ok(ManifestInstance {
hash,
size,
aki,
manifest_number,
this_update,
locations,
subordinates,
})
}
fn decode_roa_payload_state(explicit_der: &[u8]) -> Result<RoaPayloadState, CcrDecodeError> {
let mut outer = DerReader::new(explicit_der);
let mut seq = outer.take_sequence().map_err(CcrDecodeError::Parse)?;
if !outer.is_empty() {
return Err(CcrDecodeError::Parse("trailing bytes after ROAPayloadState".into()));
return Err(CcrDecodeError::Parse(
"trailing bytes after ROAPayloadState".into(),
));
}
let payload_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
let mut reader = DerReader::new(payload_der);
@@ -172,7 +219,10 @@ fn decode_roa_payload_state(explicit_der: &[u8]) -> Result<RoaPayloadState, CcrD
let (_tag, full, _value) = reader.take_any_full().map_err(CcrDecodeError::Parse)?;
rps.push(decode_roa_payload_set(full)?);
}
let hash = seq.take_octet_string().map_err(CcrDecodeError::Parse)?.to_vec();
let hash = seq
.take_octet_string()
.map_err(CcrDecodeError::Parse)?
.to_vec();
Ok(RoaPayloadState { rps, hash })
}
@@ -180,7 +230,9 @@ fn decode_roa_payload_set(der: &[u8]) -> Result<RoaPayloadSet, CcrDecodeError> {
let mut top = DerReader::new(der);
let mut seq = top.take_sequence().map_err(CcrDecodeError::Parse)?;
if !top.is_empty() {
return Err(CcrDecodeError::Parse("trailing bytes after ROAPayloadSet".into()));
return Err(CcrDecodeError::Parse(
"trailing bytes after ROAPayloadSet".into(),
));
}
let as_id = seq.take_uint_u64().map_err(CcrDecodeError::Parse)? as u32;
let blocks_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
@@ -190,14 +242,19 @@ fn decode_roa_payload_set(der: &[u8]) -> Result<RoaPayloadSet, CcrDecodeError> {
let (_tag, full, _value) = reader.take_any_full().map_err(CcrDecodeError::Parse)?;
ip_addr_blocks.push(full.to_vec());
}
Ok(RoaPayloadSet { as_id, ip_addr_blocks })
Ok(RoaPayloadSet {
as_id,
ip_addr_blocks,
})
}
fn decode_aspa_payload_state(explicit_der: &[u8]) -> Result<AspaPayloadState, CcrDecodeError> {
let mut outer = DerReader::new(explicit_der);
let mut seq = outer.take_sequence().map_err(CcrDecodeError::Parse)?;
if !outer.is_empty() {
return Err(CcrDecodeError::Parse("trailing bytes after ASPAPayloadState".into()));
return Err(CcrDecodeError::Parse(
"trailing bytes after ASPAPayloadState".into(),
));
}
let payload_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
let mut reader = DerReader::new(payload_der);
@@ -206,7 +263,10 @@ fn decode_aspa_payload_state(explicit_der: &[u8]) -> Result<AspaPayloadState, Cc
let (_tag, full, _value) = reader.take_any_full().map_err(CcrDecodeError::Parse)?;
aps.push(decode_aspa_payload_set(full)?);
}
let hash = seq.take_octet_string().map_err(CcrDecodeError::Parse)?.to_vec();
let hash = seq
.take_octet_string()
.map_err(CcrDecodeError::Parse)?
.to_vec();
Ok(AspaPayloadState { aps, hash })
}
@@ -214,7 +274,9 @@ fn decode_aspa_payload_set(der: &[u8]) -> Result<AspaPayloadSet, CcrDecodeError>
let mut top = DerReader::new(der);
let mut seq = top.take_sequence().map_err(CcrDecodeError::Parse)?;
if !top.is_empty() {
return Err(CcrDecodeError::Parse("trailing bytes after ASPAPayloadSet".into()));
return Err(CcrDecodeError::Parse(
"trailing bytes after ASPAPayloadSet".into(),
));
}
let customer_as_id = seq.take_uint_u64().map_err(CcrDecodeError::Parse)? as u32;
let providers_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
@@ -223,22 +285,35 @@ fn decode_aspa_payload_set(der: &[u8]) -> Result<AspaPayloadSet, CcrDecodeError>
while !reader.is_empty() {
providers.push(reader.take_uint_u64().map_err(CcrDecodeError::Parse)? as u32);
}
Ok(AspaPayloadSet { customer_as_id, providers })
Ok(AspaPayloadSet {
customer_as_id,
providers,
})
}
fn decode_trust_anchor_state(explicit_der: &[u8]) -> Result<TrustAnchorState, CcrDecodeError> {
let mut outer = DerReader::new(explicit_der);
let mut seq = outer.take_sequence().map_err(CcrDecodeError::Parse)?;
if !outer.is_empty() {
return Err(CcrDecodeError::Parse("trailing bytes after TrustAnchorState".into()));
return Err(CcrDecodeError::Parse(
"trailing bytes after TrustAnchorState".into(),
));
}
let skis_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
let mut reader = DerReader::new(skis_der);
let mut skis = Vec::new();
while !reader.is_empty() {
skis.push(reader.take_octet_string().map_err(CcrDecodeError::Parse)?.to_vec());
skis.push(
reader
.take_octet_string()
.map_err(CcrDecodeError::Parse)?
.to_vec(),
);
}
let hash = seq.take_octet_string().map_err(CcrDecodeError::Parse)?.to_vec();
let hash = seq
.take_octet_string()
.map_err(CcrDecodeError::Parse)?
.to_vec();
Ok(TrustAnchorState { skis, hash })
}
@@ -246,7 +321,9 @@ fn decode_router_key_state(explicit_der: &[u8]) -> Result<RouterKeyState, CcrDec
let mut outer = DerReader::new(explicit_der);
let mut seq = outer.take_sequence().map_err(CcrDecodeError::Parse)?;
if !outer.is_empty() {
return Err(CcrDecodeError::Parse("trailing bytes after RouterKeyState".into()));
return Err(CcrDecodeError::Parse(
"trailing bytes after RouterKeyState".into(),
));
}
let sets_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
let mut reader = DerReader::new(sets_der);
@@ -255,7 +332,10 @@ fn decode_router_key_state(explicit_der: &[u8]) -> Result<RouterKeyState, CcrDec
let (_tag, full, _value) = reader.take_any_full().map_err(CcrDecodeError::Parse)?;
rksets.push(decode_router_key_set(full)?);
}
let hash = seq.take_octet_string().map_err(CcrDecodeError::Parse)?.to_vec();
let hash = seq
.take_octet_string()
.map_err(CcrDecodeError::Parse)?
.to_vec();
Ok(RouterKeyState { rksets, hash })
}
@@ -263,7 +343,9 @@ fn decode_router_key_set(der: &[u8]) -> Result<RouterKeySet, CcrDecodeError> {
let mut top = DerReader::new(der);
let mut seq = top.take_sequence().map_err(CcrDecodeError::Parse)?;
if !top.is_empty() {
return Err(CcrDecodeError::Parse("trailing bytes after RouterKeySet".into()));
return Err(CcrDecodeError::Parse(
"trailing bytes after RouterKeySet".into(),
));
}
let as_id = seq.take_uint_u64().map_err(CcrDecodeError::Parse)? as u32;
let keys_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
@@ -280,14 +362,22 @@ fn decode_router_key(der: &[u8]) -> Result<RouterKey, CcrDecodeError> {
let mut top = DerReader::new(der);
let mut seq = top.take_sequence().map_err(CcrDecodeError::Parse)?;
if !top.is_empty() {
return Err(CcrDecodeError::Parse("trailing bytes after RouterKey".into()));
return Err(CcrDecodeError::Parse(
"trailing bytes after RouterKey".into(),
));
}
let ski = seq.take_octet_string().map_err(CcrDecodeError::Parse)?.to_vec();
let ski = seq
.take_octet_string()
.map_err(CcrDecodeError::Parse)?
.to_vec();
let (_tag, full, _value) = seq.take_any_full().map_err(CcrDecodeError::Parse)?;
if !seq.is_empty() {
return Err(CcrDecodeError::Parse("trailing fields in RouterKey".into()));
}
Ok(RouterKey { ski, spki_der: full.to_vec() })
Ok(RouterKey {
ski,
spki_der: full.to_vec(),
})
}
fn decode_digest_algorithm(mut seq: DerReader<'_>) -> Result<CcrDigestAlgorithm, CcrDecodeError> {
@@ -354,8 +444,7 @@ fn parse_generalized_time(bytes: &[u8]) -> Result<time::OffsetDateTime, CcrDecod
let hour = parse(8..10)? as u8;
let minute = parse(10..12)? as u8;
let second = parse(12..14)? as u8;
let month = time::Month::try_from(month)
.map_err(|e| CcrDecodeError::Parse(e.to_string()))?;
let month = time::Month::try_from(month).map_err(|e| CcrDecodeError::Parse(e.to_string()))?;
let date = time::Date::from_calendar_date(year, month, day)
.map_err(|e| CcrDecodeError::Parse(e.to_string()))?;
let timev = time::Time::from_hms(hour, minute, second)
@@ -371,7 +460,9 @@ fn decode_big_unsigned(bytes: &[u8]) -> Result<BigUnsigned, CcrDecodeError> {
return Err(CcrDecodeError::Parse("INTEGER must be non-negative".into()));
}
if bytes.len() > 1 && bytes[0] == 0x00 && (bytes[1] & 0x80) == 0 {
return Err(CcrDecodeError::Parse("INTEGER not minimally encoded".into()));
return Err(CcrDecodeError::Parse(
"INTEGER not minimally encoded".into(),
));
}
let bytes_be = if bytes.len() > 1 && bytes[0] == 0x00 {
bytes[1..].to_vec()

View File

@@ -34,34 +34,58 @@ pub fn dump_content_info_json(
})
}).unwrap_or_else(|| json!({"present": false}));
let vrps_total = content_info.content.vrps.as_ref().map(|state| {
state.rps.iter().map(|set| set.ip_addr_blocks.len()).sum::<usize>()
}).unwrap_or(0);
let vrps = content_info.content.vrps.as_ref().map(|state| {
json!({
"present": true,
"payload_sets": state.rps.len(),
"hash_hex": hex::encode(&state.hash),
"ip_addr_block_count": vrps_total,
let vrps_total = content_info
.content
.vrps
.as_ref()
.map(|state| {
state
.rps
.iter()
.map(|set| set.ip_addr_blocks.len())
.sum::<usize>()
})
}).unwrap_or_else(|| json!({"present": false}));
.unwrap_or(0);
let vrps = content_info
.content
.vrps
.as_ref()
.map(|state| {
json!({
"present": true,
"payload_sets": state.rps.len(),
"hash_hex": hex::encode(&state.hash),
"ip_addr_block_count": vrps_total,
})
})
.unwrap_or_else(|| json!({"present": false}));
let vaps = content_info.content.vaps.as_ref().map(|state| {
json!({
"present": true,
"payload_sets": state.aps.len(),
"hash_hex": hex::encode(&state.hash),
"provider_count": state.aps.iter().map(|set| set.providers.len()).sum::<usize>(),
let vaps = content_info
.content
.vaps
.as_ref()
.map(|state| {
json!({
"present": true,
"payload_sets": state.aps.len(),
"hash_hex": hex::encode(&state.hash),
"provider_count": state.aps.iter().map(|set| set.providers.len()).sum::<usize>(),
})
})
}).unwrap_or_else(|| json!({"present": false}));
.unwrap_or_else(|| json!({"present": false}));
let tas = content_info.content.tas.as_ref().map(|state| {
json!({
"present": true,
"ski_count": state.skis.len(),
"hash_hex": hex::encode(&state.hash),
let tas = content_info
.content
.tas
.as_ref()
.map(|state| {
json!({
"present": true,
"ski_count": state.skis.len(),
"hash_hex": hex::encode(&state.hash),
})
})
}).unwrap_or_else(|| json!({"present": false}));
.unwrap_or_else(|| json!({"present": false}));
let rks = content_info.content.rks.as_ref().map(|state| {
json!({

View File

@@ -24,9 +24,7 @@ pub fn encode_content_info(content_info: &CcrContentInfo) -> Result<Vec<u8>, Ccr
]))
}
pub fn encode_ccr(
ccr: &RpkiCanonicalCacheRepresentation,
) -> Result<Vec<u8>, CcrEncodeError> {
pub fn encode_ccr(ccr: &RpkiCanonicalCacheRepresentation) -> Result<Vec<u8>, CcrEncodeError> {
ccr.validate().map_err(CcrEncodeError::Validate)?;
let mut fields = Vec::new();
if ccr.version != CCR_VERSION_V0 {
@@ -156,11 +154,12 @@ pub fn encode_trust_anchor_state(state: &TrustAnchorState) -> Result<Vec<u8>, Cc
Ok(encode_sequence(&[skis, encode_octet_string(&state.hash)]))
}
pub fn encode_trust_anchor_state_payload_der(
skis: &[Vec<u8>],
) -> Result<Vec<u8>, CcrEncodeError> {
pub fn encode_trust_anchor_state_payload_der(skis: &[Vec<u8>]) -> Result<Vec<u8>, CcrEncodeError> {
Ok(encode_sequence(
&skis.iter().map(|ski| encode_octet_string(ski)).collect::<Vec<_>>(),
&skis
.iter()
.map(|ski| encode_octet_string(ski))
.collect::<Vec<_>>(),
))
}

View File

@@ -54,7 +54,6 @@ pub fn build_ccr_from_run(
})
}
fn build_router_key_state_from_runtime(
router_keys: &[RouterKeyPayload],
) -> Result<crate::ccr::model::RouterKeyState, CcrBuildError> {
@@ -109,17 +108,22 @@ pub fn write_ccr_file(
mod tests {
use super::*;
use crate::ccr::decode::decode_content_info;
use crate::data_model::ta::TrustAnchor;
use crate::data_model::tal::Tal;
use crate::storage::{RawByHashEntry, RocksStore, VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary, VcirChildEntry, VcirInstanceGate, VcirRelatedArtifact, VcirSummary, ValidatedCaInstanceResult, ValidatedManifestMeta, PackTime};
use crate::validation::objects::{AspaAttestation, RouterKeyPayload, Vrp};
use crate::data_model::manifest::ManifestObject;
use crate::data_model::roa::{IpPrefix, RoaAfi};
use crate::data_model::ta::TrustAnchor;
use crate::data_model::tal::Tal;
use crate::storage::{
PackTime, RawByHashEntry, RocksStore, ValidatedCaInstanceResult, ValidatedManifestMeta,
VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary,
VcirChildEntry, VcirInstanceGate, VcirRelatedArtifact, VcirSummary,
};
use crate::validation::objects::{AspaAttestation, RouterKeyPayload, Vrp};
use sha2::Digest;
fn sample_trust_anchor() -> TrustAnchor {
let base = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let tal_bytes = std::fs::read(base.join("tests/fixtures/tal/apnic-rfc7730-https.tal")).expect("read tal");
let tal_bytes = std::fs::read(base.join("tests/fixtures/tal/apnic-rfc7730-https.tal"))
.expect("read tal");
let ta_der = std::fs::read(base.join("tests/fixtures/ta/apnic-ta.cer")).expect("read ta");
let tal = Tal::decode_bytes(&tal_bytes).expect("decode tal");
TrustAnchor::bind_der(tal, &ta_der, None).expect("bind ta")
@@ -131,7 +135,8 @@ mod tests {
let manifest = ManifestObject::decode_der(&manifest_der).expect("decode manifest");
let hash = hex::encode(sha2::Sha256::digest(&manifest_der));
let mut raw = RawByHashEntry::from_bytes(hash.clone(), manifest_der.clone());
raw.origin_uris.push("rsync://example.test/repo/current.mft".to_string());
raw.origin_uris
.push("rsync://example.test/repo/current.mft".to_string());
raw.object_type = Some("mft".to_string());
raw.encoding = Some("der".to_string());
store.put_raw_by_hash_entry(&raw).expect("put raw");
@@ -142,19 +147,33 @@ mod tests {
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(manifest.manifest.this_update),
last_successful_validation_time: PackTime::from_utc_offset_datetime(
manifest.manifest.this_update,
),
current_manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(),
current_crl_rsync_uri: "rsync://example.test/repo/current.crl".to_string(),
validated_manifest_meta: ValidatedManifestMeta {
validated_manifest_number: manifest.manifest.manifest_number.bytes_be.clone(),
validated_manifest_this_update: PackTime::from_utc_offset_datetime(manifest.manifest.this_update),
validated_manifest_next_update: PackTime::from_utc_offset_datetime(manifest.manifest.next_update),
validated_manifest_this_update: PackTime::from_utc_offset_datetime(
manifest.manifest.this_update,
),
validated_manifest_next_update: PackTime::from_utc_offset_datetime(
manifest.manifest.next_update,
),
},
instance_gate: VcirInstanceGate {
manifest_next_update: PackTime::from_utc_offset_datetime(manifest.manifest.next_update),
current_crl_next_update: PackTime::from_utc_offset_datetime(manifest.manifest.next_update),
self_ca_not_after: PackTime::from_utc_offset_datetime(manifest.manifest.next_update),
instance_effective_until: PackTime::from_utc_offset_datetime(manifest.manifest.next_update),
manifest_next_update: PackTime::from_utc_offset_datetime(
manifest.manifest.next_update,
),
current_crl_next_update: PackTime::from_utc_offset_datetime(
manifest.manifest.next_update,
),
self_ca_not_after: PackTime::from_utc_offset_datetime(
manifest.manifest.next_update,
),
instance_effective_until: PackTime::from_utc_offset_datetime(
manifest.manifest.next_update,
),
},
child_entries: vec![VcirChildEntry {
child_manifest_rsync_uri: "rsync://example.test/repo/child.mft".to_string(),
@@ -166,7 +185,9 @@ mod tests {
child_rrdp_notification_uri: None,
child_effective_ip_resources: None,
child_effective_as_resources: None,
accepted_at_validation_time: PackTime::from_utc_offset_datetime(manifest.manifest.this_update),
accepted_at_validation_time: PackTime::from_utc_offset_datetime(
manifest.manifest.this_update,
),
}],
local_outputs: Vec::new(),
related_artifacts: vec![VcirRelatedArtifact {
@@ -204,10 +225,17 @@ mod tests {
let trust_anchor = sample_trust_anchor();
let vrps = vec![Vrp {
asn: 64496,
prefix: IpPrefix { afi: RoaAfi::Ipv4, prefix_len: 8, addr: [10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] },
prefix: IpPrefix {
afi: RoaAfi::Ipv4,
prefix_len: 8,
addr: [10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
},
max_length: 8,
}];
let aspas = vec![AspaAttestation { customer_as_id: 64496, provider_as_ids: vec![64497] }];
let aspas = vec![AspaAttestation {
customer_as_id: 64496,
provider_as_ids: vec![64497],
}];
let router_keys = vec![RouterKeyPayload {
as_id: 64496,
ski: vec![0x11; 20],
@@ -215,9 +243,19 @@ mod tests {
source_object_uri: "rsync://example.test/repo/router.cer".to_string(),
source_object_hash: hex::encode([0x11; 32]),
source_ee_cert_hash: hex::encode([0x11; 32]),
item_effective_until: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc() + time::Duration::hours(1)),
item_effective_until: PackTime::from_utc_offset_datetime(
time::OffsetDateTime::now_utc() + time::Duration::hours(1),
),
}];
let ccr = build_ccr_from_run(&store, &[trust_anchor], &vrps, &aspas, &router_keys, time::OffsetDateTime::now_utc()).expect("build ccr");
let ccr = build_ccr_from_run(
&store,
&[trust_anchor],
&vrps,
&aspas,
&router_keys,
time::OffsetDateTime::now_utc(),
)
.expect("build ccr");
assert!(ccr.mfts.is_some());
assert!(ccr.vrps.is_some());
assert!(ccr.vaps.is_some());

View File

@@ -1,12 +1,12 @@
pub mod decode;
pub mod encode;
pub mod dump;
pub mod hash;
pub mod model;
#[cfg(feature = "full")]
pub mod build;
pub mod decode;
pub mod dump;
pub mod encode;
#[cfg(feature = "full")]
pub mod export;
pub mod hash;
pub mod model;
#[cfg(feature = "full")]
pub mod verify;
@@ -16,16 +16,19 @@ pub use build::{
build_roa_payload_state, build_trust_anchor_state,
};
pub use decode::{CcrDecodeError, decode_content_info};
pub use dump::{CcrDumpError, dump_content_info_json, dump_content_info_json_value};
pub use encode::{CcrEncodeError, encode_content_info};
#[cfg(feature = "full")]
pub use export::{CcrExportError, build_ccr_from_run, write_ccr_file};
pub use dump::{CcrDumpError, dump_content_info_json, dump_content_info_json_value};
#[cfg(feature = "full")]
pub use verify::{CcrVerifyError, CcrVerifySummary, extract_vrp_rows, verify_against_report_json_path, verify_against_vcir_store, verify_against_vcir_store_path, verify_content_info, verify_content_info_bytes};
pub use hash::{compute_state_hash, verify_state_hash};
pub use model::{
AspaPayloadSet, AspaPayloadState, CcrContentInfo, CcrDigestAlgorithm,
ManifestInstance, ManifestState, RoaPayloadSet, RoaPayloadState,
RouterKey, RouterKeySet, RouterKeyState, RpkiCanonicalCacheRepresentation,
TrustAnchorState,
AspaPayloadSet, AspaPayloadState, CcrContentInfo, CcrDigestAlgorithm, ManifestInstance,
ManifestState, RoaPayloadSet, RoaPayloadState, RouterKey, RouterKeySet, RouterKeyState,
RpkiCanonicalCacheRepresentation, TrustAnchorState,
};
#[cfg(feature = "full")]
pub use verify::{
CcrVerifyError, CcrVerifySummary, extract_vrp_rows, verify_against_report_json_path,
verify_against_vcir_store, verify_against_vcir_store_path, verify_content_info,
verify_content_info_bytes,
};

View File

@@ -133,16 +133,17 @@ impl ManifestInstance {
));
}
validate_key_identifier("ManifestInstance.aki", &self.aki)?;
validate_big_unsigned_bytes("ManifestInstance.manifest_number", &self.manifest_number.bytes_be)?;
validate_big_unsigned_bytes(
"ManifestInstance.manifest_number",
&self.manifest_number.bytes_be,
)?;
if self.locations.is_empty() {
return Err("ManifestInstance.locations must contain at least one AccessDescription".into());
return Err(
"ManifestInstance.locations must contain at least one AccessDescription".into(),
);
}
for location in &self.locations {
validate_full_der_with_tag(
"ManifestInstance.locations[]",
location,
Some(0x30),
)?;
validate_full_der_with_tag("ManifestInstance.locations[]", location, Some(0x30))?;
}
if !self.subordinates.is_empty() {
validate_sorted_unique_bytes(
@@ -339,7 +340,9 @@ fn validate_big_unsigned_bytes(field: &str, bytes: &[u8]) -> Result<(), String>
return Err(format!("{field} must not be empty"));
}
if bytes.len() > 1 && bytes[0] == 0x00 {
return Err(format!("{field} must be minimally encoded as an unsigned integer"));
return Err(format!(
"{field} must be minimally encoded as an unsigned integer"
));
}
Ok(())
}

View File

@@ -82,7 +82,9 @@ pub fn verify_content_info_bytes(der: &[u8]) -> Result<CcrVerifySummary, CcrVeri
verify_content_info(&content_info)
}
pub fn verify_content_info(content_info: &CcrContentInfo) -> Result<CcrVerifySummary, CcrVerifyError> {
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;
@@ -108,7 +110,11 @@ pub fn verify_content_info(content_info: &CcrContentInfo) -> Result<CcrVerifySum
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();
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)
@@ -154,10 +160,11 @@ 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 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)?;
@@ -198,7 +205,9 @@ pub fn verify_against_vcir_store(
let Some(mfts) = &content_info.content.mfts else {
return Ok(());
};
let vcirs = store.list_vcirs().map_err(|e| CcrVerifyError::ListVcirs(e.to_string()))?;
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| {
@@ -244,7 +253,9 @@ fn verify_router_key_state_hash(state: &RouterKeyState) -> Result<(), CcrVerifyE
Ok(())
}
fn report_vrp_keys(json: &serde_json::Value) -> Result<BTreeSet<(u32, String, u16)>, CcrVerifyError> {
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);
@@ -253,7 +264,8 @@ fn report_vrp_keys(json: &serde_json::Value) -> Result<BTreeSet<(u32, String, u1
let asn = item
.get("asn")
.and_then(|v| v.as_u64())
.ok_or_else(|| CcrVerifyError::ReportParse("vrps[].asn missing".into()))? as u32;
.ok_or_else(|| CcrVerifyError::ReportParse("vrps[].asn missing".into()))?
as u32;
let prefix = item
.get("prefix")
.and_then(|v| v.as_str())
@@ -285,7 +297,11 @@ fn report_aspa_keys(json: &serde_json::Value) -> Result<BTreeSet<(u32, Vec<u32>)
.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))
.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();
@@ -294,7 +310,9 @@ fn report_aspa_keys(json: &serde_json::Value) -> Result<BTreeSet<(u32, Vec<u32>)
Ok(out)
}
pub fn extract_vrp_rows(content_info: &CcrContentInfo) -> Result<BTreeSet<(u32, String, u16)>, CcrVerifyError> {
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);
@@ -311,7 +329,9 @@ pub fn extract_vrp_rows(content_info: &CcrContentInfo) -> Result<BTreeSet<(u32,
Ok(out)
}
fn ccr_aspa_keys(content_info: &CcrContentInfo) -> Result<BTreeSet<(u32, Vec<u32>)>, CcrVerifyError> {
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);
@@ -322,24 +342,43 @@ fn ccr_aspa_keys(content_info: &CcrContentInfo) -> Result<BTreeSet<(u32, Vec<u32
Ok(out)
}
fn decode_roa_family_block(block: &[u8]) -> Result<(u16, Vec<(u8, Vec<u8>, Option<u16>)>), CcrVerifyError> {
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)))?;
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())));
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_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 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 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)
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));
}
@@ -358,37 +397,43 @@ fn format_prefix(afi: u16, addr_bytes: &[u8], prefix_len: u8) -> Result<String,
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}")))),
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))
.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::build::{build_aspa_payload_state, build_roa_payload_state};
use crate::ccr::model::{
CcrDigestAlgorithm, ManifestInstance, ManifestState,
RouterKey, RouterKeySet, RpkiCanonicalCacheRepresentation,
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::data_model::roa::{IpPrefix, RoaAfi};
use crate::storage::{
PackTime, ValidatedCaInstanceResult, ValidatedManifestMeta, VcirArtifactKind,
VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary, VcirChildEntry,
VcirInstanceGate, VcirRelatedArtifact, VcirSummary,
};
use crate::validation::objects::{AspaAttestation, Vrp};
fn sample_time() -> time::OffsetDateTime {
time::OffsetDateTime::parse(
@@ -415,21 +460,37 @@ mod tests {
};
let vrps = build_roa_payload_state(&[Vrp {
asn: 64496,
prefix: IpPrefix { afi: RoaAfi::Ipv4, prefix_len: 0, addr: [0; 16] },
prefix: IpPrefix {
afi: RoaAfi::Ipv4,
prefix_len: 0,
addr: [0; 16],
},
max_length: 0,
}]).expect("build roa state");
}])
.expect("build roa state");
let vaps = build_aspa_payload_state(&[AspaAttestation {
customer_as_id: 64496,
provider_as_ids: vec![64497],
}]).expect("build aspa state");
}])
.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()),
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 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()),
hash: crate::ccr::compute_state_hash(
&encode_router_key_state_payload_der(&rksets).unwrap(),
),
rksets,
};
CcrContentInfo::new(RpkiCanonicalCacheRepresentation {
@@ -448,19 +509,31 @@ mod tests {
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)));
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)));
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)));
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)));
assert!(matches!(
verify_content_info(&ci),
Err(CcrVerifyError::RouterKeyHashMismatch)
));
}
#[test]
@@ -472,20 +545,28 @@ mod tests {
});
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");
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(_))));
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 =
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(_))));
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}],
@@ -493,7 +574,10 @@ mod tests {
});
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 { .. })));
assert!(matches!(
verify_against_report_json_path(&sample_content_info(), &mismatch_path),
Err(CcrVerifyError::ReportAspaMismatch { .. })
));
}
#[test]
@@ -542,11 +626,26 @@ mod tests {
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() },
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 { .. })));
assert!(matches!(
verify_against_vcir_store(&sample_content_info(), &store),
Err(CcrVerifyError::VcirManifestMismatch { .. })
));
}
#[test]
@@ -557,12 +656,27 @@ mod tests {
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()) }),
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(_))));
assert!(matches!(
extract_vrp_rows(&ci),
Err(CcrVerifyError::Decode(_))
));
let bad_count = count_roa_block_entries(&[vec![0x04, 0x00]]);
assert_eq!(bad_count, 0);
}