增加aspa对象解析

This commit is contained in:
2026-02-02 15:39:22 +08:00
parent bcd4829486
commit 56ae2ca4fc
44 changed files with 1608 additions and 0 deletions

223
src/data_model/aspa.rs Normal file
View File

@@ -0,0 +1,223 @@
use crate::data_model::oid::OID_CT_ASPA;
use crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError};
use der_parser::ber::{Class};
use der_parser::der::{parse_der, DerObject, Tag};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AspaObject {
pub signed_object: RpkiSignedObject,
pub econtent_type: String,
pub aspa: AspaEContent,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AspaEContent {
pub version: u32,
pub customer_as_id: u32,
pub provider_as_ids: Vec<u32>,
}
#[derive(Debug, thiserror::Error)]
pub enum AspaDecodeError {
#[error("SignedObject decode error: {0}")]
SignedObjectDecode(#[from] SignedObjectDecodeError),
#[error("ASPA eContentType must be {OID_CT_ASPA}, got {0}")]
InvalidEContentType(String),
#[error("ASPA parse error: {0}")]
Parse(String),
#[error("ASPA trailing bytes: {0} bytes")]
TrailingBytes(usize),
#[error("ASProviderAttestation must be a SEQUENCE of 3 elements")]
InvalidAttestationSequence,
#[error("ASPA version must be 1 and MUST be explicitly encoded")]
VersionMustBeExplicitOne,
#[error("ASPA customerASID out of range (0..=4294967295), got {0}")]
CustomerAsIdOutOfRange(u64),
#[error("ASPA providers must contain at least one ASID")]
EmptyProviders,
#[error("ASPA provider ASID out of range (0..=4294967295), got {0}")]
ProviderAsIdOutOfRange(u64),
#[error("ASPA providers must be in strictly increasing order")]
ProvidersNotStrictlyIncreasing,
#[error("ASPA providers contains the customerASID ({0}) which is not allowed")]
ProvidersContainCustomer(u32),
}
#[derive(Debug, thiserror::Error)]
pub enum AspaValidateError {
#[error("ASPA EE certificate must contain AS resources extension (RFC 3779)")]
EeAsResourcesMissing,
#[error("ASPA EE certificate AS resources must not use inherit")]
EeAsResourcesInherit,
#[error("ASPA EE certificate AS resources must not include ranges")]
EeAsResourcesRangePresent,
#[error("ASPA customerASID ({customer_as_id}) does not match EE AS resources ({ee_as_id})")]
CustomerAsIdMismatch { customer_as_id: u32, ee_as_id: u32 },
#[error("ASPA EE certificate must not contain IP resources extension (RFC 3779)")]
EeIpResourcesPresent,
}
/// Minimal EE resource information required to validate ASPA payload semantics.
///
/// The caller is responsible for extracting this from the EE certificate (RFC 3779).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EeAspaResources {
/// The single ASID carried in EE's AS resources "id" element (profile requires exactly one).
pub as_id: Option<u32>,
/// Whether EE's AS resources uses inherit (must be false).
pub as_resources_inherit: bool,
/// Whether EE's AS resources includes any range elements (must be false).
pub as_resources_range_present: bool,
/// Whether EE certificate contains IP resources extension (must be false).
pub ip_resources_present: bool,
}
impl AspaObject {
pub fn decode_der(der: &[u8]) -> Result<Self, AspaDecodeError> {
let signed_object = RpkiSignedObject::decode_der(der)?;
Self::from_signed_object(signed_object)
}
pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result<Self, AspaDecodeError> {
let econtent_type = signed_object
.signed_data
.encap_content_info
.econtent_type
.clone();
if econtent_type != OID_CT_ASPA {
return Err(AspaDecodeError::InvalidEContentType(econtent_type));
}
let aspa = AspaEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?;
Ok(Self {
aspa,
signed_object,
econtent_type: OID_CT_ASPA.to_string(),
})
}
}
impl AspaEContent {
/// Decode the DER-encoded ASProviderAttestation defined in draft-ietf-sidrops-aspa-profile-21 §3.
pub fn decode_der(der: &[u8]) -> Result<Self, AspaDecodeError> {
let (rem, obj) = parse_der(der).map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
if !rem.is_empty() {
return Err(AspaDecodeError::TrailingBytes(rem.len()));
}
let seq = obj
.as_sequence()
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
if seq.len() != 3 {
return Err(AspaDecodeError::InvalidAttestationSequence);
}
// version [0] EXPLICIT INTEGER MUST be present and MUST be 1.
let v_obj = &seq[0];
if v_obj.class() != Class::ContextSpecific || v_obj.tag() != Tag(0) {
return Err(AspaDecodeError::VersionMustBeExplicitOne);
}
let inner_der = v_obj
.as_slice()
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
let (rem, inner) =
parse_der(inner_der).map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
if !rem.is_empty() {
return Err(AspaDecodeError::Parse(
"trailing bytes inside ASProviderAttestation.version".into(),
));
}
let v = inner
.as_u64()
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
if v != 1 {
return Err(AspaDecodeError::VersionMustBeExplicitOne);
}
let customer_u64 = seq[1]
.as_u64()
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
if customer_u64 > u32::MAX as u64 {
return Err(AspaDecodeError::CustomerAsIdOutOfRange(customer_u64));
}
let customer_as_id = customer_u64 as u32;
let providers = parse_providers(&seq[2], customer_as_id)?;
Ok(Self {
version: 1,
customer_as_id,
provider_as_ids: providers,
})
}
/// Validate ASPA payload against EE resources extracted by the caller.
///
/// This implements the EE/payload semantic checks described in
/// `draft-ietf-sidrops-aspa-profile-21` §4 (as summarized in `rpki/specs/08_aspa.md`).
pub fn validate_against_ee_resources(&self, ee: &EeAspaResources) -> Result<(), AspaValidateError> {
if ee.ip_resources_present {
return Err(AspaValidateError::EeIpResourcesPresent);
}
if ee.as_resources_inherit {
return Err(AspaValidateError::EeAsResourcesInherit);
}
if ee.as_resources_range_present {
return Err(AspaValidateError::EeAsResourcesRangePresent);
}
let ee_as_id = ee.as_id.ok_or(AspaValidateError::EeAsResourcesMissing)?;
if ee_as_id != self.customer_as_id {
return Err(AspaValidateError::CustomerAsIdMismatch {
customer_as_id: self.customer_as_id,
ee_as_id,
});
}
Ok(())
}
}
fn parse_providers(obj: &DerObject<'_>, customer_as_id: u32) -> Result<Vec<u32>, AspaDecodeError> {
let seq = obj
.as_sequence()
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
if seq.is_empty() {
return Err(AspaDecodeError::EmptyProviders);
}
let mut out: Vec<u32> = Vec::with_capacity(seq.len());
let mut prev: Option<u32> = None;
for item in seq {
let v = item
.as_u64()
.map_err(|e| AspaDecodeError::Parse(e.to_string()))?;
if v > u32::MAX as u64 {
return Err(AspaDecodeError::ProviderAsIdOutOfRange(v));
}
let asn = v as u32;
if asn == customer_as_id {
return Err(AspaDecodeError::ProvidersContainCustomer(customer_as_id));
}
if let Some(p) = prev {
if asn <= p {
return Err(AspaDecodeError::ProvidersNotStrictlyIncreasing);
}
}
prev = Some(asn);
out.push(asn);
}
Ok(out)
}

View File

@@ -8,3 +8,5 @@ mod oids;
pub mod oid;
pub mod signed_object;
pub mod manifest;
pub mod roa;
pub mod aspa;

View File

@@ -14,6 +14,8 @@ pub const OID_CRL_NUMBER: &str = "2.5.29.20";
pub const OID_SUBJECT_KEY_IDENTIFIER: &str = "2.5.29.14";
pub const OID_CT_RPKI_MANIFEST: &str = "1.2.840.113549.1.9.16.1.26";
pub const OID_CT_ROUTE_ORIGIN_AUTHZ: &str = "1.2.840.113549.1.9.16.1.24";
pub const OID_CT_ASPA: &str = "1.2.840.113549.1.9.16.1.49";
// X.509 extensions / access methods (RFC 5280 / RFC 6487)
pub const OID_SUBJECT_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.11";

466
src/data_model/roa.rs Normal file
View File

@@ -0,0 +1,466 @@
use crate::data_model::oid::OID_CT_ROUTE_ORIGIN_AUTHZ;
use crate::data_model::signed_object::{RpkiSignedObject, SignedObjectDecodeError};
use der_parser::ber::{BerObjectContent, Class};
use der_parser::der::{parse_der, DerObject, Tag};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RoaObject {
pub signed_object: RpkiSignedObject,
pub econtent_type: String,
pub roa: RoaEContent,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RoaEContent {
pub version: u32,
pub as_id: u32,
pub ip_addr_blocks: Vec<RoaIpAddressFamily>,
}
#[derive(Debug, thiserror::Error)]
pub enum RoaDecodeError {
#[error("SignedObject decode error: {0}")]
SignedObjectDecode(#[from] SignedObjectDecodeError),
#[error("ROA eContentType must be {OID_CT_ROUTE_ORIGIN_AUTHZ}, got {0}")]
InvalidEContentType(String),
#[error("ROA parse error: {0}")]
Parse(String),
#[error("ROA trailing bytes: {0} bytes")]
TrailingBytes(usize),
#[error("RouteOriginAttestation must be a SEQUENCE of 2 or 3 elements, got {0}")]
InvalidAttestationSequenceLen(usize),
#[error("ROA version must be 0, got {0}")]
InvalidVersion(u64),
#[error("ROA asID out of range (0..=4294967295), got {0}")]
AsIdOutOfRange(u64),
#[error("ROA ipAddrBlocks must have length 1..2, got {0}")]
InvalidIpAddrBlocksLen(usize),
#[error("ROAIPAddressFamily must be a SEQUENCE of 2 elements")]
InvalidIpAddressFamily,
#[error("ROA addressFamily must be an OCTET STRING of 2 bytes")]
InvalidAddressFamily,
#[error("ROA addressFamily AFI not supported: {0:02X?}")]
UnsupportedAfi(Vec<u8>),
#[error("ROA contains duplicate AFI {0:?}")]
DuplicateAfi(RoaAfi),
#[error("ROAAddresses must have at least one entry")]
EmptyAddressList,
#[error("ROAIPAddress must be a SEQUENCE of 1..2 elements")]
InvalidRoaIpAddress,
#[error("ROAIPAddress.address must be a BIT STRING")]
InvalidPrefixBitString,
#[error("ROAIPAddress.address has invalid unused bits encoding")]
InvalidPrefixUnusedBits,
#[error("ROAIPAddress.address prefix length {prefix_len} out of range for {afi:?}")]
PrefixLenOutOfRange { afi: RoaAfi, prefix_len: u16 },
#[error("ROAIPAddress.maxLength out of range for {afi:?}: prefix_len={prefix_len}, max_len={max_len}")]
InvalidMaxLength {
afi: RoaAfi,
prefix_len: u16,
max_len: u16,
},
}
#[derive(Debug, thiserror::Error)]
pub enum RoaValidateError {
#[error("ROA EE certificate must not contain AS resources extension (RFC 9582 §5)")]
EeAsResourcesPresent,
#[error("ROA EE certificate IP resources must not use inherit (RFC 9582 §5)")]
EeIpResourcesInherit,
#[error("ROA prefix not covered by EE certificate IP resources: {afi:?} {addr:?}/{prefix_len}")]
PrefixNotInEeResources {
afi: RoaAfi,
addr: Vec<u8>,
prefix_len: u16,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IpResourceSet {
pub prefixes: Vec<IpPrefix>,
}
impl IpResourceSet {
pub fn contains_prefix(&self, p: &IpPrefix) -> bool {
self.prefixes
.iter()
.any(|r| r.afi == p.afi && prefix_covers(r, p))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EeResources {
pub ip_resources: IpResourceSet,
pub ip_resources_inherit: bool,
pub as_resources_present: bool,
}
impl RoaObject {
pub fn decode_der(der: &[u8]) -> Result<Self, RoaDecodeError> {
let signed_object = RpkiSignedObject::decode_der(der)?;
Self::from_signed_object(signed_object)
}
pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result<Self, RoaDecodeError> {
let econtent_type = signed_object
.signed_data
.encap_content_info
.econtent_type
.clone();
if econtent_type != OID_CT_ROUTE_ORIGIN_AUTHZ {
return Err(RoaDecodeError::InvalidEContentType(econtent_type));
}
let roa = RoaEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?;
Ok(Self {
roa,
signed_object,
econtent_type: OID_CT_ROUTE_ORIGIN_AUTHZ.to_string(),
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum RoaAfi {
Ipv4,
Ipv6,
}
impl RoaAfi {
fn ub(self) -> u16 {
match self {
RoaAfi::Ipv4 => 32,
RoaAfi::Ipv6 => 128,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RoaIpAddressFamily {
pub afi: RoaAfi,
pub addresses: Vec<RoaIpAddress>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct RoaIpAddress {
pub prefix: IpPrefix,
pub max_length: Option<u16>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct IpPrefix {
pub afi: RoaAfi,
/// Prefix length in bits.
pub prefix_len: u16,
/// Network order address bytes (IPv4 4 bytes / IPv6 16 bytes), with host bits cleared.
pub addr: Vec<u8>,
}
impl RoaEContent {
/// Decode the DER-encoded RouteOriginAttestation defined in RFC 9582 §4.
pub fn decode_der(der: &[u8]) -> Result<Self, RoaDecodeError> {
let (rem, obj) = parse_der(der).map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
if !rem.is_empty() {
return Err(RoaDecodeError::TrailingBytes(rem.len()));
}
let seq = obj
.as_sequence()
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
if seq.len() != 2 && seq.len() != 3 {
return Err(RoaDecodeError::InvalidAttestationSequenceLen(seq.len()));
}
let mut idx = 0;
let mut version: u32 = 0;
if seq.len() == 3 {
let v_obj = &seq[0];
if v_obj.class() != Class::ContextSpecific || v_obj.tag() != Tag(0) {
return Err(RoaDecodeError::Parse(
"RouteOriginAttestation.version must be [0] EXPLICIT INTEGER".into(),
));
}
let inner_der = v_obj
.as_slice()
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
let (rem, inner) =
parse_der(inner_der).map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
if !rem.is_empty() {
return Err(RoaDecodeError::Parse(
"trailing bytes inside RouteOriginAttestation.version".into(),
));
}
let v = inner
.as_u64()
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
if v != 0 {
return Err(RoaDecodeError::InvalidVersion(v));
}
version = 0;
idx = 1;
}
let as_id_u64 = seq[idx]
.as_u64()
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
if as_id_u64 > u32::MAX as u64 {
return Err(RoaDecodeError::AsIdOutOfRange(as_id_u64));
}
let as_id = as_id_u64 as u32;
idx += 1;
let ip_addr_blocks = parse_ip_addr_blocks(&seq[idx])?;
let mut out = Self {
version,
as_id,
ip_addr_blocks,
};
out.canonicalize();
Ok(out)
}
pub fn canonicalize(&mut self) {
self.ip_addr_blocks.sort_by_key(|f| f.afi);
for fam in &mut self.ip_addr_blocks {
fam.addresses.sort();
fam.addresses.dedup();
}
}
/// Validate ROA payload against EE resources extracted by the caller.
///
/// This implements the ROA/EE semantic checks from RFC 9582 §5 that do not
/// require certificate path validation.
pub fn validate_against_ee_resources(&self, ee: &EeResources) -> Result<(), RoaValidateError> {
if ee.as_resources_present {
return Err(RoaValidateError::EeAsResourcesPresent);
}
if ee.ip_resources_inherit {
return Err(RoaValidateError::EeIpResourcesInherit);
}
for fam in &self.ip_addr_blocks {
for entry in &fam.addresses {
if !ee.ip_resources.contains_prefix(&entry.prefix) {
return Err(RoaValidateError::PrefixNotInEeResources {
afi: entry.prefix.afi,
addr: entry.prefix.addr.clone(),
prefix_len: entry.prefix.prefix_len,
});
}
}
}
Ok(())
}
}
fn parse_ip_addr_blocks(obj: &DerObject<'_>) -> Result<Vec<RoaIpAddressFamily>, RoaDecodeError> {
let seq = obj
.as_sequence()
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
if seq.is_empty() || seq.len() > 2 {
return Err(RoaDecodeError::InvalidIpAddrBlocksLen(seq.len()));
}
let mut out: Vec<RoaIpAddressFamily> = Vec::new();
for fam in seq {
let family = parse_ip_address_family(fam)?;
if out.iter().any(|f| f.afi == family.afi) {
return Err(RoaDecodeError::DuplicateAfi(family.afi));
}
out.push(family);
}
Ok(out)
}
fn parse_ip_address_family(obj: &DerObject<'_>) -> Result<RoaIpAddressFamily, RoaDecodeError> {
let seq = obj
.as_sequence()
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
if seq.len() != 2 {
return Err(RoaDecodeError::InvalidIpAddressFamily);
}
let afi = parse_afi(&seq[0])?;
let addresses = parse_roa_addresses(afi, &seq[1])?;
if addresses.is_empty() {
return Err(RoaDecodeError::EmptyAddressList);
}
Ok(RoaIpAddressFamily { afi, addresses })
}
fn parse_afi(obj: &DerObject<'_>) -> Result<RoaAfi, RoaDecodeError> {
let bytes = obj
.as_slice()
.map_err(|_e| RoaDecodeError::InvalidAddressFamily)?;
if bytes.len() != 2 {
return Err(RoaDecodeError::InvalidAddressFamily);
}
match bytes {
[0x00, 0x01] => Ok(RoaAfi::Ipv4),
[0x00, 0x02] => Ok(RoaAfi::Ipv6),
_ => Err(RoaDecodeError::UnsupportedAfi(bytes.to_vec())),
}
}
fn parse_roa_addresses(afi: RoaAfi, obj: &DerObject<'_>) -> Result<Vec<RoaIpAddress>, RoaDecodeError> {
let seq = obj
.as_sequence()
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
let mut out = Vec::with_capacity(seq.len());
for entry in seq {
out.push(parse_roa_ip_address(afi, entry)?);
}
Ok(out)
}
fn parse_roa_ip_address(afi: RoaAfi, obj: &DerObject<'_>) -> Result<RoaIpAddress, RoaDecodeError> {
let seq = obj
.as_sequence()
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
if seq.is_empty() || seq.len() > 2 {
return Err(RoaDecodeError::InvalidRoaIpAddress);
}
let prefix = parse_prefix_bits(afi, &seq[0])?;
let max_length = match seq.get(1) {
None => None,
Some(m) => {
let v = m
.as_u64()
.map_err(|e| RoaDecodeError::Parse(e.to_string()))?;
let max_len: u16 = v
.try_into()
.map_err(|_e| RoaDecodeError::InvalidMaxLength {
afi,
prefix_len: prefix.prefix_len,
max_len: u16::MAX,
})?;
Some(max_len)
}
};
if let Some(max_len) = max_length {
let ub = afi.ub();
if max_len > ub || max_len < prefix.prefix_len {
return Err(RoaDecodeError::InvalidMaxLength {
afi,
prefix_len: prefix.prefix_len,
max_len,
});
}
}
Ok(RoaIpAddress { prefix, max_length })
}
fn parse_prefix_bits(afi: RoaAfi, obj: &DerObject<'_>) -> Result<IpPrefix, RoaDecodeError> {
let (unused_bits, bytes) = match &obj.content {
BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()),
_ => return Err(RoaDecodeError::InvalidPrefixBitString),
};
if unused_bits > 7 {
return Err(RoaDecodeError::InvalidPrefixUnusedBits);
}
if bytes.is_empty() {
if unused_bits != 0 {
return Err(RoaDecodeError::InvalidPrefixUnusedBits);
}
} else if unused_bits != 0 {
let mask = (1u8 << unused_bits) - 1;
if (bytes[bytes.len() - 1] & mask) != 0 {
return Err(RoaDecodeError::InvalidPrefixUnusedBits);
}
}
let prefix_len = (bytes.len() * 8)
.checked_sub(unused_bits as usize)
.ok_or(RoaDecodeError::InvalidPrefixUnusedBits)? as u16;
if prefix_len > afi.ub() {
return Err(RoaDecodeError::PrefixLenOutOfRange { afi, prefix_len });
}
let addr = canonicalize_prefix_addr(afi, prefix_len, &bytes);
Ok(IpPrefix {
afi,
prefix_len,
addr,
})
}
fn canonicalize_prefix_addr(afi: RoaAfi, prefix_len: u16, bytes: &[u8]) -> Vec<u8> {
let full_len = match afi {
RoaAfi::Ipv4 => 4,
RoaAfi::Ipv6 => 16,
};
let mut addr = vec![0u8; full_len];
let copy_len = bytes.len().min(full_len);
addr[..copy_len].copy_from_slice(&bytes[..copy_len]);
if prefix_len == 0 {
return addr;
}
let last_prefix_bit = (prefix_len - 1) as usize;
let last_prefix_byte = last_prefix_bit / 8;
let rem = (prefix_len % 8) as u8;
if rem != 0 {
let mask: u8 = 0xFF << (8 - rem);
if last_prefix_byte < addr.len() {
addr[last_prefix_byte] &= mask;
}
}
addr
}
fn prefix_covers(resource: &IpPrefix, subject: &IpPrefix) -> bool {
if resource.afi != subject.afi {
return false;
}
if resource.prefix_len > subject.prefix_len {
return false;
}
let full_len = match resource.afi {
RoaAfi::Ipv4 => 4,
RoaAfi::Ipv6 => 16,
};
if resource.addr.len() != full_len || subject.addr.len() != full_len {
return false;
}
let n = resource.prefix_len as usize;
let whole = n / 8;
let rem = (n % 8) as u8;
if resource.addr[..whole] != subject.addr[..whole] {
return false;
}
if rem == 0 {
return true;
}
let mask = 0xFFu8 << (8 - rem);
(resource.addr[whole] & mask) == (subject.addr[whole] & mask)
}