use crate::data_model::common::{DerReader, der_take_tlv}; use crate::data_model::oid::OID_CT_ASPA; use crate::data_model::rc::ResourceCertificate; use crate::data_model::signed_object::{ RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError, }; #[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 AspaObjectParsed { pub signed_object: RpkiSignedObjectParsed, pub econtent_type: String, pub aspa: Option, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct AspaEContent { pub version: u32, pub customer_as_id: u32, pub provider_as_ids: Vec, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct AspaEContentParsed { der: Vec, } #[derive(Debug, thiserror::Error)] pub enum AspaParseError { #[error("signed object parse error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")] SignedObject(#[from] SignedObjectParseError), #[error("ASPA parse error: {0} (draft-ietf-sidrops-aspa-profile-21 §3; DER)")] Parse(String), #[error("ASPA trailing bytes: {0} bytes (draft-ietf-sidrops-aspa-profile-21 §3; DER)")] TrailingBytes(usize), } #[derive(Debug, thiserror::Error)] pub enum AspaProfileError { #[error("signed object profile error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")] SignedObject(#[from] SignedObjectValidateError), #[error( "ASPA eContentType must be {OID_CT_ASPA}, got {0} (draft-ietf-sidrops-aspa-profile-21 §2)" )] InvalidEContentType(String), #[error("ASPA profile decode error: {0} (draft-ietf-sidrops-aspa-profile-21 §3; DER)")] ProfileDecode(String), #[error( "ASProviderAttestation must be a SEQUENCE of 3 elements (draft-ietf-sidrops-aspa-profile-21 §3)" )] InvalidAttestationSequence, #[error( "ASPA version must be 1 and MUST be explicitly encoded (draft-ietf-sidrops-aspa-profile-21 §3.1)" )] VersionMustBeExplicitOne, #[error( "ASPA customerASID out of range (0..=4294967295), got {0} (draft-ietf-sidrops-aspa-profile-21 §3.2)" )] CustomerAsIdOutOfRange(u64), #[error( "ASPA providers must contain at least one ASID (draft-ietf-sidrops-aspa-profile-21 §3.3)" )] EmptyProviders, #[error( "ASPA provider ASID out of range (0..=4294967295), got {0} (draft-ietf-sidrops-aspa-profile-21 §3.3)" )] ProviderAsIdOutOfRange(u64), #[error( "ASPA providers must be in strictly increasing order (draft-ietf-sidrops-aspa-profile-21 §3.3)" )] ProvidersNotStrictlyIncreasing, #[error( "ASPA providers contains the customerASID ({0}) which is not allowed (draft-ietf-sidrops-aspa-profile-21 §3.3)" )] ProvidersContainCustomer(u32), } #[derive(Debug, thiserror::Error)] pub enum AspaDecodeError { #[error("{0}")] Parse(#[from] AspaParseError), #[error("{0}")] Validate(#[from] AspaProfileError), } #[derive(Debug, thiserror::Error)] pub enum AspaValidateError { #[error( "ASPA EE certificate must contain AS resources extension (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2)" )] EeAsResourcesMissing, #[error( "ASPA EE certificate AS resources must not use inherit (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2.3.3)" )] EeAsResourcesInherit, #[error( "ASPA EE certificate AS resources must not include ranges (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2.3.6-§3.2.3.7)" )] EeAsResourcesRangePresent, #[error( "ASPA EE certificate AS resources must not include RDI (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §3.2.3.5; RFC 6487 §4.8.11)" )] EeAsResourcesRdiPresent, #[error( "ASPA EE certificate AS resources must contain exactly one ASID (id element) (draft-ietf-sidrops-aspa-profile-21 §4)" )] EeAsResourcesNotSingleId, #[error( "ASPA customerASID ({customer_as_id}) does not match EE AS resources ({ee_as_id}) (draft-ietf-sidrops-aspa-profile-21 §4)" )] CustomerAsIdMismatch { customer_as_id: u32, ee_as_id: u32 }, #[error( "ASPA EE certificate must not contain IP resources extension (draft-ietf-sidrops-aspa-profile-21 §4; RFC 3779 §2.2)" )] EeIpResourcesPresent, } impl AspaObject { /// Parse step of scheme A (`parse → validate → verify`). pub fn parse_der(der: &[u8]) -> Result { let signed_object = RpkiSignedObject::parse_der(der)?; let econtent_type = signed_object .signed_data .encap_content_info .econtent_type .clone(); let aspa = signed_object .signed_data .encap_content_info .econtent .as_deref() .map(AspaEContent::parse_der) .transpose()?; Ok(AspaObjectParsed { signed_object, econtent_type, aspa, }) } /// Profile validate step of scheme A (`parse → validate → verify`). /// /// `AspaObject` is already profile-validated when constructed via `decode_der()` / /// `AspaObjectParsed::validate_profile()`. pub fn validate_profile(&self) -> Result<(), AspaProfileError> { Ok(()) } pub fn decode_der(der: &[u8]) -> Result { Ok(Self::parse_der(der)?.validate_profile()?) } pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result { let econtent_type = signed_object .signed_data .encap_content_info .econtent_type .clone(); if econtent_type != OID_CT_ASPA { return Err(AspaProfileError::InvalidEContentType(econtent_type).into()); } 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(), }) } /// Validate this ASPA's embedded EE certificate resources. pub fn validate_embedded_ee_cert(&self) -> Result<(), AspaValidateError> { let ee = &self.signed_object.signed_data.certificates[0].resource_cert; self.aspa.validate_against_ee_cert(ee) } } impl AspaEContent { /// Parse step of scheme A (`parse → validate → verify`). pub fn parse_der(der: &[u8]) -> Result { let (_tag, _value, rem) = der_take_tlv(der).map_err(AspaParseError::Parse)?; if !rem.is_empty() { return Err(AspaParseError::TrailingBytes(rem.len())); } Ok(AspaEContentParsed { der: der.to_vec() }) } /// Profile validate step of scheme A (`parse → validate → verify`). /// /// `AspaEContent` is already profile-validated when constructed via `decode_der()` / /// `AspaEContentParsed::validate_profile()`. pub fn validate_profile(&self) -> Result<(), AspaProfileError> { Ok(()) } /// Decode the DER-encoded ASProviderAttestation defined in /// draft-ietf-sidrops-aspa-profile-21 §3 (`parse + validate`). pub fn decode_der(der: &[u8]) -> Result { Ok(Self::parse_der(der)?.validate_profile()?) } /// Validate ASPA payload against the embedded EE resource certificate. /// /// 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_cert( &self, ee: &ResourceCertificate, ) -> Result<(), AspaValidateError> { if ee.tbs.extensions.ip_resources.is_some() { return Err(AspaValidateError::EeIpResourcesPresent); } let asn = ee .tbs .extensions .as_resources .as_ref() .ok_or(AspaValidateError::EeAsResourcesMissing)?; if asn.rdi.is_some() { return Err(AspaValidateError::EeAsResourcesRdiPresent); } if asn.is_asnum_inherit() { return Err(AspaValidateError::EeAsResourcesInherit); } if asn.has_any_range() { return Err(AspaValidateError::EeAsResourcesRangePresent); } let ee_as_id = asn .asnum_single_id() .ok_or(AspaValidateError::EeAsResourcesNotSingleId)?; if ee_as_id != self.customer_as_id { return Err(AspaValidateError::CustomerAsIdMismatch { customer_as_id: self.customer_as_id, ee_as_id, }); } Ok(()) } } impl AspaObjectParsed { pub fn validate_profile(self) -> Result { let signed_object = self.signed_object.validate_profile()?; let econtent_type = signed_object .signed_data .encap_content_info .econtent_type .clone(); if econtent_type != OID_CT_ASPA { return Err(AspaProfileError::InvalidEContentType(econtent_type)); } let aspa = self .aspa .ok_or_else(|| AspaProfileError::ProfileDecode("ASPA.eContent missing".into()))? .validate_profile()?; Ok(AspaObject { signed_object, econtent_type: OID_CT_ASPA.to_string(), aspa, }) } } impl AspaEContentParsed { pub fn validate_profile(self) -> Result { fn count_elements(mut r: DerReader<'_>) -> Result { let mut n = 0usize; while !r.is_empty() { r.skip_any()?; n += 1; } Ok(n) } let mut r = DerReader::new(&self.der); let mut seq = r .take_sequence() .map_err(|e| AspaProfileError::ProfileDecode(e))?; if !r.is_empty() { return Err(AspaProfileError::ProfileDecode( "trailing bytes after ASProviderAttestation".into(), )); } let elem_count = count_elements(seq).map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; if elem_count != 3 { return Err(AspaProfileError::InvalidAttestationSequence); } // version [0] EXPLICIT INTEGER MUST be present and MUST be 1. if seq .peek_tag() .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))? != 0xA0 { return Err(AspaProfileError::VersionMustBeExplicitOne); } let (inner_tag, inner_val) = seq .take_explicit(0xA0) .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; if inner_tag != 0x02 { return Err(AspaProfileError::VersionMustBeExplicitOne); } let v = crate::data_model::common::der_uint_from_bytes(inner_val) .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; if v != 1 { return Err(AspaProfileError::VersionMustBeExplicitOne); } let customer_u64 = seq .take_uint_u64() .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; if customer_u64 > u32::MAX as u64 { return Err(AspaProfileError::CustomerAsIdOutOfRange(customer_u64)); } let customer_as_id = customer_u64 as u32; let providers_seq = seq .take_sequence() .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; if !seq.is_empty() { return Err(AspaProfileError::InvalidAttestationSequence); } let providers = parse_providers_cursor(providers_seq, customer_as_id)?; Ok(AspaEContent { version: 1, customer_as_id, provider_as_ids: providers, }) } } fn parse_providers_cursor( mut seq: DerReader<'_>, customer_as_id: u32, ) -> Result, AspaProfileError> { if seq.is_empty() { return Err(AspaProfileError::EmptyProviders); } let mut out: Vec = Vec::new(); let mut prev: Option = None; while !seq.is_empty() { let v = seq .take_uint_u64() .map_err(|e| AspaProfileError::ProfileDecode(e.to_string()))?; if v > u32::MAX as u64 { return Err(AspaProfileError::ProviderAsIdOutOfRange(v)); } let asn = v as u32; if asn == customer_as_id { return Err(AspaProfileError::ProvidersContainCustomer(customer_as_id)); } if let Some(p) = prev { if asn <= p { return Err(AspaProfileError::ProvidersNotStrictlyIncreasing); } } prev = Some(asn); out.push(asn); } Ok(out) }