use crate::data_model::common::BigUnsigned; use crate::data_model::oid::{OID_CT_RPKI_MANIFEST, OID_SHA256}; use crate::data_model::rc::ResourceCertificate; use crate::data_model::signed_object::{ RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError, }; use der_parser::ber::BerObjectContent; use der_parser::der::{DerObject, Tag, parse_der}; use time::OffsetDateTime; #[derive(Clone, Debug, PartialEq, Eq)] pub struct ManifestObject { pub signed_object: RpkiSignedObject, pub econtent_type: String, pub manifest: ManifestEContent, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct ManifestObjectParsed { pub signed_object: RpkiSignedObjectParsed, pub econtent_type: String, pub manifest: Option, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct ManifestEContent { pub version: u32, pub manifest_number: BigUnsigned, pub this_update: OffsetDateTime, pub next_update: OffsetDateTime, pub file_hash_alg: String, pub files: Vec, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct ManifestEContentParsed { der: Vec, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct FileAndHash { pub file_name: String, pub hash_bytes: Vec, } #[derive(Debug, thiserror::Error)] pub enum ManifestParseError { #[error("signed object parse error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")] SignedObject(#[from] SignedObjectParseError), #[error("DER parse error: {0} (RFC 9286 §4.2; DER)")] Parse(String), #[error("trailing bytes after DER object: {0} bytes (RFC 9286 §4.2; DER)")] TrailingBytes(usize), } #[derive(Debug, thiserror::Error)] pub enum ManifestProfileError { #[error("signed object profile error: {0} (RFC 6488 §2-§3; RFC 9589 §4)")] SignedObject(#[from] SignedObjectValidateError), #[error( "eContentType must be id-ct-rpkiManifest ({OID_CT_RPKI_MANIFEST}), got {0} (RFC 9286 §4.1; RFC 9286 §4.4(1))" )] InvalidEContentType(String), #[error("manifest profile decode error: {0} (RFC 9286 §4.2; DER)")] ProfileDecode(String), #[error("Manifest must be a SEQUENCE of 5 or 6 elements, got {0} (RFC 9286 §4.2)")] InvalidManifestSequenceLen(usize), #[error("Manifest.version must be 0, got {0} (RFC 9286 §4.2.1)")] InvalidManifestVersion(u64), #[error( "Manifest.manifestNumber must be non-negative INTEGER (RFC 9286 §4.2; RFC 9286 §4.2.1)" )] InvalidManifestNumber, #[error("Manifest.manifestNumber longer than 20 octets (RFC 9286 §4.2.1)")] ManifestNumberTooLong, #[error("Manifest.thisUpdate must be GeneralizedTime (RFC 9286 §4.2)")] InvalidThisUpdate, #[error("Manifest.nextUpdate must be GeneralizedTime (RFC 9286 §4.2)")] InvalidNextUpdate, #[error("Manifest.nextUpdate must be later than thisUpdate (RFC 9286 §4.2.1)")] NextUpdateNotLater, #[error( "Manifest.fileHashAlg must be id-sha256 ({OID_SHA256}), got {0} (RFC 9286 §4.2.1; RFC 7935 §2)" )] InvalidFileHashAlg(String), #[error("Manifest.fileList must be a SEQUENCE (RFC 9286 §4.2)")] InvalidFileList, #[error("FileAndHash must be SEQUENCE of 2 (RFC 9286 §4.2)")] InvalidFileAndHash, #[error("fileList file name invalid: {0} (RFC 9286 §4.2.2)")] InvalidFileName(String), #[error("fileList hash must be BIT STRING (RFC 9286 §4.2)")] InvalidHashType, #[error( "fileList hash BIT STRING must be octet-aligned (unused bits=0) (RFC 9286 §4.2.1; DER BIT STRING)" )] HashNotOctetAligned, #[error( "fileList hash length invalid for sha256: got {0} bytes (RFC 9286 §4.2.1; RFC 7935 §2)" )] InvalidHashLength(usize), } #[derive(Debug, thiserror::Error)] pub enum ManifestDecodeError { #[error("{0}")] Parse(#[from] ManifestParseError), #[error("{0}")] Validate(#[from] ManifestProfileError), } #[derive(Debug, thiserror::Error)] pub enum ManifestValidateError { #[error( "Manifest EE certificate MUST include at least one RFC 3779 resource extension (IP or AS) (RFC 6487 §4.8.10-§4.8.11; RFC 3779; RFC 9286 §5.1)" )] EeResourcesMissing, #[error( "Manifest EE certificate IP resources MUST use inherit only (RFC 9286 §5.1; RFC 3779 §2.2.3.5)" )] EeIpResourcesNotInherit, #[error( "Manifest EE certificate AS resources MUST use inherit only (RFC 9286 §5.1; RFC 3779 §3.2.3.3)" )] EeAsResourcesNotInherit, #[error( "Manifest EE certificate AS resources rdi MUST be absent (RFC 6487 §4.8.11; RFC 3779 §3.2.3.5)" )] EeAsResourcesRdiPresent, } impl ManifestObject { /// 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 manifest = signed_object .signed_data .encap_content_info .econtent .as_deref() .map(ManifestEContent::parse_der) .transpose()?; Ok(ManifestObjectParsed { signed_object, econtent_type, manifest, }) } /// Profile validate step of scheme A (`parse → validate → verify`). /// /// `ManifestObject` is already profile-validated when constructed via `decode_der()` / /// `ManifestObjectParsed::validate_profile()`. pub fn validate_profile(&self) -> Result<(), ManifestProfileError> { 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_RPKI_MANIFEST { return Err(ManifestProfileError::InvalidEContentType(econtent_type).into()); } let manifest = ManifestEContent::decode_der(&signed_object.signed_data.encap_content_info.econtent)?; Ok(Self { signed_object, econtent_type: OID_CT_RPKI_MANIFEST.to_string(), manifest, }) } /// Validate the embedded EE certificate resources against RFC 9286 §5.1. /// /// This does **not** perform certificate path validation. It assumes `ee` is a parsed and /// profile-validated RPKI EE resource certificate. pub fn validate_against_ee_cert( &self, ee: &ResourceCertificate, ) -> Result<(), ManifestValidateError> { let ip = ee.tbs.extensions.ip_resources.as_ref(); let asn = ee.tbs.extensions.as_resources.as_ref(); if ip.is_none() && asn.is_none() { return Err(ManifestValidateError::EeResourcesMissing); } if let Some(ip) = ip { if !ip.is_all_inherit() { return Err(ManifestValidateError::EeIpResourcesNotInherit); } } if let Some(asn) = asn { if asn.rdi.is_some() { return Err(ManifestValidateError::EeAsResourcesRdiPresent); } if !asn.is_asnum_inherit() { return Err(ManifestValidateError::EeAsResourcesNotInherit); } } Ok(()) } /// Validate this manifest's embedded EE certificate resources. pub fn validate_embedded_ee_cert(&self) -> Result<(), ManifestValidateError> { let ee = &self.signed_object.signed_data.certificates[0].resource_cert; self.validate_against_ee_cert(ee) } } impl ManifestEContent { /// Parse step of scheme A (`parse → validate → verify`). pub fn parse_der(der: &[u8]) -> Result { let (rem, _obj) = parse_der(der).map_err(|e| ManifestParseError::Parse(e.to_string()))?; if !rem.is_empty() { return Err(ManifestParseError::TrailingBytes(rem.len())); } Ok(ManifestEContentParsed { der: der.to_vec() }) } /// Profile validate step of scheme A (`parse → validate → verify`). /// /// `ManifestEContent` is already profile-validated when constructed via `decode_der()` / /// `ManifestEContentParsed::validate_profile()`. pub fn validate_profile(&self) -> Result<(), ManifestProfileError> { Ok(()) } /// Decode the DER-encoded Manifest eContent defined in RFC 9286 §4.2 (`parse + validate`). pub fn decode_der(der: &[u8]) -> Result { Ok(Self::parse_der(der)?.validate_profile()?) } } impl ManifestObjectParsed { 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_RPKI_MANIFEST { return Err(ManifestProfileError::InvalidEContentType(econtent_type)); } let manifest = self .manifest .ok_or_else(|| ManifestProfileError::ProfileDecode("Manifest.eContent missing".into()))? .validate_profile()?; Ok(ManifestObject { signed_object, econtent_type: OID_CT_RPKI_MANIFEST.to_string(), manifest, }) } } impl ManifestEContentParsed { pub fn validate_profile(self) -> Result { let (_rem, obj) = parse_der(&self.der).map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?; let seq = obj .as_sequence() .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?; if seq.len() != 5 && seq.len() != 6 { return Err(ManifestProfileError::InvalidManifestSequenceLen(seq.len())); } let mut idx = 0; let mut version: u32 = 0; if seq.len() == 6 { let v_obj = &seq[0]; if v_obj.class() != der_parser::ber::Class::ContextSpecific || v_obj.tag() != Tag(0) { return Err(ManifestProfileError::ProfileDecode( "Manifest.version must be [0] EXPLICIT INTEGER".into(), )); } let inner_der = v_obj .as_slice() .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?; let (rem, inner) = parse_der(inner_der) .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?; if !rem.is_empty() { return Err(ManifestProfileError::ProfileDecode( "trailing bytes inside Manifest.version".into(), )); } let v = inner .as_u64() .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?; if v != 0 { return Err(ManifestProfileError::InvalidManifestVersion(v)); } version = 0; idx = 1; } let manifest_number = parse_manifest_number(&seq[idx])?; idx += 1; let this_update = parse_generalized_time(&seq[idx], ManifestProfileError::InvalidThisUpdate)?; idx += 1; let next_update = parse_generalized_time(&seq[idx], ManifestProfileError::InvalidNextUpdate)?; idx += 1; if next_update <= this_update { return Err(ManifestProfileError::NextUpdateNotLater); } let file_hash_alg = oid_to_string(&seq[idx])?; idx += 1; if file_hash_alg != OID_SHA256 { return Err(ManifestProfileError::InvalidFileHashAlg(file_hash_alg)); } let files = parse_file_list_sha256(&seq[idx])?; Ok(ManifestEContent { version, manifest_number, this_update, next_update, file_hash_alg: OID_SHA256.to_string(), files, }) } } fn parse_manifest_number(obj: &DerObject<'_>) -> Result { let n = obj .as_biguint() .map_err(|_e| ManifestProfileError::InvalidManifestNumber)?; let out = BigUnsigned::from_biguint(&n); if out.bytes_be.len() > 20 { return Err(ManifestProfileError::ManifestNumberTooLong); } Ok(out) } fn parse_generalized_time( obj: &DerObject<'_>, err: ManifestProfileError, ) -> Result { match &obj.content { BerObjectContent::GeneralizedTime(dt) => dt .to_datetime() .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string())), _ => Err(err), } } fn parse_file_list_sha256(obj: &DerObject<'_>) -> Result, ManifestProfileError> { let seq = obj .as_sequence() .map_err(|_e| ManifestProfileError::InvalidFileList)?; let mut out = Vec::with_capacity(seq.len()); for entry in seq { let (file_name, hash_bytes) = parse_file_and_hash(entry)?; validate_file_name(&file_name)?; if hash_bytes.len() != 32 { return Err(ManifestProfileError::InvalidHashLength(hash_bytes.len())); } out.push(FileAndHash { file_name, hash_bytes, }); } Ok(out) } fn parse_file_and_hash(obj: &DerObject<'_>) -> Result<(String, Vec), ManifestProfileError> { let seq = obj .as_sequence() .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?; if seq.len() != 2 { return Err(ManifestProfileError::InvalidFileAndHash); } let file_name = seq[0] .as_str() .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))? .to_string(); let (unused_bits, bits) = match &seq[1].content { BerObjectContent::BitString(unused, bso) => (*unused, bso.data.to_vec()), _ => return Err(ManifestProfileError::InvalidHashType), }; if unused_bits != 0 { return Err(ManifestProfileError::HashNotOctetAligned); } Ok((file_name, bits)) } fn validate_file_name(name: &str) -> Result<(), ManifestProfileError> { // RFC 9286 §4.2.2: // 1+ chars from a-zA-Z0-9-_ , then '.', then 3-letter extension. let Some((base, ext)) = name.rsplit_once('.') else { return Err(ManifestProfileError::InvalidFileName(name.to_string())); }; if base.is_empty() || ext.len() != 3 { return Err(ManifestProfileError::InvalidFileName(name.to_string())); } if !base .bytes() .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_') { return Err(ManifestProfileError::InvalidFileName(name.to_string())); } if !ext.bytes().all(|b| b.is_ascii_alphabetic()) { return Err(ManifestProfileError::InvalidFileName(name.to_string())); } let ext_lower = ext.to_ascii_lowercase(); if !crate::data_model::common::IANA_RPKI_REPOSITORY_FILENAME_EXTENSIONS .iter() .any(|&e| e == ext_lower) { return Err(ManifestProfileError::InvalidFileName(name.to_string())); } Ok(()) } fn oid_to_string(obj: &DerObject<'_>) -> Result { let oid = obj .as_oid() .map_err(|e| ManifestProfileError::ProfileDecode(e.to_string()))?; Ok(oid.to_id_string()) }