rpki/src/source/ccr.rs
2026-04-01 16:24:01 +08:00

356 lines
12 KiB
Rust

use std::fs;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, anyhow};
use der_parser::ber::{BerObject, BerObjectContent};
use der_parser::der::parse_der;
use crate::rtr::loader::{ParsedAspa, ParsedVrp, build_aspa, build_route_origin};
use crate::rtr::payload::Payload;
const VRPS_INDEX: usize = 3;
const VAPS_INDEX: usize = 4;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParsedCcrSnapshot {
pub content_type_oid: String,
pub produced_at: Option<String>,
pub vrps: Vec<ParsedVrp>,
pub vaps: Vec<ParsedAspa>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct CcrPayloadConversion {
pub payloads: Vec<Payload>,
pub invalid_vrps: Vec<String>,
pub invalid_vaps: Vec<String>,
}
pub fn load_ccr_snapshot_from_file(path: impl AsRef<Path>) -> Result<ParsedCcrSnapshot> {
let path = path.as_ref();
let bytes =
fs::read(path).with_context(|| format!("failed to read CCR file: {}", path.display()))?;
parse_ccr_bytes(&bytes).with_context(|| format!("failed to parse CCR file: {}", path.display()))
}
pub fn load_ccr_payloads_from_file(path: impl AsRef<Path>) -> Result<Vec<Payload>> {
let snapshot = load_ccr_snapshot_from_file(path)?;
snapshot_to_payloads(&snapshot)
}
pub fn load_ccr_payloads_from_file_with_options(
path: impl AsRef<Path>,
strict: bool,
) -> Result<CcrPayloadConversion> {
let snapshot = load_ccr_snapshot_from_file(path)?;
snapshot_to_payloads_with_options(&snapshot, strict)
}
pub fn find_latest_ccr_file(dir: impl AsRef<Path>) -> Result<PathBuf> {
let dir = dir.as_ref();
let mut latest: Option<PathBuf> = None;
for entry in fs::read_dir(dir)
.with_context(|| format!("failed to read CCR directory: {}", dir.display()))?
{
let entry =
entry.with_context(|| format!("failed to iterate CCR directory: {}", dir.display()))?;
let path = entry.path();
if !path.is_file() {
continue;
}
if path.extension().and_then(|ext| ext.to_str()) != Some("ccr") {
continue;
}
if latest
.as_ref()
.is_none_or(|current| file_name_key(&path) > file_name_key(current))
{
latest = Some(path);
}
}
latest.ok_or_else(|| anyhow!("no .ccr files found in {}", dir.display()))
}
pub fn snapshot_to_payloads(snapshot: &ParsedCcrSnapshot) -> Result<Vec<Payload>> {
Ok(snapshot_to_payloads_with_options(snapshot, true)?.payloads)
}
pub fn snapshot_to_payloads_with_options(
snapshot: &ParsedCcrSnapshot,
strict: bool,
) -> Result<CcrPayloadConversion> {
let mut payloads = Vec::with_capacity(snapshot.vrps.len() + snapshot.vaps.len());
let mut invalid_vrps = Vec::new();
let mut invalid_vaps = Vec::new();
for vrp in &snapshot.vrps {
match build_route_origin(vrp.clone()) {
Ok(origin) => payloads.push(Payload::RouteOrigin(origin)),
Err(err) => {
let msg = format!("invalid CCR VRP: {:?}: {}", vrp, err);
if strict {
return Err(anyhow!(msg));
}
invalid_vrps.push(msg);
}
}
}
for vap in &snapshot.vaps {
match build_aspa(vap.clone()) {
Ok(aspa) => payloads.push(Payload::Aspa(aspa)),
Err(err) => {
let msg = format!("invalid CCR VAP/ASPA: {:?}: {}", vap, err);
if strict {
return Err(anyhow!(msg));
}
invalid_vaps.push(msg);
}
}
}
Ok(CcrPayloadConversion {
payloads,
invalid_vrps,
invalid_vaps,
})
}
pub fn parse_ccr_bytes(bytes: &[u8]) -> Result<ParsedCcrSnapshot> {
let (rem, root) = parse_der(bytes).map_err(|err| anyhow!("failed to parse CCR DER: {err}"))?;
if !rem.is_empty() {
return Err(anyhow!("CCR DER has {} trailing bytes", rem.len()));
}
let root_items = sequence_items(&root)?;
if root_items.len() != 2 {
return Err(anyhow!("CCR root must contain exactly 2 items"));
}
let content_type_oid = match &root_items[0].content {
BerObjectContent::OID(oid) => oid.to_string(),
other => {
return Err(anyhow!(
"CCR root first element must be content type OID, got {other:?}"
));
}
};
let payload = decode_context_wrapped_sequence(&root_items[1])?;
let payload_items = sequence_items(&payload)?;
let produced_at = payload_items.get(1).and_then(|obj| match &obj.content {
BerObjectContent::GeneralizedTime(t) => Some(t.to_string()),
_ => None,
});
let vrps = if let Some(vrps_field) = payload_items.get(VRPS_INDEX) {
parse_vrps(vrps_field)?
} else {
Vec::new()
};
let vaps = if let Some(vaps_field) = payload_items.get(VAPS_INDEX) {
parse_vaps(vaps_field)?
} else {
Vec::new()
};
Ok(ParsedCcrSnapshot {
content_type_oid,
produced_at,
vrps,
vaps,
})
}
fn parse_vrps(field: &BerObject<'_>) -> Result<Vec<ParsedVrp>> {
let vrp_state = decode_context_wrapped_sequence(field)?;
let vrp_state_items = sequence_items(&vrp_state)?;
let roa_payload_sets = vrp_state_items
.first()
.ok_or_else(|| anyhow!("ROA payload state missing payload set list"))?;
let roa_payload_sets = sequence_items(roa_payload_sets)?;
let mut vrps = Vec::new();
for payload_set in roa_payload_sets {
let payload_set_items = sequence_items(payload_set)?;
if payload_set_items.len() != 2 {
return Err(anyhow!(
"ROAPayloadSet must contain 2 items, got {}",
payload_set_items.len()
));
}
let asn = as_u32(&payload_set_items[0], "ROAPayloadSet.asID")?;
let families = sequence_items(&payload_set_items[1])?;
for family in families {
let family_items = sequence_items(family)?;
if family_items.len() != 2 {
return Err(anyhow!(
"ROAIPAddressFamily must contain 2 items, got {}",
family_items.len()
));
}
let address_family = as_octets(&family_items[0], "ROAIPAddressFamily.addressFamily")?;
let addresses = sequence_items(&family_items[1])?;
for address in addresses {
let address_items = sequence_items(address)?;
let (prefix_addr, prefix_len, max_len) =
parse_roa_address(address_family, address_items)?;
vrps.push(ParsedVrp {
prefix_addr,
prefix_len,
max_len,
asn,
});
}
}
}
Ok(vrps)
}
fn parse_vaps(field: &BerObject<'_>) -> Result<Vec<ParsedAspa>> {
let vap_state = decode_context_wrapped_sequence(field)?;
let vap_state_items = sequence_items(&vap_state)?;
let aspa_payload_sets = vap_state_items
.first()
.ok_or_else(|| anyhow!("ASPA payload state missing payload set list"))?;
let aspa_payload_sets = sequence_items(aspa_payload_sets)?;
let mut vaps = Vec::new();
for payload_set in aspa_payload_sets {
let payload_set_items = sequence_items(payload_set)?;
if payload_set_items.len() != 2 {
return Err(anyhow!(
"ASPAPayloadSet must contain 2 items, got {}",
payload_set_items.len()
));
}
let customer_asn = as_u32(&payload_set_items[0], "ASPAPayloadSet.customerASID")?;
let provider_set = sequence_items(&payload_set_items[1])?;
let mut provider_asns = Vec::with_capacity(provider_set.len());
for provider in provider_set {
provider_asns.push(as_u32(provider, "ASPAPayloadSet.providerASID")?);
}
vaps.push(ParsedAspa {
customer_asn,
provider_asns,
});
}
Ok(vaps)
}
fn parse_roa_address(address_family: &[u8], items: &[BerObject<'_>]) -> Result<(IpAddr, u8, u8)> {
let address = items
.first()
.ok_or_else(|| anyhow!("ROAIPAddress missing address field"))?;
let (unused_bits, bit_string) = match &address.content {
BerObjectContent::BitString(unused_bits, bit_string) => (*unused_bits, bit_string),
other => {
return Err(anyhow!(
"ROAIPAddress.address must be BIT STRING, got {other:?}"
));
}
};
let prefix_len = (bit_string.data.len() * 8)
.checked_sub(usize::from(unused_bits))
.ok_or_else(|| anyhow!("invalid ROAIPAddress BIT STRING length"))?;
let prefix_len = u8::try_from(prefix_len)
.map_err(|_| anyhow!("prefix length {prefix_len} does not fit in u8"))?;
let max_len = match items.get(1) {
Some(value) => {
let max_len = as_u32(value, "ROAIPAddress.maxLength")?;
u8::try_from(max_len).map_err(|_| anyhow!("maxLength {max_len} does not fit in u8"))?
}
None => prefix_len,
};
let prefix_addr = match address_family {
[0, 1] | [0, 1, ..] => {
let mut octets = [0u8; 4];
if bit_string.data.len() > octets.len() {
return Err(anyhow!(
"IPv4 ROAIPAddress too long: {} bytes",
bit_string.data.len()
));
}
octets[..bit_string.data.len()].copy_from_slice(bit_string.data);
IpAddr::V4(Ipv4Addr::from(octets))
}
[0, 2] | [0, 2, ..] => {
let mut octets = [0u8; 16];
if bit_string.data.len() > octets.len() {
return Err(anyhow!(
"IPv6 ROAIPAddress too long: {} bytes",
bit_string.data.len()
));
}
octets[..bit_string.data.len()].copy_from_slice(bit_string.data);
IpAddr::V6(Ipv6Addr::from(octets))
}
_ => {
return Err(anyhow!(
"unsupported ROA address family octets: {:?}",
address_family
));
}
};
Ok((prefix_addr, prefix_len, max_len))
}
fn sequence_items<'a>(obj: &'a BerObject<'a>) -> Result<&'a [BerObject<'a>]> {
match &obj.content {
BerObjectContent::Sequence(items) => Ok(items),
other => Err(anyhow!("expected SEQUENCE, got {other:?}")),
}
}
fn decode_context_wrapped_sequence<'a>(obj: &'a BerObject<'a>) -> Result<BerObject<'a>> {
match &obj.content {
BerObjectContent::Unknown(any) => {
let (rem, inner) = parse_der(any.data)
.map_err(|err| anyhow!("failed to parse encapsulated DER: {err}"))?;
if !rem.is_empty() {
return Err(anyhow!("encapsulated DER has {} trailing bytes", rem.len()));
}
Ok(inner)
}
other => Err(anyhow!(
"expected context-specific wrapped field, got {other:?}"
)),
}
}
fn as_u32(obj: &BerObject<'_>, field_name: &str) -> Result<u32> {
obj.as_u32()
.map_err(|err| anyhow!("{field_name} must be INTEGER fitting in u32: {err}"))
}
fn as_octets<'a>(obj: &'a BerObject<'a>, field_name: &str) -> Result<&'a [u8]> {
match &obj.content {
BerObjectContent::OctetString(bytes) => Ok(bytes),
other => Err(anyhow!("{field_name} must be OCTET STRING, got {other:?}")),
}
}
fn file_name_key(path: &Path) -> String {
path.file_name()
.and_then(|name| name.to_str())
.map(|name| name.to_ascii_lowercase())
.unwrap_or_default()
}