290 lines
8.3 KiB
Rust
290 lines
8.3 KiB
Rust
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<u8>,
|
|
}
|
|
|
|
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<u64> {
|
|
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<u8, String> {
|
|
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<DerReader<'a>, 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<u64, String> {
|
|
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<u64, String> {
|
|
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<u8>);
|
|
|
|
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, "<invalid X.509 Name DER>");
|
|
};
|
|
if !rem.is_empty() {
|
|
return write!(f, "<invalid X.509 Name DER (trailing bytes)>");
|
|
}
|
|
write!(f, "{name}")
|
|
}
|
|
}
|
|
|
|
/// Filename extensions registered in IANA "RPKI Repository Name Schemes".
|
|
///
|
|
/// Source: <https://www.iana.org/assignments/rpki/rpki.xhtml>
|
|
/// 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"];
|