394 lines
13 KiB
Rust
394 lines
13 KiB
Rust
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<AspaEContentParsed>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct AspaEContent {
|
|
pub version: u32,
|
|
pub customer_as_id: u32,
|
|
pub provider_as_ids: Vec<u32>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct AspaEContentParsed {
|
|
der: Vec<u8>,
|
|
}
|
|
|
|
#[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<AspaObjectParsed, AspaParseError> {
|
|
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<Self, AspaDecodeError> {
|
|
Ok(Self::parse_der(der)?.validate_profile()?)
|
|
}
|
|
|
|
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(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<AspaEContentParsed, AspaParseError> {
|
|
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<Self, AspaDecodeError> {
|
|
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<AspaObject, AspaProfileError> {
|
|
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<AspaEContent, AspaProfileError> {
|
|
fn count_elements(mut r: DerReader<'_>) -> Result<usize, String> {
|
|
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<Vec<u32>, AspaProfileError> {
|
|
if seq.is_empty() {
|
|
return Err(AspaProfileError::EmptyProviders);
|
|
}
|
|
|
|
let mut out: Vec<u32> = Vec::new();
|
|
let mut prev: Option<u32> = 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)
|
|
}
|