增加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)
}