20260324_2 增加ccr & router key support

This commit is contained in:
2026-03-26 11:52:06 +08:00
parent d6d44669b4
commit fe8b89d829
33 changed files with 5337 additions and 27 deletions

800
src/ccr/build.rs Normal file
View File

@@ -0,0 +1,800 @@
use std::collections::{BTreeMap, BTreeSet};
use sha2::Digest;
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::compute_state_hash;
use crate::ccr::model::{
AspaPayloadSet, AspaPayloadState, ManifestInstance, ManifestState, RoaPayloadSet,
RoaPayloadState, RouterKey, RouterKeySet, RouterKeyState, TrustAnchorState,
};
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::validation::objects::{AspaAttestation, Vrp};
#[derive(Debug, thiserror::Error)]
pub enum CcrBuildError {
#[error("trust anchor set must not be empty")]
EmptyTrustAnchors,
#[error("trust anchor certificate missing SubjectKeyIdentifier")]
MissingTrustAnchorSki,
#[error("ROA payload state encoding failed: {0}")]
RoaEncode(String),
#[error("ASPA payload state encoding failed: {0}")]
AspaEncode(String),
#[error("TrustAnchor state encoding failed: {0}")]
TrustAnchorEncode(String),
#[error("manifest artifact missing in VCIR: {0}")]
MissingManifestArtifact(String),
#[error("manifest raw bytes missing in store for {manifest_rsync_uri}: {sha256_hex}")]
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 },
#[error("manifest decode failed for {manifest_rsync_uri}: {detail}")]
ManifestDecode { manifest_rsync_uri: String, detail: String },
#[error("manifest EE certificate missing AuthorityKeyIdentifier: {0}")]
ManifestEeMissingAki(String),
#[error("manifest EE certificate missing Subject Information Access extension: {0}")]
ManifestEeMissingSia(String),
#[error("manifest EE certificate SIA is not the EE form: {0}")]
ManifestEeSiaWrongVariant(String),
#[error("manifest child SKI is not valid lowercase hex or not 20 bytes: {0}")]
InvalidChildSki(String),
#[error("manifest AccessDescription contains unsupported accessMethod OID: {0}")]
UnsupportedAccessMethodOid(String),
#[error("manifest state encoding failed: {0}")]
ManifestEncode(String),
#[error("duplicate manifest hash with conflicting content for URI: {0}")]
DuplicateManifestHashConflict(String),
#[error("router key state encoding failed: {0}")]
RouterKeyEncode(String),
}
pub fn build_roa_payload_state(vrps: &[Vrp]) -> Result<RoaPayloadState, CcrBuildError> {
let mut grouped: BTreeMap<u32, BTreeSet<RoaPayloadKey>> = BTreeMap::new();
for vrp in vrps {
grouped
.entry(vrp.asn)
.or_default()
.insert(RoaPayloadKey::from_vrp(vrp));
}
let mut rps = Vec::with_capacity(grouped.len());
for (asn, entries) in grouped {
let mut families: BTreeMap<u16, Vec<RoaPayloadKey>> = BTreeMap::new();
for entry in entries {
families.entry(entry.afi).or_default().push(entry);
}
let mut ip_addr_blocks = Vec::with_capacity(families.len());
for (afi, entries) in families {
ip_addr_blocks.push(encode_roa_ip_address_family(afi, &entries));
}
rps.push(RoaPayloadSet {
as_id: asn,
ip_addr_blocks,
});
}
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),
})
}
pub fn build_aspa_payload_state(
attestations: &[AspaAttestation],
) -> Result<AspaPayloadState, CcrBuildError> {
let mut grouped: BTreeMap<u32, BTreeSet<u32>> = BTreeMap::new();
for attestation in attestations {
grouped
.entry(attestation.customer_as_id)
.or_default()
.extend(attestation.provider_as_ids.iter().copied());
}
let aps: Vec<AspaPayloadSet> = grouped
.into_iter()
.map(|(customer_as_id, providers)| AspaPayloadSet {
customer_as_id,
providers: providers.into_iter().collect(),
})
.collect();
let payload_der = encode_aspa_payload_state_payload_der(&aps)
.map_err(|e| CcrBuildError::AspaEncode(e.to_string()))?;
Ok(AspaPayloadState {
aps,
hash: compute_state_hash(&payload_der),
})
}
pub fn build_trust_anchor_state(
trust_anchors: &[TrustAnchor],
) -> Result<TrustAnchorState, CcrBuildError> {
if trust_anchors.is_empty() {
return Err(CcrBuildError::EmptyTrustAnchors);
}
let mut skis: BTreeSet<Vec<u8>> = BTreeSet::new();
for ta in trust_anchors {
let ski = ta
.ta_certificate
.rc_ca
.tbs
.extensions
.subject_key_identifier
.clone()
.ok_or(CcrBuildError::MissingTrustAnchorSki)?;
skis.insert(ski);
}
let skis: Vec<Vec<u8>> = skis.into_iter().collect();
let payload_der = encode_trust_anchor_state_payload_der(&skis)
.map_err(|e| CcrBuildError::TrustAnchorEncode(e.to_string()))?;
Ok(TrustAnchorState {
skis,
hash: compute_state_hash(&payload_der),
})
}
pub fn build_manifest_state_from_vcirs(
store: &RocksStore,
vcirs: &[ValidatedCaInstanceResult],
) -> Result<ManifestState, CcrBuildError> {
let mut mis_by_hash: BTreeMap<Vec<u8>, ManifestInstance> = BTreeMap::new();
let mut most_recent_update = time::OffsetDateTime::UNIX_EPOCH;
for vcir in vcirs {
let manifest_artifact = vcir
.related_artifacts
.iter()
.find(|artifact| {
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()))?;
let raw_entry = store
.get_raw_by_hash_entry(&manifest_artifact.sha256)
.map_err(|e| CcrBuildError::LoadManifestRawBytes {
manifest_rsync_uri: vcir.current_manifest_rsync_uri.clone(),
detail: e.to_string(),
})?
.ok_or_else(|| CcrBuildError::MissingManifestRawBytes {
manifest_rsync_uri: vcir.current_manifest_rsync_uri.clone(),
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 ee = &manifest.signed_object.signed_data.certificates[0].resource_cert;
let aki = ee
.tbs
.extensions
.authority_key_identifier
.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()))?;
let locations = match sia {
SubjectInfoAccess::Ee(ee_sia) => ee_sia
.access_descriptions
.iter()
.map(encode_access_description_der)
.collect::<Result<Vec<_>, _>>()?,
SubjectInfoAccess::Ca(_) => {
return Err(CcrBuildError::ManifestEeSiaWrongVariant(
vcir.current_manifest_rsync_uri.clone(),
))
}
};
let subordinates = collect_subordinate_ski_bytes(vcir)?;
let this_update = vcir
.validated_manifest_meta
.validated_manifest_this_update
.parse()
.map_err(|e| CcrBuildError::ManifestDecode {
manifest_rsync_uri: vcir.current_manifest_rsync_uri.clone(),
detail: format!("invalid validated_manifest_this_update: {e}"),
})?;
if this_update > most_recent_update {
most_recent_update = this_update;
}
let instance = ManifestInstance {
hash: sha2::Sha256::digest(&raw_entry.bytes).to_vec(),
size: raw_entry.bytes.len() as u64,
aki,
manifest_number: crate::data_model::common::BigUnsigned {
bytes_be: vcir.validated_manifest_meta.validated_manifest_number.clone(),
},
this_update,
locations,
subordinates,
};
match mis_by_hash.get(instance.hash.as_slice()) {
Some(existing) if existing != &instance => {
return Err(CcrBuildError::DuplicateManifestHashConflict(
vcir.current_manifest_rsync_uri.clone(),
));
}
Some(_) => {}
None => {
mis_by_hash.insert(instance.hash.clone(), instance);
}
}
}
let mis: Vec<ManifestInstance> = mis_by_hash.into_values().collect();
let payload_der = encode_manifest_state_payload_der(&mis)
.map_err(|e| CcrBuildError::ManifestEncode(e.to_string()))?;
Ok(ManifestState {
mis,
most_recent_update,
hash: compute_state_hash(&payload_der),
})
}
fn collect_subordinate_ski_bytes(
vcir: &ValidatedCaInstanceResult,
) -> Result<Vec<Vec<u8>>, CcrBuildError> {
let mut skis = BTreeSet::new();
for child in &vcir.child_entries {
let bytes = hex::decode(&child.child_ski)
.map_err(|_| CcrBuildError::InvalidChildSki(child.child_ski.clone()))?;
if bytes.len() != 20 {
return Err(CcrBuildError::InvalidChildSki(child.child_ski.clone()));
}
skis.insert(bytes);
}
Ok(skis.into_iter().collect())
}
fn encode_access_description_der(ad: &AccessDescription) -> Result<Vec<u8>, CcrBuildError> {
let oid = encode_oid_from_string(&ad.access_method_oid)?;
let uri = encode_tlv(0x86, ad.access_location.as_bytes().to_vec());
Ok(encode_sequence(&[oid, uri]))
}
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())))
.collect::<Result<Vec<_>, _>>()?;
if arcs.len() < 2 {
return Err(CcrBuildError::UnsupportedAccessMethodOid(oid.to_string()));
}
if arcs[0] > 2 || (arcs[0] < 2 && arcs[1] >= 40) {
return Err(CcrBuildError::UnsupportedAccessMethodOid(oid.to_string()));
}
let mut body = Vec::new();
body.push((arcs[0] * 40 + arcs[1]) as u8);
for arc in &arcs[2..] {
encode_base128(*arc, &mut body);
}
Ok(encode_tlv(0x06, body))
}
fn encode_base128(mut value: u64, out: &mut Vec<u8>) {
let mut tmp = vec![(value & 0x7F) as u8];
value >>= 7;
while value > 0 {
tmp.push(((value & 0x7F) as u8) | 0x80);
value >>= 7;
}
tmp.reverse();
out.extend_from_slice(&tmp);
}
pub fn build_router_key_state(
router_certs: &[BgpsecRouterCertificate],
) -> Result<RouterKeyState, CcrBuildError> {
let mut grouped: BTreeMap<u32, BTreeSet<RouterKey>> = BTreeMap::new();
for cert in router_certs {
let key = RouterKey {
ski: cert.subject_key_identifier.clone(),
spki_der: cert.spki_der.clone(),
};
for asn in &cert.asns {
grouped.entry(*asn).or_default().insert(key.clone());
}
}
let rksets: Vec<RouterKeySet> = grouped
.into_iter()
.map(|(as_id, router_keys)| RouterKeySet {
as_id,
router_keys: router_keys.into_iter().collect(),
})
.collect();
let payload_der = encode_router_key_state_payload_der(&rksets)
.map_err(|e| CcrBuildError::RouterKeyEncode(e.to_string()))?;
Ok(RouterKeyState {
rksets,
hash: compute_state_hash(&payload_der),
})
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct RoaPayloadKey {
afi: u16,
addr: Vec<u8>,
prefix_len: u8,
max_length: u8,
}
impl RoaPayloadKey {
fn from_vrp(vrp: &Vrp) -> Self {
let afi = match vrp.prefix.afi {
RoaAfi::Ipv4 => 1,
RoaAfi::Ipv6 => 2,
};
Self {
afi,
addr: vrp.prefix.addr.to_vec(),
prefix_len: vrp.prefix.prefix_len as u8,
max_length: vrp.max_length as u8,
}
}
}
fn encode_roa_ip_address_family(afi: u16, entries: &[RoaPayloadKey]) -> Vec<u8> {
let address_family = afi.to_be_bytes();
let addresses = entries
.iter()
.map(encode_roa_ip_address)
.collect::<Vec<_>>();
encode_sequence(&[
encode_octet_string(&address_family),
encode_sequence(&addresses),
])
}
fn encode_roa_ip_address(entry: &RoaPayloadKey) -> Vec<u8> {
let (unused_bits, content) = encode_prefix_bit_string(&entry.addr, entry.prefix_len);
let mut fields = vec![encode_bit_string(unused_bits, &content)];
if entry.max_length != entry.prefix_len {
fields.push(encode_integer_u8(entry.max_length));
}
encode_sequence(&fields)
}
fn encode_prefix_bit_string(addr: &[u8], prefix_len: u8) -> (u8, Vec<u8>) {
if prefix_len == 0 {
return (0, Vec::new());
}
let octets = ((prefix_len as usize) + 7) / 8;
let mut content = addr[..octets].to_vec();
let rem = prefix_len % 8;
let unused = if rem == 0 { 0 } else { 8 - rem };
if unused > 0 {
let mask = 0xFFu8 << unused;
let last = content.last_mut().expect("octets > 0");
*last &= mask;
}
(unused, content)
}
fn encode_integer_u8(v: u8) -> Vec<u8> {
encode_integer_bytes(vec![v])
}
fn encode_integer_bytes(mut bytes: Vec<u8>) -> Vec<u8> {
if bytes.is_empty() {
bytes.push(0);
}
if bytes[0] & 0x80 != 0 {
bytes.insert(0, 0);
}
encode_tlv(0x02, bytes)
}
fn encode_bit_string(unused_bits: u8, content: &[u8]) -> Vec<u8> {
let mut value = Vec::with_capacity(content.len() + 1);
value.push(unused_bits);
value.extend_from_slice(content);
encode_tlv(0x03, value)
}
fn encode_octet_string(bytes: &[u8]) -> Vec<u8> {
encode_tlv(0x04, bytes.to_vec())
}
fn encode_sequence(elements: &[Vec<u8>]) -> Vec<u8> {
let total_len: usize = elements.iter().map(Vec::len).sum();
let mut buf = Vec::with_capacity(total_len);
for element in elements {
buf.extend_from_slice(element);
}
encode_tlv(0x30, buf)
}
fn encode_tlv(tag: u8, value: Vec<u8>) -> Vec<u8> {
let mut out = Vec::with_capacity(1 + 9 + value.len());
out.push(tag);
encode_length(value.len(), &mut out);
out.extend_from_slice(&value);
out
}
fn encode_length(len: usize, out: &mut Vec<u8>) {
if len < 0x80 {
out.push(len as u8);
return;
}
let mut bytes = Vec::new();
let mut value = len;
while value > 0 {
bytes.push((value & 0xFF) as u8);
value >>= 8;
}
bytes.reverse();
out.push(0x80 | (bytes.len() as u8));
out.extend_from_slice(&bytes);
}
#[cfg(test)]
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::model::{CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation};
use crate::data_model::roa::{IpPrefix, RoaAfi};
use crate::data_model::ta::TrustAnchor;
use crate::data_model::tal::Tal;
fn sample_vrp_v4(asn: u32, a: [u8; 4], prefix_len: u16, max_length: u16) -> Vrp {
Vrp {
asn,
prefix: IpPrefix {
afi: RoaAfi::Ipv4,
prefix_len,
addr: [a[0], a[1], a[2], a[3], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
},
max_length,
}
}
fn sample_vrp_v6(asn: u32, a: [u8; 16], prefix_len: u16, max_length: u16) -> Vrp {
Vrp {
asn,
prefix: IpPrefix {
afi: RoaAfi::Ipv6,
prefix_len,
addr: a,
},
max_length,
}
}
fn decode_family_block(block: &[u8]) -> (u16, Vec<(u8, Vec<u8>, Option<u8>)>) {
let mut top = crate::data_model::common::DerReader::new(block);
let mut seq = top.take_sequence().expect("family seq");
assert!(top.is_empty());
let afi_bytes = seq.take_octet_string().expect("afi octet string");
let afi = u16::from_be_bytes([afi_bytes[0], afi_bytes[1]]);
let mut addrs = seq.take_sequence().expect("addresses seq");
let mut entries = Vec::new();
while !addrs.is_empty() {
let mut addr_seq = addrs.take_sequence().expect("roa ip addr seq");
let (unused_bits, content) = addr_seq.take_bit_string().expect("bit string");
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().expect("maxlen") as u8)
};
entries.push((prefix_len, content.to_vec(), max_len));
}
(afi, entries)
}
fn sample_trust_anchor(path_tal: &str, path_ta: &str) -> TrustAnchor {
let base = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let tal_bytes = std::fs::read(base.join(path_tal)).expect("read tal");
let ta_der = std::fs::read(base.join(path_ta)).expect("read ta");
let tal = Tal::decode_bytes(&tal_bytes).expect("decode tal");
TrustAnchor::bind_der(tal, &ta_der, None).expect("bind ta")
}
fn sample_router_cert(asns: Vec<u32>, ski_fill: u8, spki_fill: u8) -> BgpsecRouterCertificate {
let ta = sample_trust_anchor(
"tests/fixtures/tal/apnic-rfc7730-https.tal",
"tests/fixtures/ta/apnic-ta.cer",
);
BgpsecRouterCertificate {
raw_der: vec![0x01, 0x02],
resource_cert: ta.ta_certificate.rc_ca,
subject_key_identifier: vec![ski_fill; 20],
spki_der: vec![0x30, 0x03, 0x03, 0x01, spki_fill],
asns,
}
}
fn sample_manifest_vcir(
manifest_uri: &str,
manifest_der: &[u8],
child_ski_hex: &str,
) -> ValidatedCaInstanceResult {
let manifest = ManifestObject::decode_der(manifest_der).expect("decode manifest fixture");
let hash = hex::encode(sha2::Sha256::digest(manifest_der));
let now = manifest.manifest.this_update;
let next = manifest.manifest.next_update;
crate::storage::ValidatedCaInstanceResult {
manifest_rsync_uri: manifest_uri.to_string(),
parent_manifest_rsync_uri: None,
tal_id: "test-tal".to_string(),
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),
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),
},
instance_gate: crate::storage::VcirInstanceGate {
manifest_next_update: crate::storage::PackTime::from_utc_offset_datetime(next),
current_crl_next_update: crate::storage::PackTime::from_utc_offset_datetime(next),
self_ca_not_after: crate::storage::PackTime::from_utc_offset_datetime(next),
instance_effective_until: crate::storage::PackTime::from_utc_offset_datetime(next),
},
child_entries: vec![crate::storage::VcirChildEntry {
child_manifest_rsync_uri: format!("{manifest_uri}/child.mft"),
child_cert_rsync_uri: format!("{manifest_uri}/child.cer"),
child_cert_hash: "aa".repeat(32),
child_ski: child_ski_hex.to_string(),
child_rsync_base_uri: format!("{manifest_uri}/"),
child_publication_point_rsync_uri: format!("{manifest_uri}/"),
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),
}],
local_outputs: Vec::new(),
related_artifacts: vec![crate::storage::VcirRelatedArtifact {
artifact_role: crate::storage::VcirArtifactRole::Manifest,
artifact_kind: crate::storage::VcirArtifactKind::Mft,
uri: Some(manifest_uri.to_string()),
sha256: hash,
object_type: Some("mft".to_string()),
validation_status: crate::storage::VcirArtifactValidationStatus::Accepted,
}],
summary: crate::storage::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: crate::storage::VcirAuditSummary {
failed_fetch_eligible: true,
last_failed_fetch_reason: None,
warning_count: 0,
audit_flags: Vec::new(),
},
}
}
#[test]
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 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());
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");
assert_eq!(state.mis.len(), 2);
assert!(state.mis[0].hash < state.mis[1].hash);
let expected_subordinates = [
hex::decode(vcir_a.child_entries[0].child_ski.clone()).unwrap(),
hex::decode(vcir_b.child_entries[0].child_ski.clone()).unwrap(),
];
let actual_subordinates = state
.mis
.iter()
.map(|mi| {
assert_eq!(mi.subordinates.len(), 1);
assert!(!mi.locations.is_empty());
mi.subordinates[0].clone()
})
.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");
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(),
]
.into_iter()
.max()
.unwrap();
assert_eq!(state.most_recent_update, max_time);
}
#[test]
fn build_manifest_state_from_vcirs_empty_uses_epoch() {
let store_dir = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
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");
assert!(crate::ccr::verify_state_hash(&state.hash, &payload_der));
}
#[test]
fn build_router_key_state_groups_dedupes_and_sorts() {
let certs = vec![
sample_router_cert(vec![64497], 0x21, 0xA1),
sample_router_cert(vec![64496, 64497], 0x11, 0xA0),
sample_router_cert(vec![64496], 0x11, 0xA0),
sample_router_cert(vec![64496], 0x11, 0xA2),
];
let state = build_router_key_state(&certs).expect("build router key state");
assert_eq!(state.rksets.len(), 2);
assert_eq!(state.rksets[0].as_id, 64496);
assert_eq!(state.rksets[1].as_id, 64497);
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");
assert!(crate::ccr::verify_state_hash(&state.hash, &payload_der));
}
#[test]
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");
assert!(crate::ccr::verify_state_hash(&state.hash, &payload_der));
}
#[test]
fn build_roa_payload_state_groups_dedupes_and_sorts() {
let vrps = vec![
sample_vrp_v4(64497, [10, 1, 0, 0], 16, 16),
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),
];
let state = build_roa_payload_state(&vrps).expect("build roa state");
assert_eq!(state.rps.len(), 2);
assert_eq!(state.rps[0].as_id, 64496);
assert_eq!(state.rps[1].as_id, 64497);
assert_eq!(state.rps[0].ip_addr_blocks.len(), 2);
let (afi4, entries4) = decode_family_block(&state.rps[0].ip_addr_blocks[0]);
let (afi6, entries6) = decode_family_block(&state.rps[0].ip_addr_blocks[1]);
assert_eq!(afi4, 1);
assert_eq!(afi6, 2);
assert_eq!(entries4.len(), 2);
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)));
let payload_der = encode_roa_payload_state_payload_der(&state.rps).expect("encode payload");
assert!(crate::ccr::verify_state_hash(&state.hash, &payload_der));
}
#[test]
fn build_roa_payload_state_empty_is_valid_and_hashes_empty_sequence() {
let state = build_roa_payload_state(&[]).expect("empty roa state");
assert!(state.rps.is_empty());
let payload_der = encode_roa_payload_state_payload_der(&state.rps).expect("encode payload");
assert!(crate::ccr::verify_state_hash(&state.hash, &payload_der));
}
#[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] },
];
let state = build_aspa_payload_state(&aspas).expect("build aspa state");
assert_eq!(state.aps.len(), 2);
assert_eq!(state.aps[0].customer_as_id, 64496);
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 {
version: 0,
hash_alg: CcrDigestAlgorithm::Sha256,
produced_at: time::OffsetDateTime::now_utc(),
mfts: None,
vrps: None,
vaps: Some(state.clone()),
tas: None,
rks: None,
},
))
.expect("encode ccr")).expect("decode ccr");
assert_eq!(decoded.content.vaps, Some(state));
assert!(!encoded.is_empty());
}
#[test]
fn build_trust_anchor_state_collects_sorted_unique_skis() {
let apnic = sample_trust_anchor(
"tests/fixtures/tal/apnic-rfc7730-https.tal",
"tests/fixtures/ta/apnic-ta.cer",
);
let arin = sample_trust_anchor(
"tests/fixtures/tal/arin.tal",
"tests/fixtures/ta/arin-ta.cer",
);
let state = build_trust_anchor_state(&[apnic.clone(), arin.clone(), apnic])
.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");
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());
}
#[test]
fn build_trust_anchor_state_rejects_empty_and_missing_ski() {
let err = build_trust_anchor_state(&[]).expect_err("empty TAs must fail");
assert!(err.to_string().contains("trust anchor set"), "{err}");
let mut ta = sample_trust_anchor(
"tests/fixtures/tal/apnic-rfc7730-https.tal",
"tests/fixtures/ta/apnic-ta.cer",
);
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}");
}
}

382
src/ccr/decode.rs Normal file
View File

@@ -0,0 +1,382 @@
use crate::ccr::model::{
AspaPayloadSet, AspaPayloadState, CCR_VERSION_V0, CcrContentInfo, CcrDigestAlgorithm,
ManifestInstance, ManifestState, RoaPayloadSet, RoaPayloadState, RouterKey, RouterKeySet,
RouterKeyState, RpkiCanonicalCacheRepresentation, TrustAnchorState,
};
use crate::data_model::common::{BigUnsigned, DerReader};
use crate::data_model::oid::{OID_CT_RPKI_CCR, OID_CT_RPKI_CCR_RAW, OID_SHA256, OID_SHA256_RAW};
use der_parser::der::parse_der_oid;
#[derive(Debug, thiserror::Error)]
pub enum CcrDecodeError {
#[error("DER parse error: {0}")]
Parse(String),
#[error("unexpected contentType OID: expected {expected}, got {actual}")]
UnexpectedContentType { expected: &'static str, actual: String },
#[error("unexpected digest algorithm OID: expected {expected}, got {actual}")]
UnexpectedDigestAlgorithm { expected: &'static str, actual: String },
#[error("CCR model validation failed after decode: {0}")]
Validate(String),
}
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()));
}
let content_type_raw = seq.take_tag(0x06).map_err(CcrDecodeError::Parse)?;
if content_type_raw != OID_CT_RPKI_CCR_RAW {
return Err(CcrDecodeError::UnexpectedContentType {
expected: OID_CT_RPKI_CCR,
actual: oid_string(content_type_raw)?,
});
}
let inner = seq.take_tag(0xA0).map_err(CcrDecodeError::Parse)?;
if !seq.is_empty() {
return Err(CcrDecodeError::Parse("trailing fields in ContentInfo".into()));
}
let content = decode_ccr(inner)?;
let ci = CcrContentInfo::new(content);
ci.validate().map_err(CcrDecodeError::Validate)?;
Ok(ci)
}
pub fn decode_ccr(der: &[u8]) -> Result<RpkiCanonicalCacheRepresentation, 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 CCR".into()));
}
let version = if !seq.is_empty() && seq.peek_tag().map_err(CcrDecodeError::Parse)? == 0xA0 {
let explicit = seq.take_tag(0xA0).map_err(CcrDecodeError::Parse)?;
let mut inner = DerReader::new(explicit);
let version = inner.take_uint_u64().map_err(CcrDecodeError::Parse)? as u32;
if !inner.is_empty() {
return Err(CcrDecodeError::Parse(
"trailing bytes inside CCR version EXPLICIT".into(),
));
}
version
} else {
CCR_VERSION_V0
};
let hash_alg = decode_digest_algorithm(seq.take_sequence().map_err(CcrDecodeError::Parse)?)?;
let produced_at = parse_generalized_time(seq.take_tag(0x18).map_err(CcrDecodeError::Parse)?)?;
let mut mfts = None;
let mut vrps = None;
let mut vaps = None;
let mut tas = None;
let mut rks = None;
while !seq.is_empty() {
let tag = seq.peek_tag().map_err(CcrDecodeError::Parse)?;
let (tag_read, value) = seq.take_any().map_err(CcrDecodeError::Parse)?;
debug_assert_eq!(tag, tag_read);
match tag {
0xA1 => mfts = Some(decode_manifest_state(value)?),
0xA2 => vrps = Some(decode_roa_payload_state(value)?),
0xA3 => vaps = Some(decode_aspa_payload_state(value)?),
0xA4 => tas = Some(decode_trust_anchor_state(value)?),
0xA5 => rks = Some(decode_router_key_state(value)?),
_ => {
return Err(CcrDecodeError::Parse(format!(
"unexpected CCR field tag 0x{tag:02X}"
)))
}
}
}
let ccr = RpkiCanonicalCacheRepresentation {
version,
hash_alg,
produced_at,
mfts,
vrps,
vaps,
tas,
rks,
};
ccr.validate().map_err(CcrDecodeError::Validate)?;
Ok(ccr)
}
fn decode_manifest_state(explicit_der: &[u8]) -> Result<ManifestState, 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 ManifestState".into()));
}
let mis_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
let mut mis_reader = DerReader::new(mis_der);
let mut mis = Vec::new();
while !mis_reader.is_empty() {
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();
if !seq.is_empty() {
return Err(CcrDecodeError::Parse("trailing fields in ManifestState".into()));
}
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()));
}
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 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)?;
locations.push(full.to_vec());
}
let subordinates = if !seq.is_empty() {
let subordinate_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
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
} else {
Vec::new()
};
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()));
}
let payload_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
let mut reader = DerReader::new(payload_der);
let mut rps = Vec::new();
while !reader.is_empty() {
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();
Ok(RoaPayloadState { rps, hash })
}
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()));
}
let as_id = seq.take_uint_u64().map_err(CcrDecodeError::Parse)? as u32;
let blocks_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
let mut reader = DerReader::new(blocks_der);
let mut ip_addr_blocks = Vec::new();
while !reader.is_empty() {
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 })
}
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()));
}
let payload_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
let mut reader = DerReader::new(payload_der);
let mut aps = Vec::new();
while !reader.is_empty() {
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();
Ok(AspaPayloadState { aps, hash })
}
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()));
}
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)?;
let mut reader = DerReader::new(providers_der);
let mut providers = Vec::new();
while !reader.is_empty() {
providers.push(reader.take_uint_u64().map_err(CcrDecodeError::Parse)? as u32);
}
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()));
}
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());
}
let hash = seq.take_octet_string().map_err(CcrDecodeError::Parse)?.to_vec();
Ok(TrustAnchorState { skis, hash })
}
fn decode_router_key_state(explicit_der: &[u8]) -> Result<RouterKeyState, 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 RouterKeyState".into()));
}
let sets_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
let mut reader = DerReader::new(sets_der);
let mut rksets = Vec::new();
while !reader.is_empty() {
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();
Ok(RouterKeyState { rksets, hash })
}
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()));
}
let as_id = seq.take_uint_u64().map_err(CcrDecodeError::Parse)? as u32;
let keys_der = seq.take_tag(0x30).map_err(CcrDecodeError::Parse)?;
let mut reader = DerReader::new(keys_der);
let mut router_keys = Vec::new();
while !reader.is_empty() {
let (_tag, full, _value) = reader.take_any_full().map_err(CcrDecodeError::Parse)?;
router_keys.push(decode_router_key(full)?);
}
Ok(RouterKeySet { as_id, router_keys })
}
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()));
}
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() })
}
fn decode_digest_algorithm(mut seq: DerReader<'_>) -> Result<CcrDigestAlgorithm, CcrDecodeError> {
let oid_raw = seq.take_tag(0x06).map_err(CcrDecodeError::Parse)?;
if oid_raw != OID_SHA256_RAW {
return Err(CcrDecodeError::UnexpectedDigestAlgorithm {
expected: OID_SHA256,
actual: oid_string(oid_raw)?,
});
}
if !seq.is_empty() {
let tag = seq.peek_tag().map_err(CcrDecodeError::Parse)?;
if tag == 0x05 {
let null = seq.take_tag(0x05).map_err(CcrDecodeError::Parse)?;
if !null.is_empty() {
return Err(CcrDecodeError::Parse(
"AlgorithmIdentifier NULL parameters must be empty".into(),
));
}
}
}
if !seq.is_empty() {
return Err(CcrDecodeError::Parse(
"trailing fields in DigestAlgorithmIdentifier".into(),
));
}
Ok(CcrDigestAlgorithm::Sha256)
}
fn oid_string(raw_body: &[u8]) -> Result<String, CcrDecodeError> {
let der = {
let mut out = Vec::with_capacity(raw_body.len() + 2);
out.push(0x06);
if raw_body.len() < 0x80 {
out.push(raw_body.len() as u8);
} else {
return Err(CcrDecodeError::Parse("OID too long".into()));
}
out.extend_from_slice(raw_body);
out
};
let (_rem, oid) = parse_der_oid(&der).map_err(|e| CcrDecodeError::Parse(e.to_string()))?;
let oid = oid
.as_oid_val()
.map_err(|e| CcrDecodeError::Parse(e.to_string()))?;
Ok(oid.to_string())
}
fn parse_generalized_time(bytes: &[u8]) -> Result<time::OffsetDateTime, CcrDecodeError> {
let s = std::str::from_utf8(bytes).map_err(|e| CcrDecodeError::Parse(e.to_string()))?;
if s.len() != 15 || !s.ends_with('Z') {
return Err(CcrDecodeError::Parse(
"GeneralizedTime must be YYYYMMDDHHMMSSZ".into(),
));
}
let parse = |range: std::ops::Range<usize>| -> Result<u32, CcrDecodeError> {
s[range]
.parse::<u32>()
.map_err(|e| CcrDecodeError::Parse(e.to_string()))
};
let year = parse(0..4)? as i32;
let month = parse(4..6)? as u8;
let day = parse(6..8)? as u8;
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 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)
.map_err(|e| CcrDecodeError::Parse(e.to_string()))?;
Ok(time::PrimitiveDateTime::new(date, timev).assume_utc())
}
fn decode_big_unsigned(bytes: &[u8]) -> Result<BigUnsigned, CcrDecodeError> {
if bytes.is_empty() {
return Err(CcrDecodeError::Parse("INTEGER has empty content".into()));
}
if bytes[0] & 0x80 != 0 {
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()));
}
let bytes_be = if bytes.len() > 1 && bytes[0] == 0x00 {
bytes[1..].to_vec()
} else {
bytes.to_vec()
};
Ok(BigUnsigned { bytes_be })
}

88
src/ccr/dump.rs Normal file
View File

@@ -0,0 +1,88 @@
use crate::ccr::decode::{CcrDecodeError, decode_content_info};
use serde_json::json;
#[derive(Debug, thiserror::Error)]
pub enum CcrDumpError {
#[error("CCR decode failed: {0}")]
Decode(#[from] CcrDecodeError),
#[error("format time failed: {0}")]
FormatTime(String),
}
pub fn dump_content_info_json_value(der: &[u8]) -> Result<serde_json::Value, CcrDumpError> {
let content_info = decode_content_info(der)?;
dump_content_info_json(&content_info)
}
pub fn dump_content_info_json(
content_info: &crate::ccr::model::CcrContentInfo,
) -> Result<serde_json::Value, CcrDumpError> {
let produced_at = content_info
.content
.produced_at
.to_offset(time::UtcOffset::UTC)
.format(&time::format_description::well_known::Rfc3339)
.map_err(|e| CcrDumpError::FormatTime(e.to_string()))?;
let mfts = content_info.content.mfts.as_ref().map(|state| {
json!({
"present": true,
"manifest_instances": state.mis.len(),
"most_recent_update_rfc3339_utc": state.most_recent_update.to_offset(time::UtcOffset::UTC).format(&time::format_description::well_known::Rfc3339).unwrap(),
"hash_hex": hex::encode(&state.hash),
})
}).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,
})
}).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>(),
})
}).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),
})
}).unwrap_or_else(|| json!({"present": false}));
let rks = content_info.content.rks.as_ref().map(|state| {
json!({
"present": true,
"router_key_sets": state.rksets.len(),
"router_key_count": state.rksets.iter().map(|set| set.router_keys.len()).sum::<usize>(),
"hash_hex": hex::encode(&state.hash),
})
}).unwrap_or_else(|| json!({"present": false}));
Ok(json!({
"content_type_oid": content_info.content_type_oid,
"version": content_info.content.version,
"hash_alg": content_info.content.hash_alg.oid(),
"produced_at_rfc3339_utc": produced_at,
"state_aspects": {
"mfts": mfts,
"vrps": vrps,
"vaps": vaps,
"tas": tas,
"rks": rks,
}
}))
}

304
src/ccr/encode.rs Normal file
View File

@@ -0,0 +1,304 @@
use crate::ccr::model::{
AspaPayloadSet, AspaPayloadState, CCR_VERSION_V0, CcrContentInfo, CcrDigestAlgorithm,
ManifestInstance, ManifestState, RoaPayloadSet, RoaPayloadState, RouterKey, RouterKeySet,
RouterKeyState, RpkiCanonicalCacheRepresentation, TrustAnchorState,
};
use crate::data_model::common::BigUnsigned;
use crate::data_model::oid::{OID_CT_RPKI_CCR_RAW, OID_SHA256_RAW};
#[derive(Debug, thiserror::Error)]
pub enum CcrEncodeError {
#[error("CCR model validation failed: {0}")]
Validate(String),
#[error("GeneralizedTime formatting failed: {0}")]
ProducedAtFormat(String),
}
pub fn encode_content_info(content_info: &CcrContentInfo) -> Result<Vec<u8>, CcrEncodeError> {
content_info.validate().map_err(CcrEncodeError::Validate)?;
let content_der = encode_ccr(&content_info.content)?;
Ok(encode_sequence(&[
encode_oid(OID_CT_RPKI_CCR_RAW),
encode_explicit(0, &content_der),
]))
}
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 {
fields.push(encode_explicit(0, &encode_integer_u32(ccr.version)));
}
fields.push(encode_digest_algorithm(&ccr.hash_alg));
fields.push(encode_generalized_time(ccr.produced_at)?);
if let Some(mfts) = &ccr.mfts {
fields.push(encode_explicit(1, &encode_manifest_state(mfts)?));
}
if let Some(vrps) = &ccr.vrps {
fields.push(encode_explicit(2, &encode_roa_payload_state(vrps)?));
}
if let Some(vaps) = &ccr.vaps {
fields.push(encode_explicit(3, &encode_aspa_payload_state(vaps)?));
}
if let Some(tas) = &ccr.tas {
fields.push(encode_explicit(4, &encode_trust_anchor_state(tas)?));
}
if let Some(rks) = &ccr.rks {
fields.push(encode_explicit(5, &encode_router_key_state(rks)?));
}
Ok(encode_sequence(&fields))
}
pub fn encode_manifest_state(state: &ManifestState) -> Result<Vec<u8>, CcrEncodeError> {
state.validate().map_err(CcrEncodeError::Validate)?;
let mis = encode_manifest_state_payload_der(&state.mis)?;
Ok(encode_sequence(&[
mis,
encode_generalized_time(state.most_recent_update)?,
encode_octet_string(&state.hash),
]))
}
pub fn encode_manifest_state_payload_der(
instances: &[ManifestInstance],
) -> Result<Vec<u8>, CcrEncodeError> {
Ok(encode_sequence(
&instances
.iter()
.map(encode_manifest_instance)
.collect::<Result<Vec<_>, _>>()?,
))
}
fn encode_manifest_instance(instance: &ManifestInstance) -> Result<Vec<u8>, CcrEncodeError> {
instance.validate().map_err(CcrEncodeError::Validate)?;
let mut fields = vec![
encode_octet_string(&instance.hash),
encode_integer_u64(instance.size),
encode_octet_string(&instance.aki),
encode_integer_bigunsigned(&instance.manifest_number),
encode_generalized_time(instance.this_update)?,
encode_sequence(&instance.locations),
];
if !instance.subordinates.is_empty() {
fields.push(encode_sequence(
&instance
.subordinates
.iter()
.map(|ski| encode_octet_string(ski))
.collect::<Vec<_>>(),
));
}
Ok(encode_sequence(&fields))
}
pub fn encode_roa_payload_state(state: &RoaPayloadState) -> Result<Vec<u8>, CcrEncodeError> {
state.validate().map_err(CcrEncodeError::Validate)?;
let rps = encode_roa_payload_state_payload_der(&state.rps)?;
Ok(encode_sequence(&[rps, encode_octet_string(&state.hash)]))
}
pub fn encode_roa_payload_state_payload_der(
sets: &[RoaPayloadSet],
) -> Result<Vec<u8>, CcrEncodeError> {
Ok(encode_sequence(
&sets
.iter()
.map(encode_roa_payload_set)
.collect::<Result<Vec<_>, _>>()?,
))
}
fn encode_roa_payload_set(set: &RoaPayloadSet) -> Result<Vec<u8>, CcrEncodeError> {
set.validate().map_err(CcrEncodeError::Validate)?;
Ok(encode_sequence(&[
encode_integer_u32(set.as_id),
encode_sequence(&set.ip_addr_blocks),
]))
}
pub fn encode_aspa_payload_state(state: &AspaPayloadState) -> Result<Vec<u8>, CcrEncodeError> {
state.validate().map_err(CcrEncodeError::Validate)?;
let aps = encode_aspa_payload_state_payload_der(&state.aps)?;
Ok(encode_sequence(&[aps, encode_octet_string(&state.hash)]))
}
pub fn encode_aspa_payload_state_payload_der(
sets: &[AspaPayloadSet],
) -> Result<Vec<u8>, CcrEncodeError> {
Ok(encode_sequence(
&sets
.iter()
.map(encode_aspa_payload_set)
.collect::<Result<Vec<_>, _>>()?,
))
}
fn encode_aspa_payload_set(set: &AspaPayloadSet) -> Result<Vec<u8>, CcrEncodeError> {
set.validate().map_err(CcrEncodeError::Validate)?;
Ok(encode_sequence(&[
encode_integer_u32(set.customer_as_id),
encode_sequence(
&set.providers
.iter()
.map(|provider| encode_integer_u32(*provider))
.collect::<Vec<_>>(),
),
]))
}
pub fn encode_trust_anchor_state(state: &TrustAnchorState) -> Result<Vec<u8>, CcrEncodeError> {
state.validate().map_err(CcrEncodeError::Validate)?;
let skis = encode_trust_anchor_state_payload_der(&state.skis)?;
Ok(encode_sequence(&[skis, encode_octet_string(&state.hash)]))
}
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<_>>(),
))
}
pub fn encode_router_key_state(state: &RouterKeyState) -> Result<Vec<u8>, CcrEncodeError> {
state.validate().map_err(CcrEncodeError::Validate)?;
let rksets = encode_router_key_state_payload_der(&state.rksets)?;
Ok(encode_sequence(&[rksets, encode_octet_string(&state.hash)]))
}
pub fn encode_router_key_state_payload_der(
sets: &[RouterKeySet],
) -> Result<Vec<u8>, CcrEncodeError> {
Ok(encode_sequence(
&sets
.iter()
.map(encode_router_key_set)
.collect::<Result<Vec<_>, _>>()?,
))
}
fn encode_router_key_set(set: &RouterKeySet) -> Result<Vec<u8>, CcrEncodeError> {
set.validate().map_err(CcrEncodeError::Validate)?;
Ok(encode_sequence(&[
encode_integer_u32(set.as_id),
encode_sequence(
&set.router_keys
.iter()
.map(encode_router_key)
.collect::<Result<Vec<_>, _>>()?,
),
]))
}
fn encode_router_key(key: &RouterKey) -> Result<Vec<u8>, CcrEncodeError> {
key.validate().map_err(CcrEncodeError::Validate)?;
Ok(encode_sequence(&[
encode_octet_string(&key.ski),
key.spki_der.clone(),
]))
}
fn encode_digest_algorithm(alg: &CcrDigestAlgorithm) -> Vec<u8> {
match alg {
CcrDigestAlgorithm::Sha256 => encode_sequence(&[encode_oid(OID_SHA256_RAW)]),
}
}
fn encode_generalized_time(t: time::OffsetDateTime) -> Result<Vec<u8>, CcrEncodeError> {
let t = t.to_offset(time::UtcOffset::UTC);
let s = format!(
"{:04}{:02}{:02}{:02}{:02}{:02}Z",
t.year(),
u8::from(t.month()),
t.day(),
t.hour(),
t.minute(),
t.second()
);
Ok(encode_tlv(0x18, s.into_bytes()))
}
fn encode_integer_u32(v: u32) -> Vec<u8> {
encode_integer_bytes(unsigned_integer_bytes(v as u64))
}
fn encode_integer_u64(v: u64) -> Vec<u8> {
encode_integer_bytes(unsigned_integer_bytes(v))
}
fn encode_integer_bigunsigned(v: &BigUnsigned) -> Vec<u8> {
encode_integer_bytes(v.bytes_be.clone())
}
fn encode_integer_bytes(mut bytes: Vec<u8>) -> Vec<u8> {
if bytes.is_empty() {
bytes.push(0);
}
if bytes[0] & 0x80 != 0 {
bytes.insert(0, 0);
}
encode_tlv(0x02, bytes)
}
fn unsigned_integer_bytes(v: u64) -> Vec<u8> {
if v == 0 {
return vec![0];
}
let mut out = Vec::new();
let mut n = v;
while n > 0 {
out.push((n & 0xFF) as u8);
n >>= 8;
}
out.reverse();
out
}
fn encode_oid(raw_body: &[u8]) -> Vec<u8> {
encode_tlv(0x06, raw_body.to_vec())
}
fn encode_octet_string(bytes: &[u8]) -> Vec<u8> {
encode_tlv(0x04, bytes.to_vec())
}
fn encode_explicit(tag_number: u8, inner_der: &[u8]) -> Vec<u8> {
encode_tlv(0xA0 + tag_number, inner_der.to_vec())
}
fn encode_sequence(elements: &[Vec<u8>]) -> Vec<u8> {
let total_len: usize = elements.iter().map(Vec::len).sum();
let mut buf = Vec::with_capacity(total_len);
for element in elements {
buf.extend_from_slice(element);
}
encode_tlv(0x30, buf)
}
fn encode_tlv(tag: u8, value: Vec<u8>) -> Vec<u8> {
let mut out = Vec::with_capacity(1 + 9 + value.len());
out.push(tag);
encode_length(value.len(), &mut out);
out.extend_from_slice(&value);
out
}
fn encode_length(len: usize, out: &mut Vec<u8>) {
if len < 0x80 {
out.push(len as u8);
return;
}
let mut bytes = Vec::new();
let mut value = len;
while value > 0 {
bytes.push((value & 0xFF) as u8);
value >>= 8;
}
bytes.reverse();
out.push(0x80 | (bytes.len() as u8));
out.extend_from_slice(&bytes);
}

234
src/ccr/export.rs Normal file
View File

@@ -0,0 +1,234 @@
use crate::ccr::build::{
CcrBuildError, build_aspa_payload_state, build_manifest_state_from_vcirs,
build_roa_payload_state, build_trust_anchor_state,
};
use crate::ccr::encode::{CcrEncodeError, encode_content_info};
use crate::ccr::model::{CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation};
use crate::data_model::ta::TrustAnchor;
use crate::storage::RocksStore;
use crate::validation::objects::{AspaAttestation, RouterKeyPayload, Vrp};
use std::path::Path;
#[derive(Debug, thiserror::Error)]
pub enum CcrExportError {
#[error("list VCIRs failed: {0}")]
ListVcirs(String),
#[error("build CCR state failed: {0}")]
Build(#[from] CcrBuildError),
#[error("encode CCR failed: {0}")]
Encode(#[from] CcrEncodeError),
#[error("write CCR file failed: {0}: {1}")]
Write(String, String),
}
pub fn build_ccr_from_run(
store: &RocksStore,
trust_anchors: &[TrustAnchor],
vrps: &[Vrp],
aspas: &[AspaAttestation],
router_keys: &[RouterKeyPayload],
produced_at: time::OffsetDateTime,
) -> Result<RpkiCanonicalCacheRepresentation, CcrExportError> {
let vcirs = store
.list_vcirs()
.map_err(|e| CcrExportError::ListVcirs(e.to_string()))?;
let mfts = Some(build_manifest_state_from_vcirs(store, &vcirs)?);
let vrps = Some(build_roa_payload_state(vrps)?);
let vaps = Some(build_aspa_payload_state(aspas)?);
let tas = Some(build_trust_anchor_state(trust_anchors)?);
let rks = Some(build_router_key_state_from_runtime(router_keys)?);
Ok(RpkiCanonicalCacheRepresentation {
version: 0,
hash_alg: CcrDigestAlgorithm::Sha256,
produced_at,
mfts,
vrps,
vaps,
tas,
rks,
})
}
fn build_router_key_state_from_runtime(
router_keys: &[RouterKeyPayload],
) -> Result<crate::ccr::model::RouterKeyState, CcrBuildError> {
use crate::ccr::model::{RouterKey, RouterKeySet};
use std::collections::{BTreeMap, BTreeSet};
let mut grouped: BTreeMap<u32, BTreeSet<RouterKey>> = BTreeMap::new();
for router_key in router_keys {
grouped
.entry(router_key.as_id)
.or_default()
.insert(RouterKey {
ski: router_key.ski.clone(),
spki_der: router_key.spki_der.clone(),
});
}
let rksets = grouped
.into_iter()
.map(|(as_id, router_keys)| RouterKeySet {
as_id,
router_keys: router_keys.into_iter().collect(),
})
.collect::<Vec<_>>();
build_router_key_state_from_sets(&rksets)
}
fn build_router_key_state_from_sets(
rksets: &[crate::ccr::model::RouterKeySet],
) -> Result<crate::ccr::model::RouterKeyState, CcrBuildError> {
let der = crate::ccr::encode::encode_router_key_state_payload_der(rksets)
.map_err(|e| CcrBuildError::RouterKeyEncode(e.to_string()))?;
Ok(crate::ccr::model::RouterKeyState {
rksets: rksets.to_vec(),
hash: crate::ccr::compute_state_hash(&der),
})
}
pub fn write_ccr_file(
path: &Path,
ccr: &RpkiCanonicalCacheRepresentation,
) -> Result<(), CcrExportError> {
let der = encode_content_info(&CcrContentInfo::new(ccr.clone()))?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| CcrExportError::Write(path.display().to_string(), e.to_string()))?;
}
std::fs::write(path, der)
.map_err(|e| CcrExportError::Write(path.display().to_string(), e.to_string()))
}
#[cfg(test)]
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 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 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")
}
fn sample_vcir_and_raw(store: &RocksStore) -> ValidatedCaInstanceResult {
let base = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let manifest_der = std::fs::read(base.join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft")).expect("read manifest");
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.object_type = Some("mft".to_string());
raw.encoding = Some("der".to_string());
store.put_raw_by_hash_entry(&raw).expect("put raw");
ValidatedCaInstanceResult {
manifest_rsync_uri: "rsync://example.test/repo/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(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),
},
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),
},
child_entries: vec![VcirChildEntry {
child_manifest_rsync_uri: "rsync://example.test/repo/child.mft".to_string(),
child_cert_rsync_uri: "rsync://example.test/repo/child.cer".to_string(),
child_cert_hash: "aa".repeat(32),
child_ski: "33".repeat(20),
child_rsync_base_uri: "rsync://example.test/repo/".to_string(),
child_publication_point_rsync_uri: "rsync://example.test/repo/".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(manifest.manifest.this_update),
}],
local_outputs: Vec::new(),
related_artifacts: vec![VcirRelatedArtifact {
artifact_role: VcirArtifactRole::Manifest,
artifact_kind: VcirArtifactKind::Mft,
uri: Some("rsync://example.test/repo/current.mft".to_string()),
sha256: hash,
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(),
},
}
}
#[test]
fn build_and_write_ccr_from_run_exports_der_content_info() {
let td = tempfile::tempdir().expect("tempdir");
let db_path = td.path().join("db");
let store = RocksStore::open(&db_path).expect("open rocksdb");
let vcir = sample_vcir_and_raw(&store);
store.put_vcir(&vcir).expect("put vcir");
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] },
max_length: 8,
}];
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],
spki_der: vec![0x30, 0x00],
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)),
}];
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());
assert!(ccr.tas.is_some());
assert!(ccr.rks.is_some());
assert_eq!(ccr.rks.as_ref().unwrap().rksets.len(), 1);
let out = td.path().join("out/example.ccr");
write_ccr_file(&out, &ccr).expect("write ccr");
let der = std::fs::read(&out).expect("read ccr file");
let decoded = decode_content_info(&der).expect("decode ccr");
assert_eq!(decoded.content.version, 0);
assert!(decoded.content.mfts.is_some());
}
}

9
src/ccr/hash.rs Normal file
View File

@@ -0,0 +1,9 @@
use sha2::Digest;
pub fn compute_state_hash(payload_der: &[u8]) -> Vec<u8> {
sha2::Sha256::digest(payload_der).to_vec()
}
pub fn verify_state_hash(expected: &[u8], payload_der: &[u8]) -> bool {
compute_state_hash(payload_der).as_slice() == expected
}

25
src/ccr/mod.rs Normal file
View File

@@ -0,0 +1,25 @@
pub mod build;
pub mod decode;
pub mod encode;
pub mod export;
pub mod verify;
pub mod dump;
pub mod hash;
pub mod model;
pub use build::{
CcrBuildError, build_aspa_payload_state, build_manifest_state_from_vcirs,
build_roa_payload_state, build_trust_anchor_state,
};
pub use decode::{CcrDecodeError, decode_content_info};
pub use encode::{CcrEncodeError, encode_content_info};
pub use export::{CcrExportError, build_ccr_from_run, write_ccr_file};
pub use dump::{CcrDumpError, dump_content_info_json, dump_content_info_json_value};
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,
};

395
src/ccr/model.rs Normal file
View File

@@ -0,0 +1,395 @@
use crate::data_model::common::{BigUnsigned, der_take_tlv};
use crate::data_model::oid::{OID_CT_RPKI_CCR, OID_SHA256};
pub const CCR_VERSION_V0: u32 = 0;
pub const DIGEST_LEN_SHA256: usize = 32;
pub const KEY_IDENTIFIER_LEN_SHA1: usize = 20;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CcrDigestAlgorithm {
Sha256,
}
impl CcrDigestAlgorithm {
pub fn oid(&self) -> &'static str {
match self {
Self::Sha256 => OID_SHA256,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CcrContentInfo {
pub content_type_oid: String,
pub content: RpkiCanonicalCacheRepresentation,
}
impl CcrContentInfo {
pub fn new(content: RpkiCanonicalCacheRepresentation) -> Self {
Self {
content_type_oid: OID_CT_RPKI_CCR.to_string(),
content,
}
}
pub fn validate(&self) -> Result<(), String> {
if self.content_type_oid != OID_CT_RPKI_CCR {
return Err(format!(
"contentType must be {OID_CT_RPKI_CCR}, got {}",
self.content_type_oid
));
}
self.content.validate()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RpkiCanonicalCacheRepresentation {
pub version: u32,
pub hash_alg: CcrDigestAlgorithm,
pub produced_at: time::OffsetDateTime,
pub mfts: Option<ManifestState>,
pub vrps: Option<RoaPayloadState>,
pub vaps: Option<AspaPayloadState>,
pub tas: Option<TrustAnchorState>,
pub rks: Option<RouterKeyState>,
}
impl RpkiCanonicalCacheRepresentation {
pub fn validate(&self) -> Result<(), String> {
if self.version != CCR_VERSION_V0 {
return Err(format!("CCR version must be 0, got {}", self.version));
}
if !matches!(self.hash_alg, CcrDigestAlgorithm::Sha256) {
return Err("CCR hashAlg must be SHA-256".into());
}
if self.mfts.is_none()
&& self.vrps.is_none()
&& self.vaps.is_none()
&& self.tas.is_none()
&& self.rks.is_none()
{
return Err("at least one of mfts/vrps/vaps/tas/rks must be present".into());
}
if let Some(mfts) = &self.mfts {
mfts.validate()?;
}
if let Some(vrps) = &self.vrps {
vrps.validate()?;
}
if let Some(vaps) = &self.vaps {
vaps.validate()?;
}
if let Some(tas) = &self.tas {
tas.validate()?;
}
if let Some(rks) = &self.rks {
rks.validate()?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ManifestState {
pub mis: Vec<ManifestInstance>,
pub most_recent_update: time::OffsetDateTime,
pub hash: Vec<u8>,
}
impl ManifestState {
pub fn validate(&self) -> Result<(), String> {
validate_sha256_digest("ManifestState.hash", &self.hash)?;
validate_sorted_unique_by(
&self.mis,
|item| item.hash.as_slice(),
"ManifestState.mis must be sorted by hash and unique",
)?;
for instance in &self.mis {
instance.validate()?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ManifestInstance {
pub hash: Vec<u8>,
pub size: u64,
pub aki: Vec<u8>,
pub manifest_number: BigUnsigned,
pub this_update: time::OffsetDateTime,
pub locations: Vec<Vec<u8>>,
pub subordinates: Vec<Vec<u8>>,
}
impl ManifestInstance {
pub fn validate(&self) -> Result<(), String> {
validate_sha256_digest("ManifestInstance.hash", &self.hash)?;
if self.size < 1000 {
return Err(format!(
"ManifestInstance.size must be >= 1000, got {}",
self.size
));
}
validate_key_identifier("ManifestInstance.aki", &self.aki)?;
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());
}
for location in &self.locations {
validate_full_der_with_tag(
"ManifestInstance.locations[]",
location,
Some(0x30),
)?;
}
if !self.subordinates.is_empty() {
validate_sorted_unique_bytes(
&self.subordinates,
KEY_IDENTIFIER_LEN_SHA1,
"ManifestInstance.subordinates must be sorted/unique 20-byte SKIs",
)?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RoaPayloadState {
pub rps: Vec<RoaPayloadSet>,
pub hash: Vec<u8>,
}
impl RoaPayloadState {
pub fn validate(&self) -> Result<(), String> {
validate_sha256_digest("ROAPayloadState.hash", &self.hash)?;
validate_sorted_unique_by(
&self.rps,
|item| &item.as_id,
"ROAPayloadState.rps must be sorted by asID and unique",
)?;
for set in &self.rps {
set.validate()?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RoaPayloadSet {
pub as_id: u32,
pub ip_addr_blocks: Vec<Vec<u8>>,
}
impl RoaPayloadSet {
pub fn validate(&self) -> Result<(), String> {
if self.ip_addr_blocks.is_empty() || self.ip_addr_blocks.len() > 2 {
return Err(format!(
"ROAPayloadSet.ip_addr_blocks must contain 1..=2 entries, got {}",
self.ip_addr_blocks.len()
));
}
for block in &self.ip_addr_blocks {
validate_full_der_with_tag("ROAPayloadSet.ip_addr_blocks[]", block, Some(0x30))?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AspaPayloadState {
pub aps: Vec<AspaPayloadSet>,
pub hash: Vec<u8>,
}
impl AspaPayloadState {
pub fn validate(&self) -> Result<(), String> {
validate_sha256_digest("ASPAPayloadState.hash", &self.hash)?;
validate_sorted_unique_by(
&self.aps,
|item| &item.customer_as_id,
"ASPAPayloadState.aps must be sorted by customerASID and unique",
)?;
for set in &self.aps {
set.validate()?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AspaPayloadSet {
pub customer_as_id: u32,
pub providers: Vec<u32>,
}
impl AspaPayloadSet {
pub fn validate(&self) -> Result<(), String> {
if self.providers.is_empty() {
return Err("ASPAPayloadSet.providers must be non-empty".into());
}
validate_sorted_unique_by(
&self.providers,
|provider| provider,
"ASPAPayloadSet.providers must be sorted ascending and unique",
)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TrustAnchorState {
pub skis: Vec<Vec<u8>>,
pub hash: Vec<u8>,
}
impl TrustAnchorState {
pub fn validate(&self) -> Result<(), String> {
if self.skis.is_empty() {
return Err("TrustAnchorState.skis must be non-empty".into());
}
validate_sha256_digest("TrustAnchorState.hash", &self.hash)?;
validate_sorted_unique_bytes(
&self.skis,
KEY_IDENTIFIER_LEN_SHA1,
"TrustAnchorState.skis must be sorted/unique 20-byte SKIs",
)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RouterKeyState {
pub rksets: Vec<RouterKeySet>,
pub hash: Vec<u8>,
}
impl RouterKeyState {
pub fn validate(&self) -> Result<(), String> {
validate_sha256_digest("RouterKeyState.hash", &self.hash)?;
validate_sorted_unique_by(
&self.rksets,
|item| &item.as_id,
"RouterKeyState.rksets must be sorted by asID and unique",
)?;
for rkset in &self.rksets {
rkset.validate()?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RouterKeySet {
pub as_id: u32,
pub router_keys: Vec<RouterKey>,
}
impl RouterKeySet {
pub fn validate(&self) -> Result<(), String> {
if self.router_keys.is_empty() {
return Err("RouterKeySet.router_keys must be non-empty".into());
}
validate_sorted_unique_by(
&self.router_keys,
|key| key,
"RouterKeySet.router_keys must be sorted by SKI and unique by (SKI, SPKI DER)",
)?;
for key in &self.router_keys {
key.validate()?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct RouterKey {
pub ski: Vec<u8>,
pub spki_der: Vec<u8>,
}
impl RouterKey {
pub fn validate(&self) -> Result<(), String> {
validate_key_identifier("RouterKey.ski", &self.ski)?;
validate_full_der_with_tag("RouterKey.spki_der", &self.spki_der, Some(0x30))
}
}
fn validate_sha256_digest(field: &str, bytes: &[u8]) -> Result<(), String> {
if bytes.len() != DIGEST_LEN_SHA256 {
return Err(format!(
"{field} must be {DIGEST_LEN_SHA256} bytes, got {}",
bytes.len()
));
}
Ok(())
}
fn validate_key_identifier(field: &str, bytes: &[u8]) -> Result<(), String> {
if bytes.len() != KEY_IDENTIFIER_LEN_SHA1 {
return Err(format!(
"{field} must be {KEY_IDENTIFIER_LEN_SHA1} bytes, got {}",
bytes.len()
));
}
Ok(())
}
fn validate_big_unsigned_bytes(field: &str, bytes: &[u8]) -> Result<(), String> {
if bytes.is_empty() {
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"));
}
Ok(())
}
fn validate_sorted_unique_by<T, K: Ord + ?Sized>(
values: &[T],
key_fn: impl Fn(&T) -> &K,
message: &str,
) -> Result<(), String> {
for window in values.windows(2) {
if key_fn(&window[0]) >= key_fn(&window[1]) {
return Err(message.to_string());
}
}
Ok(())
}
fn validate_sorted_unique_bytes(
values: &[Vec<u8>],
expected_len: usize,
message: &str,
) -> Result<(), String> {
for value in values {
if value.len() != expected_len {
return Err(message.to_string());
}
}
for window in values.windows(2) {
if window[0] >= window[1] {
return Err(message.to_string());
}
}
Ok(())
}
fn validate_full_der_with_tag(
field: &str,
der: &[u8],
expected_tag: Option<u8>,
) -> Result<(), String> {
let (tag, _value, rem) = der_take_tlv(der).map_err(|e| format!("{field}: {e}"))?;
if !rem.is_empty() {
return Err(format!("{field}: trailing bytes after DER object"));
}
if let Some(expected_tag) = expected_tag {
if tag != expected_tag {
return Err(format!(
"{field}: unexpected tag 0x{tag:02X}, expected 0x{expected_tag:02X}"
));
}
}
Ok(())
}

569
src/ccr/verify.rs Normal file
View File

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