use crate::model::common::der_take_tlv; use crate::model::common::{BigUnsigned, UtcTime}; use crate::model::oid::{OID_CT_RPKI_MANIFEST, OID_SHA256}; use crate::model::rc::ResourceCertificate; use crate::model::signed_object::{ RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectDecodeError, SignedObjectParseError, SignedObjectValidateError, }; #[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: UtcTime, pub next_update: UtcTime, pub file_hash_alg: String, /// DER-encoded content bytes of `Manifest.fileList` (SEQUENCE OF FileAndHash). pub file_list_der: Vec, /// Count of FileAndHash entries in `fileList`. pub file_count: usize, } #[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: [u8; 32], } #[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), } impl From for ManifestProfileError { fn from(value: SignedObjectDecodeError) -> Self { match value { SignedObjectDecodeError::Parse(e) => ManifestProfileError::ProfileDecode(e.to_string()), SignedObjectDecodeError::Validate(e) => ManifestProfileError::SignedObject(e), } } } #[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 decode_der_with_strict_options( der: &[u8], strict_cms_der: bool, strict_name: bool, ) -> Result { let signed_object = RpkiSignedObject::decode_der_with_strict_options(der, strict_cms_der, strict_name) .map_err(ManifestProfileError::from)?; Self::from_signed_object(signed_object) } 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 && !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 (_tag, _value, rem) = der_take_tlv(der).map_err(ManifestParseError::Parse)?; 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()?) } /// Parse and return the manifest fileList. /// /// Note: `ManifestEContent` is profile-validated when produced via `decode_der()`, so this /// should only fail due to internal inconsistencies (or if constructed manually). pub fn parse_files(&self) -> Result, ManifestProfileError> { parse_file_list_sha256_fast(&self.file_list_der) } pub fn file_count(&self) -> usize { self.file_count } } 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 { decode_manifest_econtent_fast(&self.der) } } fn validate_file_name_bytes(bytes: &[u8]) -> Result<(), ManifestProfileError> { // RFC 9286 §4.2.2: // 1+ chars from a-zA-Z0-9-_ , then '.', then 3-letter extension. if bytes.len() < 5 { return Err(ManifestProfileError::InvalidFileName( String::from_utf8_lossy(bytes).into_owned(), )); }; // "followed by a single . (DOT), followed by a three letter extension" // -> the dot must be exactly 4 bytes from the end. let dot_pos = bytes.len() - 4; if bytes[dot_pos] != b'.' { return Err(ManifestProfileError::InvalidFileName( String::from_utf8_lossy(bytes).into_owned(), )); } #[inline(always)] fn valid_base_char(b: u8) -> bool { // RFC 9286 allowed set: a-zA-Z0-9-_ b.is_ascii_digit() || b.is_ascii_lowercase() || b.is_ascii_uppercase() || b == b'-' || b == b'_' } for &b in &bytes[..dot_pos] { if (b & 0x80) != 0 || !valid_base_char(b) { return Err(ManifestProfileError::InvalidFileName( String::from_utf8_lossy(bytes).into_owned(), )); } } let e0 = bytes[dot_pos + 1]; let e1 = bytes[dot_pos + 2]; let e2 = bytes[dot_pos + 3]; if (e0 & 0x80) != 0 || (e1 & 0x80) != 0 || (e2 & 0x80) != 0 { return Err(ManifestProfileError::InvalidFileName( String::from_utf8_lossy(bytes).into_owned(), )); } #[inline(always)] fn lower_if_alpha(b: u8) -> Option { match b { b'a'..=b'z' => Some(b), b'A'..=b'Z' => Some(b + 32), _ => None, } } let Some(l0) = lower_if_alpha(e0) else { return Err(ManifestProfileError::InvalidFileName( String::from_utf8_lossy(bytes).into_owned(), )); }; let Some(l1) = lower_if_alpha(e1) else { return Err(ManifestProfileError::InvalidFileName( String::from_utf8_lossy(bytes).into_owned(), )); }; let Some(l2) = lower_if_alpha(e2) else { return Err(ManifestProfileError::InvalidFileName( String::from_utf8_lossy(bytes).into_owned(), )); }; match [l0, l1, l2] { // Full IANA list (see `common.rs`). [b'a', b's', b'a'] | [b'c', b'e', b'r'] | [b'c', b'r', b'l'] | [b'g', b'b', b'r'] | [b'm', b'f', b't'] | [b'r', b'o', b'a'] | [b's', b'i', b'g'] | [b't', b'a', b'k'] => Ok(()), _ => Err(ManifestProfileError::InvalidFileName( String::from_utf8_lossy(bytes).into_owned(), )), } } fn decode_manifest_econtent_fast(der: &[u8]) -> Result { let (tag, mut seq_content, rem) = der_take_tlv(der) .map_err(|e| ManifestProfileError::ProfileDecode(format!("DER decode error: {e}")))?; if !rem.is_empty() { return Err(ManifestProfileError::ProfileDecode(format!( "trailing bytes after DER object: {} bytes", rem.len() ))); } if tag != 0x30 { return Err(ManifestProfileError::ProfileDecode( "Manifest eContent must be SEQUENCE".into(), )); } let seq_len = der_count_elements(seq_content).map_err(ManifestProfileError::ProfileDecode)?; if seq_len != 5 && seq_len != 6 { return Err(ManifestProfileError::InvalidManifestSequenceLen(seq_len)); } let mut version: u32 = 0; if seq_len == 6 { let Some(&first_tag) = seq_content.first() else { return Err(ManifestProfileError::InvalidManifestSequenceLen(0)); }; if first_tag != 0xA0 { return Err(ManifestProfileError::ProfileDecode( "Manifest.version must be [0] EXPLICIT INTEGER".into(), )); } let (_cs_tag, cs_value, after) = der_take_tlv(seq_content).map_err(|e| { ManifestProfileError::ProfileDecode(format!("Manifest.version decode error: {e}")) })?; seq_content = after; let (inner_tag, inner_value, inner_rem) = der_take_tlv(cs_value).map_err(|e| { ManifestProfileError::ProfileDecode(format!("Manifest.version inner decode error: {e}")) })?; if !inner_rem.is_empty() { return Err(ManifestProfileError::ProfileDecode( "trailing bytes inside Manifest.version".into(), )); } if inner_tag != 0x02 { return Err(ManifestProfileError::ProfileDecode( "Manifest.version must be [0] EXPLICIT INTEGER".into(), )); } let v = der_integer_to_u64(inner_value).map_err(|e| { ManifestProfileError::ProfileDecode(format!("Manifest.version decode error: {e}")) })?; if v != 0 { return Err(ManifestProfileError::InvalidManifestVersion(v)); } version = 0; } let (mn_tag, mn_value, after) = der_take_tlv(seq_content).map_err(|e| { ManifestProfileError::ProfileDecode(format!("Manifest.manifestNumber decode error: {e}")) })?; seq_content = after; if mn_tag != 0x02 { return Err(ManifestProfileError::InvalidManifestNumber); } let manifest_number = der_integer_to_bigunsigned(mn_value)?; let (tu_tag, tu_value, after) = der_take_tlv(seq_content).map_err(|e| { ManifestProfileError::ProfileDecode(format!("Manifest.thisUpdate decode error: {e}")) })?; seq_content = after; if tu_tag != 0x18 { return Err(ManifestProfileError::InvalidThisUpdate); } let this_update = parse_generalized_time_bytes(tu_value).map_err(ManifestProfileError::ProfileDecode)?; let (nu_tag, nu_value, after) = der_take_tlv(seq_content).map_err(|e| { ManifestProfileError::ProfileDecode(format!("Manifest.nextUpdate decode error: {e}")) })?; seq_content = after; if nu_tag != 0x18 { return Err(ManifestProfileError::InvalidNextUpdate); } let next_update = parse_generalized_time_bytes(nu_value).map_err(ManifestProfileError::ProfileDecode)?; if next_update <= this_update { return Err(ManifestProfileError::NextUpdateNotLater); } let (oid_tag, oid_value, after) = der_take_tlv(seq_content).map_err(|e| { ManifestProfileError::ProfileDecode(format!("Manifest.fileHashAlg decode error: {e}")) })?; seq_content = after; if oid_tag != 0x06 { return Err(ManifestProfileError::ProfileDecode( "Manifest.fileHashAlg must be OBJECT IDENTIFIER".into(), )); } if !oid_content_is_sha256(oid_value) { return Err(ManifestProfileError::InvalidFileHashAlg( oid_content_to_string(oid_value), )); } let (fl_tag, fl_value, after) = der_take_tlv(seq_content).map_err(|e| { ManifestProfileError::ProfileDecode(format!("Manifest.fileList decode error: {e}")) })?; seq_content = after; if fl_tag != 0x30 { return Err(ManifestProfileError::InvalidFileList); } let file_count = validate_file_list_sha256_fast(fl_value)?; let file_list_der = fl_value.to_vec(); if !seq_content.is_empty() { return Err(ManifestProfileError::InvalidManifestSequenceLen(seq_len)); } Ok(ManifestEContent { version, manifest_number, this_update, next_update, file_hash_alg: OID_SHA256.to_string(), file_list_der, file_count, }) } fn validate_file_list_sha256_fast(content: &[u8]) -> Result { let mut cur = content; let mut count: usize = 0; while !cur.is_empty() { let (tag, value, rem) = der_take_tlv(cur).map_err(|e| { ManifestProfileError::ProfileDecode(format!("fileList entry decode error: {e}")) })?; cur = rem; if tag != 0x30 { return Err(ManifestProfileError::InvalidFileAndHash); } let mut entry = value; let (fn_tag, fn_value, entry_rem) = der_take_tlv(entry).map_err(|e| { ManifestProfileError::ProfileDecode(format!("fileList fileName decode error: {e}")) })?; entry = entry_rem; if fn_tag != 0x16 { return Err(ManifestProfileError::InvalidFileAndHash); } if entry.is_empty() { return Err(ManifestProfileError::InvalidFileAndHash); } validate_file_name_bytes(fn_value)?; let (hash_tag, hash_value, entry_rem) = der_take_tlv(entry).map_err(|_e| { // Missing second element should map to "SEQUENCE of 2" shape error. ManifestProfileError::InvalidFileAndHash })?; entry = entry_rem; if !entry.is_empty() { return Err(ManifestProfileError::InvalidFileAndHash); } if hash_tag != 0x03 { return Err(ManifestProfileError::InvalidHashType); } if hash_value.is_empty() { return Err(ManifestProfileError::InvalidHashLength(0)); } let unused_bits = hash_value[0]; if unused_bits != 0 { return Err(ManifestProfileError::HashNotOctetAligned); } let bits = &hash_value[1..]; if bits.len() != 32 { return Err(ManifestProfileError::InvalidHashLength(bits.len())); } count += 1; } Ok(count) } fn parse_file_list_sha256_fast(content: &[u8]) -> Result, ManifestProfileError> { // Heuristic initial capacity (avoid a full pre-scan, which is expensive for xlarge manifests). // Each FileAndHash entry is typically tens of bytes; 80 is a conservative average. let est = (content.len() / 80).clamp(16, 4096); let mut cur = content; let mut out: Vec = Vec::with_capacity(est); while !cur.is_empty() { let (tag, value, rem) = der_take_tlv(cur).map_err(|e| { ManifestProfileError::ProfileDecode(format!("fileList entry decode error: {e}")) })?; cur = rem; if tag != 0x30 { return Err(ManifestProfileError::InvalidFileAndHash); } let mut entry = value; let (fn_tag, fn_value, entry_rem) = der_take_tlv(entry).map_err(|e| { ManifestProfileError::ProfileDecode(format!("fileList fileName decode error: {e}")) })?; entry = entry_rem; if fn_tag != 0x16 { return Err(ManifestProfileError::InvalidFileAndHash); } if entry.is_empty() { return Err(ManifestProfileError::InvalidFileAndHash); } let file_name = validate_and_copy_file_name(fn_value)?; let (hash_tag, hash_value, entry_rem) = der_take_tlv(entry).map_err(|_e| { // Missing second element should map to "SEQUENCE of 2" shape error. ManifestProfileError::InvalidFileAndHash })?; entry = entry_rem; if !entry.is_empty() { return Err(ManifestProfileError::InvalidFileAndHash); } if hash_tag != 0x03 { return Err(ManifestProfileError::InvalidHashType); } if hash_value.is_empty() { return Err(ManifestProfileError::InvalidHashLength(0)); } let unused_bits = hash_value[0]; if unused_bits != 0 { return Err(ManifestProfileError::HashNotOctetAligned); } let bits = &hash_value[1..]; if bits.len() != 32 { return Err(ManifestProfileError::InvalidHashLength(bits.len())); } let mut hash_bytes = [0u8; 32]; hash_bytes.copy_from_slice(bits); out.push(FileAndHash { file_name, hash_bytes, }); } Ok(out) } fn validate_and_copy_file_name(bytes: &[u8]) -> Result { validate_file_name_bytes(bytes)?; String::from_utf8(bytes.to_vec()).map_err(|_| { ManifestProfileError::InvalidFileName(String::from_utf8_lossy(bytes).into_owned()) }) } fn der_count_elements(mut input: &[u8]) -> Result { let mut count: usize = 0; while !input.is_empty() { let (_tag, _value, rem) = der_take_tlv(input)?; input = rem; count += 1; } Ok(count) } fn der_integer_to_u64(bytes: &[u8]) -> Result { if bytes.is_empty() { return Err("INTEGER empty".into()); } // Reject negative (two's complement). if bytes[0] & 0x80 != 0 { return Err("INTEGER is negative".into()); } if bytes.len() > 8 { return Err("INTEGER too large".into()); } let mut v: u64 = 0; for &b in bytes { v = (v << 8) | (b as u64); } Ok(v) } fn der_integer_to_bigunsigned(bytes: &[u8]) -> Result { if bytes.is_empty() { return Err(ManifestProfileError::InvalidManifestNumber); } // Two's complement: for non-negative values, a leading 0x00 may be present. if bytes[0] & 0x80 != 0 { return Err(ManifestProfileError::InvalidManifestNumber); } let mut start = 0usize; while start + 1 < bytes.len() && bytes[start] == 0 { start += 1; } let mut minimal = bytes[start..].to_vec(); if minimal.is_empty() { minimal.push(0); } if minimal.len() > 20 { return Err(ManifestProfileError::ManifestNumberTooLong); } Ok(BigUnsigned { bytes_be: minimal }) } fn parse_generalized_time_bytes(bytes: &[u8]) -> Result { // Accept "YYYYMMDDHHMMSSZ" and also allow optional fractional seconds (".fff...Z"). if !bytes.is_ascii() { return Err("GeneralizedTime not ASCII".into()); } let s = std::str::from_utf8(bytes).map_err(|e| e.to_string())?; if !s.ends_with('Z') { return Err("GeneralizedTime must end with 'Z'".into()); } let core = &s[..s.len() - 1]; let (main, frac) = core .split_once('.') .map_or((core, None), |(a, b)| (a, Some(b))); if main.len() != 14 || !main.bytes().all(|b| b.is_ascii_digit()) { return Err("GeneralizedTime must be YYYYMMDDHHMMSS[.fff]Z".into()); } let year: i32 = main[0..4].parse().map_err(|_| "bad year")?; let month: u8 = main[4..6].parse().map_err(|_| "bad month")?; let day: u8 = main[6..8].parse().map_err(|_| "bad day")?; let hour: u8 = main[8..10].parse().map_err(|_| "bad hour")?; let minute: u8 = main[10..12].parse().map_err(|_| "bad minute")?; let second: u8 = main[12..14].parse().map_err(|_| "bad second")?; let nanosecond: u32 = if let Some(frac) = frac { if frac.is_empty() || !frac.bytes().all(|b| b.is_ascii_digit()) { return Err("bad fractional seconds".into()); } let mut ns: u32 = 0; let mut scale: u32 = 1_000_000_000; for (i, ch) in frac.bytes().enumerate() { if i >= 9 { break; } scale /= 10; ns += ((ch - b'0') as u32) * scale; } ns } else { 0 }; let date = time::Date::from_calendar_date( year, time::Month::try_from(month).map_err(|_| "bad month")?, day, ) .map_err(|e| e.to_string())?; let t = time::Time::from_hms_nano(hour, minute, second, nanosecond).map_err(|e| e.to_string())?; Ok(date.with_time(t).assume_utc()) } fn oid_content_is_sha256(bytes: &[u8]) -> bool { // 2.16.840.1.101.3.4.2.1 let mut arcs = oid_content_iter(bytes); const EXPECTED: &[u64] = &[2, 16, 840, 1, 101, 3, 4, 2, 1]; for &e in EXPECTED { match arcs.next() { Some(v) if v == e => {} _ => return false, } } arcs.next().is_none() } fn oid_content_to_string(bytes: &[u8]) -> String { let arcs: Vec = oid_content_iter(bytes).collect(); if arcs.is_empty() { return "".to_string(); } let mut s = String::new(); for (i, a) in arcs.iter().enumerate() { if i > 0 { s.push('.'); } s.push_str(&a.to_string()); } s } fn oid_content_iter(bytes: &[u8]) -> impl Iterator + '_ { struct It<'a> { bytes: &'a [u8], pos: usize, first_done: bool, first_a0: u64, first_a1: u64, emit_first_idx: u8, } impl<'a> Iterator for It<'a> { type Item = u64; fn next(&mut self) -> Option { if !self.first_done { if self.bytes.is_empty() { self.first_done = true; return None; } let first = self.bytes[0] as u64; self.first_a0 = first / 40; self.first_a1 = first % 40; self.pos = 1; self.first_done = true; self.emit_first_idx = 0; } if self.emit_first_idx == 0 { self.emit_first_idx = 1; return Some(self.first_a0); } if self.emit_first_idx == 1 { self.emit_first_idx = 2; return Some(self.first_a1); } if self.pos >= self.bytes.len() { return None; } let mut v: u64 = 0; while self.pos < self.bytes.len() { let b = self.bytes[self.pos]; self.pos += 1; v = (v << 7) | ((b & 0x7F) as u64); if b & 0x80 == 0 { return Some(v); } } None } } It { bytes, pos: 0, first_done: false, first_a0: 0, first_a1: 0, emit_first_idx: 0, } } #[cfg(test)] mod tests { use super::*; fn tlv(tag: u8, value: &[u8]) -> Vec { assert!(value.len() < 128); let mut out = Vec::with_capacity(2 + value.len()); out.push(tag); out.push(value.len() as u8); out.extend_from_slice(value); out } fn tlv_long_len(tag: u8, len_bytes: &[u8], value: &[u8]) -> Vec { let mut out = Vec::with_capacity(2 + len_bytes.len() + value.len()); out.push(tag); out.push(0x80 | (len_bytes.len() as u8)); out.extend_from_slice(len_bytes); out.extend_from_slice(value); out } #[test] fn der_take_tlv_supports_short_and_long_form_lengths_and_errors() { let v = b"abc"; let der = tlv(0x04, v); let (tag, val, rem) = der_take_tlv(&der).expect("short len"); assert_eq!(tag, 0x04); assert_eq!(val, v); assert!(rem.is_empty()); // Long-form length with 1 length byte (130). let v = vec![b'x'; 130]; let der = tlv_long_len(0x04, &[0x82], &v); let (tag, val, rem) = der_take_tlv(&der).expect("long len 1"); assert_eq!(tag, 0x04); assert_eq!(val.len(), 130); assert!(rem.is_empty()); // Long-form length with 2 length bytes (256). let v = vec![b'y'; 256]; let der = tlv_long_len(0x04, &[0x01, 0x00], &v); let (tag, val, rem) = der_take_tlv(&der).expect("long len 2"); assert_eq!(tag, 0x04); assert_eq!(val.len(), 256); assert!(rem.is_empty()); assert!(der_take_tlv(&[]).is_err()); assert!(der_take_tlv(&[0x04]).is_err()); // High-tag-number form not supported. assert!(der_take_tlv(&[0x1F, 0x01, 0x00]).is_err()); // Indefinite length is not allowed in DER. assert!(der_take_tlv(&[0x04, 0x80]).is_err()); // Invalid long-form length encoding. assert!(der_take_tlv(&[0x04, 0x81]).is_err()); assert!(der_take_tlv(&[0x04, 0x89]).is_err()); } #[test] fn parse_generalized_time_bytes_accepts_fraction_and_rejects_invalid() { let t = parse_generalized_time_bytes(b"20260101000000Z").expect("basic time"); assert_eq!(t.year(), 2026); let t = parse_generalized_time_bytes(b"20260101000000.1Z").expect("fractional"); assert_eq!(t.nanosecond(), 100_000_000); assert!(parse_generalized_time_bytes(b"20260101000000").is_err()); assert!(parse_generalized_time_bytes(b"20260101000000+00").is_err()); assert!(parse_generalized_time_bytes(b"2026010100000Z").is_err()); assert!(parse_generalized_time_bytes(b"20261301000000Z").is_err()); assert!(parse_generalized_time_bytes(b"20260132000000Z").is_err()); assert!(parse_generalized_time_bytes(&[0xFF]).is_err()); } #[test] fn oid_helpers_accept_sha256_and_format_invalid() { // 2.16.840.1.101.3.4.2.1 let sha256_oid_content = [0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01]; assert!(oid_content_is_sha256(&sha256_oid_content)); assert!(!oid_content_is_sha256(&[0x55, 0x04, 0x03])); // 2.5.4.3 assert_eq!(oid_content_to_string(&[]), "".to_string()); } #[test] fn validate_file_list_sha256_fast_counts_and_rejects_bad_hash() { fn file_and_hash(file: &str, digest: u8) -> Vec { let mut hash = vec![0u8; 33]; hash[0] = 0; // unused bits for b in &mut hash[1..] { *b = digest; } let ia5 = tlv(0x16, file.as_bytes()); let bit = tlv(0x03, &hash); let mut entry = Vec::new(); entry.extend_from_slice(&ia5); entry.extend_from_slice(&bit); tlv(0x30, &entry) } let mut list = Vec::new(); list.extend_from_slice(&file_and_hash("A.cer", 0xAA)); list.extend_from_slice(&file_and_hash("B.roa", 0xBB)); assert_eq!(validate_file_list_sha256_fast(&list).expect("count"), 2); // Wrong hash length. let mut bad = Vec::new(); let ia5 = tlv(0x16, b"A.cer"); let bit = tlv(0x03, &[0u8; 2]); // too short let mut entry = Vec::new(); entry.extend_from_slice(&ia5); entry.extend_from_slice(&bit); bad.extend_from_slice(&tlv(0x30, &entry)); assert!(matches!( validate_file_list_sha256_fast(&bad), Err(ManifestProfileError::InvalidHashLength(_)) )); } }