use x509_parser::asn1_rs::Tag; use x509_parser::prelude::FromDer; use x509_parser::x509::AlgorithmIdentifier; pub type UtcTime = time::OffsetDateTime; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Asn1TimeEncoding { UtcTime, GeneralizedTime, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Asn1TimeUtc { pub utc: UtcTime, pub encoding: Asn1TimeEncoding, } impl Asn1TimeUtc { /// Validate Time encoding rules (RFC 5280): years 1950-2049 use UTCTime, /// other years use GeneralizedTime. pub fn validate_encoding_rfc5280( &self, field: &'static str, ) -> Result<(), InvalidTimeEncodingError> { let year = self.utc.year(); let expected = if year <= 2049 { Asn1TimeEncoding::UtcTime } else { Asn1TimeEncoding::GeneralizedTime }; if self.encoding != expected { return Err(InvalidTimeEncodingError { field, year, encoding: self.encoding, }); } Ok(()) } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct BigUnsigned { /// Minimal big-endian bytes. For zero, this is `[0]`. pub bytes_be: Vec, } impl BigUnsigned { pub fn from_biguint(n: &der_parser::num_bigint::BigUint) -> Self { let mut bytes = n.to_bytes_be(); if bytes.is_empty() { bytes.push(0); } Self { bytes_be: bytes } } pub fn to_hex_upper(&self) -> String { hex::encode_upper(&self.bytes_be) } pub fn to_u64(&self) -> Option { if self.bytes_be.len() > 8 { return None; } let mut value: u64 = 0; for &b in &self.bytes_be { value = (value << 8) | (b as u64); } Some(value) } } #[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] #[error( "{field} time encoding invalid for year {year}: got {encoding:?} (RFC 5280 §4.1.2.5; RFC 5280 §5.1.2.4-§5.1.2.6)" )] pub struct InvalidTimeEncodingError { pub field: &'static str, pub year: i32, pub encoding: Asn1TimeEncoding, } pub fn asn1_time_to_model(t: x509_parser::time::ASN1Time) -> Asn1TimeUtc { let encoding = if t.is_utctime() { Asn1TimeEncoding::UtcTime } else { Asn1TimeEncoding::GeneralizedTime }; Asn1TimeUtc { utc: t.to_datetime(), encoding, } } pub fn algorithm_params_absent_or_null(sig: &AlgorithmIdentifier<'_>) -> bool { match sig.parameters.as_ref() { None => true, Some(p) if p.tag() == Tag::Null => true, Some(_p) => false, } } /// Take a single DER TLV (Tag-Length-Value) from the start of `input`. /// /// This helper supports: /// - short- and long-form lengths (up to 8 length bytes) /// - only low-tag-number form tags (no high-tag-number form) /// - definite length only (DER forbids indefinite length) /// /// Returns: `(tag_byte, value_bytes, remaining_bytes)`. pub(crate) fn der_take_tlv(input: &[u8]) -> Result<(u8, &[u8], &[u8]), String> { if input.len() < 2 { return Err("truncated DER (need tag+len)".into()); } let tag = input[0]; if (tag & 0x1F) == 0x1F { return Err("high-tag-number form not supported".into()); } let len0 = input[1]; if len0 == 0x80 { return Err("indefinite length not allowed in DER".into()); } let (len, hdr_len) = if len0 & 0x80 == 0 { (len0 as usize, 2usize) } else { let n = (len0 & 0x7F) as usize; if n == 0 || n > 8 { return Err("invalid DER length".into()); } if input.len() < 2 + n { return Err("truncated DER (length bytes)".into()); } let mut l: usize = 0; for &b in &input[2..2 + n] { l = (l << 8) | (b as usize); } (l, 2 + n) }; if input.len() < hdr_len + len { return Err("truncated DER (value bytes)".into()); } let value = &input[hdr_len..hdr_len + len]; let rem = &input[hdr_len + len..]; Ok((tag, value, rem)) } /// Minimal streaming DER reader built on `der_take_tlv`. /// /// This is intentionally small and only supports the subset of DER needed by /// RPKI object eContent decoders (ROA/ASPA), to avoid constructing a generic AST /// (which is expensive on large objects such as ROAs with thousands of prefixes). #[derive(Clone, Copy)] pub(crate) struct DerReader<'a> { buf: &'a [u8], } impl<'a> DerReader<'a> { pub(crate) fn new(buf: &'a [u8]) -> Self { Self { buf } } pub(crate) fn is_empty(&self) -> bool { self.buf.is_empty() } pub(crate) fn peek_tag(&self) -> Result { self.buf .first() .copied() .ok_or_else(|| "truncated DER".into()) } pub(crate) fn take_any(&mut self) -> Result<(u8, &'a [u8]), String> { let (tag, value, rem) = der_take_tlv(self.buf)?; self.buf = rem; Ok((tag, value)) } pub(crate) fn take_any_full(&mut self) -> Result<(u8, &'a [u8], &'a [u8]), String> { let (tag, value, rem) = der_take_tlv(self.buf)?; let consumed = self.buf.len() - rem.len(); let full = &self.buf[..consumed]; self.buf = rem; Ok((tag, full, value)) } pub(crate) fn skip_any(&mut self) -> Result<(), String> { let _ = self.take_any()?; Ok(()) } pub(crate) fn take_tag(&mut self, expected_tag: u8) -> Result<&'a [u8], String> { let (tag, value) = self.take_any()?; if tag != expected_tag { return Err(format!( "unexpected tag: got 0x{tag:02X}, expected 0x{expected_tag:02X}" )); } Ok(value) } pub(crate) fn take_sequence(&mut self) -> Result, String> { let value = self.take_tag(0x30)?; Ok(DerReader::new(value)) } pub(crate) fn take_octet_string(&mut self) -> Result<&'a [u8], String> { self.take_tag(0x04) } pub(crate) fn take_bit_string(&mut self) -> Result<(u8, &'a [u8]), String> { let v = self.take_tag(0x03)?; if v.is_empty() { return Err("BIT STRING content is empty".into()); } Ok((v[0], &v[1..])) } pub(crate) fn take_uint_u64(&mut self) -> Result { let v = self.take_tag(0x02)?; der_uint_from_bytes(v) } pub(crate) fn take_explicit( &mut self, expected_outer_tag: u8, ) -> Result<(u8, &'a [u8]), String> { let inner_der = self.take_tag(expected_outer_tag)?; let (tag, value, rem) = der_take_tlv(inner_der)?; if !rem.is_empty() { return Err("trailing bytes inside EXPLICIT value".into()); } Ok((tag, value)) } } pub(crate) fn der_uint_from_bytes(bytes: &[u8]) -> Result { if bytes.is_empty() { return Err("INTEGER has empty content".into()); } // Disallow negative values. if (bytes[0] & 0x80) != 0 { return Err("INTEGER is negative".into()); } // DER requires minimal encoding for INTEGER. if bytes.len() > 1 && bytes[0] == 0x00 && (bytes[1] & 0x80) == 0 { return Err("INTEGER not minimally encoded".into()); } if bytes.len() > 8 { return Err("INTEGER does not fit u64".into()); } let mut v: u64 = 0; for &b in bytes { v = (v << 8) | (b as u64); } Ok(v) } #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct X509NameDer(pub Vec); impl X509NameDer { pub fn as_raw(&self) -> &[u8] { &self.0 } } impl std::fmt::Display for X509NameDer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let Ok((rem, name)) = x509_parser::x509::X509Name::from_der(&self.0) else { return write!(f, ""); }; if !rem.is_empty() { return write!(f, ""); } write!(f, "{name}") } } /// Filename extensions registered in IANA "RPKI Repository Name Schemes". /// /// Source: /// Snapshot date: 2026-01-28. /// /// Notes: /// - Includes entries marked TEMPORARY/DEPRECATED by IANA (e.g., `asa`, `gbr`). pub const IANA_RPKI_REPOSITORY_FILENAME_EXTENSIONS: &[&str] = &["asa", "cer", "crl", "gbr", "mft", "roa", "sig", "tak"];